/** * stage-libsignal.ts — put the libsignal JNI native (linux-aarch64) into the * apt-repo staging pool, building it if we don't already have it. * * WHY THIS IS BUILT ON THE DEV MACHINE AND NOT IN THE E2E NETWORK * * The signal module needs a native that upstream does not publish for * linux-aarch64 (see modules/signal/build/). Compiling it takes minutes, * pulls hundreds of crates and a BoringSSL submodule, and needs crates.io and * GitHub — none of which exist inside the sealed e2e network, and all of which * would blow a 5-minute test budget. * * So the compile happens HERE, at `cele2e build-infra` time, on the host's own * network, using the module's own recipe. The deploy then just installs a * .deb, which is exactly what it does in production. The e2e network stays * sealed and the deploy test stays fast, while the recipe is still exercised * for real on every rebuild rather than rotting untested. * * Cached by version: the compile only re-runs when the version changes or the * cached .deb is deleted. * * Graceful no-op when Docker is unavailable or the build fails — build-infra * still completes, and the signal deploy test is what fails loudly (with an * empty repo) rather than breaking the build for everyone. Mirrors how * stageAptRepo treats a missing nfpm. */ import { spawnSync } from 'node:child_process'; import { copyFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; const green = '\x1b[32m'; const yellow = '\x1b[33m'; const dim = '\x1b[2m'; const reset = '\x1b[0m'; /** * Must match `modules/signal/manifest.yml`'s `libsignal_deb_url` default and * `modules/signal/build/build.sh`. The build script verifies the pairing * against the real signal-cli release, so a mismatch here fails the build * loudly rather than producing a native that silently will not load. */ const LIBSIGNAL_VERSION = '0.96.3'; const DEB_NAME = `libsignal-jni_${LIBSIGNAL_VERSION}_arm64.deb`; export function stageLibsignal(repoRoot: string, pkgDir: string): boolean { const buildDir = join(repoRoot, 'modules', 'signal', 'build'); if (!existsSync(buildDir)) return false; // Only aarch64 needs this native at all — an x86_64 host uses the one already // bundled in the libsignal-client JAR. Building it there would mean an // emulated compile of something nothing would install. if (process.arch !== 'arm64') { console.log( ` ${'libsignal-jni'.padEnd(28)} ${dim}skip (host is ${process.arch}; JAR ships that native)${reset}`, ); return true; } const pool = join(pkgDir, '.apt-repo-cache', 'pool'); const cache = join(pkgDir, '.libsignal-cache'); mkdirSync(pool, { recursive: true }); mkdirSync(cache, { recursive: true }); const cached = join(cache, DEB_NAME); if (!existsSync(cached)) { process.stdout.write(` ${'libsignal-jni (build)'.padEnd(28)} `); const t0 = Date.now(); const image = 'celilo-e2e/libsignal-builder'; const build = spawnSync( 'docker', ['build', '--platform', 'linux/arm64', '-q', '-t', image, buildDir], { stdio: 'pipe' }, ); if (build.status !== 0) { console.log(`${yellow}skip${reset} ${dim}(docker build failed)${reset}`); return false; } // Named volumes keep the cargo registry and the checkout across runs, so a // version bump is a few minutes rather than a fresh fetch of everything. const run = spawnSync( 'docker', [ 'run', '--rm', '--platform', 'linux/arm64', '-v', `${cache}:/out`, '-v', 'celilo-libsignal-work:/work', '-v', 'celilo-libsignal-cargo:/root/.cargo/registry', image, ], { stdio: 'pipe' }, ); if (run.status !== 0 || !existsSync(cached)) { console.log(`${yellow}skip${reset} ${dim}(build failed)${reset}`); return false; } console.log(`${green}✔${reset} ${dim}${Math.round((Date.now() - t0) / 1000)}s${reset}`); } process.stdout.write(` ${'libsignal-jni (stage)'.padEnd(28)} `); for (const file of readdirSync(cache).filter((f) => f.endsWith('.deb'))) { copyFileSync(join(cache, file), join(pool, file)); } console.log(`${green}✔${reset} ${dim}${DEB_NAME}${reset}`); return true; }