commit 1edc5d7d67f084941c2162dc71bd8a417189f265 (tree)
parent 54bb8d2dd9369f5e5b43b4773878edc32fd3851e
Author: Andrew Kelley <andrew@ziglang.org>
Date: Fri, 22 May 2026 17:51:19 -0700
Maker: implement FindProgram (lazy)
Diffstat:
8 files changed, 175 insertions(+), 12 deletions(-)
diff --git a/lib/compiler/Maker/Step.zig b/lib/compiler/Maker/Step.zig
@@ -19,6 +19,7 @@ const WebServer = @import("WebServer.zig");
const Maker = @import("../Maker.zig");
pub const Compile = @import("Step/Compile.zig");
+pub const FindProgram = @import("Step/FindProgram.zig");
pub const Fmt = @import("Step/Fmt.zig");
pub const InstallArtifact = @import("Step/InstallArtifact.zig");
pub const InstallDir = @import("Step/InstallDir.zig");
@@ -78,7 +79,7 @@ pub const Extended = union(enum) {
compile: Compile,
config_header: Todo,
fail: Fail,
- find_program: Todo,
+ find_program: FindProgram,
fmt: Fmt,
install_artifact: InstallArtifact,
install_dir: InstallDir,
diff --git a/lib/compiler/Maker/Step/FindProgram.zig b/lib/compiler/Maker/Step/FindProgram.zig
@@ -0,0 +1,120 @@
+const FindProgram = @This();
+const builtin = @import("builtin");
+
+const std = @import("std");
+const Io = std.Io;
+const Configuration = std.Build.Configuration;
+const assert = std.debug.assert;
+
+const Step = @import("../Step.zig");
+const Maker = @import("../../Maker.zig");
+
+pub fn make(
+ find_program: *FindProgram,
+ step_index: Configuration.Step.Index,
+ maker: *Maker,
+ progress_node: std.Progress.Node,
+) Step.ExtendedMakeError!void {
+ _ = find_program;
+ _ = progress_node;
+ const graph = maker.graph;
+ const step = maker.stepByIndex(step_index);
+ const arena = graph.arena; // TODO don't leak into the process arena
+ const conf = &maker.scanned_config.configuration;
+ const conf_step = step_index.ptr(conf);
+ const conf_fp = conf_step.extended.get(conf.extra).find_program;
+ const found_path = conf_fp.found_path;
+ const names = conf_fp.names.slice(conf);
+
+ // In case we fail at the end.
+ var err_msg: std.ArrayList(u8) = .empty;
+ try err_msg.appendSlice(arena, "program not found. searched paths:\n");
+
+ for (names) |name_index| {
+ const name = name_index.slice(conf);
+
+ if (Io.Dir.path.isAbsolute(name)) {
+ if (try checkCandidate(maker, step, found_path, &err_msg, name)) return;
+
+ continue;
+ }
+
+ for (graph.search_prefixes.items) |search_prefix| {
+ const full_path = try Io.Dir.path.join(arena, &.{ search_prefix, "bin", name });
+
+ if (try checkCandidate(maker, step, found_path, &err_msg, full_path)) return;
+ }
+ }
+
+ if (graph.environ_map.get("PATH")) |PATH| {
+ for (names) |name_index| {
+ const name = name_index.slice(conf);
+
+ var it = std.mem.tokenizeScalar(u8, PATH, Io.Dir.path.delimiter);
+ while (it.next()) |p| {
+ const full_path = try Io.Dir.path.join(arena, &.{ p, name });
+
+ if (try checkCandidate(maker, step, found_path, &err_msg, full_path)) return;
+ }
+ }
+ }
+
+ assert(err_msg.items[err_msg.items.len - 1] == '\n');
+ const chopped = err_msg.items[0 .. err_msg.items.len - 1];
+ try step.result_error_msgs.append(arena, chopped);
+ return error.MakeFailed;
+}
+
+fn checkCandidate(
+ maker: *Maker,
+ step: *Step,
+ found_path: Configuration.GeneratedFileIndex,
+ err_msg: *std.ArrayList(u8),
+ full_path: []const u8,
+) !bool {
+ const graph = maker.graph;
+ const arena = graph.arena; // TODO don't leak into process arena
+ const io = graph.io;
+
+ if (Io.Dir.cwd().access(io, full_path, .{ .execute = true })) |_| {
+ maker.generatedPath(found_path).* = .initCwd(full_path);
+ return true;
+ } else |err| switch (err) {
+ error.Canceled => |e| return e,
+ error.FileNotFound, error.AccessDenied, error.PermissionDenied => |e| {
+ try err_msg.print(arena, "{t} {s}\n", .{ e, full_path });
+ },
+ else => |e| return step.fail(maker, "failed accessing {s}: {t}", .{ full_path, e }),
+ }
+
+ if (builtin.os.tag == .windows) {
+ if (graph.environ_map.get("PATHEXT")) |PATHEXT| {
+ var it = std.mem.tokenizeScalar(u8, PATHEXT, Io.Dir.path.delimiter);
+ while (it.next()) |ext| {
+ if (!supportedWindowsProgramExtension(ext)) continue;
+
+ const extended_path = try std.mem.concat(arena, &.{ full_path, ext });
+
+ if (Io.Dir.cwd().access(io, extended_path, .{ .execute = true })) |_| {
+ maker.generatedPath(found_path).* = .initCwd(extended_path);
+ return true;
+ } 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 });
+ },
+ else => |e| return step.fail(maker, "failed accessing {s}: {t}", .{ extended_path, e }),
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+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;
+}
diff --git a/lib/compiler/configurer.zig b/lib/compiler/configurer.zig
@@ -906,7 +906,13 @@ fn serialize(b: *std.Build, wc: *Configuration.Wip, writer: *Io.Writer) !void {
.msg = sf.error_msg,
});
},
- .find_program => @panic("TODO"),
+ .find_program => e: {
+ const fp: *Step.FindProgram = @fieldParentPtr("step", step);
+ break :e try wc.addExtraErased(Configuration.Step.FindProgram, .{
+ .names = fp.names,
+ .found_path = fp.found_path,
+ });
+ },
.fmt => e: {
const sf: *Step.Fmt = @fieldParentPtr("step", step);
break :e try wc.addExtraErased(Configuration.Step.Fmt, .{
diff --git a/lib/std/Build.zig b/lib/std/Build.zig
@@ -1719,14 +1719,15 @@ 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.
///
+/// 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:
/// * `findProgram`
-pub fn findProgramLazy(b: *Build, names: []const []const u8) LazyPath {
- const graph = b.graph;
- const wc = &graph.wip_configuration;
- const string_list = wc.addStringList(names) catch @panic("OOM");
- _ = string_list;
- @panic("TODO");
+pub fn findProgramLazy(b: *Build, options: Step.FindProgram.Options) LazyPath {
+ return .{ .generated = .{ .index = Step.FindProgram.create(b, options).found_path } };
}
/// Immediately (in the configure phase), searches for an executable on the host
diff --git a/lib/std/Build/Configuration.zig b/lib/std/Build/Configuration.zig
@@ -1192,9 +1192,9 @@ pub const Step = extern struct {
};
pub const FindProgram = struct {
- flags: @This().Flags,
+ flags: @This().Flags = .{},
names: StringList,
- generated_file: GeneratedFileIndex,
+ found_path: GeneratedFileIndex,
pub const Flags = packed struct(u32) {
tag: Tag = .find_program,
diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig
@@ -78,19 +78,20 @@ pub fn Type(comptime tag: Tag) type {
}
pub const CheckFile = @import("Step/CheckFile.zig");
+pub const Compile = @import("Step/Compile.zig");
pub const ConfigHeader = @import("Step/ConfigHeader.zig");
pub const Fail = @import("Step/Fail.zig");
+pub const FindProgram = @import("Step/FindProgram.zig");
pub const Fmt = @import("Step/Fmt.zig");
pub const InstallArtifact = @import("Step/InstallArtifact.zig");
pub const InstallDir = @import("Step/InstallDir.zig");
pub const InstallFile = @import("Step/InstallFile.zig");
pub const ObjCopy = @import("Step/ObjCopy.zig");
-pub const Compile = @import("Step/Compile.zig");
pub const Options = @import("Step/Options.zig");
pub const Run = @import("Step/Run.zig");
pub const TranslateC = @import("Step/TranslateC.zig");
-pub const WriteFile = @import("Step/WriteFile.zig");
pub const UpdateSourceFiles = @import("Step/UpdateSourceFiles.zig");
+pub const WriteFile = @import("Step/WriteFile.zig");
pub const TopLevel = struct {
pub const base_tag: Step.Tag = .top_level;
diff --git a/lib/std/Build/Step/FindProgram.zig b/lib/std/Build/Step/FindProgram.zig
@@ -0,0 +1,31 @@
+const FindProgram = @This();
+
+const std = @import("std");
+const Step = std.Build.Step;
+const Configuration = std.Build.Configuration;
+
+step: Step,
+found_path: Configuration.GeneratedFileIndex,
+names: Configuration.StringList,
+
+pub const base_tag: Step.Tag = .find_program;
+
+pub const Options = struct {
+ names: []const []const u8,
+};
+
+pub fn create(owner: *std.Build, options: Options) *FindProgram {
+ const graph = owner.graph;
+ const wc = &graph.wip_configuration;
+ const fp = graph.create(FindProgram);
+ fp.* = .{
+ .step = .init(.{
+ .tag = base_tag,
+ .name = owner.fmt("find program {s} ({d} candidates)", .{ options.names[0], options.names.len }),
+ .owner = owner,
+ }),
+ .found_path = graph.addGeneratedFile(&fp.step),
+ .names = wc.addStringList(options.names) catch @panic("OOM"),
+ };
+ return fp;
+}
diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig
@@ -409,7 +409,10 @@ pub const PathNameError = error{
};
pub const AccessError = error{
+ /// The requested `AccessOptions` would be denied to the file, or search
+ /// permission is denied for one of the directories in the path prefix.
AccessDenied,
+ /// Write permission was requested but the file is immutable.
PermissionDenied,
FileNotFound,
InputOutput,