#!/usr/bin/env bun import { $ } from "bun"; import * as path from "node:path"; const RUST_AFFECTING_FILE_NAMES = [ "Cargo.toml", "Cargo.lock", "build.rs", "rust-toolchain", "rust-toolchain.toml", "clippy.toml", ".clippy.toml", "rustfmt.toml", ".rustfmt.toml", ] as const satisfies readonly string[]; const TASK_COMMANDS = { "check:rs": [ ["cargo", "fmt", "--all", "--", "--check"], ["cargo", "clippy", "--workspace", "--", "-D", "warnings"], ], "fix:rs": [ ["cargo", "fmt", "--all"], [ "cargo", "clippy", "--workspace", "--fix", "--allow-dirty", "--no-deps", "--allow-staged", "--allow-no-vcs", ], ], "fmt:rs": [["cargo", "fmt", "--all"]], "lint:rs": [["cargo", "clippy", "--workspace", "--", "-D", "warnings"]], "test:rs": [["cargo", "nextest", "run", "--workspace", "--status-level=fail", "--final-status-level=fail"]], } as const satisfies Record; type RustTaskName = keyof typeof TASK_COMMANDS; const repoRoot = path.join(import.meta.dir, ".."); const taskName = process.argv[2]; if (!isRustTaskName(taskName)) { console.error(`Unknown Rust task: ${taskName ?? "(missing)"}`); process.exit(1); } if (!(isCI() || (await hasRustAffectingChanges()))) { console.log(`Skipping ${taskName} (not in CI and no Rust-affecting changes were found).`); process.exit(0); } for (const command of TASK_COMMANDS[taskName]) { const exitCode = await runCommand(command); if (exitCode !== 0) { process.exit(exitCode); } } function isRustTaskName(value: string | undefined): value is RustTaskName { return value != null && value in TASK_COMMANDS; } function isCI(): boolean { const value = Bun.env.CI; if (!value) return false; const normalized = value.trim().toLowerCase(); return normalized !== "" && normalized !== "0" && normalized !== "false"; } async function hasRustAffectingChanges(): Promise { const result = await $`git status --porcelain -z`.cwd(repoRoot).quiet().nothrow(); if (result.exitCode !== 0) { const stderr = result.stderr.toString().trim(); const suffix = stderr === "" ? `exit ${result.exitCode}` : stderr; console.warn(`Warning: failed to inspect git status: ${suffix}. Running ${taskName} conservatively.`); return true; } return getChangedPathsFromPorcelain(result.stdout).some(isRustAffectingPath); } function getChangedPathsFromPorcelain(buf: Uint8Array): string[] { const entries = new TextDecoder().decode(buf).split("\0").filter(Boolean); const changedPaths: string[] = []; for (let index = 0; index < entries.length; index += 1) { const entry = entries[index]; if (entry.length < 4) continue; const status = entry.slice(0, 2); const changedPath = entry.slice(3); if (changedPath !== "") { changedPaths.push(changedPath); } if (status.includes("R") || status.includes("C")) { const renamedPath = entries[index + 1]; if (renamedPath) { changedPaths.push(renamedPath); index += 1; } } } return changedPaths; } function isRustAffectingPath(changedPath: string): boolean { const normalized = changedPath.replace(/\\/g, "/"); const fileName = normalized.slice(normalized.lastIndexOf("/") + 1); return ( normalized.endsWith(".rs") || normalized.startsWith(".cargo/") || isOneOf(fileName, RUST_AFFECTING_FILE_NAMES) ); } function isOneOf(value: string, values: readonly T[]): value is T { return values.some(entry => entry === value); } async function runCommand(command: readonly string[]): Promise { const proc = Bun.spawn([...command], { cwd: repoRoot, stdin: "inherit", stdout: "inherit", stderr: "inherit", }); return proc.exited; }