Commit 9959319d53

Matt Knight <mattnite@proton.me>
2023-08-11 00:32:55
Compare user input for multiple dependency build variants (#16600)
1 parent a190582
Changed files (1)
lib
lib/std/Build.zig
@@ -127,7 +127,42 @@ dep_prefix: []const u8 = "",
 modules: std.StringArrayHashMap(*Module),
 /// A map from build root dirs to the corresponding `*Dependency`. This is shared with all child
 /// `Build`s.
-initialized_deps: *std.StringHashMap(*Dependency),
+initialized_deps: *InitializedDepMap,
+
+const InitializedDepMap = std.HashMap(InitializedDepKey, *Dependency, InitializedDepContext, std.hash_map.default_max_load_percentage);
+const InitializedDepKey = struct {
+    build_root_string: []const u8,
+    user_input_options: UserInputOptionsMap,
+};
+
+const InitializedDepContext = struct {
+    allocator: Allocator,
+
+    pub fn hash(self: @This(), k: InitializedDepKey) u64 {
+        var hasher = std.hash.Wyhash.init(0);
+        hasher.update(k.build_root_string);
+        hashUserInputOptionsMap(self.allocator, k.user_input_options, &hasher);
+        return hasher.final();
+    }
+
+    pub fn eql(self: @This(), lhs: InitializedDepKey, rhs: InitializedDepKey) bool {
+        _ = self;
+        if (!std.mem.eql(u8, lhs.build_root_string, rhs.build_root_string))
+            return false;
+
+        if (lhs.user_input_options.count() != rhs.user_input_options.count())
+            return false;
+
+        var it = lhs.user_input_options.iterator();
+        while (it.next()) |lhs_entry| {
+            const rhs_value = rhs.user_input_options.get(lhs_entry.key_ptr.*) orelse return false;
+            if (!userValuesAreSame(lhs_entry.value_ptr.*.value, rhs_value.value))
+                return false;
+        }
+
+        return true;
+    }
+};
 
 pub const ExecError = error{
     ReadFailure,
@@ -213,8 +248,8 @@ pub fn create(
     const env_map = try allocator.create(EnvMap);
     env_map.* = try process.getEnvMap(allocator);
 
-    const initialized_deps = try allocator.create(std.StringHashMap(*Dependency));
-    initialized_deps.* = std.StringHashMap(*Dependency).init(allocator);
+    const initialized_deps = try allocator.create(InitializedDepMap);
+    initialized_deps.* = InitializedDepMap.initContext(allocator, .{ .allocator = allocator });
 
     const self = try allocator.create(Build);
     self.* = .{
@@ -280,14 +315,14 @@ fn createChild(
     parent: *Build,
     dep_name: []const u8,
     build_root: Cache.Directory,
-    args: anytype,
+    user_input_options: UserInputOptionsMap,
 ) !*Build {
-    const child = try createChildOnly(parent, dep_name, build_root);
-    try applyArgs(child, args);
+    const child = try createChildOnly(parent, dep_name, build_root, user_input_options);
+    try determineAndApplyInstallPrefix(child);
     return child;
 }
 
-fn createChildOnly(parent: *Build, dep_name: []const u8, build_root: Cache.Directory) !*Build {
+fn createChildOnly(parent: *Build, dep_name: []const u8, build_root: Cache.Directory, user_input_options: UserInputOptionsMap) !*Build {
     const allocator = parent.allocator;
     const child = try allocator.create(Build);
     child.* = .{
@@ -309,7 +344,7 @@ fn createChildOnly(parent: *Build, dep_name: []const u8, build_root: Cache.Direc
             }),
             .description = "Remove build artifacts from prefix path",
         },
-        .user_input_options = UserInputOptionsMap.init(allocator),
+        .user_input_options = user_input_options,
         .available_options_map = AvailableOptionsMap.init(allocator),
         .available_options_list = ArrayList(AvailableOption).init(allocator),
         .verbose = parent.verbose,
@@ -361,57 +396,152 @@ fn createChildOnly(parent: *Build, dep_name: []const u8, build_root: Cache.Direc
     return child;
 }
 
-fn applyArgs(b: *Build, args: anytype) !void {
+fn userInputOptionsFromArgs(allocator: Allocator, args: anytype) UserInputOptionsMap {
+    var user_input_options = UserInputOptionsMap.init(allocator);
     inline for (@typeInfo(@TypeOf(args)).Struct.fields) |field| {
         const v = @field(args, field.name);
         const T = @TypeOf(v);
         switch (T) {
             CrossTarget => {
-                try b.user_input_options.put(field.name, .{
+                user_input_options.put(field.name, .{
                     .name = field.name,
-                    .value = .{ .scalar = try v.zigTriple(b.allocator) },
+                    .value = .{ .scalar = v.zigTriple(allocator) catch @panic("OOM") },
                     .used = false,
-                });
-                try b.user_input_options.put("cpu", .{
+                }) catch @panic("OOM");
+                user_input_options.put("cpu", .{
                     .name = "cpu",
-                    .value = .{ .scalar = try serializeCpu(b.allocator, v.getCpu()) },
+                    .value = .{ .scalar = serializeCpu(allocator, v.getCpu()) catch unreachable },
                     .used = false,
-                });
+                }) catch @panic("OOM");
             },
             []const u8 => {
-                try b.user_input_options.put(field.name, .{
+                user_input_options.put(field.name, .{
                     .name = field.name,
                     .value = .{ .scalar = v },
                     .used = false,
-                });
+                }) catch @panic("OOM");
             },
             else => switch (@typeInfo(T)) {
                 .Bool => {
-                    try b.user_input_options.put(field.name, .{
+                    user_input_options.put(field.name, .{
                         .name = field.name,
                         .value = .{ .scalar = if (v) "true" else "false" },
                         .used = false,
-                    });
+                    }) catch @panic("OOM");
                 },
                 .Enum, .EnumLiteral => {
-                    try b.user_input_options.put(field.name, .{
+                    user_input_options.put(field.name, .{
                         .name = field.name,
                         .value = .{ .scalar = @tagName(v) },
                         .used = false,
-                    });
+                    }) catch @panic("OOM");
                 },
                 .Int => {
-                    try b.user_input_options.put(field.name, .{
+                    user_input_options.put(field.name, .{
                         .name = field.name,
-                        .value = .{ .scalar = try std.fmt.allocPrint(b.allocator, "{d}", .{v}) },
+                        .value = .{ .scalar = std.fmt.allocPrint(allocator, "{d}", .{v}) catch @panic("OOM") },
                         .used = false,
-                    });
+                    }) catch @panic("OOM");
                 },
                 else => @compileError("option '" ++ field.name ++ "' has unsupported type: " ++ @typeName(T)),
             },
         }
     }
 
+    return user_input_options;
+}
+
+const OrderedUserValue = union(enum) {
+    flag: void,
+    scalar: []const u8,
+    list: ArrayList([]const u8),
+    map: ArrayList(Pair),
+
+    const Pair = struct {
+        name: []const u8,
+        value: OrderedUserValue,
+        fn lessThan(_: void, lhs: Pair, rhs: Pair) bool {
+            return std.ascii.lessThanIgnoreCase(lhs.name, rhs.name);
+        }
+    };
+
+    fn hash(self: OrderedUserValue, hasher: *std.hash.Wyhash) void {
+        switch (self) {
+            .flag => {},
+            .scalar => |scalar| hasher.update(scalar),
+            // lists are already ordered
+            .list => |list| for (list.items) |list_entry|
+                hasher.update(list_entry),
+            .map => |map| for (map.items) |map_entry| {
+                hasher.update(map_entry.name);
+                map_entry.value.hash(hasher);
+            },
+        }
+    }
+
+    fn mapFromUnordered(allocator: Allocator, unordered: std.StringHashMap(*const UserValue)) ArrayList(Pair) {
+        var ordered = ArrayList(Pair).init(allocator);
+        var it = unordered.iterator();
+        while (it.next()) |entry| {
+            ordered.append(.{
+                .name = entry.key_ptr.*,
+                .value = OrderedUserValue.fromUnordered(allocator, entry.value_ptr.*.*),
+            }) catch @panic("OOM");
+        }
+
+        std.mem.sortUnstable(Pair, ordered.items, {}, Pair.lessThan);
+        return ordered;
+    }
+
+    fn fromUnordered(allocator: Allocator, unordered: UserValue) OrderedUserValue {
+        return switch (unordered) {
+            .flag => .{ .flag = {} },
+            .scalar => |scalar| .{ .scalar = scalar },
+            .list => |list| .{ .list = list },
+            .map => |map| .{ .map = OrderedUserValue.mapFromUnordered(allocator, map) },
+        };
+    }
+};
+
+const OrderedUserInputOption = struct {
+    name: []const u8,
+    value: OrderedUserValue,
+    used: bool,
+
+    fn hash(self: OrderedUserInputOption, hasher: *std.hash.Wyhash) void {
+        hasher.update(self.name);
+        self.value.hash(hasher);
+    }
+
+    fn fromUnordered(allocator: Allocator, user_input_option: UserInputOption) OrderedUserInputOption {
+        return OrderedUserInputOption{
+            .name = user_input_option.name,
+            .used = user_input_option.used,
+            .value = OrderedUserValue.fromUnordered(allocator, user_input_option.value),
+        };
+    }
+
+    fn lessThan(_: void, lhs: OrderedUserInputOption, rhs: OrderedUserInputOption) bool {
+        return std.ascii.lessThanIgnoreCase(lhs.name, rhs.name);
+    }
+};
+
+// The hash should be consistent with the same values given a different order.
+// This function takes a user input map, orders it, then hashes the contents.
+fn hashUserInputOptionsMap(allocator: Allocator, user_input_options: UserInputOptionsMap, hasher: *std.hash.Wyhash) void {
+    var ordered = ArrayList(OrderedUserInputOption).init(allocator);
+    var it = user_input_options.iterator();
+    while (it.next()) |entry|
+        ordered.append(OrderedUserInputOption.fromUnordered(allocator, entry.value_ptr.*)) catch @panic("OOM");
+
+    std.mem.sortUnstable(OrderedUserInputOption, ordered.items, {}, OrderedUserInputOption.lessThan);
+
+    // juice it
+    for (ordered.items) |user_option|
+        user_option.hash(hasher);
+}
+
+fn determineAndApplyInstallPrefix(b: *Build) !void {
     // Create an installation directory local to this package. This will be used when
     // dependant packages require a standard prefix, such as include directories for C headers.
     var hash = b.cache.hash;
@@ -419,7 +549,11 @@ fn applyArgs(b: *Build, args: anytype) !void {
     // implementation is modified in a non-backwards-compatible way.
     hash.add(@as(u32, 0xd8cb0055));
     hash.addBytes(b.dep_prefix);
-    // TODO additionally update the hash with `args`.
+
+    var wyhash = std.hash.Wyhash.init(0);
+    hashUserInputOptionsMap(b.allocator, b.user_input_options, &wyhash);
+    hash.add(wyhash.final());
+
     const digest = hash.final();
     const install_prefix = try b.cache_root.join(b.allocator, &.{ "i", &digest });
     b.resolveInstallPrefix(install_prefix, .{});
@@ -1597,6 +1731,53 @@ pub fn anonymousDependency(
     return dependencyInner(b, name, build_root, build_zig, args);
 }
 
+fn userValuesAreSame(lhs: UserValue, rhs: UserValue) bool {
+    switch (lhs) {
+        .flag => {},
+        .scalar => |lhs_scalar| {
+            const rhs_scalar = switch (rhs) {
+                .scalar => |scalar| scalar,
+                else => return false,
+            };
+
+            if (!std.mem.eql(u8, lhs_scalar, rhs_scalar))
+                return false;
+        },
+        .list => |lhs_list| {
+            const rhs_list = switch (rhs) {
+                .list => |list| list,
+                else => return false,
+            };
+
+            if (lhs_list.items.len != rhs_list.items.len)
+                return false;
+
+            for (lhs_list.items, rhs_list.items) |lhs_list_entry, rhs_list_entry| {
+                if (!std.mem.eql(u8, lhs_list_entry, rhs_list_entry))
+                    return false;
+            }
+        },
+        .map => |lhs_map| {
+            const rhs_map = switch (rhs) {
+                .map => |map| map,
+                else => return false,
+            };
+
+            if (lhs_map.count() != rhs_map.count())
+                return false;
+
+            var lhs_it = lhs_map.iterator();
+            while (lhs_it.next()) |lhs_entry| {
+                const rhs_value = rhs_map.get(lhs_entry.key_ptr.*) orelse return false;
+                if (!userValuesAreSame(lhs_entry.value_ptr.*.*, rhs_value.*))
+                    return false;
+            }
+        },
+    }
+
+    return true;
+}
+
 pub fn dependencyInner(
     b: *Build,
     name: []const u8,
@@ -1604,10 +1785,12 @@ pub fn dependencyInner(
     comptime build_zig: type,
     args: anytype,
 ) *Dependency {
-    if (b.initialized_deps.get(build_root_string)) |dep| {
-        // TODO: check args are the same
+    const user_input_options = userInputOptionsFromArgs(b.allocator, args);
+    if (b.initialized_deps.get(.{
+        .build_root_string = build_root_string,
+        .user_input_options = user_input_options,
+    })) |dep|
         return dep;
-    }
 
     const build_root: std.Build.Cache.Directory = .{
         .path = build_root_string,
@@ -1618,7 +1801,7 @@ pub fn dependencyInner(
             process.exit(1);
         },
     };
-    const sub_builder = b.createChild(name, build_root, args) catch @panic("unhandled error");
+    const sub_builder = b.createChild(name, build_root, user_input_options) catch @panic("unhandled error");
     sub_builder.runBuild(build_zig) catch @panic("unhandled error");
 
     if (sub_builder.validateUserInputDidItFail()) {
@@ -1628,8 +1811,10 @@ pub fn dependencyInner(
     const dep = b.allocator.create(Dependency) catch @panic("OOM");
     dep.* = .{ .builder = sub_builder };
 
-    b.initialized_deps.put(build_root_string, dep) catch @panic("OOM");
-
+    b.initialized_deps.put(.{
+        .build_root_string = build_root_string,
+        .user_input_options = user_input_options,
+    }, dep) catch @panic("OOM");
     return dep;
 }