authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-22 18:36:51-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-25 18:54:36-07:00
log92038675af545ecdbe1f1b4cbd1095a8b3264e07
tree45da6169d4b61b423df05cea93dc745ebd293683
parent1edc5d7d67f084941c2162dc71bd8a417189f265

zig build: implement findProgram (not lazy)


4 files changed, 105 insertions(+), 12 deletions(-)

lib/compiler/Maker/Step/FindProgram.zig+1-1
......@@ -101,7 +101,7 @@ fn checkCandidate(
101101 } else |err| switch (err) {
102102 error.Canceled => |e| return e,
103103 error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
104 try err_msg.print(arena, "{s} {t}\n", .{ extended_path, e });
104 try err_msg.print(arena, "{t} {s}\n", .{ e, extended_path });
105105 },
106106 else => |e| return step.fail(maker, "failed accessing {s}: {t}", .{ extended_path, e }),
107107 }
lib/compiler/configurer.zig+8
......@@ -117,6 +117,8 @@ pub fn main(init: process.Init.Minimal) !void {
117117 } else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| {
118118 graph.cache_poison = std.meta.stringToEnum(std.Build.Graph.CachePoison, rest) orelse
119119 fatalWithHint("expected --cache-poison=[pure|poisoned|disallowed|ignored]; found: {s}", .{arg});
120 } else if (mem.eql(u8, arg, "--search-prefix")) {
121 try graph.search_prefixes.append(arena, nextArgOrFatal(args, &arg_i));
120122 } else {
121123 fatalWithHint("unrecognized argument: {s}", .{arg});
122124 }
......@@ -1319,6 +1321,12 @@ fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
13191321 return args[idx.*];
13201322}
13211323
1324fn nextArgOrFatal(args: []const [:0]const u8, idx: *usize) [:0]const u8 {
1325 return nextArg(args, idx) orelse {
1326 fatalWithHint("expected argument after {q}", .{args[idx.* - 1]});
1327 };
1328}
1329
13221330fn expectArgOrFatal(args: []const [:0]const u8, index_ptr: *usize, first: []const u8) []const u8 {
13231331 const next_arg = nextArg(args, index_ptr) orelse fatal("missing {q} argument", .{first});
13241332 if (!mem.eql(u8, first, next_arg)) fatal("expected {q} instead of {q}", .{ first, next_arg });
lib/std/Build.zig+86-10
......@@ -99,6 +99,8 @@ pub const Graph = struct {
9999 wip_configuration: Configuration.Wip,
100100
101101 cache_poison: CachePoison = .pure,
102 /// Observing this data causes cache poisoning. See `CachePoison`.
103 search_prefixes: std.ArrayList([]const u8) = .empty,
102104
103105 /// If the cache is poisoned means that the **configure logic** had side
104106 /// 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 {
17061708/// Creates an anonymous `Step` that searches for an executable on the host that
17071709/// has more than one possible name.
17081710///
1709/// Names are searched in order, observing search prefixes first and then PATH
1710/// environment variable.
1711///
17121711/// Returns the `LazyPath` of the found executable. The search only takes place
17131712/// if the `LazyPath` will be used by a depending `Step`.
17141713///
......@@ -1719,6 +1718,9 @@ pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 {
17191718/// globally installed and will therefore be possibly found in one of the
17201719/// search prefix paths.
17211720///
1721/// Names are searched in order, observing search prefixes first and then PATH
1722/// environment variable.
1723///
17221724/// Windows file name extensions are searched automatically, respecting the
17231725/// PATHEXT environment variable, so they need not be included in this list.
17241726/// 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 {
17301732 return .{ .generated = .{ .index = Step.FindProgram.create(b, options).found_path } };
17311733}
17321734
1735pub const FindProgramOptions = Step.FindProgram.Options;
1736
17331737/// Immediately (in the configure phase), searches for an executable on the host
17341738/// that has more than one possible name.
17351739///
1740/// Calling this function poisons the configuration cache, so it is only
1741/// appropriate when the existence of the program or its output needs to be
1742/// observed by configuration logic. For more information, see
1743/// `Graph.CachePoison` documentation.
1744///
17361745/// Names are searched in order, observing search prefixes first and then PATH
17371746/// environment variable.
17381747///
1739/// Calling this function poisons the configuration cache. For more
1740/// information, see `Graph.CachePoison` documentation.
1748/// Windows file name extensions are searched automatically, respecting the
1749/// PATHEXT environment variable, so they need not be included in this list.
1750/// However, even on Windows, the names will be checked without appending
1751/// extensions first, so that can be used as a priority system.
17411752///
17421753/// See also:
17431754/// * `findProgramLazy`
1744pub fn findProgram(b: *Build, names: []const []const u8) ?[]const u8 {
1755pub fn findProgram(b: *Build, options: FindProgramOptions) ?[]const u8 {
17451756 const graph = b.graph;
1746 const wc = &graph.wip_configuration;
1747 const string_list = wc.addStringList(names) catch @panic("OOM");
1748 _ = string_list;
1757
1758 // Because it observes search prefixes and contents of directories in PATH.
17491759 graph.poisonCache();
1750 @panic("TODO");
1760
1761 for (options.names) |name| {
1762 if (Io.Dir.path.isAbsolute(name)) {
1763 if (tryFindProgram(b, name)) |found| return found;
1764 }
1765 for (graph.search_prefixes.items) |search_prefix| {
1766 const full_path = b.pathJoin(&.{ search_prefix, "bin", name });
1767 if (tryFindProgram(b, full_path)) |found| return found;
1768 }
1769 }
1770
1771 if (b.graph.environ_map.get("PATH")) |PATH| {
1772 for (options.names) |name| {
1773 var it = mem.tokenizeScalar(u8, PATH, Io.Dir.path.delimiter);
1774 while (it.next()) |p| {
1775 const full_path = b.pathJoin(&.{ p, name });
1776 if (tryFindProgram(b, full_path)) |found| return found;
1777 }
1778 }
1779 }
1780
1781 return null;
1782}
1783
1784fn supportedWindowsProgramExtension(ext: []const u8) bool {
1785 inline for (@typeInfo(std.process.WindowsExtension).@"enum".fields) |field| {
1786 if (std.ascii.eqlIgnoreCase(ext, "." ++ field.name)) return true;
1787 }
1788 return false;
1789}
1790
1791fn tryFindProgram(b: *Build, full_path: []const u8) ?[]const u8 {
1792 const graph = b.graph;
1793 const io = graph.io;
1794 const arena = graph.arena;
1795
1796 if (Io.Dir.cwd().access(io, full_path, .{ .execute = true })) |_| {
1797 return full_path;
1798 } else |err| switch (err) {
1799 error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
1800 if (graph.verbose) log.info("searched: {t} {s}", .{ e, full_path });
1801 },
1802 else => |e| return panic("failed accessing {s}: {t}", .{ full_path, e }),
1803 }
1804
1805 if (builtin.os.tag == .windows) {
1806 if (b.graph.environ_map.get("PATHEXT")) |PATHEXT| {
1807 var it = mem.tokenizeScalar(u8, PATHEXT, fs.path.delimiter);
1808
1809 while (it.next()) |ext| {
1810 if (!supportedWindowsProgramExtension(ext)) continue;
1811
1812 const extended_path = try mem.concat(arena, &.{ full_path, ext });
1813
1814 if (Io.Dir.cwd().access(io, extended_path, .{ .execute = true })) |_| {
1815 return extended_path;
1816 } else |err| switch (err) {
1817 error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
1818 if (graph.verbose) log.info("searched: {t} {s}", .{ e, extended_path });
1819 },
1820 else => |e| return panic("failed accessing {s}: {t}", .{ extended_path, e }),
1821 }
1822 }
1823 }
1824 }
1825
1826 return null;
17511827}
17521828
17531829/// Deprecated; use `runFallible`.
src/main.zig+10-1
......@@ -5014,7 +5014,7 @@ fn cmdBuild(
50145014 while (i < args.len) : (i += 1) {
50155015 const arg = args[i];
50165016 if (mem.startsWith(u8, arg, "-")) {
5017 try configure_argv.ensureUnusedCapacity(arena, 1);
5017 try configure_argv.ensureUnusedCapacity(arena, 2);
50185018
50195019 if (mem.startsWith(u8, arg, "-D") or
50205020 mem.startsWith(u8, arg, "-fsys=") or
......@@ -5055,6 +5055,15 @@ fn cmdBuild(
50555055 // Intentionally is added both to make and configure but
50565056 // does not go into the cache hash.
50575057 configure_argv.appendAssumeCapacity(arg);
5058 } else if (mem.eql(u8, arg, "--search-prefix")) {
5059 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
5060 i += 1;
5061 // This argument is cache poisonous: it does not go into
5062 // the cache and configurer must set the poison bit when
5063 // choosing to observe it.
5064 configure_argv.addManyAsArrayAssumeCapacity(2).* = .{ arg, args[i] };
5065 (try make_argv.addManyAsArray(arena, 2)).* = .{ arg, args[i] };
5066 continue;
50585067 } else if (mem.eql(u8, arg, "--build-file")) {
50595068 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
50605069 i += 1;