next.js/scripts/docker-native-build.js
docker-native-build.js212 lines5.5 KB
#!/usr/bin/env node
// Local wrapper for running native docker builds.
//
// Usage: node scripts/docker-native-build.js [flags] [filter]
//   --quick        Use release-with-assertions profile (no LTO, faster)
//   --host-target  Share host target/ dir with container for caching
//   --rebuild      Force Docker image rebuild
//   --test         Smoke-test built binaries (native arch only)
//   filter         Substring match on target name (e.g. "musl", "x86_64")

const { execFileSync } = require('child_process')
const path = require('path')
const fs = require('fs')
const os = require('os')

const REPO_ROOT = path.resolve(__dirname, '..')
const DOCKER_IMAGE = 'next-swc-builder:latest'

const TARGETS = [
  {
    target: 'x86_64-unknown-linux-gnu',
    arch: 'x86_64',
    abi: 'gnu',
    napiPlatform: 'linux-x64-gnu',
  },
  {
    target: 'aarch64-unknown-linux-gnu',
    arch: 'aarch64',
    abi: 'gnu',
    napiPlatform: 'linux-arm64-gnu',
  },
  {
    target: 'x86_64-unknown-linux-musl',
    arch: 'x86_64',
    abi: 'musl',
    napiPlatform: 'linux-x64-musl',
  },
  {
    target: 'aarch64-unknown-linux-musl',
    arch: 'aarch64',
    abi: 'musl',
    napiPlatform: 'linux-arm64-musl',
  },
]

// Map uname -m to our arch names
const HOST_ARCH =
  os.arch() === 'arm64' || os.arch() === 'aarch64' ? 'aarch64' : 'x86_64'

// --- Parse args ---
const { parseArgs } = require('node:util')
const { values: flags, positionals } = parseArgs({
  args: process.argv.slice(2),
  options: {
    quick: { type: 'boolean', default: false },
    'host-target': { type: 'boolean', default: false },
    rebuild: { type: 'boolean', default: false },
    test: { type: 'boolean', default: false },
    help: { type: 'boolean', short: 'h', default: false },
  },
  allowPositionals: true,
})

if (flags.help) {
  console.log(
    'Usage: node scripts/docker-native-build.js [--quick] [--host-target] [--rebuild] [--test] [filter]'
  )
  process.exit(0)
}

const quick = flags.quick
const hostTarget = flags['host-target']
const rebuild = flags.rebuild
const test = flags.test
const filter = positionals[0] || ''

// --- Filter targets ---
let targets = TARGETS
if (filter) {
  targets = TARGETS.filter((t) => t.target.includes(filter))
}
if (targets.length === 0) {
  console.error(`No targets match filter: "${filter}"`)
  console.error('Available:', TARGETS.map((t) => t.target).join(', '))
  process.exit(1)
}

// --- Build/restore Docker image ---
function ensureDockerImage() {
  const args = rebuild ? ['--force'] : []
  execFileSync(
    'node',
    [path.join(__dirname, 'docker-image-cache.js'), ...args],
    { stdio: 'inherit' }
  )
}

ensureDockerImage()

// --- Build targets ---
const buildTask = quick
  ? 'build-native-release-with-assertions'
  : 'build-native-release'

if (quick) {
  console.log(
    'Quick mode: using release-with-assertions profile (no LTO, 64 codegen units)'
  )
}
console.log(
  `Building ${targets.length} target(s): ${targets.map((t) => t.target).join(', ')}\n`
)

const HOME = os.homedir()
const VOLUMES = [
  `${HOME}/.cargo/git:/root/.cargo/git`,
  `${HOME}/.cargo/registry:/root/.cargo/registry`,
  `${REPO_ROOT}:/build`,
]

for (const { target, arch, abi, napiPlatform } of targets) {
  console.log('='.repeat(50))
  console.log(`Building: ${target}`)
  console.log(`Docker:   ${DOCKER_IMAGE}`)
  console.log(`Task:     ${buildTask}`)
  console.log('='.repeat(50))

  // Clean only this target's previous build (preserve other targets' .node files)
  const nativeDir = path.join(REPO_ROOT, 'packages/next-swc/native')
  const nodeFile = path.join(nativeDir, `next-swc.${napiPlatform}.node`)
  if (fs.existsSync(nodeFile)) fs.unlinkSync(nodeFile)

  const ENV = {
    CI: '1',
    RUST_BACKTRACE: '1',
    CARGO_TERM_COLOR: 'always',
    CARGO_INCREMENTAL: '0',
    TARGET: target,
    ABI: abi,
    ARCH: arch,
    BUILD_TASK: buildTask,
  }

  const dockerArgs = [
    'run',
    '--rm',
    ...Object.entries(ENV).flatMap(([k, v]) => ['-e', `${k}=${v}`]),
    ...VOLUMES.flatMap((v) => ['-v', v]),
    ...(hostTarget ? [] : ['-v', '/build/target']),
    '-w',
    '/build',
    '--entrypoint',
    'bash',
    DOCKER_IMAGE,
    '-xeo',
    'pipefail',
    'scripts/docker-native-build.sh',
  ]

  execFileSync('docker', dockerArgs, { stdio: 'inherit' })

  console.log(`\nSuccessfully built: ${target}\n`)
}

// --- Smoke test ---
if (test) {
  console.log('='.repeat(50))
  console.log('Running smoke tests...')
  console.log('='.repeat(50))

  for (const { target, arch, abi, napiPlatform } of targets) {
    // Skip cross-built binaries (would need qemu)
    if (arch !== HOST_ARCH) {
      console.log(`Skipping smoke test for ${target} (cross-built, needs qemu)`)
      continue
    }

    const testImage = abi === 'musl' ? 'node:20-alpine' : 'node:20-slim'
    const nodeFile = `./packages/next-swc/native/next-swc.${napiPlatform}.node`

    console.log(`Testing ${target} in ${testImage}...`)

    const testScript = [
      `const b = require('${nodeFile}')`,
      `const t = b.getTargetTriple()`,
      `console.log('OK: getTargetTriple() =', t)`,
      `if (!t.includes('linux')) { console.error('FAIL: expected linux in triple'); process.exit(1) }`,
    ].join('; ')

    execFileSync(
      'docker',
      [
        'run',
        '--rm',
        '-v',
        `${REPO_ROOT}:/work`,
        '-w',
        '/work',
        testImage,
        'node',
        '-e',
        testScript,
      ],
      { stdio: 'inherit' }
    )

    console.log(`Smoke test passed: ${target}\n`)
  }
}

console.log('All targets built successfully!')
Quest for Codev2.0.0
/
SIGN IN