diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 01609320e7..449bee1e2a 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -1871,6 +1871,36 @@ pub const GeneratedFile = struct { } }; +// dirnameAllowEmpty is a variant of fs.path.dirname +// that allows "" to refer to the root for relative paths. +// +// For context, dirname("foo") and dirname("") are both null. +// However, for relative paths, we want dirname("foo") to be "" +// so that we can join it with another path (e.g. build root, cache root, etc.) +// +// dirname("") should still be null, because we can't go up any further. +fn dirnameAllowEmpty(path: []const u8) ?[]const u8 { + return fs.path.dirname(path) orelse { + if (fs.path.isAbsolute(path) or path.len == 0) return null; + + return ""; + }; +} + +test dirnameAllowEmpty { + try std.testing.expectEqualStrings( + "foo", + dirnameAllowEmpty("foo" ++ fs.path.sep_str ++ "bar") orelse @panic("unexpected null"), + ); + + try std.testing.expectEqualStrings( + "", + dirnameAllowEmpty("foo") orelse @panic("unexpected null"), + ); + + try std.testing.expect(dirnameAllowEmpty("") == null); +} + /// A reference to an existing or future path. pub const LazyPath = union(enum) { /// A source file path relative to build root. @@ -1882,6 +1912,17 @@ pub const LazyPath = union(enum) { /// not available until built by a build step. generated: *const GeneratedFile, + /// One of the parent directories of a file generated by an interface. + /// The path is not available until built by a build step. + generated_dirname: struct { + generated: *const GeneratedFile, + + /// The number of parent directories to go up. + /// 0 means the directory of the generated file, + /// 1 means the parent of that directory, and so on. + up: usize, + }, + /// An absolute path or a path relative to the current working directory of /// the build runner process. /// This is uncommon but used for system environment paths such as `--zig-lib-dir` which @@ -1902,12 +1943,72 @@ pub const LazyPath = union(enum) { return LazyPath{ .path = path }; } + /// Returns a lazy path referring to the directory containing this path. + /// + /// The dirname is not allowed to escape the logical root for underlying path. + /// For example, if the path is relative to the build root, + /// the dirname is not allowed to traverse outside of the build root. + /// Similarly, if the path is a generated file inside zig-cache, + /// the dirname is not allowed to traverse outside of zig-cache. + pub fn dirname(self: LazyPath) LazyPath { + return switch (self) { + .generated => |gen| .{ .generated_dirname = .{ .generated = gen, .up = 0 } }, + .generated_dirname => |gen| .{ .generated_dirname = .{ .generated = gen.generated, .up = gen.up + 1 } }, + .path => |p| .{ + .path = dirnameAllowEmpty(p) orelse { + dumpBadDirnameHelp(null, null, + \\dirname() attempted to traverse outside the build root. + \\This is not allowed. + \\ + , .{}) catch {}; + @panic("misconfigured build script"); + }, + }, + .cwd_relative => |p| .{ + .cwd_relative = dirnameAllowEmpty(p) orelse { + // If we get null, it means one of two things: + // - p was absolute, and is now root + // - p was relative, and is now "" + // In either case, the build script tried to go too far + // and we should panic. + if (fs.path.isAbsolute(p)) { + dumpBadDirnameHelp(null, null, + \\dirname() attempted to traverse outside the root. + \\No more directories left to go up. + \\ + , .{}) catch {}; + @panic("misconfigured build script"); + } else { + dumpBadDirnameHelp(null, null, + \\dirname() attempted to traverse outside the current working directory. + \\This is not allowed. + \\ + , .{}) catch {}; + @panic("misconfigured build script"); + } + }, + }, + .dependency => |dep| .{ .dependency = .{ + .dependency = dep.dependency, + .sub_path = dirnameAllowEmpty(dep.sub_path) orelse { + dumpBadDirnameHelp(null, null, + \\dirname() attempted to traverse outside the dependency root. + \\This is not allowed. + \\ + , .{}) catch {}; + @panic("misconfigured build script"); + }, + } }, + }; + } + /// Returns a string that can be shown to represent the file source. /// Either returns the path or `"generated"`. pub fn getDisplayName(self: LazyPath) []const u8 { return switch (self) { .path, .cwd_relative => self.path, .generated => "generated", + .generated_dirname => "generated", .dependency => "dependency", }; } @@ -1917,6 +2018,7 @@ pub const LazyPath = union(enum) { switch (self) { .path, .cwd_relative, .dependency => {}, .generated => |gen| other_step.dependOn(gen.step), + .generated_dirname => |gen| other_step.dependOn(gen.generated.step), } } @@ -1941,6 +2043,39 @@ pub const LazyPath = union(enum) { dumpBadGetPathHelp(gen.step, stderr, src_builder, asking_step) catch {}; @panic("misconfigured build script"); }, + .generated_dirname => |gen| { + const cache_root_path = src_builder.cache_root.path orelse + (src_builder.cache_root.join(src_builder.allocator, &.{"."}) catch @panic("OOM")); + + const gen_step = gen.generated.step; + var path = getPath2(LazyPath{ .generated = gen.generated }, src_builder, asking_step); + var i: usize = 0; + while (i <= gen.up) : (i += 1) { + // path is absolute. + // dirname will return null only if we're at root. + // Typically, we'll stop well before that at the cache root. + path = fs.path.dirname(path) orelse { + dumpBadDirnameHelp(gen_step, asking_step, + \\dirname() reached root. + \\No more directories left to go up. + \\ + , .{}) catch {}; + @panic("misconfigured build script"); + }; + + if (mem.eql(u8, path, cache_root_path) and i < gen.up) { + // If we hit the cache root and there's still more to go, + // the script attempted to go too far. + dumpBadDirnameHelp(gen_step, asking_step, + \\dirname() attempted to traverse outside the cache root. + \\This is not allowed. + \\ + , .{}) catch {}; + @panic("misconfigured build script"); + } + } + return path; + }, .dependency => |dep| { return dep.dependency.builder.pathJoin(&[_][]const u8{ dep.dependency.builder.build_root.path.?, @@ -1956,11 +2091,53 @@ pub const LazyPath = union(enum) { .path => |p| .{ .path = b.dupePath(p) }, .cwd_relative => |p| .{ .cwd_relative = b.dupePath(p) }, .generated => |gen| .{ .generated = gen }, + .generated_dirname => |gen| .{ + .generated_dirname = .{ + .generated = gen.generated, + .up = gen.up, + }, + }, .dependency => |dep| .{ .dependency = dep }, }; } }; +fn dumpBadDirnameHelp( + fail_step: ?*Step, + asking_step: ?*Step, + comptime msg: []const u8, + args: anytype, +) anyerror!void { + debug.getStderrMutex().lock(); + defer debug.getStderrMutex().unlock(); + + const stderr = io.getStdErr(); + const w = stderr.writer(); + try w.print(msg, args); + + const tty_config = std.io.tty.detectConfig(stderr); + + if (fail_step) |s| { + tty_config.setColor(w, .red) catch {}; + try stderr.writeAll(" The step was created by this stack trace:\n"); + tty_config.setColor(w, .reset) catch {}; + + s.dump(stderr); + } + + if (asking_step) |as| { + tty_config.setColor(w, .red) catch {}; + 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}); + tty_config.setColor(w, .reset) catch {}; + + as.dump(stderr); + } + + tty_config.setColor(w, .red) catch {}; + try stderr.writeAll(" Hope that helps. Proceeding to panic.\n"); + tty_config.setColor(w, .reset) catch {}; +} + /// In this function the stderr mutex has already been locked. pub fn dumpBadGetPathHelp( s: *Step, diff --git a/lib/std/Build/Step/ConfigHeader.zig b/lib/std/Build/Step/ConfigHeader.zig index c352b2460c..59b8f610a2 100644 --- a/lib/std/Build/Step/ConfigHeader.zig +++ b/lib/std/Build/Step/ConfigHeader.zig @@ -59,7 +59,7 @@ pub fn create(owner: *std.Build, options: Options) *ConfigHeader { if (options.style.getPath()) |s| default_include_path: { const sub_path = switch (s) { .path => |path| path, - .generated => break :default_include_path, + .generated, .generated_dirname => break :default_include_path, .cwd_relative => |sub_path| sub_path, .dependency => |dependency| dependency.sub_path, }; diff --git a/test/standalone.zig b/test/standalone.zig index b26e85c159..45ba46c015 100644 --- a/test/standalone.zig +++ b/test/standalone.zig @@ -179,6 +179,10 @@ pub const build_cases = [_]BuildCase{ .build_root = "test/standalone/dep_shared_builtin", .import = @import("standalone/dep_shared_builtin/build.zig"), }, + .{ + .build_root = "test/standalone/dirname", + .import = @import("standalone/dirname/build.zig"), + }, .{ .build_root = "test/standalone/empty_env", .import = @import("standalone/empty_env/build.zig"), diff --git a/test/standalone/dirname/build.zig b/test/standalone/dirname/build.zig new file mode 100644 index 0000000000..272ed54b38 --- /dev/null +++ b/test/standalone/dirname/build.zig @@ -0,0 +1,84 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + + const test_step = b.step("test", "Test it"); + b.default_step = test_step; + + const touch_src = std.Build.LazyPath{ + .path = "touch.zig", + }; + + const touch = b.addExecutable(.{ + .name = "touch", + .root_source_file = touch_src, + .optimize = .Debug, + .target = target, + }); + const generated = b.addRunArtifact(touch).addOutputFileArg("subdir" ++ std.fs.path.sep_str ++ "generated.txt"); + + const exists_in = b.addExecutable(.{ + .name = "exists_in", + .root_source_file = .{ .path = "exists_in.zig" }, + .optimize = .Debug, + .target = target, + }); + + const has_basename = b.addExecutable(.{ + .name = "has_basename", + .root_source_file = .{ .path = "has_basename.zig" }, + .optimize = .Debug, + .target = target, + }); + + // Known path: + addTestRun(test_step, exists_in, touch_src.dirname(), &.{"touch.zig"}); + + // Generated file: + addTestRun(test_step, exists_in, generated.dirname(), &.{"generated.txt"}); + + // Generated file multiple levels: + addTestRun(test_step, exists_in, generated.dirname().dirname(), &.{ + "subdir" ++ std.fs.path.sep_str ++ "generated.txt", + }); + + // Cache root: + const cache_dir = b.cache_root.path orelse + (b.cache_root.join(b.allocator, &.{"."}) catch @panic("OOM")); + addTestRun( + test_step, + has_basename, + generated.dirname().dirname().dirname().dirname(), + &.{std.fs.path.basename(cache_dir)}, + ); + + // Absolute path: + const abs_path = setup_abspath: { + const temp_dir = b.makeTempPath(); + + var dir = std.fs.openDirAbsolute(temp_dir, .{}) catch @panic("failed to open temp dir"); + defer dir.close(); + + var file = dir.createFile("foo.txt", .{}) catch @panic("failed to create file"); + file.close(); + + break :setup_abspath std.Build.LazyPath{ .cwd_relative = temp_dir }; + }; + addTestRun(test_step, exists_in, abs_path, &.{"foo.txt"}); +} + +// Runs exe with the parameters [dirname, args...]. +// Expects the exit code to be 0. +fn addTestRun( + test_step: *std.Build.Step, + exe: *std.Build.Step.Compile, + dirname: std.Build.LazyPath, + args: []const []const u8, +) void { + const run = test_step.owner.addRunArtifact(exe); + run.addDirectoryArg(dirname); + run.addArgs(args); + run.expectExitCode(0); + test_step.dependOn(&run.step); +} diff --git a/test/standalone/dirname/exists_in.zig b/test/standalone/dirname/exists_in.zig new file mode 100644 index 0000000000..6730200b3f --- /dev/null +++ b/test/standalone/dirname/exists_in.zig @@ -0,0 +1,46 @@ +//! Verifies that a file exists in a directory. +//! +//! Usage: +//! +//! ``` +//! exists_in