also looking at this
fix(dev): validate the pinned Zig toolchain before builds
#299
5 files
+137
-14
| @@ -0,0 +1,61 @@ | ||
| 1 | import { readFileSync } from 'node:fs' | |
| 2 | import { join, resolve } from 'node:path' | |
| 3 | ||
| 4 | export function pinnedZigVersion(root: string): string { | |
| 5 | const lock = JSON.parse(readFileSync(join(root, 'pantry.lock'), 'utf8')) | |
| 6 | const version = lock.workspaces?.['']?.system?.['ziglang.org'] | |
| 7 | if (typeof version !== 'string' || !/^\d+\.\d+\.\d+(?:-dev\.\d+_[a-f0-9]+)?$/.test(version)) | |
| 8 | throw new Error('pantry.lock must contain an exact Zig version in the root workspace system dependencies') | |
| 9 | return version.replace('_', '+') | |
| 10 | } | |
| 11 | ||
| 12 | export function resolveZig(root: string, env: NodeJS.ProcessEnv = process.env): { path: string, version: string, overridden: boolean } { | |
| 13 | const expected = pinnedZigVersion(root) | |
| 14 | const executable = env.CRAFT_ZIG || 'zig' | |
| 15 | const path = Bun.which(executable, { PATH: env.PATH, cwd: root }) | |
| 16 | const remedy = 'Activate the pinned Pantry toolchain, or set CRAFT_ZIG to its executable. To intentionally test a different snapshot, also set CRAFT_ALLOW_UNPINNED_ZIG=1.' | |
| 17 | if (!path) | |
| 18 | throw new Error(`Zig ${expected} was not found (${executable}). ${remedy}`) | |
| 19 | const result = Bun.spawnSync([path, 'version'], { cwd: root, env, stdout: 'pipe', stderr: 'pipe' }) | |
| 20 | if (result.exitCode !== 0) | |
| 21 | throw new Error(`Cannot read Zig version from ${path}: ${result.stderr.toString().trim()}. ${remedy}`) | |
| 22 | const version = result.stdout.toString().trim() | |
| 23 | if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.+-]+)?$/.test(version)) | |
| 24 | throw new Error(`Invalid Zig version from ${path}: ${JSON.stringify(version)}`) | |
| 25 | const overridden = version !== expected | |
| 26 | if (overridden && env.CRAFT_ALLOW_UNPINNED_ZIG !== '1') | |
| 27 | throw new Error(`Zig version mismatch: ${path} reports ${version}; pantry.lock requires ${expected}. ${remedy}`) | |
| 28 | return { path, version, overridden } | |
| 29 | } | |
| 30 | ||
| 31 | export function runZig(root: string, args: string[], env: NodeJS.ProcessEnv = process.env): number { | |
| 32 | const command = [...args] | |
| 33 | const core = command[0] === '--core' | |
| 34 | if (core) command.shift() | |
| 35 | const versioned = command[0] === '--versioned' | |
| 36 | if (versioned) command.shift() | |
| 37 | if (versioned) { | |
| 38 | const { version } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) | |
| 39 | command.push(`-Dversion=${version}`) | |
| 40 | } | |
| 41 | const zig = resolveZig(root, env) | |
| 42 | console.error(`Zig ${zig.version}: ${zig.path}${zig.overridden ? ' (explicit unpinned override)' : ''}`) | |
| 43 | const result = Bun.spawnSync([zig.path, ...command], { | |
| 44 | cwd: core ? join(root, 'packages/zig') : root, | |
| 45 | env, | |
| 46 | stdin: 'inherit', | |
| 47 | stdout: 'inherit', | |
| 48 | stderr: 'inherit', | |
| 49 | }) | |
| 50 | return result.exitCode | |
| 51 | } | |
| 52 | ||
| 53 | if (import.meta.main) { | |
| 54 | try { | |
| 55 | process.exitCode = runZig(resolve(import.meta.dir, '..'), process.argv.slice(2)) | |
| 56 | } | |
| 57 | catch (error) { | |
| 58 | console.error(error instanceof Error ? error.message : String(error)) | |
| 59 | process.exitCode = 1 | |
| 60 | } | |
| 61 | } | |