mirror of
https://github.com/aphrody-labs/bun-distribution.git
synced 2026-10-14 07:59:17 +00:00
161 lines
6.7 KiB
TypeScript
161 lines
6.7 KiB
TypeScript
#!/usr/bin/env bun
|
|
/** Inspect immutable Git manifests before migrating flat crate ownership. */
|
|
import { dirname, resolve } from "node:path";
|
|
import { runProcess } from "@aphrody/bun-workspace/process";
|
|
|
|
type Manifest = {
|
|
package?: { name?: string; version?: string | { workspace: boolean } };
|
|
workspace?: { dependencies?: Record<string, unknown>; members?: string[]; exclude?: string[] };
|
|
dependencies?: Record<string, unknown>;
|
|
"dev-dependencies"?: Record<string, unknown>;
|
|
"build-dependencies"?: Record<string, unknown>;
|
|
target?: Record<string, Manifest>;
|
|
};
|
|
const root = resolve(import.meta.dir, "..");
|
|
if (process.argv.slice(2).some(value => value === "--help" || value === "-h")) {
|
|
console.log(
|
|
"Usage: bun scripts/crate-inventory.ts [owner=checkout ...]\nInspect pinned crate manifests; write .local/crate-inventory.json without changing sources.",
|
|
);
|
|
process.exit(0);
|
|
}
|
|
const extraSources = process.argv.slice(2).map(value => {
|
|
const separator = value.indexOf("=");
|
|
if (separator < 1) throw new Error("Source argument must be owner=checkout");
|
|
return { owner: value.slice(0, separator), path: resolve(root, value.slice(separator + 1)) };
|
|
});
|
|
async function git(path: string, args: string[]) {
|
|
const result = await runProcess(["git", ...args], path, {
|
|
timeoutMs: 30_000,
|
|
maxOutputBytes: 8 * 1024 * 1024,
|
|
});
|
|
if (result.code !== 0) throw new Error(`Git source inventory failed: ${args[0]}`);
|
|
return result.stdout.trim();
|
|
}
|
|
const gitlinks = (await git(root, ["ls-files", "--stage", "-z"]))
|
|
.split("\0")
|
|
.filter(line => line.startsWith("160000 "))
|
|
.map(line => line.slice(line.indexOf("\t") + 1))
|
|
.filter(path => path.startsWith("libraries/") || path.startsWith("crates/") || path === "toolchains/bun")
|
|
.map(path => ({ owner: path, path: resolve(root, path) }));
|
|
const sources = [...gitlinks, ...extraSources];
|
|
if (new Set(sources.map(source => source.owner)).size !== sources.length) throw new Error("Duplicate source owner");
|
|
const crates: {
|
|
owner: string;
|
|
revision: string;
|
|
manifest: string;
|
|
name: string;
|
|
destination: string;
|
|
dependencies: string[];
|
|
migrated: boolean;
|
|
declaredWorkspaceMember: boolean;
|
|
}[] = [];
|
|
const unavailable: string[] = [];
|
|
const sourcePins: { owner: string; revision: string; manifests: number }[] = [];
|
|
for (const source of sources) {
|
|
if (
|
|
!(await Bun.file(resolve(source.path, ".git")).exists()) &&
|
|
!(await Bun.file(resolve(source.path, ".git/HEAD")).exists())
|
|
) {
|
|
unavailable.push(source.owner);
|
|
continue;
|
|
}
|
|
const revision = await git(source.path, ["rev-parse", "HEAD"]);
|
|
const paths = (await git(source.path, ["ls-tree", "-r", "--name-only", revision]))
|
|
.split("\n")
|
|
.filter(path => path === "Cargo.toml" || path.endsWith("/Cargo.toml"));
|
|
const manifests = new Map<string, Manifest>();
|
|
const objects = await runProcess(["git", "cat-file", "--batch"], source.path, {
|
|
timeoutMs: 30_000,
|
|
maxOutputBytes: 8 * 1024 * 1024,
|
|
input: new TextEncoder().encode(paths.map(path => `${revision}:${path}\n`).join("")),
|
|
});
|
|
if (objects.code !== 0) throw new Error("Immutable manifest batch failed");
|
|
const bytes = new TextEncoder().encode(objects.stdout);
|
|
let offset = 0;
|
|
for (const path of paths) {
|
|
const end = bytes.indexOf(10, offset);
|
|
const header = new TextDecoder().decode(bytes.subarray(offset, end));
|
|
const match = /^[a-f0-9]+ blob (\d+)$/.exec(header);
|
|
if (!match) throw new Error("Invalid Git manifest object");
|
|
const size = Number(match[1]);
|
|
offset = end + 1;
|
|
manifests.set(path, Bun.TOML.parse(new TextDecoder().decode(bytes.subarray(offset, offset + size))) as Manifest);
|
|
offset += size + 1;
|
|
}
|
|
const workspace = manifests.get("Cargo.toml")?.workspace;
|
|
sourcePins.push({ owner: source.owner, revision, manifests: paths.length });
|
|
for (const [path, manifest] of manifests) {
|
|
const name = manifest.package?.name;
|
|
if (!name) continue;
|
|
let directory = dirname(path).replaceAll("\\", "/");
|
|
let inherited: Record<string, unknown> = {};
|
|
while (true) {
|
|
const parent = manifests.get(directory === "." ? "Cargo.toml" : `${directory}/Cargo.toml`);
|
|
if (parent?.workspace) {
|
|
inherited = parent.workspace.dependencies ?? {};
|
|
break;
|
|
}
|
|
if (directory === ".") break;
|
|
directory = dirname(directory).replaceAll("\\", "/");
|
|
}
|
|
const names = new Set<string>();
|
|
for (const table of [manifest, ...Object.values(manifest.target ?? {})]) {
|
|
for (const kind of ["dependencies", "dev-dependencies", "build-dependencies"] as const) {
|
|
for (const [alias, value] of Object.entries(table[kind] ?? {})) {
|
|
let dependency = value;
|
|
if (typeof value === "object" && value !== null && "workspace" in value)
|
|
dependency = inherited[alias] ?? value;
|
|
const packageName =
|
|
typeof dependency === "object" && dependency !== null && "package" in dependency
|
|
? String(dependency.package)
|
|
: alias;
|
|
names.add(packageName);
|
|
}
|
|
}
|
|
}
|
|
crates.push({
|
|
owner: source.owner,
|
|
revision,
|
|
manifest: path,
|
|
name,
|
|
destination: `crates/${name}`,
|
|
dependencies: [...names].sort(),
|
|
migrated: source.owner === `crates/${name}` && path === "Cargo.toml",
|
|
declaredWorkspaceMember:
|
|
path === "Cargo.toml" ||
|
|
Boolean(
|
|
workspace?.members?.some(pattern =>
|
|
new Bun.Glob(pattern.replace(/\/$/, "")).match(dirname(path).replaceAll("\\", "/")),
|
|
) &&
|
|
!workspace.exclude?.some(pattern =>
|
|
new Bun.Glob(pattern.replace(/\/$/, "")).match(dirname(path).replaceAll("\\", "/")),
|
|
),
|
|
),
|
|
});
|
|
}
|
|
}
|
|
const ownersByName = new Map<string, string[]>();
|
|
for (const crate of crates) ownersByName.set(crate.name, [...(ownersByName.get(crate.name) ?? []), crate.owner]);
|
|
const collisions = [...ownersByName]
|
|
.filter(([, owners]) => owners.length > 1)
|
|
.map(([name, owners]) => ({ name, owners }));
|
|
const report = {
|
|
schemaVersion: 1,
|
|
generatedAt: new Date().toISOString(),
|
|
rootRevision: await git(root, ["rev-parse", "HEAD"]),
|
|
sourcePins,
|
|
counts: {
|
|
sources: sourcePins.length,
|
|
crates: crates.length,
|
|
migrated: crates.filter(crate => crate.migrated).length,
|
|
declaredWorkspaceCrates: crates.filter(crate => crate.declaredWorkspaceMember).length,
|
|
remainingDeclared: crates.filter(crate => crate.declaredWorkspaceMember && !crate.migrated).length,
|
|
},
|
|
unavailable,
|
|
collisions,
|
|
crates,
|
|
};
|
|
const output = resolve(root, ".local/crate-inventory.json");
|
|
await Bun.write(output, JSON.stringify(report, null, 2) + "\n");
|
|
console.log(JSON.stringify({ ...report.counts, collisions: collisions.length, unavailable, report: output }, null, 2));
|