mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
feat(zig): resolve path deps through the dep's build.zig and src/root.zig
The bare-name resolver only knew `src/<name>.zig` and `src/main.zig`.
`zig init` has written `src/root.zig` for libraries since 0.12, so the
default library layout never resolved. Now: the root the dep's own
build.zig declares (`b.addModule("<name>", .{ .root_source_file =
b.path("…") })`, name-matched module first), then src/root.zig,
src/<name>.zig, src/main.zig. `normalizeZigDepPath` is shared by the
loader and the resolver.
This commit is contained in:
parent
307f7054de
commit
69831484ef
3 changed files with 203 additions and 33 deletions
|
|
@ -14,11 +14,12 @@
|
|||
* Bare-name (build.zig.zon) resolution is handled when a parsed
|
||||
* ZigBuildZonConfig is supplied (see `loadZigBuildZon`). `.url`-based deps
|
||||
* unpack into a build cache outside the repo and so are returned as null;
|
||||
* `.path`-based deps are resolved through the conventional
|
||||
* `<dep_root>/src/<name>.zig` or `<dep_root>/src/main.zig` layout.
|
||||
* `.path`-based deps are resolved through the root the dep's own build.zig
|
||||
* declares, then the conventional `<dep_root>/src/root.zig`,
|
||||
* `<dep_root>/src/<name>.zig`, `<dep_root>/src/main.zig` layouts.
|
||||
*/
|
||||
|
||||
import type { ZigBuildZonConfig } from '../language-config.js';
|
||||
import { normalizeZigDepPath, type ZigBuildZonConfig } from '../language-config.js';
|
||||
|
||||
const ZIG_STDLIB_NAMES = new Set(['std', 'builtin', 'root']);
|
||||
|
||||
|
|
@ -76,14 +77,22 @@ export function resolveZigImportInternal(
|
|||
if (buildZon) {
|
||||
const depPath = buildZon.pathDeps.get(importPath);
|
||||
if (depPath) {
|
||||
const normalized = normalizeDepPath(depPath);
|
||||
const normalized = normalizeZigDepPath(depPath);
|
||||
if (normalized !== null) {
|
||||
// Conventional Zig layout: <pkg_root>/src/<name>.zig (matches the
|
||||
// package's primary module name) or <pkg_root>/src/main.zig. A dep at
|
||||
// `.path = "."` (the repo itself) normalizes to '' and must not grow a
|
||||
// leading slash — `allFiles` keys are repo-relative.
|
||||
// What the dep's own build.zig declares comes first (its
|
||||
// `addModule` root_source_file — see `parseZigBuildModuleRoots`), then
|
||||
// the conventional layouts: `src/root.zig` (the `zig init` library
|
||||
// root since 0.12), `src/<name>.zig` (older name-matched convention),
|
||||
// `src/main.zig` (executables / older inits). A dep at `.path = "."`
|
||||
// (the repo itself) normalizes to '' and must not grow a leading slash
|
||||
// — `allFiles` keys are repo-relative.
|
||||
const prefix = normalized === '' ? '' : `${normalized}/`;
|
||||
const candidates = [`${prefix}src/${importPath}.zig`, `${prefix}src/main.zig`];
|
||||
const candidates = [
|
||||
...(buildZon.moduleRoots?.get(importPath) ?? []),
|
||||
`${prefix}src/root.zig`,
|
||||
`${prefix}src/${importPath}.zig`,
|
||||
`${prefix}src/main.zig`,
|
||||
];
|
||||
for (const c of candidates) {
|
||||
if (allFiles.has(c)) return c;
|
||||
}
|
||||
|
|
@ -95,24 +104,3 @@ export function resolveZigImportInternal(
|
|||
// unconventional layout).
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a `.path` value from build.zig.zon into a repo-relative form.
|
||||
* Returns null for paths that escape the repo root (start with `..`) or
|
||||
* are absolute — those point to files we don't index in `allFilePaths`.
|
||||
* `.` / `./` normalize to the empty string (the repo root itself).
|
||||
*/
|
||||
function normalizeDepPath(depPath: string): string | null {
|
||||
if (depPath.startsWith('/')) return null;
|
||||
const parts: string[] = [];
|
||||
for (const part of depPath.split('/')) {
|
||||
if (part === '' || part === '.') continue;
|
||||
if (part === '..') {
|
||||
if (parts.length === 0) return null;
|
||||
parts.pop();
|
||||
} else {
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,12 +93,22 @@ export interface ZigBuildZonConfig {
|
|||
* Map of dependency name -> the raw `.path = "..."` value, exactly as
|
||||
* written in build.zig.zon (relative to the repo root, and possibly
|
||||
* escaping it: `../local_dep`). Consumers normalize — see
|
||||
* `normalizeDepPath` in `import-resolvers/zig.ts`, which rejects absolute
|
||||
* `normalizeZigDepPath` below, which rejects absolute
|
||||
* and repo-escaping values. `.url`-based deps cannot be resolved to a
|
||||
* repo-local file (they unpack into a build cache outside the repo) and so
|
||||
* are not included here.
|
||||
*/
|
||||
pathDeps: Map<string, string>;
|
||||
/**
|
||||
* Per path-dep: repo-relative root source files the dep's own `build.zig`
|
||||
* declares (`b.addModule("name", .{ .root_source_file = b.path("src/x.zig")
|
||||
* })`), keyed by dep name, in file order. Entries whose module name matches
|
||||
* the dep name come first — that is the module a consumer's
|
||||
* `@import("<dep>")` maps to under the ecosystem convention that the zon key
|
||||
* and the module name agree. Absent (or empty) when the dep has no readable
|
||||
* `build.zig`; the resolver then falls back to the conventional layouts.
|
||||
*/
|
||||
moduleRoots?: Map<string, readonly string[]>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -511,13 +521,92 @@ export async function loadSwiftPackageConfig(repoRoot: string): Promise<SwiftPac
|
|||
* or string cannot declare a dep or truncate the block.
|
||||
*/
|
||||
export async function loadZigBuildZon(repoRoot: string): Promise<ZigBuildZonConfig | null> {
|
||||
let config: ZigBuildZonConfig | null;
|
||||
try {
|
||||
const zonPath = path.join(repoRoot, 'build.zig.zon');
|
||||
const raw = await fs.readFile(zonPath, 'utf-8');
|
||||
return parseZigBuildZon(raw);
|
||||
config = parseZigBuildZon(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (config === null) return null;
|
||||
|
||||
// A path dep's importable root is whatever ITS build.zig declares, not a
|
||||
// fixed layout: read `root_source_file` per `addModule` and remember it
|
||||
// repo-relative. Best effort — an unreadable build.zig just leaves the
|
||||
// conventional-layout fallback in place.
|
||||
const moduleRoots = new Map<string, readonly string[]>();
|
||||
for (const [depName, depPath] of config.pathDeps) {
|
||||
const rel = normalizeZigDepPath(depPath);
|
||||
if (rel === null) continue;
|
||||
let buildZig: string;
|
||||
try {
|
||||
buildZig = await fs.readFile(path.join(repoRoot, rel, 'build.zig'), 'utf-8');
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const roots = parseZigBuildModuleRoots(buildZig, depName).map((r) =>
|
||||
rel === '' ? r : `${rel}/${r}`,
|
||||
);
|
||||
if (roots.length > 0) moduleRoots.set(depName, roots);
|
||||
}
|
||||
return moduleRoots.size > 0 ? { ...config, moduleRoots } : config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a `.path` value from build.zig.zon into a repo-relative form.
|
||||
* Returns null for paths that escape the repo root (start with `..`) or
|
||||
* are absolute — those point to files we don't index. `.` / `./` normalize
|
||||
* to the empty string (the repo root itself). Shared with the import
|
||||
* resolver so both sides agree on which deps are in-repo.
|
||||
*/
|
||||
export function normalizeZigDepPath(depPath: string): string | null {
|
||||
if (depPath.startsWith('/')) return null;
|
||||
const parts: string[] = [];
|
||||
for (const part of depPath.replace(/\\/g, '/').split('/')) {
|
||||
if (part === '' || part === '.') continue;
|
||||
if (part === '..') {
|
||||
if (parts.length === 0) return null;
|
||||
parts.pop();
|
||||
} else {
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* The `root_source_file` paths a `build.zig` declares, dep-relative, with the
|
||||
* module whose `addModule("<name>", …)` name equals `preferredName` first.
|
||||
*
|
||||
* Reads two shapes, which between them cover `zig init` output and the
|
||||
* common hand-written build scripts:
|
||||
* - `b.addModule("name", .{ .root_source_file = b.path("src/root.zig") })`
|
||||
* - any other `.root_source_file = b.path("…")` (exe/lib/test artifacts),
|
||||
* kept as unnamed fallbacks in file order.
|
||||
* A `.zig` under `b.path` is required — `.{ .cwd_relative = … }` and
|
||||
* `LazyPath` values computed at build time are not resolvable statically and
|
||||
* are skipped. Duplicates collapse to the first occurrence.
|
||||
*/
|
||||
export function parseZigBuildModuleRoots(buildZig: string, preferredName: string): string[] {
|
||||
const named: string[] = [];
|
||||
const unnamed: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const add = (into: string[], p: string): void => {
|
||||
const norm = normalizeZigDepPath(p);
|
||||
if (norm === null || norm === '' || !norm.endsWith('.zig') || seen.has(norm)) return;
|
||||
seen.add(norm);
|
||||
into.push(norm);
|
||||
};
|
||||
const namedRe =
|
||||
/addModule\(\s*"([^"\n]+)"\s*,\s*\.\{[^}]*?\.root_source_file\s*=\s*b\.path\(\s*"([^"\n]+)"\s*\)/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = namedRe.exec(buildZig)) !== null) {
|
||||
if (m[1] === preferredName) add(named, m[2]!);
|
||||
}
|
||||
const anyRe = /\.root_source_file\s*=\s*b\.path\(\s*"([^"\n]+)"\s*\)/g;
|
||||
while ((m = anyRe.exec(buildZig)) !== null) add(unnamed, m[1]!);
|
||||
return [...named, ...unnamed];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,8 +3,19 @@
|
|||
* imports and bare-name imports resolved through build.zig.zon.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { resolveZigImportInternal } from '../../src/core/ingestion/import-resolvers/zig.js';
|
||||
import { parseZigBuildZon } from '../../src/core/ingestion/language-config.js';
|
||||
import {
|
||||
loadZigBuildZon,
|
||||
parseZigBuildModuleRoots,
|
||||
parseZigBuildZon,
|
||||
} from '../../src/core/ingestion/language-config.js';
|
||||
|
||||
const FIXTURES = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'../fixtures/lang-resolution',
|
||||
);
|
||||
|
||||
describe('resolveZigImportInternal', () => {
|
||||
it('returns null for stdlib / builtin / root', () => {
|
||||
|
|
@ -68,6 +79,35 @@ describe('resolveZigImportInternal', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('resolves a bare name to `<root>/src/root.zig` — the `zig init` library root since 0.12', () => {
|
||||
// 0.12+ `zig init` writes src/root.zig for libraries (0.14 writes both
|
||||
// root.zig and main.zig). Knowing only src/<name>.zig and src/main.zig
|
||||
// left every such dep unresolved.
|
||||
const files = new Set<string>(['src/main.zig', 'libs/geo/src/root.zig']);
|
||||
const zon = { pathDeps: new Map([['geo', 'libs/geo']]) };
|
||||
expect(resolveZigImportInternal('src/main.zig', 'geo', files, zon)).toBe(
|
||||
'libs/geo/src/root.zig',
|
||||
);
|
||||
});
|
||||
|
||||
it('prefers the root the dep’s own build.zig declares over every conventional layout', () => {
|
||||
// A dep can call its module root anything (`lib/geo.zig`); when its
|
||||
// build.zig says so, that beats src/root.zig even if both exist.
|
||||
const files = new Set<string>([
|
||||
'src/main.zig',
|
||||
'libs/geo/lib/geo.zig',
|
||||
'libs/geo/src/root.zig',
|
||||
'libs/geo/src/main.zig',
|
||||
]);
|
||||
const zon = {
|
||||
pathDeps: new Map([['geo', 'libs/geo']]),
|
||||
moduleRoots: new Map([['geo', ['libs/geo/lib/geo.zig']]]),
|
||||
};
|
||||
expect(resolveZigImportInternal('src/main.zig', 'geo', files, zon)).toBe(
|
||||
'libs/geo/lib/geo.zig',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to `<root>/src/main.zig` when no `<name>.zig` exists', () => {
|
||||
const files = new Set<string>(['src/main.zig', 'vendor/ziggit/src/main.zig']);
|
||||
const buildZon = { pathDeps: new Map([['ziggit', 'vendor/ziggit']]) };
|
||||
|
|
@ -246,3 +286,56 @@ describe('parseZigBuildZon', () => {
|
|||
expect(parseZigBuildZon(raw)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseZigBuildModuleRoots', () => {
|
||||
it('reads `addModule("<name>", .{ .root_source_file = b.path("…") })`, preferring the named module', () => {
|
||||
const buildZig = `
|
||||
const std = @import("std");
|
||||
pub fn build(b: *std.Build) void {
|
||||
const lib = b.addStaticLibrary(.{ .name = "geo", .root_source_file = b.path("src/lib_entry.zig") });
|
||||
_ = b.addModule("helpers", .{ .root_source_file = b.path("src/helpers.zig") });
|
||||
_ = b.addModule("geo", .{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = b.standardTargetOptions(.{}),
|
||||
});
|
||||
b.installArtifact(lib);
|
||||
}
|
||||
`;
|
||||
// The module named like the dep comes first; the others stay as ordered
|
||||
// fallbacks (an importer's `@import("geo")` maps to the "geo" module).
|
||||
expect(parseZigBuildModuleRoots(buildZig, 'geo')).toEqual([
|
||||
'src/root.zig',
|
||||
'src/lib_entry.zig',
|
||||
'src/helpers.zig',
|
||||
]);
|
||||
});
|
||||
|
||||
it('skips roots that are not a static `b.path("….zig")` and normalizes `./`', () => {
|
||||
const buildZig = `
|
||||
_ = b.addModule("x", .{ .root_source_file = .{ .cwd_relative = "/abs/x.zig" } });
|
||||
_ = b.addModule("y", .{ .root_source_file = b.path("./src/y.zig") });
|
||||
_ = b.addModule("z", .{ .root_source_file = generated.getPath() });
|
||||
_ = b.addModule("w", .{ .root_source_file = b.path("../outside.zig") });
|
||||
`;
|
||||
expect(parseZigBuildModuleRoots(buildZig, 'y')).toEqual(['src/y.zig']);
|
||||
});
|
||||
|
||||
it('returns [] for a build.zig that declares no module root', () => {
|
||||
expect(parseZigBuildModuleRoots('pub fn build(b: *std.Build) void { _ = b; }', 'x')).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadZigBuildZon (zig-idioms fixture)', () => {
|
||||
it('reads each path dep’s build.zig for its module roots and leaves deps without one to the layout fallback', async () => {
|
||||
const config = await loadZigBuildZon(path.join(FIXTURES, 'zig-idioms'));
|
||||
expect(config).not.toBeNull();
|
||||
expect([...config!.pathDeps.keys()].sort()).toEqual(['geo', 'oldlib']);
|
||||
// geo/build.zig: addModule("geo", .{ .root_source_file = b.path("src/root.zig") })
|
||||
expect(config!.moduleRoots?.get('geo')).toEqual(['libs/geo/src/root.zig']);
|
||||
// oldlib has no build.zig → no entry; the resolver falls back to
|
||||
// src/root.zig → src/oldlib.zig → src/main.zig.
|
||||
expect(config!.moduleRoots?.has('oldlib')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue