implement package instances

a way to to specifically allow a particular dependency to create
multiple instances of packages by supplying unique sets of options.
This commit is contained in:
Andrew Kelley committed 2026-10-27 18:39:57 -07:00
1 parent 5179c695c0
commit 0aec429522
2 files changed
+62 -17

No files matched your search

+31 -3
View File
@@ -208,6 +208,10 @@ pub const JobQueue = struct {
/// Identifies paths that override all packages in the tree with matching
/// project ids.
fork_set: ForkSet = .empty,
/// Set of package paths into the dependency tree that are exempt from the rule that prevents
/// packages from being instantiated with multiple different sets of options. Package names in
/// the path are separated by '.'.
instances: InstanceSet = .empty,
pub const Mode = enum {
/// Non-lazy dependencies are always fetched.
@@ -220,6 +224,8 @@ pub const JobQueue = struct {
pub const UnlazySet = std.array_hash_map.Auto(Package.Hash, void);
pub const ForkSet = std.array_hash_map.Custom(Fork, void, Fork.Context, false);
pub const InstanceSet = std.array_hash_map.String(void);
pub const Fork = struct {
path: Path,
manifest_ast: std.zig.Ast,
@@ -257,6 +263,7 @@ pub const JobQueue = struct {
if (jq.all_fetches.items.len == 0) return;
const gpa = jq.all_fetches.items[0].arena.child_allocator;
jq.table.deinit(gpa);
jq.instances.deinit(gpa);
// These must be deinitialized in reverse order because subsequent
// `Fetch` instances are allocated in prior ones' arenas.
// Sorry, I know it's a bit weird, but it slightly simplifies the
@@ -405,6 +412,10 @@ pub const JobQueue = struct {
.{ std.zig.fmtString(name), std.zig.fmtString(h.toSlice()) },
);
}
try w.writeAll("};\n\npub const instances: []const []const u8 = &.{\n");
for (jq.instances.keys()) |pkg_path| {
try w.print(" {q},\n", .{pkg_path});
}
try w.writeAll("};\n");
}
@@ -413,6 +424,7 @@ pub const JobQueue = struct {
\\pub const Dep = struct { []const u8, []const u8 };
\\pub const packages = struct {};
\\pub const root_deps: []const Dep = &.{};
\\pub const instances: []const []const u8 = &.{};
\\
);
}
@@ -916,7 +928,7 @@ pub fn computedPackageHash(f: *const Fetch) Package.Hash {
if (f.parentDep()) |dep| {
if (dep.version_tok != .none) {
const v = std.mem.print(&version_buffer, "{f}", .{dep.version}) catch unreachable;
return .init(f.computed_hash.digest, f.name, v, no_fingerprint, saturated_size);
return .init(f.computed_hash.digest, f.depName(), v, no_fingerprint, saturated_size);
}
}
return .init(f.computed_hash.digest, "N", "V", no_fingerprint, saturated_size);
@@ -1029,6 +1041,17 @@ fn queueJobsForDeps(f: *Fetch) RunError!void {
for (dep_names, deps, 0..) |dep_name, dep, dep_index_usize| {
const dep_index: u32 = @intCast(dep_index_usize);
const full_dep_name = if (f.name.len == 0)
dep_name
else
try std.mem.join(parent_arena, ".", &.{ f.name, dep_name });
try f.job_queue.instances.ensureUnusedCapacity(gpa, dep.instances.count());
for (dep.instances.keys()) |pkg_path| {
const full_pkg_path = try std.mem.join(parent_arena, ".", &.{ full_dep_name, pkg_path });
f.job_queue.instances.putAssumeCapacity(full_pkg_path, {});
}
var unversioned = dep.unversioned;
const new_fetch = &new_fetches[new_fetch_index];
const location: Location = switch (dep.location) {
@@ -1073,7 +1096,7 @@ fn queueJobsForDeps(f: *Fetch) RunError!void {
break :l .{ .relative_path = new_root };
},
};
prog_names[new_fetch_index] = dep_name;
prog_names[new_fetch_index] = full_dep_name;
new_fetch_index += 1;
f.job_queue.all_fetches.appendAssumeCapacity(new_fetch);
new_fetch.* = .{
@@ -1084,7 +1107,7 @@ fn queueJobsForDeps(f: *Fetch) RunError!void {
.selected_projects = .empty,
.arena = std.heap.ArenaAllocator.init(gpa),
.location = location,
.name = dep_name,
.name = full_dep_name,
.lazy_status = switch (f.job_queue.mode) {
.needed => if (dep.lazy) .available else .eager,
.all => .eager,
@@ -2179,6 +2202,11 @@ fn hashTok(f: *const Fetch) ?std.zig.Ast.TokenIndex {
return dep.hash_tok.unwrap();
}
fn depName(f: *const Fetch) []const u8 {
const start = std.mem.findLastScalar(u8, f.name, '.') orelse 0;
return f.name[start..];
}
// Detects executable header: ELF or Macho-O magic header or shebang line.
const FileHeader = struct {
header: [4]u8 = undefined,
+31 -14
View File
@@ -472,7 +472,7 @@ pub fn create(
fn createChild(
parent: *Build,
dep_name: []const u8,
dep_prefix: []const u8,
root: Cache.Path,
pkg_hash: []const u8,
pkg_deps: AvailableDeps,
@@ -505,7 +505,7 @@ fn createChild(
.default_step = undefined,
.top_level_steps = .{},
.debug_log_scopes = parent.debug_log_scopes,
.dep_prefix = parent.fmt("{s}{s}.", .{ parent.dep_prefix, dep_name }),
.dep_prefix = dep_prefix,
.modules = .empty,
.named_writefiles = .empty,
.named_lazy_paths = .empty,
@@ -2123,15 +2123,15 @@ pub const PackageEntry = struct {
};
/// Build system implementation detail.
pub const package_map: std.StaticStringMap(PackageEntry) = blk: {
pub const package_map: std.StaticStringMap(PackageEntry) = map: {
const deps = @import("root").dependencies;
const decl_names = @typeInfo(deps.packages).@"struct".decl_names;
var kvs: [decl_names.len]struct { []const u8, PackageEntry } = undefined;
@setEvalBranchQuota(decl_names.len + 100);
for (decl_names, 0..) |decl_name, i| {
@setEvalBranchQuota(decl_names.len + 1);
for (decl_names, &kvs) |decl_name, *kv| {
const pkg = @field(deps.packages, decl_name);
const available = !@hasDecl(pkg, "available") or pkg.available;
kvs[i] = .{ decl_name, .{
kv.* = .{ decl_name, .{
.hash = decl_name,
.available = available,
.build_root = if (available) pkg.build_root else "",
@@ -2146,7 +2146,16 @@ pub const package_map: std.StaticStringMap(PackageEntry) = blk: {
} };
}
const frozen = kvs;
break :blk .initComptime(&frozen);
break :map .initComptime(&frozen);
};
pub const instance_map: std.StaticStringMap(void) = map: {
const instances = @import("root").dependencies.instances;
var kvs: [instances.len]struct { []const u8 } = undefined;
@setEvalBranchQuota(instances.len + 1);
for (&kvs, instances) |*kv, pkg_path| kv.* = .{pkg_path};
const frozen = kvs;
break :map .initComptime(&frozen);
};
/// Declares that the current configuration does in fact require a potentially
@@ -2259,13 +2268,21 @@ fn dependencyResolved(
.unversioned = entry.unversioned,
.options = package_options,
};
const child_dep_prefix = std.fmt.allocPrint(arena, "{s}{s}.", .{ b.dep_prefix, name }) catch @panic("OOM");
const dedup = if (graph.deduplicated_dependencies.getEntry(package_instance)) |kv| dedup: {
const dep_dependency = kv.value_ptr.*;
const dep_pkg_hash = kv.key_ptr.pkg_hash;
const dep_options = kv.key_ptr.options;
const old_dep_prefix = dep_dependency.builder.dep_prefix[0 .. dep_dependency.builder.dep_prefix.len - 1];
const new_dep_prefix = child_dep_prefix[0 .. child_dep_prefix.len - 1];
if (kv.key_ptr.unversioned or entry.unversioned) {
log.debug("checking {q} and {q}", .{ old_dep_prefix, new_dep_prefix });
if (kv.key_ptr.unversioned or entry.unversioned or
instance_map.has(old_dep_prefix) or
instance_map.has(new_dep_prefix))
{
// Opting out of version conflict resolution. Use the other map instead. We only
// bother to populate the other map if this one doesn't contain it however.
if (mem.eql(u8, dep_pkg_hash, entry.hash) and PackageOptions.eql(dep_options, package_options)) {
@@ -2277,16 +2294,15 @@ fn dependencyResolved(
break :dedup false;
}
const dep_prefix = dep_dependency.builder.dep_prefix[0 .. dep_dependency.builder.dep_prefix.len - 1];
if (!mem.eql(u8, dep_pkg_hash, entry.hash)) {
log.err("package version conflict: {s} ({s})", .{ dep_pkg_hash, dep_prefix });
log.err("package version conflict: {s} ({s}{s})", .{ entry.hash, b.dep_prefix, name });
log.err("package version conflict: {s} ({s})", .{ dep_pkg_hash, old_dep_prefix });
log.err("package version conflict: {s} ({s})", .{ entry.hash, new_dep_prefix });
std.debug.dumpCurrentStackTrace(.{ .first_address = first_return_address });
process.exit(1);
}
if (!PackageOptions.eql(dep_options, package_options)) {
log.err("package {s} ({s} vs {s}{s}) reinstantiated with non-matching options", .{
dep_pkg_hash, dep_prefix, b.dep_prefix, name,
log.err("package {s} ({s} vs {s}) reinstantiated with non-matching options", .{
dep_pkg_hash, old_dep_prefix, new_dep_prefix,
});
PackageOptions.logDifferences(dep_options, package_options);
std.debug.dumpCurrentStackTrace(.{ .first_address = first_return_address });
@@ -2303,7 +2319,8 @@ fn dependencyResolved(
},
};
const sub_builder = b.createChild(name, dep_root, entry.hash, entry.deps, package_options.*) catch @panic("OOM");
const sub_builder = b.createChild(child_dep_prefix, dep_root, entry.hash, entry.deps, package_options.*) catch
@panic("OOM");
if (entry.run_build) |run_build| {
run_build(sub_builder);