authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-22 17:51:19-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-05-25 18:54:36-07:00
log1edc5d7d67f084941c2162dc71bd8a417189f265
treee94f181fa0d9d0ee6fa8d901d698981c4130639b
parent54bb8d2dd9369f5e5b43b4773878edc32fd3851e

Maker: implement FindProgram (lazy)


8 files changed, 175 insertions(+), 12 deletions(-)

lib/compiler/Maker/Step.zig+2-1
......@@ -19,6 +19,7 @@ const WebServer = @import("WebServer.zig");
1919const Maker = @import("../Maker.zig");
2020
2121pub const Compile = @import("Step/Compile.zig");
22pub const FindProgram = @import("Step/FindProgram.zig");
2223pub const Fmt = @import("Step/Fmt.zig");
2324pub const InstallArtifact = @import("Step/InstallArtifact.zig");
2425pub const InstallDir = @import("Step/InstallDir.zig");
......@@ -78,7 +79,7 @@ pub const Extended = union(enum) {
7879 compile: Compile,
7980 config_header: Todo,
8081 fail: Fail,
81 find_program: Todo,
82 find_program: FindProgram,
8283 fmt: Fmt,
8384 install_artifact: InstallArtifact,
8485 install_dir: InstallDir,
lib/compiler/Maker/Step/FindProgram.zig created+120
......@@ -0,0 +1,120 @@
1const FindProgram = @This();
2const builtin = @import("builtin");
3
4const std = @import("std");
5const Io = std.Io;
6const Configuration = std.Build.Configuration;
7const assert = std.debug.assert;
8
9const Step = @import("../Step.zig");
10const Maker = @import("../../Maker.zig");
11
12pub fn make(
13 find_program: *FindProgram,
14 step_index: Configuration.Step.Index,
15 maker: *Maker,
16 progress_node: std.Progress.Node,
17) Step.ExtendedMakeError!void {
18 _ = find_program;
19 _ = progress_node;
20 const graph = maker.graph;
21 const step = maker.stepByIndex(step_index);
22 const arena = graph.arena; // TODO don't leak into the process arena
23 const conf = &maker.scanned_config.configuration;
24 const conf_step = step_index.ptr(conf);
25 const conf_fp = conf_step.extended.get(conf.extra).find_program;
26 const found_path = conf_fp.found_path;
27 const names = conf_fp.names.slice(conf);
28
29 // In case we fail at the end.
30 var err_msg: std.ArrayList(u8) = .empty;
31 try err_msg.appendSlice(arena, "program not found. searched paths:\n");
32
33 for (names) |name_index| {
34 const name = name_index.slice(conf);
35
36 if (Io.Dir.path.isAbsolute(name)) {
37 if (try checkCandidate(maker, step, found_path, &err_msg, name)) return;
38
39 continue;
40 }
41
42 for (graph.search_prefixes.items) |search_prefix| {
43 const full_path = try Io.Dir.path.join(arena, &.{ search_prefix, "bin", name });
44
45 if (try checkCandidate(maker, step, found_path, &err_msg, full_path)) return;
46 }
47 }
48
49 if (graph.environ_map.get("PATH")) |PATH| {
50 for (names) |name_index| {
51 const name = name_index.slice(conf);
52
53 var it = std.mem.tokenizeScalar(u8, PATH, Io.Dir.path.delimiter);
54 while (it.next()) |p| {
55 const full_path = try Io.Dir.path.join(arena, &.{ p, name });
56
57 if (try checkCandidate(maker, step, found_path, &err_msg, full_path)) return;
58 }
59 }
60 }
61
62 assert(err_msg.items[err_msg.items.len - 1] == '\n');
63 const chopped = err_msg.items[0 .. err_msg.items.len - 1];
64 try step.result_error_msgs.append(arena, chopped);
65 return error.MakeFailed;
66}
67
68fn checkCandidate(
69 maker: *Maker,
70 step: *Step,
71 found_path: Configuration.GeneratedFileIndex,
72 err_msg: *std.ArrayList(u8),
73 full_path: []const u8,
74) !bool {
75 const graph = maker.graph;
76 const arena = graph.arena; // TODO don't leak into process arena
77 const io = graph.io;
78
79 if (Io.Dir.cwd().access(io, full_path, .{ .execute = true })) |_| {
80 maker.generatedPath(found_path).* = .initCwd(full_path);
81 return true;
82 } else |err| switch (err) {
83 error.Canceled => |e| return e,
84 error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
85 try err_msg.print(arena, "{t} {s}\n", .{ e, full_path });
86 },
87 else => |e| return step.fail(maker, "failed accessing {s}: {t}", .{ full_path, e }),
88 }
89
90 if (builtin.os.tag == .windows) {
91 if (graph.environ_map.get("PATHEXT")) |PATHEXT| {
92 var it = std.mem.tokenizeScalar(u8, PATHEXT, Io.Dir.path.delimiter);
93 while (it.next()) |ext| {
94 if (!supportedWindowsProgramExtension(ext)) continue;
95
96 const extended_path = try std.mem.concat(arena, &.{ full_path, ext });
97
98 if (Io.Dir.cwd().access(io, extended_path, .{ .execute = true })) |_| {
99 maker.generatedPath(found_path).* = .initCwd(extended_path);
100 return true;
101 } else |err| switch (err) {
102 error.Canceled => |e| return e,
103 error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
104 try err_msg.print(arena, "{s} {t}\n", .{ extended_path, e });
105 },
106 else => |e| return step.fail(maker, "failed accessing {s}: {t}", .{ extended_path, e }),
107 }
108 }
109 }
110 }
111
112 return false;
113}
114
115fn supportedWindowsProgramExtension(ext: []const u8) bool {
116 inline for (@typeInfo(std.process.WindowsExtension).@"enum".fields) |field| {
117 if (std.ascii.eqlIgnoreCase(ext, "." ++ field.name)) return true;
118 }
119 return false;
120}
lib/compiler/configurer.zig+7-1
......@@ -906,7 +906,13 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void {
906906 .msg = sf.error_msg,
907907 });
908908 },
909 .find_program => @panic("TODO"),
909 .find_program => e: {
910 const fp: *Step.FindProgram = @fieldParentPtr("step", step);
911 break :e try wc.addExtraErased(Configuration.Step.FindProgram, .{
912 .names = fp.names,
913 .found_path = fp.found_path,
914 });
915 },
910916 .fmt => e: {
911917 const sf: *Step.Fmt = @fieldParentPtr("step", step);
912918 break :e try wc.addExtraErased(Configuration.Step.Fmt, .{
lib/std/Build.zig+7-6
......@@ -1719,14 +1719,15 @@ pub fn fmt(b: *Build, comptime format: []const u8, args: anytype) []u8 {
17191719/// globally installed and will therefore be possibly found in one of the
17201720/// search prefix paths.
17211721///
1722/// Windows file name extensions are searched automatically, respecting the
1723/// PATHEXT environment variable, so they need not be included in this list.
1724/// However, even on Windows, the names will be checked without appending
1725/// extensions first, so that can be used as a priority system.
1726///
17221727/// See also:
17231728/// * `findProgram`
1724pub fn findProgramLazy(b: *Build, names: []const []const u8) LazyPath {
1725 const graph = b.graph;
1726 const wc = &graph.wip_configuration;
1727 const string_list = wc.addStringList(names) catch @panic("OOM");
1728 _ = string_list;
1729 @panic("TODO");
1729pub fn findProgramLazy(b: *Build, options: Step.FindProgram.Options) LazyPath {
1730 return .{ .generated = .{ .index = Step.FindProgram.create(b, options).found_path } };
17301731}
17311732
17321733/// Immediately (in the configure phase), searches for an executable on the host
lib/std/Build/Configuration.zig+2-2
......@@ -1192,9 +1192,9 @@ pub const Step = extern struct {
11921192 };
11931193
11941194 pub const FindProgram = struct {
1195 flags: @This().Flags,
1195 flags: @This().Flags = .{},
11961196 names: StringList,
1197 generated_file: GeneratedFileIndex,
1197 found_path: GeneratedFileIndex,
11981198
11991199 pub const Flags = packed struct(u32) {
12001200 tag: Tag = .find_program,
lib/std/Build/Step.zig+3-2
......@@ -78,19 +78,20 @@ pub fn Type(comptime tag: Tag) type {
7878}
7979
8080pub const CheckFile = @import("Step/CheckFile.zig");
81pub const Compile = @import("Step/Compile.zig");
8182pub const ConfigHeader = @import("Step/ConfigHeader.zig");
8283pub const Fail = @import("Step/Fail.zig");
84pub const FindProgram = @import("Step/FindProgram.zig");
8385pub const Fmt = @import("Step/Fmt.zig");
8486pub const InstallArtifact = @import("Step/InstallArtifact.zig");
8587pub const InstallDir = @import("Step/InstallDir.zig");
8688pub const InstallFile = @import("Step/InstallFile.zig");
8789pub const ObjCopy = @import("Step/ObjCopy.zig");
88pub const Compile = @import("Step/Compile.zig");
8990pub const Options = @import("Step/Options.zig");
9091pub const Run = @import("Step/Run.zig");
9192pub const TranslateC = @import("Step/TranslateC.zig");
92pub const WriteFile = @import("Step/WriteFile.zig");
9393pub const UpdateSourceFiles = @import("Step/UpdateSourceFiles.zig");
94pub const WriteFile = @import("Step/WriteFile.zig");
9495
9596pub const TopLevel = struct {
9697 pub const base_tag: Step.Tag = .top_level;
lib/std/Build/Step/FindProgram.zig created+31
......@@ -0,0 +1,31 @@
1const FindProgram = @This();
2
3const std = @import("std");
4const Step = std.Build.Step;
5const Configuration = std.Build.Configuration;
6
7step: Step,
8found_path: Configuration.GeneratedFileIndex,
9names: Configuration.StringList,
10
11pub const base_tag: Step.Tag = .find_program;
12
13pub const Options = struct {
14 names: []const []const u8,
15};
16
17pub fn create(owner: *std.Build, options: Options) *FindProgram {
18 const graph = owner.graph;
19 const wc = &graph.wip_configuration;
20 const fp = graph.create(FindProgram);
21 fp.* = .{
22 .step = .init(.{
23 .tag = base_tag,
24 .name = owner.fmt("find program {s} ({d} candidates)", .{ options.names[0], options.names.len }),
25 .owner = owner,
26 }),
27 .found_path = graph.addGeneratedFile(&fp.step),
28 .names = wc.addStringList(options.names) catch @panic("OOM"),
29 };
30 return fp;
31}
lib/std/Io/Dir.zig+3
......@@ -409,7 +409,10 @@ pub const PathNameError = error{
409409};
410410
411411pub const AccessError = error{
412 /// The requested `AccessOptions` would be denied to the file, or search
413 /// permission is denied for one of the directories in the path prefix.
412414 AccessDenied,
415 /// Write permission was requested but the file is immutable.
413416 PermissionDenied,
414417 FileNotFound,
415418 InputOutput,