zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

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:
Mlib/compiler/Maker/Step/FindProgram.zig | 2+-
Mlib/compiler/configurer.zig | 8++++++++
Mlib/std/Build.zig | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/main.zig | 11++++++++++-
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;