Files
vercel__next.js/scripts/install-native.mjs
Sebastian "Sebbie" Silbermann 69acc33f03 [ci] Make automatic native binding install opt-in (#95114)
A `pnpm install` installs native bindings for Next.js by default. This
makes sense for local development where half of the team doesn't work on
native code and just needs the most recent bindings. However, CI almost
exclusively wants to test with bindings that are built from the current
commit. The rest doesn't need native bindings at all.

The only exception is `test_examples` which is likely not functional.
Though we still support it by allowing to opt-into automatically
installing native bindings in CI.

## test plan

- [x] fetch test timings still skips:
https://github.com/vercel/next.js/actions/runs/28087707205/job/83157705446?pr=95114#step:7:228
2026-06-24 22:39:36 +02:00

135 lines
4.5 KiB
JavaScript

import os from 'os'
import path from 'path'
import execa from 'execa'
import fs from 'fs'
import fsp from 'fs/promises'
import { outdent } from 'outdent'
;(async function () {
// Automatically installing native bindings is opt-in in CI and opt-out in local development.
// Most CI jobs use native bindings built from the commit they run on anyway.
const rawSkip = process.env.NEXT_SKIP_NATIVE_POSTINSTALL
const skipNativePostinstall =
rawSkip == null || rawSkip === ''
? process.env.CI === 'true'
: rawSkip !== '0' && rawSkip !== 'false'
if (skipNativePostinstall) {
console.log(
`Skipping next-swc postinstall (NEXT_SKIP_NATIVE_POSTINSTALL=${String(JSON.stringify(rawSkip))}, CI=${String(JSON.stringify(process.env.CI))})`
)
return
}
const preferOffline = process.env.NEXT_TEST_PREFER_OFFLINE === '1'
let cwd = process.cwd()
const { version: nextVersion } = JSON.parse(
fs.readFileSync(path.join(cwd, 'packages', 'next', 'package.json'))
)
const { packageManager } = JSON.parse(
fs.readFileSync(path.join(cwd, 'package.json'))
)
try {
// if installed swc package version matches monorepo version
// we can skip re-installing
for (const pkg of fs.readdirSync(path.join(cwd, 'node_modules', '@next'))) {
if (
pkg.startsWith('swc-') &&
JSON.parse(
fs.readFileSync(
path.join(cwd, 'node_modules', '@next', pkg, 'package.json')
)
).version === nextVersion
) {
console.log(`@next/${pkg}@${nextVersion} already installed, skipping`)
return
}
}
} catch {}
try {
let tmpdir = path.join(os.tmpdir(), `next-swc-${Date.now()}`)
fs.mkdirSync(tmpdir, { recursive: true })
let pkgJson = {
name: 'dummy-package',
version: '1.0.0',
optionalDependencies: {
'@next/swc-darwin-arm64': nextVersion,
'@next/swc-darwin-x64': nextVersion,
'@next/swc-linux-arm64-gnu': nextVersion,
'@next/swc-linux-arm64-musl': nextVersion,
'@next/swc-linux-x64-gnu': nextVersion,
'@next/swc-linux-x64-musl': nextVersion,
'@next/swc-win32-arm64-msvc': nextVersion,
'@next/swc-win32-x64-msvc': nextVersion,
},
packageManager,
}
fs.writeFileSync(path.join(tmpdir, 'package.json'), JSON.stringify(pkgJson))
fs.writeFileSync(
path.join(tmpdir, 'pnpm-workspace.yaml'),
'' +
outdent`
nodeLinker: hoisted
` +
'\n' +
// Propagate security related settings from file://./../../pnpm-workspace.yaml
outdent`
blockExoticSubdeps: true
minimumReleaseAge: 2880 # 48 hrs
minimumReleaseAgeExclude:
- '@next/*'
- '@turbo/*'
- '@vercel/*'
- '@workflow/*'
- babel-plugin-react-compiler
- next
- react
- react-dom
- react-is
- react-server-dom-*
- scheduler
- turbo
`
)
const args = ['install', '--lockfile=false', '--ignore-scripts']
if (preferOffline) {
args.push('--prefer-offline')
}
await execa('pnpm', args, { cwd: tmpdir })
/** @type {string[]} */
let pkgs
try {
pkgs = fs.readdirSync(path.join(tmpdir, 'node_modules/@next'), {})
} catch (error) {
throw new Error(
'No binary candidate found.\n' +
`This environment is not supported by Next.js or publish of ${nextVersion} is incomplete.\n` +
'If binaries are built from source, set `NEXT_SKIP_NATIVE_POSTINSTALL=1`.',
{ cause: error }
)
}
fs.mkdirSync(path.join(cwd, 'node_modules/@next'), { recursive: true })
await Promise.all(
pkgs.map(async (pkg) => {
const from = path.join(tmpdir, 'node_modules/@next', pkg)
const to = path.join(cwd, 'node_modules/@next', pkg)
// The directory from pnpm store is a symlink, which can not be overwritten,
// so we remove the existing directory before copying
await fsp.rm(to, { recursive: true, force: true })
// Renaming is flaky on Windows, and the tmpdir is going to be deleted anyway,
// so we use copy the directory instead
return fsp.cp(from, to, { force: true, recursive: true })
})
)
fs.rmSync(tmpdir, { recursive: true, force: true })
console.log('Installed the following binary packages:', pkgs)
} catch (e) {
throw new Error('Failed to install @next/swc binary packages', { cause: e })
}
})()