Files
pnpm/pnpm/scripts/rustfmt.mjs
Zoltan KochanandClaude Opus 5.5 474066e358 fix(ci): build the rustfmt fork with the pinned nightly's rustc (#16825)
The formatter fork is built with `rustup run <nightly> cargo install`,
and that cargo took `rustc` from PATH. Under `pnpm pipeline` the first
rustc on PATH is pnpm's managed stable Rust, which has no rustc-dev
crates, so the build failed with "can't find crate for `rustc_ast`".

CI only builds the fork when its binary cache misses. The cache entry
was evicted on 2026-10-10, and from then on every Rust CI run, main
included, failed the Format job.

RUSTC is now set to the compiler `rustup which` reports for the pinned
toolchain.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:04:24 +02:00

132 lines
5.2 KiB
JavaScript

import { spawnSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath, pathToFileURL } from 'node:url'
const pin = JSON.parse(fs.readFileSync(new URL('./rustfmt.json', import.meta.url), 'utf8'))
const cacheRoot = path.join(os.homedir(), '.cache', 'pnpm', 'rustfmt')
export function installationPath (config, root = cacheRoot) {
if (!/^[a-f0-9]{40}$/.test(config.revision)) {
throw new Error('rustfmt.json must pin a full Git commit SHA')
}
if (!/^nightly-\d{4}-\d{2}-\d{2}$/.test(config.toolchain)) {
throw new Error('rustfmt.json must pin a dated nightly toolchain')
}
return path.join(root, `${process.platform}-${process.arch}`, config.toolchain, config.revision)
}
export function ensureInstallation (config, { root = cacheRoot, run = spawnSync } = {}) {
const destination = installationPath(config, root)
const components = run('rustup', ['component', 'list', '--toolchain', config.toolchain, '--installed'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
})
if (components.error != null) throw components.error
if (components.status !== 0 || !hasRequiredComponents(components.stdout)) {
checkedRun(run, ['toolchain', 'install', config.toolchain, '--profile', 'minimal',
'--component', 'rustc-dev', '--component', 'llvm-tools-preview'])
}
if (isInstalled(destination)) return destination
fs.mkdirSync(path.dirname(destination), { recursive: true })
const staging = fs.mkdtempSync(`${destination}-install-`)
try {
// An external working directory keeps the workspace's Cargo source replacement out of this build.
checkedRun(run, ['run', config.toolchain, 'cargo', 'install', '--git', config.repository,
'--rev', config.revision, '--locked', '--root', staging,
'--bin', 'rustfmt', '--bin', 'cargo-fmt', 'rustfmt-nightly'], {
cwd: staging,
env: buildEnvironment(toolchainRustc(run, config.toolchain)),
})
if (!isInstalled(staging)) throw new Error('The rustfmt build did not install both formatter binaries')
try {
fs.renameSync(staging, destination)
} catch (error) {
// A concurrent invocation may have finished installing the same immutable revision first.
if (!['EEXIST', 'ENOTEMPTY', 'EPERM'].includes(error.code) || !isInstalled(destination)) throw error
}
} finally {
fs.rmSync(staging, { recursive: true, force: true })
}
return destination
}
export function formatterCommand (config, destination, args) {
const raw = args[0] === '--rustfmt'
return {
args: ['run', config.toolchain, binaryPath(destination, raw ? 'rustfmt' : 'cargo-fmt'),
...(raw ? args.slice(1) : ['fmt', ...args])],
env: { ...process.env, RUSTFMT: binaryPath(destination, 'rustfmt') },
}
}
function hasRequiredComponents (output) {
const components = new Set(output.trim().split(/\r?\n/))
return [...components].some(component => {
if (!component.startsWith('cargo-')) return false
const host = component.slice('cargo-'.length)
return ['rustc', 'rustc-dev', 'llvm-tools'].every(name => components.has(`${name}-${host}`))
})
}
function binaryPath (destination, name) {
return path.join(destination, 'bin', `${name}${process.platform === 'win32' ? '.exe' : ''}`)
}
function isInstalled (destination) {
return ['rustfmt', 'cargo-fmt'].every(name => fs.existsSync(binaryPath(destination, name)))
}
// RUSTC is pinned rather than left to PATH: `pnpm pipeline` puts its own
// stable Rust first on PATH, and the fork needs the nightly's rustc-dev crates.
function buildEnvironment (rustc) {
const env = { ...process.env, RUSTC: rustc }
for (const name of ['RUSTC_WRAPPER', 'RUSTC_WORKSPACE_WRAPPER', 'RUSTFLAGS',
'CARGO_ENCODED_RUSTFLAGS', 'CARGO_TARGET_DIR', 'CARGO_BUILD_TARGET']) {
delete env[name]
}
return env
}
function toolchainRustc (run, toolchain) {
const result = run('rustup', ['which', 'rustc', '--toolchain', toolchain], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 2],
})
if (result.error != null) throw result.error
if (result.status !== 0) throw new Error(`rustup which rustc --toolchain ${toolchain} failed (${result.status ?? result.signal})`)
return result.stdout.trim()
}
function checkedRun (run, args, options = {}) {
const result = run('rustup', args, { stdio: ['ignore', 2, 2], ...options })
if (result.error != null) throw result.error
if (result.status !== 0) throw new Error(`rustup ${args.join(' ')} failed (${result.status ?? result.signal})`)
}
function main () {
const args = process.argv.slice(2)
if (args[0] === '--cache-path') {
process.stdout.write(`${installationPath(pin)}\n`)
return
}
const destination = ensureInstallation(pin)
if (args[0] === '--install') return
const command = formatterCommand(pin, destination, args)
const result = spawnSync('rustup', command.args, { env: command.env, stdio: 'inherit' })
if (result.error != null) throw result.error
process.exitCode = result.status ?? 1
}
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
try {
main()
} catch (error) {
process.stderr.write(`${path.basename(fileURLToPath(import.meta.url))}: ${error.message}\n`)
process.exitCode = 1
}
}