mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
## 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+ | ✅ |
96 lines
3.1 KiB
JavaScript
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`)
|
|
}
|
|
}
|