import {execSync} from 'node:child_process'; import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmdirSync, rmSync, statSync, unlinkSync, } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import {toolchains} from './toolchains'; const isWin = os.platform() === 'win32'; const where = isWin ? 'where' : 'which'; if (os.platform() === 'win32') { console.log('Windows CI is broken - needs to be cross-compiled'); process.exit(0); } function isMusl() { // @ts-expect-error const {glibcVersionRuntime} = process.report.getReport().header; return !glibcVersionRuntime; } const targets = [ 'x86_64-unknown-linux-musl', 'aarch64-unknown-linux-gnu', 'x86_64-unknown-linux-gnu', 'aarch64-apple-darwin', 'x86_64-apple-darwin', 'aarch64-unknown-linux-musl', ]; export const getTarget = () => { switch (process.platform) { case 'win32': switch (process.arch) { case 'x64': return 'x86_64-pc-windows-gnu'; default: throw new Error( `Unsupported architecture on Windows: ${process.arch}`, ); } case 'darwin': switch (process.arch) { case 'x64': return 'x86_64-apple-darwin'; case 'arm64': return 'aarch64-apple-darwin'; default: throw new Error(`Unsupported architecture on macOS: ${process.arch}`); } case 'linux': switch (process.arch) { case 'x64': if (isMusl()) { return 'x86_64-unknown-linux-musl'; } return 'x86_64-unknown-linux-gnu'; case 'arm64': if (isMusl()) { return 'aarch64-unknown-linux-musl'; } return 'aarch64-unknown-linux-gnu'; default: throw new Error(`Unsupported architecture on Linux: ${process.arch}`); } default: throw new Error( `Unsupported OS: ${process.platform}, architecture: ${process.arch}`, ); } }; const hasCargo = () => { try { execSync(`${where} cargo`); return true; } catch (err) { return false; } }; const debug = process.argv.includes('--debug'); const mode = debug ? 'debug' : 'release'; const copyDestinations = { 'aarch64-unknown-linux-gnu': { from: `target/aarch64-unknown-linux-gnu/${mode}/remotion`, to: '../compositor-linux-arm64-gnu/remotion', dir: '../compositor-linux-arm64-gnu', }, 'aarch64-unknown-linux-musl': { from: `target/aarch64-unknown-linux-musl/${mode}/remotion`, to: '../compositor-linux-arm64-musl/remotion', dir: '../compositor-linux-arm64-musl', }, 'x86_64-unknown-linux-gnu': { from: `target/x86_64-unknown-linux-gnu/${mode}/remotion`, to: '../compositor-linux-x64-gnu/remotion', dir: '../compositor-linux-x64-gnu', }, 'x86_64-unknown-linux-musl': { from: `target/x86_64-unknown-linux-musl/${mode}/remotion`, to: '../compositor-linux-x64-musl/remotion', dir: '../compositor-linux-x64-musl', }, 'x86_64-apple-darwin': { from: `target/x86_64-apple-darwin/${mode}/remotion`, to: '../compositor-darwin-x64/remotion', dir: '../compositor-darwin-x64', }, 'aarch64-apple-darwin': { from: `target/aarch64-apple-darwin/${mode}/remotion`, to: '../compositor-darwin-arm64/remotion', dir: '../compositor-darwin-arm64', }, 'x86_64-pc-windows-gnu': { from: `target/x86_64-pc-windows-gnu/${mode}/remotion.exe`, to: '../compositor-win32-x64-msvc/remotion.exe', dir: '../compositor-win32-x64-msvc', }, }; if (!hasCargo()) { console.log('Environment has no cargo. Skipping Rust builds.'); process.exit(0); } const nativeArch = getTarget(); const all = process.argv.includes('--all'); const cloudrun = process.argv.includes('--cloudrun'); const lambda = process.argv.includes('--lambda'); if (!existsSync('toolchains') && all) { throw new Error( 'Run "bun install-toolchain.ts" if you want to build all platforms', ); } for (const toolchain of toolchains) { if (!existsSync(path.join('toolchains', toolchain)) && all) { throw new Error( `Toolchain for ${toolchain} not found. Run "node install-toolchain.mjs" if you want to build all platforms`, ); } } const stdout = execSync('cargo metadata --format-version=1'); const {packages} = JSON.parse(stdout as unknown as string); const rustFfmpegSys = packages.find((p) => p.name === 'ffmpeg-sys-next'); if (!rustFfmpegSys) { console.error( 'Could not find ffmpeg-sys-next when running cargo metadata --format-version=1', ); process.exit(1); } const manifest = rustFfmpegSys.manifest_path; const binariesDirectory = path.join(path.dirname(manifest), 'zips'); const archs = all ? targets : lambda ? ['aarch64-unknown-linux-gnu'] : cloudrun ? ['x86_64-unknown-linux-gnu'] : [nativeArch]; for (const arch of archs) { const ffmpegFolder = path.join(copyDestinations[arch].dir, 'ffmpeg'); if (existsSync(ffmpegFolder)) { rmSync(ffmpegFolder, {recursive: true}); } mkdirSync(ffmpegFolder); // strip-components: extract in a flat folder structure execSync( `tar xf ${binariesDirectory}/${arch}.gz -C ${ffmpegFolder} --strip-components 2`, { stdio: 'inherit', }, ); const filesInFfmpegFolder = readdirSync(ffmpegFolder); const filesToDelete = filesInFfmpegFolder.filter((file) => { return ( file.endsWith('.h') || file.endsWith('.a') || file.endsWith('.la') || file.endsWith('.hpp') || statSync(path.join(ffmpegFolder, file)).isDirectory() ); }); for (const file of filesToDelete) { rmSync(path.join(ffmpegFolder, file), {recursive: true}); } const filesInFfmpegFolder2 = readdirSync(ffmpegFolder); for (const file of filesInFfmpegFolder2) { if (file === 'ffmpeg') { renameSync( path.join(ffmpegFolder, file), path.join(ffmpegFolder, '..', 'ffmpeg_'), ); continue; } renameSync( path.join(ffmpegFolder, file), path.join(ffmpegFolder, '..', file), ); } rmdirSync(path.join(ffmpegFolder, '..', 'ffmpeg')); if (existsSync(path.join(ffmpegFolder, '..', 'bin'))) { rmSync(path.join(ffmpegFolder, '..', 'bin'), {recursive: true}); } if (existsSync(path.join(ffmpegFolder, '..', 'ffmpeg_'))) { renameSync( path.join(ffmpegFolder, '..', 'ffmpeg_'), path.join(ffmpegFolder, '..', 'ffmpeg'), ); } const command = `cargo build ${debug ? '' : '--release'} --target=${arch}`; console.log(command); // debuginfo will keep symbols, which are used for backtrace. // symbols makes it a tiny bit smaller, but error messages will be hard to debug. const rPathOrigin = arch.includes('linux') ? `-C link-args=-Wl,-rpath,'$ORIGIN'` : ''; const macOSHeaderPad = arch.includes('darwin') ? `-C link-args=-Wl,-headerpad_max_install_names` : ''; const optimizations = all ? `-C opt-level=3 -C lto=fat -C strip=debuginfo -C embed-bitcode=yes ${rPathOrigin} ${macOSHeaderPad}` : macOSHeaderPad; execSync(command, { stdio: 'inherit', env: { ...process.env, RUSTFLAGS: optimizations, CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: nativeArch === 'aarch64-unknown-linux-gnu' ? undefined : 'toolchains/aarch64_gnu_toolchain/bin/aarch64-unknown-linux-gnu-gcc', CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: nativeArch === 'aarch64-unknown-linux-musl' ? undefined : 'toolchains/aarch64_musl_toolchain/bin/aarch64-unknown-linux-musl-gcc', CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER: nativeArch === 'x86_64-unknown-linux-gnu' ? undefined : path.join( process.cwd(), 'toolchains/x86_64_gnu_toolchain/bin/x86_64-unknown-linux-gnu-gcc', ), CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: nativeArch === 'x86_64-unknown-linux-musl' ? undefined : 'toolchains/x86_64_musl_toolchain/bin/x86_64-unknown-linux-musl-gcc', }, }); const copyInstructions = copyDestinations[arch]; copyFileSync(copyInstructions.from, copyInstructions.to); const output = execSync('npm pack --json', { cwd: copyDestinations[arch].dir, stdio: 'pipe', }); const filename = JSON.parse(output.toString('utf-8'))[0].filename.replace( /^@remotion\//, 'remotion-', ); const tgzPath = path.join( process.cwd(), copyDestinations[arch].dir, filename, ); if (arch.includes('linux')) { execSync( `patchelf --force-rpath --set-rpath '$ORIGIN' ${copyDestinations[arch].dir}/remotion`, ); } const filesize = lstatSync(tgzPath).size; console.log('Zipped size:', (filesize / 1000000).toFixed(2) + 'MB'); unlinkSync(tgzPath); }