commit 92038675af545ecdbe1f1b4cbd1095a8b3264e07 (tree)
parent 1edc5d7d67f084941c2162dc71bd8a417189f265
Author: Andrew Kelley <andrew@ziglang.org>
Date: Fri, 22 May 2026 18:36:51 -0700
zig build: implement findProgram (not lazy)
Diffstat:
4 files changed, 105 insertions(+), 12 deletions(-)
diff --git a/lib/compiler/Maker/Step/FindProgram.zig b/lib/compiler/Maker/Step/FindProgram.zig
@@ -101,7 +101,7 @@ fn checkCandidate(
} else |err| switch (err) {
error.Canceled => |e| return e,
error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
- try err_msg.print(arena, "{s} {t}\n", .{ extended_path, e });
+ try err_msg.print(arena, "{t} {s}\n", .{ e, extended_path });
},
else => |e| return step.fail(maker, "failed accessing {s}: {t}", .{ extended_path, e }),
}
diff --git a/lib/compiler/configurer.zig b/lib/compiler/configurer.zig
@@ -117,6 +117,8 @@ pub fn main(init: process.Init.Minimal) !void {
} else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| {
graph.cache_poison = std.meta.stringToEnum(std.Build.Graph.CachePoison, rest) orelse
fatalWithHint("expected --cache-poison=[pure|poisoned|disallowed|ignored]; found: {s}", .{arg});
+ } else if (mem.eql(u8, arg, "--search-prefix")) {
+ try graph.search_prefixes.append(arena, nextArgOrFatal(args, &arg_i));
} else {
fatalWithHint("unrecognized argument: {s}", .{arg});
}
@@ -1319,6 +1321,12 @@ fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
return args[idx.*];
}
+fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 {
+ return nextArg(args, idx) orelse {
+ fatalWithHint("expected argument after {q}", .{args[idx.* - 1]});
+ };
+}
+
fn expectArgOrFatal(args: []const [:0]const u8, index_ptr: *usize, first: []const u8) []const u8 {
const next_arg = nextArg(args, index_ptr) orelse fatal("missing {q} argument", .{first});
if (!mem.eql(u8, first, next_arg)) fatal("expected {q} instead of {q}", .{ first, next_arg });
diff --git a/lib/std/Build.zig b/lib/std/Build.zig
@@ -99,6 +99,8 @@ pub const Graph = struct {
wip_configuration: Configuration.Wip,
cache_poison: CachePoison = .pure,
+ /// Observing this data causes cache poisoning. See `CachePoison`.
+ search_prefixes: std.ArrayList([]const u8) = .empty,
/// If the cache is poisoned means that the **configure logic** had side
/// effects, or otherwise did something that could not be tracked by the
@@ -1706,9 +1708,6 @@ pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 {
/// Creates an anonymous `Step` that searches for an executable on the host that
/// has more than one possible name.
///
-/// Names are searched in order, observing search prefixes first and then PATH
-/// environment variable.
-///
/// Returns the `LazyPath` of the found executable. The search only takes place
/// if the `LazyPath` will be used by a depending `Step`.
///
@@ -1719,6 +1718,9 @@ pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 {
/// globally installed and will therefore be possibly found in one of the
/// search prefix paths.
///
+/// Names are searched in order, observing search prefixes first and then PATH
+/// environment variable.
+///
/// Windows file name extensions are searched automatically, respecting the
/// PATHEXT environment variable, so they need not be included in this list.
/// However, even on Windows, the names will be checked without appending
@@ -1730,24 +1732,98 @@ pub fn findProgramLazy(b: *Build, options: Step.FindProgram.Options) LazyPath {
return .{ .generated = .{ .index = Step.FindProgram.create(b, options).found_path } };
}
+pub const FindProgramOptions = Step.FindProgram.Options;
+
/// Immediately (in the configure phase), searches for an executable on the host
/// that has more than one possible name.
///
+/// Calling this function poisons the configuration cache, so it is only
+/// appropriate when the existence of the program or its output needs to be
+/// observed by configuration logic. For more information, see
+/// `Graph.CachePoison` documentation.
+///
/// Names are searched in order, observing search prefixes first and then PATH
/// environment variable.
///
-/// Calling this function poisons the configuration cache. For more
-/// information, see `Graph.CachePoison` documentation.
+/// Windows file name extensions are searched automatically, respecting the
+/// PATHEXT environment variable, so they need not be included in this list.
+/// However, even on Windows, the names will be checked without appending
+/// extensions first, so that can be used as a priority system.
///
/// See also:
/// * `findProgramLazy`
-pub fn findProgram(b: *Build, names: []const []const u8) ?[]const u8 {
+pub fn findProgram(b: *Build, options: FindProgramOptions) ?[]const u8 {
const graph = b.graph;
- const wc = &graph.wip_configuration;
- const string_list = wc.addStringList(names) catch @panic("OOM");
- _ = string_list;
+
+ // Because it observes search prefixes and contents of directories in PATH.
graph.poisonCache();
- @panic("TODO");
+
+ for (options.names) |name| {
+ if (Io.Dir.path.isAbsolute(name)) {
+ if (tryFindProgram(b, name)) |found| return found;
+ }
+ for (graph.search_prefixes.items) |search_prefix| {
+ const full_path = b.pathJoin(&.{ search_prefix, "bin", name });
+ if (tryFindProgram(b, full_path)) |found| return found;
+ }
+ }
+
+ if (b.graph.environ_map.get("PATH")) |PATH| {
+ for (options.names) |name| {
+ var it = mem.tokenizeScalar(u8, PATH, Io.Dir.path.delimiter);
+ while (it.next()) |p| {
+ const full_path = b.pathJoin(&.{ p, name });
+ if (tryFindProgram(b, full_path)) |found| return found;
+ }
+ }
+ }
+
+ return null;
+}
+
+fn supportedWindowsProgramExtension(ext: []const u8) bool {
+ inline for (@typeInfo(std.process.WindowsExtension).@"enum".fields) |field| {
+ if (std.ascii.eqlIgnoreCase(ext, "." ++ field.name)) return true;
+ }
+ return false;
+}
+
+fn tryFindProgram(b: *Build, full_path: []const u8) ?[]const u8 {
+ const graph = b.graph;
+ const io = graph.io;
+ const arena = graph.arena;
+
+ if (Io.Dir.cwd().access(io, full_path, .{ .execute = true })) |_| {
+ return full_path;
+ } else |err| switch (err) {
+ error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
+ if (graph.verbose) log.info("searched: {t} {s}", .{ e, full_path });
+ },
+ else => |e| return panic("failed accessing {s}: {t}", .{ full_path, e }),
+ }
+
+ if (builtin.os.tag == .windows) {
+ if (b.graph.environ_map.get("PATHEXT")) |PATHEXT| {
+ var it = mem.tokenizeScalar(u8, PATHEXT, fs.path.delimiter);
+
+ while (it.next()) |ext| {
+ if (!supportedWindowsProgramExtension(ext)) continue;
+
+ const extended_path = try mem.concat(arena, &.{ full_path, ext });
+
+ if (Io.Dir.cwd().access(io, extended_path, .{ .execute = true })) |_| {
+ return extended_path;
+ } else |err| switch (err) {
+ error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
+ if (graph.verbose) log.info("searched: {t} {s}", .{ e, extended_path });
+ },
+ else => |e| return panic("failed accessing {s}: {t}", .{ extended_path, e }),
+ }
+ }
+ }
+ }
+
+ return null;
}
/// Deprecated; use `runFallible`.
diff --git a/src/main.zig b/src/main.zig
@@ -5014,7 +5014,7 @@ fn cmdBuild(
while (i < args.len) : (i += 1) {
const arg = args[i];
if (mem.startsWith(u8, arg, "-")) {
- try configure_argv.ensureUnusedCapacity(arena, 1);
+ try configure_argv.ensureUnusedCapacity(arena, 2);
if (mem.startsWith(u8, arg, "-D") or
mem.startsWith(u8, arg, "-fsys=") or
@@ -5055,6 +5055,15 @@ fn cmdBuild(
// Intentionally is added both to make and configure but
// does not go into the cache hash.
configure_argv.appendAssumeCapacity(arg);
+ } else if (mem.eql(u8, arg, "--search-prefix")) {
+ if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
+ i += 1;
+ // This argument is cache poisonous: it does not go into
+ // the cache and configurer must set the poison bit when
+ // choosing to observe it.
+ configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ arg, args[i] };
+ (try make_argv.addManyAsArray(arena, 2)).* = .{ arg, args[i] };
+ continue;
} else if (mem.eql(u8, arg, "--build-file")) {
if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
i += 1;