authorgravatar for mail@abhinavg.netAbhinav Gupta <mail@abhinavg.net> 2024-01-04 15:47:28-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2024-01-04 18:47:28-05:00
logd3a163f86845bcc7d20e5fd54174e480d1a8ac33
tree4e4085a2798bffeee7a7ef6167bd9c833c12d06d
parent501a2350ab804f3e5d4253826bcdfb7a3f5d92fb
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

build/LazyPath: Add dirname (#18371)

Adds a variant to the LazyPath union representing a parent directory of a generated path. ```zig const LazyPath = union(enum) { generated_dirname: struct { generated: *const GeneratedFile, up: usize, }, // ... } ``` These can be constructed with the new method: ```zig pub fn dirname(self: LazyPath) LazyPath ``` For the cases where the LazyPath is already known (`.path`, `.cwd_relative`, and `dependency`) this is evaluated right away. For dirnames of generated files and their dirnames, this is evaluated at getPath time. dirname calls can be chained, but for safety, they are not allowed to escape outside a root defined for each case: - path: This is relative to the build root, so dirname can't escape outside the build root. - generated: Can't escape the zig-cache. - cwd_relative: This can be a relative or absolute path. If relative, can't escape the current directory, and if absolute, can't go beyond root (/). - dependency: Can't escape the dependency's root directory. Testing: I've included a standalone case for many of the happy cases. I couldn't find an easy way to test the negatives, though, because tests cannot yet expect panics.

7 files changed, 406 insertions(+), 1 deletions(-)

lib/std/Build.zig+177
......@@ -1871,6 +1871,36 @@ pub const GeneratedFile = struct {
18711871 }
18721872};
18731873
1874// dirnameAllowEmpty is a variant of fs.path.dirname
1875// that allows "" to refer to the root for relative paths.
1876//
1877// For context, dirname("foo") and dirname("") are both null.
1878// However, for relative paths, we want dirname("foo") to be ""
1879// so that we can join it with another path (e.g. build root, cache root, etc.)
1880//
1881// dirname("") should still be null, because we can't go up any further.
1882fn dirnameAllowEmpty(path: []const u8) ?[]const u8 {
1883 return fs.path.dirname(path) orelse {
1884 if (fs.path.isAbsolute(path) or path.len == 0) return null;
1885
1886 return "";
1887 };
1888}
1889
1890test dirnameAllowEmpty {
1891 try std.testing.expectEqualStrings(
1892 "foo",
1893 dirnameAllowEmpty("foo" ++ fs.path.sep_str ++ "bar") orelse @panic("unexpected null"),
1894 );
1895
1896 try std.testing.expectEqualStrings(
1897 "",
1898 dirnameAllowEmpty("foo") orelse @panic("unexpected null"),
1899 );
1900
1901 try std.testing.expect(dirnameAllowEmpty("") == null);
1902}
1903
18741904/// A reference to an existing or future path.
18751905pub const LazyPath = union(enum) {
18761906 /// A source file path relative to build root.
......@@ -1882,6 +1912,17 @@ pub const LazyPath = union(enum) {
18821912 /// not available until built by a build step.
18831913 generated: *const GeneratedFile,
18841914
1915 /// One of the parent directories of a file generated by an interface.
1916 /// The path is not available until built by a build step.
1917 generated_dirname: struct {
1918 generated: *const GeneratedFile,
1919
1920 /// The number of parent directories to go up.
1921 /// 0 means the directory of the generated file,
1922 /// 1 means the parent of that directory, and so on.
1923 up: usize,
1924 },
1925
18851926 /// An absolute path or a path relative to the current working directory of
18861927 /// the build runner process.
18871928 /// This is uncommon but used for system environment paths such as `--zig-lib-dir` which
......@@ -1902,12 +1943,72 @@ pub const LazyPath = union(enum) {
19021943 return LazyPath{ .path = path };
19031944 }
19041945
1946 /// Returns a lazy path referring to the directory containing this path.
1947 ///
1948 /// The dirname is not allowed to escape the logical root for underlying path.
1949 /// For example, if the path is relative to the build root,
1950 /// the dirname is not allowed to traverse outside of the build root.
1951 /// Similarly, if the path is a generated file inside zig-cache,
1952 /// the dirname is not allowed to traverse outside of zig-cache.
1953 pub fn dirname(self: LazyPath) LazyPath {
1954 return switch (self) {
1955 .generated => |gen| .{ .generated_dirname = .{ .generated = gen, .up = 0 } },
1956 .generated_dirname => |gen| .{ .generated_dirname = .{ .generated = gen.generated, .up = gen.up + 1 } },
1957 .path => |p| .{
1958 .path = dirnameAllowEmpty(p) orelse {
1959 dumpBadDirnameHelp(null, null,
1960 \\dirname() attempted to traverse outside the build root.
1961 \\This is not allowed.
1962 \\
1963 , .{}) catch {};
1964 @panic("misconfigured build script");
1965 },
1966 },
1967 .cwd_relative => |p| .{
1968 .cwd_relative = dirnameAllowEmpty(p) orelse {
1969 // If we get null, it means one of two things:
1970 // - p was absolute, and is now root
1971 // - p was relative, and is now ""
1972 // In either case, the build script tried to go too far
1973 // and we should panic.
1974 if (fs.path.isAbsolute(p)) {
1975 dumpBadDirnameHelp(null, null,
1976 \\dirname() attempted to traverse outside the root.
1977 \\No more directories left to go up.
1978 \\
1979 , .{}) catch {};
1980 @panic("misconfigured build script");
1981 } else {
1982 dumpBadDirnameHelp(null, null,
1983 \\dirname() attempted to traverse outside the current working directory.
1984 \\This is not allowed.
1985 \\
1986 , .{}) catch {};
1987 @panic("misconfigured build script");
1988 }
1989 },
1990 },
1991 .dependency => |dep| .{ .dependency = .{
1992 .dependency = dep.dependency,
1993 .sub_path = dirnameAllowEmpty(dep.sub_path) orelse {
1994 dumpBadDirnameHelp(null, null,
1995 \\dirname() attempted to traverse outside the dependency root.
1996 \\This is not allowed.
1997 \\
1998 , .{}) catch {};
1999 @panic("misconfigured build script");
2000 },
2001 } },
2002 };
2003 }
2004
19052005 /// Returns a string that can be shown to represent the file source.
19062006 /// Either returns the path or `"generated"`.
19072007 pub fn getDisplayName(self: LazyPath) []const u8 {
19082008 return switch (self) {
19092009 .path, .cwd_relative => self.path,
19102010 .generated => "generated",
2011 .generated_dirname => "generated",
19112012 .dependency => "dependency",
19122013 };
19132014 }
......@@ -1917,6 +2018,7 @@ pub const LazyPath = union(enum) {
19172018 switch (self) {
19182019 .path, .cwd_relative, .dependency => {},
19192020 .generated => |gen| other_step.dependOn(gen.step),
2021 .generated_dirname => |gen| other_step.dependOn(gen.generated.step),
19202022 }
19212023 }
19222024
......@@ -1941,6 +2043,39 @@ pub const LazyPath = union(enum) {
19412043 dumpBadGetPathHelp(gen.step, stderr, src_builder, asking_step) catch {};
19422044 @panic("misconfigured build script");
19432045 },
2046 .generated_dirname => |gen| {
2047 const cache_root_path = src_builder.cache_root.path orelse
2048 (src_builder.cache_root.join(src_builder.allocator, &.{"."}) catch @panic("OOM"));
2049
2050 const gen_step = gen.generated.step;
2051 var path = getPath2(LazyPath{ .generated = gen.generated }, src_builder, asking_step);
2052 var i: usize = 0;
2053 while (i <= gen.up) : (i += 1) {
2054 // path is absolute.
2055 // dirname will return null only if we're at root.
2056 // Typically, we'll stop well before that at the cache root.
2057 path = fs.path.dirname(path) orelse {
2058 dumpBadDirnameHelp(gen_step, asking_step,
2059 \\dirname() reached root.
2060 \\No more directories left to go up.
2061 \\
2062 , .{}) catch {};
2063 @panic("misconfigured build script");
2064 };
2065
2066 if (mem.eql(u8, path, cache_root_path) and i < gen.up) {
2067 // If we hit the cache root and there's still more to go,
2068 // the script attempted to go too far.
2069 dumpBadDirnameHelp(gen_step, asking_step,
2070 \\dirname() attempted to traverse outside the cache root.
2071 \\This is not allowed.
2072 \\
2073 , .{}) catch {};
2074 @panic("misconfigured build script");
2075 }
2076 }
2077 return path;
2078 },
19442079 .dependency => |dep| {
19452080 return dep.dependency.builder.pathJoin(&[_][]const u8{
19462081 dep.dependency.builder.build_root.path.?,
......@@ -1956,11 +2091,53 @@ pub const LazyPath = union(enum) {
19562091 .path => |p| .{ .path = b.dupePath(p) },
19572092 .cwd_relative => |p| .{ .cwd_relative = b.dupePath(p) },
19582093 .generated => |gen| .{ .generated = gen },
2094 .generated_dirname => |gen| .{
2095 .generated_dirname = .{
2096 .generated = gen.generated,
2097 .up = gen.up,
2098 },
2099 },
19592100 .dependency => |dep| .{ .dependency = dep },
19602101 };
19612102 }
19622103};
19632104
2105fn dumpBadDirnameHelp(
2106 fail_step: ?*Step,
2107 asking_step: ?*Step,
2108 comptime msg: []const u8,
2109 args: anytype,
2110) anyerror!void {
2111 debug.getStderrMutex().lock();
2112 defer debug.getStderrMutex().unlock();
2113
2114 const stderr = io.getStdErr();
2115 const w = stderr.writer();
2116 try w.print(msg, args);
2117
2118 const tty_config = std.io.tty.detectConfig(stderr);
2119
2120 if (fail_step) |s| {
2121 tty_config.setColor(w, .red) catch {};
2122 try stderr.writeAll(" The step was created by this stack trace:\n");
2123 tty_config.setColor(w, .reset) catch {};
2124
2125 s.dump(stderr);
2126 }
2127
2128 if (asking_step) |as| {
2129 tty_config.setColor(w, .red) catch {};
2130 try stderr.writer().print(" The step '{s}' that is missing a dependency on the above step was created by this stack trace:\n", .{as.name});
2131 tty_config.setColor(w, .reset) catch {};
2132
2133 as.dump(stderr);
2134 }
2135
2136 tty_config.setColor(w, .red) catch {};
2137 try stderr.writeAll(" Hope that helps. Proceeding to panic.\n");
2138 tty_config.setColor(w, .reset) catch {};
2139}
2140
19642141/// In this function the stderr mutex has already been locked.
19652142pub fn dumpBadGetPathHelp(
19662143 s: *Step,
lib/std/Build/Step/ConfigHeader.zig+1-1
......@@ -59,7 +59,7 @@ pub fn create(owner: *std.Build, options: Options) *ConfigHeader {
5959 if (options.style.getPath()) |s| default_include_path: {
6060 const sub_path = switch (s) {
6161 .path => |path| path,
62 .generated => break :default_include_path,
62 .generated, .generated_dirname => break :default_include_path,
6363 .cwd_relative => |sub_path| sub_path,
6464 .dependency => |dependency| dependency.sub_path,
6565 };
test/standalone.zig+4
......@@ -179,6 +179,10 @@ pub const build_cases = [_]BuildCase{
179179 .build_root = "test/standalone/dep_shared_builtin",
180180 .import = @import("standalone/dep_shared_builtin/build.zig"),
181181 },
182 .{
183 .build_root = "test/standalone/dirname",
184 .import = @import("standalone/dirname/build.zig"),
185 },
182186 .{
183187 .build_root = "test/standalone/empty_env",
184188 .import = @import("standalone/empty_env/build.zig"),
test/standalone/dirname/build.zig created+84
......@@ -0,0 +1,84 @@
1const std = @import("std");
2
3pub fn build(b: *std.Build) void {
4 const target = b.standardTargetOptions(.{});
5
6 const test_step = b.step("test", "Test it");
7 b.default_step = test_step;
8
9 const touch_src = std.Build.LazyPath{
10 .path = "touch.zig",
11 };
12
13 const touch = b.addExecutable(.{
14 .name = "touch",
15 .root_source_file = touch_src,
16 .optimize = .Debug,
17 .target = target,
18 });
19 const generated = b.addRunArtifact(touch).addOutputFileArg("subdir" ++ std.fs.path.sep_str ++ "generated.txt");
20
21 const exists_in = b.addExecutable(.{
22 .name = "exists_in",
23 .root_source_file = .{ .path = "exists_in.zig" },
24 .optimize = .Debug,
25 .target = target,
26 });
27
28 const has_basename = b.addExecutable(.{
29 .name = "has_basename",
30 .root_source_file = .{ .path = "has_basename.zig" },
31 .optimize = .Debug,
32 .target = target,
33 });
34
35 // Known path:
36 addTestRun(test_step, exists_in, touch_src.dirname(), &.{"touch.zig"});
37
38 // Generated file:
39 addTestRun(test_step, exists_in, generated.dirname(), &.{"generated.txt"});
40
41 // Generated file multiple levels:
42 addTestRun(test_step, exists_in, generated.dirname().dirname(), &.{
43 "subdir" ++ std.fs.path.sep_str ++ "generated.txt",
44 });
45
46 // Cache root:
47 const cache_dir = b.cache_root.path orelse
48 (b.cache_root.join(b.allocator, &.{"."}) catch @panic("OOM"));
49 addTestRun(
50 test_step,
51 has_basename,
52 generated.dirname().dirname().dirname().dirname(),
53 &.{std.fs.path.basename(cache_dir)},
54 );
55
56 // Absolute path:
57 const abs_path = setup_abspath: {
58 const temp_dir = b.makeTempPath();
59
60 var dir = std.fs.openDirAbsolute(temp_dir, .{}) catch @panic("failed to open temp dir");
61 defer dir.close();
62
63 var file = dir.createFile("foo.txt", .{}) catch @panic("failed to create file");
64 file.close();
65
66 break :setup_abspath std.Build.LazyPath{ .cwd_relative = temp_dir };
67 };
68 addTestRun(test_step, exists_in, abs_path, &.{"foo.txt"});
69}
70
71// Runs exe with the parameters [dirname, args...].
72// Expects the exit code to be 0.
73fn addTestRun(
74 test_step: *std.Build.Step,
75 exe: *std.Build.Step.Compile,
76 dirname: std.Build.LazyPath,
77 args: []const []const u8,
78) void {
79 const run = test_step.owner.addRunArtifact(exe);
80 run.addDirectoryArg(dirname);
81 run.addArgs(args);
82 run.expectExitCode(0);
83 test_step.dependOn(&run.step);
84}
test/standalone/dirname/exists_in.zig created+46
......@@ -0,0 +1,46 @@
1//! Verifies that a file exists in a directory.
2//!
3//! Usage:
4//!
5//! ```
6//! exists_in <dir> <path>
7//! ```
8//!
9//! Where `<dir>/<path>` is the full path to the file.
10//! `<dir>` must be an absolute path.
11
12const std = @import("std");
13
14pub fn main() !void {
15 var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
16 const arena = arena_state.allocator();
17 defer arena_state.deinit();
18
19 try run(arena);
20}
21
22fn run(allocator: std.mem.Allocator) !void {
23 var args = try std.process.argsWithAllocator(allocator);
24 defer args.deinit();
25 _ = args.next() orelse unreachable; // skip binary name
26
27 const dir_path = args.next() orelse {
28 std.log.err("missing <dir> argument", .{});
29 return error.BadUsage;
30 };
31
32 if (!std.fs.path.isAbsolute(dir_path)) {
33 std.log.err("expected <dir> to be an absolute path", .{});
34 return error.BadUsage;
35 }
36
37 const relpath = args.next() orelse {
38 std.log.err("missing <path> argument", .{});
39 return error.BadUsage;
40 };
41
42 var dir = try std.fs.openDirAbsolute(dir_path, .{});
43 defer dir.close();
44
45 _ = try dir.statFile(relpath);
46}
test/standalone/dirname/has_basename.zig created+50
......@@ -0,0 +1,50 @@
1//! Checks that the basename of the given path matches a string.
2//!
3//! Usage:
4//!
5//! ```
6//! has_basename <path> <basename>
7//! ```
8//!
9//! <path> must be absolute.
10//!
11//! Returns a non-zero exit code if basename
12//! does not match the given string.
13
14const std = @import("std");
15
16pub fn main() !void {
17 var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
18 const arena = arena_state.allocator();
19 defer arena_state.deinit();
20
21 try run(arena);
22}
23
24fn run(allocator: std.mem.Allocator) !void {
25 var args = try std.process.argsWithAllocator(allocator);
26 defer args.deinit();
27 _ = args.next() orelse unreachable; // skip binary name
28
29 const path = args.next() orelse {
30 std.log.err("missing <path> argument", .{});
31 return error.BadUsage;
32 };
33
34 if (!std.fs.path.isAbsolute(path)) {
35 std.log.err("path must be absolute", .{});
36 return error.BadUsage;
37 }
38
39 const basename = args.next() orelse {
40 std.log.err("missing <basename> argument", .{});
41 return error.BadUsage;
42 };
43
44 const actual_basename = std.fs.path.basename(path);
45 if (std.mem.eql(u8, actual_basename, basename)) {
46 return;
47 }
48
49 return error.NotEqual;
50}
test/standalone/dirname/touch.zig created+44
......@@ -0,0 +1,44 @@
1//! Creates a file at the given path, if it doesn't already exist.
2//!
3//! ```
4//! touch <path>
5//! ```
6//!
7//! Path must be absolute.
8
9const std = @import("std");
10
11pub fn main() !void {
12 var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
13 const arena = arena_state.allocator();
14 defer arena_state.deinit();
15
16 try run(arena);
17}
18
19fn run(allocator: std.mem.Allocator) !void {
20 var args = try std.process.argsWithAllocator(allocator);
21 defer args.deinit();
22 _ = args.next() orelse unreachable; // skip binary name
23
24 const path = args.next() orelse {
25 std.log.err("missing <path> argument", .{});
26 return error.BadUsage;
27 };
28
29 if (!std.fs.path.isAbsolute(path)) {
30 std.log.err("path must be absolute: {s}", .{path});
31 return error.BadUsage;
32 }
33
34 const dir_path = std.fs.path.dirname(path) orelse unreachable;
35 const basename = std.fs.path.basename(path);
36
37 var dir = try std.fs.openDirAbsolute(dir_path, .{});
38 defer dir.close();
39
40 _ = dir.statFile(basename) catch {
41 var file = try dir.createFile(basename, .{});
42 file.close();
43 };
44}