Commit d3a163f868
Changed files (7)
lib
std
Build
Step
test
standalone
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,
};
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,
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);
+}
test/standalone/dirname/exists_in.zig
@@ -0,0 +1,46 @@
+//! Verifies that a file exists in a directory.
+//!
+//! Usage:
+//!
+//! ```
+//! exists_in <dir> <path>
+//! ```
+//!
+//! Where `<dir>/<path>` is the full path to the file.
+//! `<dir>` must be an absolute path.
+
+const std = @import("std");
+
+pub fn main() !void {
+ var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ const arena = arena_state.allocator();
+ defer arena_state.deinit();
+
+ try run(arena);
+}
+
+fn run(allocator: std.mem.Allocator) !void {
+ var args = try std.process.argsWithAllocator(allocator);
+ defer args.deinit();
+ _ = args.next() orelse unreachable; // skip binary name
+
+ const dir_path = args.next() orelse {
+ std.log.err("missing <dir> argument", .{});
+ return error.BadUsage;
+ };
+
+ if (!std.fs.path.isAbsolute(dir_path)) {
+ std.log.err("expected <dir> to be an absolute path", .{});
+ return error.BadUsage;
+ }
+
+ const relpath = args.next() orelse {
+ std.log.err("missing <path> argument", .{});
+ return error.BadUsage;
+ };
+
+ var dir = try std.fs.openDirAbsolute(dir_path, .{});
+ defer dir.close();
+
+ _ = try dir.statFile(relpath);
+}
test/standalone/dirname/has_basename.zig
@@ -0,0 +1,50 @@
+//! Checks that the basename of the given path matches a string.
+//!
+//! Usage:
+//!
+//! ```
+//! has_basename <path> <basename>
+//! ```
+//!
+//! <path> must be absolute.
+//!
+//! Returns a non-zero exit code if basename
+//! does not match the given string.
+
+const std = @import("std");
+
+pub fn main() !void {
+ var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ const arena = arena_state.allocator();
+ defer arena_state.deinit();
+
+ try run(arena);
+}
+
+fn run(allocator: std.mem.Allocator) !void {
+ var args = try std.process.argsWithAllocator(allocator);
+ defer args.deinit();
+ _ = args.next() orelse unreachable; // skip binary name
+
+ const path = args.next() orelse {
+ std.log.err("missing <path> argument", .{});
+ return error.BadUsage;
+ };
+
+ if (!std.fs.path.isAbsolute(path)) {
+ std.log.err("path must be absolute", .{});
+ return error.BadUsage;
+ }
+
+ const basename = args.next() orelse {
+ std.log.err("missing <basename> argument", .{});
+ return error.BadUsage;
+ };
+
+ const actual_basename = std.fs.path.basename(path);
+ if (std.mem.eql(u8, actual_basename, basename)) {
+ return;
+ }
+
+ return error.NotEqual;
+}
test/standalone/dirname/touch.zig
@@ -0,0 +1,44 @@
+//! Creates a file at the given path, if it doesn't already exist.
+//!
+//! ```
+//! touch <path>
+//! ```
+//!
+//! Path must be absolute.
+
+const std = @import("std");
+
+pub fn main() !void {
+ var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ const arena = arena_state.allocator();
+ defer arena_state.deinit();
+
+ try run(arena);
+}
+
+fn run(allocator: std.mem.Allocator) !void {
+ var args = try std.process.argsWithAllocator(allocator);
+ defer args.deinit();
+ _ = args.next() orelse unreachable; // skip binary name
+
+ const path = args.next() orelse {
+ std.log.err("missing <path> argument", .{});
+ return error.BadUsage;
+ };
+
+ if (!std.fs.path.isAbsolute(path)) {
+ std.log.err("path must be absolute: {s}", .{path});
+ return error.BadUsage;
+ }
+
+ const dir_path = std.fs.path.dirname(path) orelse unreachable;
+ const basename = std.fs.path.basename(path);
+
+ var dir = try std.fs.openDirAbsolute(dir_path, .{});
+ defer dir.close();
+
+ _ = dir.statFile(basename) catch {
+ var file = try dir.createFile(basename, .{});
+ file.close();
+ };
+}
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"),