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(...@@ -101,7 +101,7 @@ fn checkCandidate(
101 } else |err| switch (err) {101 } else |err| switch (err) {
102 error.Canceled => |e| return e,102 error.Canceled => |e| return e,
103 error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {103 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 });
105 },105 },
106 else => |e| return step.fail(maker, "failed accessing {s}: {t}", .{ extended_path, e }),106 else => |e| return step.fail(maker, "failed accessing {s}: {t}", .{ extended_path, e }),
107 }107 }
lib/compiler/configurer.zig+8
...@@ -117,6 +117,8 @@ pub fn main(init: process.Init.Minimal) !void {...@@ -117,6 +117,8 @@ pub fn main(init: process.Init.Minimal) !void {
117 } else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| {117 } else if (mem.cutPrefix(u8, arg, "--cache-poison=")) |rest| {
118 graph.cache_poison = std.meta.stringToEnum(std.Build.Graph.CachePoison, rest) orelse118 graph.cache_poison = std.meta.stringToEnum(std.Build.Graph.CachePoison, rest) orelse
119 fatalWithHint("expected --cache-poison=[pure|poisoned|disallowed|ignored]; found: {s}", .{arg});119 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));
120 } else {122 } else {
121 fatalWithHint("unrecognized argument: {s}", .{arg});123 fatalWithHint("unrecognized argument: {s}", .{arg});
122 }124 }
...@@ -1319,6 +1321,12 @@ fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {...@@ -1319,6 +1321,12 @@ fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 {
1319 return args[idx.*];1321 return args[idx.*];
1320}1322}
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
1322fn expectArgOrFatal(args: []const [:0]const u8, index_ptr: *usize, first: []const u8) []const u8 {1330fn expectArgOrFatal(args: []const [:0]const u8, index_ptr: *usize, first: []const u8) []const u8 {
1323 const next_arg = nextArg(args, index_ptr) orelse fatal("missing {q} argument", .{first});1331 const next_arg = nextArg(args, index_ptr) orelse fatal("missing {q} argument", .{first});
1324 if (!mem.eql(u8, first, next_arg)) fatal("expected {q} instead of {q}", .{ first, next_arg });1332 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 {...@@ -99,6 +99,8 @@ pub const Graph = struct {
99 wip_configuration: Configuration.Wip,99 wip_configuration: Configuration.Wip,
100100
101 cache_poison: CachePoison = .pure,101 cache_poison: CachePoison = .pure,
102 /// Observing this data causes cache poisoning. See `CachePoison`.
103 search_prefixes: std.ArrayList([]const u8) = .empty,
102104
103 /// If the cache is poisoned means that the **configure logic** had side105 /// If the cache is poisoned means that the **configure logic** had side
104 /// effects, or otherwise did something that could not be tracked by the106 /// 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 {...@@ -1706,9 +1708,6 @@ pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 {
1706/// Creates an anonymous `Step` that searches for an executable on the host that1708/// Creates an anonymous `Step` that searches for an executable on the host that
1707/// has more than one possible name.1709/// has more than one possible name.
1708///1710///
1709/// Names are searched in order, observing search prefixes first and then PATH
1710/// environment variable.
1711///
1712/// Returns the `LazyPath` of the found executable. The search only takes place1711/// Returns the `LazyPath` of the found executable. The search only takes place
1713/// if the `LazyPath` will be used by a depending `Step`.1712/// if the `LazyPath` will be used by a depending `Step`.
1714///1713///
...@@ -1719,6 +1718,9 @@ pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 {...@@ -1719,6 +1718,9 @@ pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 {
1719/// globally installed and will therefore be possibly found in one of the1718/// globally installed and will therefore be possibly found in one of the
1720/// search prefix paths.1719/// search prefix paths.
1721///1720///
1721/// Names are searched in order, observing search prefixes first and then PATH
1722/// environment variable.
1723///
1722/// Windows file name extensions are searched automatically, respecting the1724/// Windows file name extensions are searched automatically, respecting the
1723/// PATHEXT environment variable, so they need not be included in this list.1725/// PATHEXT environment variable, so they need not be included in this list.
1724/// However, even on Windows, the names will be checked without appending1726/// 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 {...@@ -1730,24 +1732,98 @@ pub fn findProgramLazy(b: *Build, options: Step.FindProgram.Options) LazyPath {
1730 return .{ .generated = .{ .index = Step.FindProgram.create(b, options).found_path } };1732 return .{ .generated = .{ .index = Step.FindProgram.create(b, options).found_path } };
1731}1733}
17321734
1735pub const FindProgramOptions = Step.FindProgram.Options;
1736
1733/// Immediately (in the configure phase), searches for an executable on the host1737/// Immediately (in the configure phase), searches for an executable on the host
1734/// that has more than one possible name.1738/// that has more than one possible name.
1735///1739///
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///
1736/// Names are searched in order, observing search prefixes first and then PATH1745/// Names are searched in order, observing search prefixes first and then PATH
1737/// environment variable.1746/// environment variable.
1738///1747///
1739/// Calling this function poisons the configuration cache. For more1748/// Windows file name extensions are searched automatically, respecting the
1740/// information, see `Graph.CachePoison` documentation.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.
1741///1752///
1742/// See also:1753/// See also:
1743/// * `findProgramLazy`1754/// * `findProgramLazy`
1744pub fn findProgram(b: *Build, names: []const []const u8) ?[]const u8 {1755pub fn findProgram(b: *Build, options: FindProgramOptions) ?[]const u8 {
1745 const graph = b.graph;1756 const graph = b.graph;
1746 const wc = &graph.wip_configuration;1757
1747 const string_list = wc.addStringList(names) catch @panic("OOM");1758 // Because it observes search prefixes and contents of directories in PATH.
1748 _ = string_list;
1749 graph.poisonCache();1759 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;
1751}1827}
17521828
1753/// Deprecated; use `runFallible`.1829/// Deprecated; use `runFallible`.
src/main.zig+10-1
...@@ -5014,7 +5014,7 @@ fn cmdBuild(...@@ -5014,7 +5014,7 @@ fn cmdBuild(
5014 while (i < args.len) : (i += 1) {5014 while (i < args.len) : (i += 1) {
5015 const arg = args[i];5015 const arg = args[i];
5016 if (mem.startsWith(u8, arg, "-")) {5016 if (mem.startsWith(u8, arg, "-")) {
5017 try configure_argv.ensureUnusedCapacity(arena, 1);5017 try configure_argv.ensureUnusedCapacity(arena, 2);
50185018
5019 if (mem.startsWith(u8, arg, "-D") or5019 if (mem.startsWith(u8, arg, "-D") or
5020 mem.startsWith(u8, arg, "-fsys=") or5020 mem.startsWith(u8, arg, "-fsys=") or
...@@ -5055,6 +5055,15 @@ fn cmdBuild(...@@ -5055,6 +5055,15 @@ fn cmdBuild(
5055 // Intentionally is added both to make and configure but5055 // Intentionally is added both to make and configure but
5056 // does not go into the cache hash.5056 // does not go into the cache hash.
5057 configure_argv.appendAssumeCapacity(arg);5057 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;
5058 } else if (mem.eql(u8, arg, "--build-file")) {5067 } else if (mem.eql(u8, arg, "--build-file")) {
5059 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});5068 if (i + 1 >= args.len) fatal("expected argument after '{s}'", .{arg});
5060 i += 1;5069 i += 1;