Commit 8f867eaf84

Andrew Kelley <andrew@ziglang.org>
2024-02-01 06:02:27
build system: introduce system library integration
* New --help section * Add b.systemLibraryOption * Rework the build runner CLI logic a bit
1 parent f2e249e
Changed files (2)
lib/std/Build.zig
@@ -28,6 +28,9 @@ allocator: Allocator,
 user_input_options: UserInputOptionsMap,
 available_options_map: AvailableOptionsMap,
 available_options_list: ArrayList(AvailableOption),
+/// All Build instances share this hash map.
+system_library_options: *std.StringArrayHashMapUnmanaged(SystemLibraryMode),
+system_package_mode: bool,
 verbose: bool,
 verbose_link: bool,
 verbose_cc: bool,
@@ -100,6 +103,21 @@ available_deps: AvailableDeps,
 
 const AvailableDeps = []const struct { []const u8, []const u8 };
 
+pub const SystemLibraryMode = enum {
+    /// User asked for the library to be disabled.
+    /// The build runner has not confirmed whether the setting is recognized yet.
+    user_disabled,
+    /// User asked for the library to be enabled.
+    /// The build runner has not confirmed whether the setting is recognized yet.
+    user_enabled,
+    /// The build runner has confirmed that this setting is recognized.
+    /// System integration with this library has been resolved to off.
+    declared_disabled,
+    /// The build runner has confirmed that this setting is recognized.
+    /// System integration with this library has been resolved to on.
+    declared_enabled,
+};
+
 const InitializedDepMap = std.HashMap(InitializedDepKey, *Dependency, InitializedDepContext, std.hash_map.default_max_load_percentage);
 const InitializedDepKey = struct {
     build_root_string: []const u8,
@@ -216,6 +234,7 @@ pub fn create(
     host: ResolvedTarget,
     cache: *Cache,
     available_deps: AvailableDeps,
+    system_library_options: *std.StringArrayHashMapUnmanaged(SystemLibraryMode),
 ) !*Build {
     const env_map = try allocator.create(EnvMap);
     env_map.* = try process.getEnvMap(allocator);
@@ -278,6 +297,8 @@ pub fn create(
         .named_writefiles = std.StringArrayHashMap(*Step.WriteFile).init(allocator),
         .initialized_deps = initialized_deps,
         .available_deps = available_deps,
+        .system_library_options = system_library_options,
+        .system_package_mode = false,
     };
     try self.top_level_steps.put(allocator, self.install_tls.step.name, &self.install_tls);
     try self.top_level_steps.put(allocator, self.uninstall_tls.step.name, &self.uninstall_tls);
@@ -297,7 +318,13 @@ fn createChild(
     return child;
 }
 
-fn createChildOnly(parent: *Build, dep_name: []const u8, build_root: Cache.Directory, pkg_deps: AvailableDeps, user_input_options: UserInputOptionsMap) !*Build {
+fn createChildOnly(
+    parent: *Build,
+    dep_name: []const u8,
+    build_root: Cache.Directory,
+    pkg_deps: AvailableDeps,
+    user_input_options: UserInputOptionsMap,
+) !*Build {
     const allocator = parent.allocator;
     const child = try allocator.create(Build);
     child.* = .{
@@ -366,6 +393,8 @@ fn createChildOnly(parent: *Build, dep_name: []const u8, build_root: Cache.Direc
         .named_writefiles = std.StringArrayHashMap(*Step.WriteFile).init(allocator),
         .initialized_deps = parent.initialized_deps,
         .available_deps = pkg_deps,
+        .system_library_options = parent.system_library_options,
+        .system_package_mode = parent.system_package_mode,
     };
     try child.top_level_steps.put(allocator, child.install_tls.step.name, &child.install_tls);
     try child.top_level_steps.put(allocator, child.uninstall_tls.step.name, &child.uninstall_tls);
@@ -1367,7 +1396,7 @@ pub fn addUserInputOption(self: *Build, name_raw: []const u8, value_raw: []const
             });
         },
         .flag => {
-            log.warn("Option '-D{s}={s}' conflicts with flag '-D{s}'.", .{ name, value, name });
+            log.warn("option '-D{s}={s}' conflicts with flag '-D{s}'.", .{ name, value, name });
             return true;
         },
         .map => |*map| {
@@ -1427,17 +1456,17 @@ fn markInvalidUserInput(self: *Build) void {
     self.invalid_user_input = true;
 }
 
-pub fn validateUserInputDidItFail(self: *Build) bool {
-    // make sure all args are used
-    var it = self.user_input_options.iterator();
+pub fn validateUserInputDidItFail(b: *Build) bool {
+    // Make sure all args are used.
+    var it = b.user_input_options.iterator();
     while (it.next()) |entry| {
         if (!entry.value_ptr.used) {
-            log.err("Invalid option: -D{s}", .{entry.key_ptr.*});
-            self.markInvalidUserInput();
+            log.err("invalid option: -D{s}", .{entry.key_ptr.*});
+            b.markInvalidUserInput();
         }
     }
 
-    return self.invalid_user_input;
+    return b.invalid_user_input;
 }
 
 fn allocPrintCmd(ally: Allocator, opt_cwd: ?[]const u8, argv: []const []const u8) ![]u8 {
@@ -2296,6 +2325,31 @@ pub fn wantSharedLibSymLinks(target: Target) bool {
     return target.os.tag != .windows;
 }
 
+pub fn systemLibraryOption(b: *Build, name: []const u8) bool {
+    const gop = b.system_library_options.getOrPut(b.allocator, name) catch @panic("OOM");
+    if (gop.found_existing) switch (gop.value_ptr.*) {
+        .user_disabled => {
+            gop.value_ptr.* = .declared_disabled;
+            return false;
+        },
+        .user_enabled => {
+            gop.value_ptr.* = .declared_enabled;
+            return true;
+        },
+        .declared_disabled => return false,
+        .declared_enabled => return true,
+    } else {
+        gop.key_ptr.* = b.dupe(name);
+        if (b.system_package_mode) {
+            gop.value_ptr.* = .declared_enabled;
+            return true;
+        } else {
+            gop.value_ptr.* = .declared_disabled;
+            return false;
+        }
+    }
+}
+
 test {
     _ = Cache;
     _ = Step;
lib/build_runner.zig
@@ -76,6 +76,8 @@ pub fn main() !void {
     cache.addPrefix(global_cache_directory);
     cache.hash.addBytes(builtin.zig_version_string);
 
+    var system_library_options: std.StringArrayHashMapUnmanaged(std.Build.SystemLibraryMode) = .{};
+
     const builder = try std.Build.create(
         arena,
         zig_exe,
@@ -85,8 +87,8 @@ pub fn main() !void {
         host,
         &cache,
         dependencies.root_deps,
+        &system_library_options,
     );
-    defer builder.destroy();
 
     var targets = ArrayList([]const u8).init(arena);
     var debug_log_scopes = ArrayList([]const u8).init(arena);
@@ -100,64 +102,50 @@ pub fn main() !void {
     var color: Color = .auto;
     var seed: u32 = 0;
     var prominent_compile_errors: bool = false;
+    var help_menu: bool = false;
+    var steps_menu: bool = false;
 
-    const stderr_stream = io.getStdErr().writer();
     const stdout_stream = io.getStdOut().writer();
 
     while (nextArg(args, &arg_idx)) |arg| {
         if (mem.startsWith(u8, arg, "-D")) {
             const option_contents = arg[2..];
-            if (option_contents.len == 0) {
-                std.debug.print("Expected option name after '-D'\n\n", .{});
-                usageAndErr(builder, false, stderr_stream);
-            }
+            if (option_contents.len == 0)
+                fatalWithHint("expected option name after '-D'", .{});
             if (mem.indexOfScalar(u8, option_contents, '=')) |name_end| {
                 const option_name = option_contents[0..name_end];
                 const option_value = option_contents[name_end + 1 ..];
                 if (try builder.addUserInputOption(option_name, option_value))
-                    usageAndErr(builder, false, stderr_stream);
+                    fatal("  access the help menu with 'zig build -h'", .{});
             } else {
                 if (try builder.addUserInputFlag(option_contents))
-                    usageAndErr(builder, false, stderr_stream);
+                    fatal("  access the help menu with 'zig build -h'", .{});
             }
         } else if (mem.startsWith(u8, arg, "-")) {
             if (mem.eql(u8, arg, "--verbose")) {
                 builder.verbose = true;
             } else if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
-                return usage(builder, false, stdout_stream);
+                help_menu = true;
             } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--prefix")) {
-                install_prefix = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected argument after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
+                install_prefix = nextArgOrFatal(args, &arg_idx);
             } else if (mem.eql(u8, arg, "-l") or mem.eql(u8, arg, "--list-steps")) {
-                return steps(builder, false, stdout_stream);
+                steps_menu = true;
+            } else if (mem.eql(u8, arg, "--system-lib")) {
+                const name = nextArgOrFatal(args, &arg_idx);
+                builder.system_library_options.put(arena, name, .user_enabled) catch @panic("OOM");
+            } else if (mem.eql(u8, arg, "--no-system-lib")) {
+                const name = nextArgOrFatal(args, &arg_idx);
+                builder.system_library_options.put(arena, name, .user_disabled) catch @panic("OOM");
             } else if (mem.eql(u8, arg, "--prefix-lib-dir")) {
-                dir_list.lib_dir = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected argument after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
+                dir_list.lib_dir = nextArgOrFatal(args, &arg_idx);
             } else if (mem.eql(u8, arg, "--prefix-exe-dir")) {
-                dir_list.exe_dir = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected argument after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
+                dir_list.exe_dir = nextArgOrFatal(args, &arg_idx);
             } else if (mem.eql(u8, arg, "--prefix-include-dir")) {
-                dir_list.include_dir = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected argument after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
+                dir_list.include_dir = nextArgOrFatal(args, &arg_idx);
             } else if (mem.eql(u8, arg, "--sysroot")) {
-                const sysroot = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected argument after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
-                builder.sysroot = sysroot;
+                builder.sysroot = nextArgOrFatal(args, &arg_idx);
             } else if (mem.eql(u8, arg, "--maxrss")) {
-                const max_rss_text = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected argument after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
+                const max_rss_text = nextArgOrFatal(args, &arg_idx);
                 max_rss = std.fmt.parseIntSizeSuffix(max_rss_text, 10) catch |err| {
                     std.debug.print("invalid byte size: '{s}': {s}\n", .{
                         max_rss_text, @errorName(err),
@@ -167,66 +155,45 @@ pub fn main() !void {
             } else if (mem.eql(u8, arg, "--skip-oom-steps")) {
                 skip_oom_steps = true;
             } else if (mem.eql(u8, arg, "--search-prefix")) {
-                const search_prefix = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected argument after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
+                const search_prefix = nextArgOrFatal(args, &arg_idx);
                 builder.addSearchPrefix(search_prefix);
             } else if (mem.eql(u8, arg, "--libc")) {
-                const libc_file = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected argument after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
-                builder.libc_file = libc_file;
+                builder.libc_file = nextArgOrFatal(args, &arg_idx);
             } else if (mem.eql(u8, arg, "--color")) {
-                const next_arg = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected [auto|on|off] after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
+                const next_arg = nextArg(args, &arg_idx) orelse
+                    fatalWithHint("expected [auto|on|off] after '{s}'", .{arg});
                 color = std.meta.stringToEnum(Color, next_arg) orelse {
-                    std.debug.print("Expected [auto|on|off] after {s}, found '{s}'\n\n", .{ arg, next_arg });
-                    usageAndErr(builder, false, stderr_stream);
+                    fatalWithHint("expected [auto|on|off] after '{s}', found '{s}'", .{
+                        arg, next_arg,
+                    });
                 };
             } else if (mem.eql(u8, arg, "--summary")) {
-                const next_arg = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected [all|failures|none] after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
+                const next_arg = nextArg(args, &arg_idx) orelse
+                    fatalWithHint("expected [all|failures|none] after '{s}'", .{arg});
                 summary = std.meta.stringToEnum(Summary, next_arg) orelse {
-                    std.debug.print("Expected [all|failures|none] after {s}, found '{s}'\n\n", .{ arg, next_arg });
-                    usageAndErr(builder, false, stderr_stream);
+                    fatalWithHint("expected [all|failures|none] after '{s}', found '{s}'", .{
+                        arg, next_arg,
+                    });
                 };
             } else if (mem.eql(u8, arg, "--zig-lib-dir")) {
-                builder.zig_lib_dir = .{ .cwd_relative = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected argument after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                } };
+                builder.zig_lib_dir = .{ .cwd_relative = nextArgOrFatal(args, &arg_idx) };
             } else if (mem.eql(u8, arg, "--seed")) {
-                const next_arg = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected u32 after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
+                const next_arg = nextArg(args, &arg_idx) orelse
+                    fatalWithHint("expected u32 after '{s}'", .{arg});
                 seed = std.fmt.parseUnsigned(u32, next_arg, 0) catch |err| {
-                    std.debug.print("unable to parse seed '{s}' as 32-bit integer: {s}\n", .{
+                    fatal("unable to parse seed '{s}' as 32-bit integer: {s}\n", .{
                         next_arg, @errorName(err),
                     });
-                    process.exit(1);
                 };
             } else if (mem.eql(u8, arg, "--debug-log")) {
-                const next_arg = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected argument after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
+                const next_arg = nextArgOrFatal(args, &arg_idx);
                 try debug_log_scopes.append(next_arg);
             } else if (mem.eql(u8, arg, "--debug-pkg-config")) {
                 builder.debug_pkg_config = true;
             } else if (mem.eql(u8, arg, "--debug-compile-errors")) {
                 builder.debug_compile_errors = true;
             } else if (mem.eql(u8, arg, "--glibc-runtimes")) {
-                builder.glibc_runtimes_dir = nextArg(args, &arg_idx) orelse {
-                    std.debug.print("Expected argument after {s}\n\n", .{arg});
-                    usageAndErr(builder, false, stderr_stream);
-                };
+                builder.glibc_runtimes_dir = nextArgOrFatal(args, &arg_idx);
             } else if (mem.eql(u8, arg, "--verbose-link")) {
                 builder.verbose_link = true;
             } else if (mem.eql(u8, arg, "--verbose-air")) {
@@ -292,8 +259,7 @@ pub fn main() !void {
                 builder.args = argsRest(args, arg_idx);
                 break;
             } else {
-                std.debug.print("Unrecognized argument: {s}\n\n", .{arg});
-                usageAndErr(builder, false, stderr_stream);
+                fatalWithHint("unrecognized argument: '{s}'", .{arg});
             }
         } else {
             try targets.append(arg);
@@ -319,8 +285,17 @@ pub fn main() !void {
         try builder.runBuild(root);
     }
 
-    if (builder.validateUserInputDidItFail())
-        usageAndErr(builder, true, stderr_stream);
+    if (builder.validateUserInputDidItFail()) {
+        fatal("  access the help menu with 'zig build -h'", .{});
+    }
+
+    validateSystemLibraryOptions(builder);
+
+    if (help_menu)
+        return usage(builder, stdout_stream);
+
+    if (steps_menu)
+        return steps(builder, stdout_stream);
 
     var run: Run = .{
         .max_rss = max_rss,
@@ -389,7 +364,7 @@ fn runStepNames(
         for (0..step_names.len) |i| {
             const step_name = step_names[step_names.len - i - 1];
             const s = b.top_level_steps.get(step_name) orelse {
-                std.debug.print("no step named '{s}'. Access the help menu with 'zig build -h'\n", .{step_name});
+                std.debug.print("no step named '{s}'\n  access the help menu with 'zig build -h'\n", .{step_name});
                 process.exit(1);
             };
             step_stack.putAssumeCapacity(&s.step, {});
@@ -1037,13 +1012,7 @@ fn printErrorMessages(b: *std.Build, failing_step: *Step, run: *const Run) !void
     }
 }
 
-fn steps(builder: *std.Build, already_ran_build: bool, out_stream: anytype) !void {
-    // run the build script to collect the options
-    if (!already_ran_build) {
-        builder.resolveInstallPrefix(null, .{});
-        try builder.runBuild(root);
-    }
-
+fn steps(builder: *std.Build, out_stream: anytype) !void {
     const allocator = builder.allocator;
     for (builder.top_level_steps.values()) |top_level_step| {
         const name = if (&top_level_step.step == builder.default_step)
@@ -1054,29 +1023,22 @@ fn steps(builder: *std.Build, already_ran_build: bool, out_stream: anytype) !voi
     }
 }
 
-fn usage(builder: *std.Build, already_ran_build: bool, out_stream: anytype) !void {
-    // run the build script to collect the options
-    if (!already_ran_build) {
-        builder.resolveInstallPrefix(null, .{});
-        try builder.runBuild(root);
-    }
-
+fn usage(b: *std.Build, out_stream: anytype) !void {
     try out_stream.print(
-        \\
         \\Usage: {s} build [steps] [options]
         \\
         \\Steps:
         \\
-    , .{builder.zig_exe});
-    try steps(builder, true, out_stream);
+    , .{b.zig_exe});
+    try steps(b, out_stream);
 
     try out_stream.writeAll(
         \\
         \\General Options:
-        \\  -p, --prefix [path]          Override default install prefix
-        \\  --prefix-lib-dir [path]      Override default library directory path
-        \\  --prefix-exe-dir [path]      Override default executable directory path
-        \\  --prefix-include-dir [path]  Override default include directory path
+        \\  -p, --prefix [path]          Where to put installed files (default: zig-out)
+        \\  --prefix-lib-dir [path]      Where to put installed libraries
+        \\  --prefix-exe-dir [path]      Where to put installed executables
+        \\  --prefix-include-dir [path]  Where to put installed C header files
         \\
         \\  --sysroot [path]             Set the system root directory (usually /)
         \\  --search-prefix [path]       Add a path to look for binaries, libraries, headers
@@ -1116,16 +1078,15 @@ fn usage(builder: *std.Build, already_ran_build: bool, out_stream: anytype) !voi
         \\
     );
 
-    const allocator = builder.allocator;
-    if (builder.available_options_list.items.len == 0) {
+    const arena = b.allocator;
+    if (b.available_options_list.items.len == 0) {
         try out_stream.print("  (none)\n", .{});
     } else {
-        for (builder.available_options_list.items) |option| {
-            const name = try fmt.allocPrint(allocator, "  -D{s}=[{s}]", .{
+        for (b.available_options_list.items) |option| {
+            const name = try fmt.allocPrint(arena, "  -D{s}=[{s}]", .{
                 option.name,
                 @tagName(option.type_id),
             });
-            defer allocator.free(name);
             try out_stream.print("{s:<30} {s}\n", .{ name, option.description });
             if (option.enum_options) |enum_options| {
                 const padding = " " ** 33;
@@ -1137,6 +1098,31 @@ fn usage(builder: *std.Build, already_ran_build: bool, out_stream: anytype) !voi
         }
     }
 
+    try out_stream.writeAll(
+        \\
+        \\System Integration Options:
+        \\  --system [dir]               System Package Mode. Disable fetching; prefer system libs
+        \\  --host-target [triple]       Use the provided target as the host
+        \\  --host-cpu [cpu]             Use the provided CPU as the host
+        \\  --system-lib [name]          Use the system-provided library
+        \\  --no-system-lib [name]       Do not use the system-provided library
+        \\
+        \\  Available System Library Integrations:        Enabled:
+        \\
+    );
+    if (b.system_library_options.entries.len == 0) {
+        try out_stream.writeAll("  (none)                                        -\n");
+    } else {
+        for (b.system_library_options.keys(), b.system_library_options.values()) |name, v| {
+            const status = switch (v) {
+                .declared_enabled => "yes",
+                .declared_disabled => "no",
+                .user_enabled, .user_disabled => unreachable, // already emitted error
+            };
+            try out_stream.print("    {s:<43} {s}\n", .{ name, status });
+        }
+    }
+
     try out_stream.writeAll(
         \\
         \\Advanced Options:
@@ -1161,17 +1147,19 @@ fn usage(builder: *std.Build, already_ran_build: bool, out_stream: anytype) !voi
     );
 }
 
-fn usageAndErr(builder: *std.Build, already_ran_build: bool, out_stream: anytype) noreturn {
-    usage(builder, already_ran_build, out_stream) catch {};
-    process.exit(1);
-}
-
 fn nextArg(args: [][:0]const u8, idx: *usize) ?[:0]const u8 {
     if (idx.* >= args.len) return null;
     defer idx.* += 1;
     return args[idx.*];
 }
 
+fn nextArgOrFatal(args: [][:0]const u8, idx: *usize) [:0]const u8 {
+    return nextArg(args, idx) orelse {
+        std.debug.print("expected argument after '{s}'\n  access the help menu with 'zig build -h'\n", .{args[idx.*]});
+        process.exit(1);
+    };
+}
+
 fn argsRest(args: [][:0]const u8, idx: usize) ?[][:0]const u8 {
     if (idx >= args.len) return null;
     return args[idx..];
@@ -1202,3 +1190,32 @@ fn renderOptions(ttyconf: std.io.tty.Config) std.zig.ErrorBundle.RenderOptions {
         .include_reference_trace = ttyconf != .no_color,
     };
 }
+
+fn fatalWithHint(comptime f: []const u8, args: anytype) noreturn {
+    std.debug.print(f ++ "\n  access the help menu with 'zig build -h'\n", args);
+    process.exit(1);
+}
+
+fn fatal(comptime f: []const u8, args: anytype) noreturn {
+    std.debug.print(f ++ "\n", args);
+    process.exit(1);
+}
+
+fn validateSystemLibraryOptions(b: *std.Build) void {
+    var bad = false;
+    for (b.system_library_options.keys(), b.system_library_options.values()) |k, v| {
+        switch (v) {
+            .user_disabled, .user_enabled => {
+                // The user tried to enable or disable a system library integration, but
+                // the build script did not recognize that option.
+                std.debug.print("system library name not recognized by build script: '{s}'\n", .{k});
+                bad = true;
+            },
+            .declared_disabled, .declared_enabled => {},
+        }
+    }
+    if (bad) {
+        std.debug.print("  access the help menu with 'zig build -h'\n", .{});
+        process.exit(1);
+    }
+}