Files
next.js/scripts/docker-image-cache.js
T
Matt MastracciandGitHub b163a8bf66 [turbopack] Rebuild the docker build scripts (#91799)
## Why

The native binary build system uses 3 different napi-rs Docker images
that are opaque, can't be customized, and require runtime toolchain
downloads. Each CI run downloads compilers and sysroots from scratch,
wasting ~5 minutes per build. There's no way to customize the build
environment or share toolchains across the 4 Linux targets.

Some smoke tests are added to ensure that node.js can actually load the
binaries we build - we don't actually perform much beyond that, just
making sure it survives the dynamic linker and correctly exposes the
node APIs.

We also are able to enable `--icf=all`. This appears to (consistently)
chop off about ~1-2% of the binary size.

## What

Replaces the 3 napi-rs Docker images with a single custom
`next-swc-builder` Dockerfile based on Ubuntu 20.04 that can
cross-compile all 4 Linux targets (x86_64/aarch64 × gnu/musl) from
either an x86_64 or aarch64 host. All toolchains are baked into the
image.

Key changes:
- **`docker/native-builder.Dockerfile`**: Ubuntu 20.04 with clang/lld,
GNU cross-sysroots via `crossbuild-essential`, musl sysroots from
musl.cc, Node.js 20, pinned Rust nightly, and `cargo-rustflags` for flag
resolution
- **`scripts/docker-native-build.sh`**: Inner build script that
constructs RUSTFLAGS via `cargo rustflags --config` (merges
`.cargo/config.toml` with cross-compilation overrides), sets up CC/CXX
per target, and runs the napi build
- **`scripts/docker-native-build.js`**: Node.js local build orchestrator
replacing the bash wrapper, with `--quick`, `--rebuild`, `--test` flags
and smoke testing via `getTargetTriple()`
- **`scripts/docker-image-cache.js`**: Turbo-cached Docker image
(save/load with `--load` for cache hit handling)
- **`.cargo/config.toml`**: Added musl `crt-static` flag, removed
cross-compilation flags (now in build script)
- **CI workflows**: Docker image cached via turbo remote cache,
`build-docker-image` turbo task, simplified `build_and_deploy.yml`

## How

All 4 Linux targets use clang as the C compiler + linker driver with
`gnu-lld-cc` flavor (rust-lld does actual linking). Cross-compilation
uses `--target` + `--sysroot`:
- **GNU targets**: Ubuntu multiarch sysroots from
`crossbuild-essential-{amd64,arm64}`
- **Musl targets**: musl.cc cross-toolchain sysroots with GCC crt files
copied into sysroot lib (works around clang 10's `--gcc-toolchain`
limitation)

RUSTFLAGS are resolved inside the container via `cargo rustflags` which
merges `.cargo/config.toml` (base flags like `-Zunstable-options`, musl
`crt-static`) with per-target cross flags passed as `--config` inline
TOML.

The Docker image is cached via turbo's remote cache (~885 MB tar). On
cache hit, `docker load` restores it in seconds. On miss, the full image
build takes ~2 minutes.

### glibc compatibility

Building on Ubuntu 20.04 (glibc 2.31) ensures broad compatibility. The
table below shows the minimum glibc version required by major Linux
distributions ([source](https://repology.org/project/glibc/versions)):

| Distribution | glibc | Compatible? |
|---|---|---|
| Ubuntu 20.04 (Focal) | 2.31 | ✅ baseline |
| Ubuntu 22.04 (Jammy) | 2.35 | ✅ |
| Ubuntu 24.04 (Noble) | 2.39 | ✅ |
| Debian 11 (Bullseye) | 2.31 | ✅ |
| Debian 12 (Bookworm) | 2.36 | ✅ |
| RHEL / AlmaLinux 8 | 2.28 | ❌ EOL May 2029 (security only) |
| RHEL / AlmaLinux 9 | 2.34 | ✅ |
| Amazon Linux 2 | 2.26 | ❌ EOL June 2026 |
| Amazon Linux 2023 | 2.34 | ✅ |
| Fedora 40+ | 2.39+ | ✅ |
| Arch Linux | 2.41+ | ✅ |
2026-03-24 13:07:37 -06:00

96 lines
3.1 KiB
JavaScript

#!/usr/bin/env node
// @ts-check
//
// Build or restore the next-swc-builder Docker image.
//
// This script is both a turbo task AND a post-turbo loader:
//
// 1. `pnpm -F @next/swc build-docker-image` (turbo task):
// - On cache miss: turbo runs this script, which builds the image and saves
// target/docker-image.tar for turbo to cache as output.
// - On cache hit: turbo restores target/docker-image.tar and SKIPS this script.
//
// 2. `node scripts/docker-image-cache.js --load` (post-turbo step):
// - If target/docker-image.tar exists (turbo cache hit), loads it into docker.
// - If the image is already loaded, does nothing.
// - Cleans up the tar after loading.
//
// Usage:
// node scripts/docker-image-cache.js # build image + save tar (turbo task)
// node scripts/docker-image-cache.js --load # load tar into docker if present
// node scripts/docker-image-cache.js --force # always rebuild
const { execSync } = require('child_process')
const path = require('path')
const fs = require('fs')
const os = require('os')
const { parseArgs } = require('node:util')
const { values: flags } = parseArgs({
args: process.argv.slice(2),
options: {
force: { type: 'boolean', default: false },
load: { type: 'boolean', default: false },
},
})
const REPO_ROOT = path.resolve(__dirname, '..')
const IMAGE_NAME = 'next-swc-builder:latest'
const IMAGE_TAR = path.join(REPO_ROOT, 'target/docker-image.tar')
const force = flags.force
const load = flags.load
function imageExists() {
try {
execSync(`docker image inspect ${IMAGE_NAME}`, { stdio: 'ignore' })
return true
} catch {
return false
}
}
function buildImage() {
console.log(`Building Docker image: ${IMAGE_NAME}`)
const ctx = fs.mkdtempSync(path.join(os.tmpdir(), 'next-swc-docker-'))
fs.copyFileSync(
path.join(REPO_ROOT, 'rust-toolchain.toml'),
path.join(ctx, 'rust-toolchain.toml')
)
try {
execSync(
`docker build -t ${IMAGE_NAME} -f ${path.join(REPO_ROOT, 'scripts/native-builder.Dockerfile')} ${ctx}`,
{ stdio: 'inherit' }
)
} finally {
fs.rmSync(ctx, { recursive: true, force: true })
}
}
if (load) {
// Post-turbo step: load the cached tar if present, or build if missing
if (imageExists() && !force) {
console.log('Docker image already loaded')
} else if (fs.existsSync(IMAGE_TAR)) {
console.log('Loading Docker image from turbo cache...')
execSync(`docker load -i ${IMAGE_TAR}`, { stdio: 'inherit' })
fs.unlinkSync(IMAGE_TAR)
console.log('Docker image restored from cache')
} else {
console.log('No cached image — building from scratch')
buildImage()
}
} else {
// Turbo task: build and save tar for caching
if (force && fs.existsSync(IMAGE_TAR)) fs.unlinkSync(IMAGE_TAR)
if (!imageExists() || force) {
buildImage()
}
if (!fs.existsSync(IMAGE_TAR)) {
console.log('Saving Docker image for turbo cache...')
fs.mkdirSync(path.dirname(IMAGE_TAR), { recursive: true })
execSync(`docker save ${IMAGE_NAME} -o ${IMAGE_TAR}`, { stdio: 'inherit' })
const size = fs.statSync(IMAGE_TAR).size
console.log(`Saved: ${(size / 1024 / 1024).toFixed(0)} MB`)
}
}