Skip to content

Commit

Permalink
build_runner: add min_zig_version build.zig config
Browse files Browse the repository at this point in the history
build_runner now checks build.zig for min_zig_version configuration, i.e.

```zig
//config min_zig_version 0.10.0-dev.2836+2360f8c49
```

This provides a standard for projects to document their min zig version. In
addition, if zig detects it's too old, it will print an error:

```
error: zig is too old, have 0.9.0 but build.zig has min_zig_version 0.10.0
```

Note that supporting a max_zig_version would be more difficult.  Since
zig version numbers contain the commit height, we can tell whether Zig
is "too old", but we can't tell whether zig it "too new".  If commit height
A is less than B, then commit A is too old, but if A is greater than B,
then A is not necessarily too new because it could just be a different
branch with extra commits.
  • Loading branch information
marler8997 authored and andrewrk committed Oct 12, 2022
1 parent 0b47e69 commit 5780064
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 0 deletions.
46 changes: 46 additions & 0 deletions lib/std/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3820,3 +3820,49 @@ test "LibExeObjStep.addPackage" {
const dupe = exe.packages.items[0];
try std.testing.expectEqualStrings(pkg_top.name, dupe.name);
}

pub const Config = struct {
min_zig_version: ?std.zig.Version = null,
};

pub fn readConfig(build_dir: std.fs.Dir, basename: []const u8) !Config {
// right now we only support '//config min_zig_version VERSION' so 200 should be enough
var buf: [200]u8 = undefined;
var build_file = try build_dir.openFile(basename, .{});
defer build_file.close();

var config = Config{ };

while (true) {
const config_prefix = "//config";
const line = build_file.reader().readUntilDelimiter(&buf, '\n') catch |err| switch (err) {
error.StreamTooLong => {
if (std.mem.startsWith(u8, &buf, config_prefix))
return error.ConfigLineTooLong;
break;
},
error.EndOfStream => break,
else => |e| return e,
};
if (!std.mem.startsWith(u8, line, config_prefix)) break;
const config_str = std.mem.trimRight(u8, line[config_prefix.len..], " \r");
var it = std.mem.tokenize(u8, config_str, " ");
const name = it.next() orelse {
std.log.err("build.zig '//config' line is empty", .{});
return error.InvalidBuildConfigLine;
};
const data = blk: {
var data = it.next() orelse break :blk "";
data.len = config_str.len - (@ptrToInt(data.ptr) - @ptrToInt(config_str.ptr));
break :blk data;
};
if (std.mem.eql(u8, name, "min_zig_version")) {
config.min_zig_version = try std.zig.Version.parse(data);
} else {
std.log.err("unknown build.zig '//config {s}...'", .{name});
return error.UnknownConfig;
}
}

return config;
}
1 change: 1 addition & 0 deletions lib/std/zig.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub const number_literal = @import("zig/number_literal.zig");
pub const Ast = @import("zig/Ast.zig");
pub const system = @import("zig/system.zig");
pub const CrossTarget = @import("zig/CrossTarget.zig");
pub const Version = @import("zig/Version.zig");

// Character literal parsing
pub const ParsedCharLiteral = string_literal.ParsedCharLiteral;
Expand Down
123 changes: 123 additions & 0 deletions lib/std/zig/Version.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
const std = @import("std");
const Version = @This();

version: std.builtin.Version,
opt_dev: ?Dev,

pub fn parse(version_str: []const u8) error{InvalidZigVersion}!Version {
const semver = std.SemanticVersion.parse(version_str) catch |err| {
std.log.err("zig version '{s}' is not a valid semantic version: {s}", .{ version_str, @errorName(err) });
return error.InvalidZigVersion;
};
var result = Version{
.version = .{
.major = @intCast(u32, semver.major),
.minor = @intCast(u32, semver.minor),
.patch = @intCast(u32, semver.patch),
},
.opt_dev = null,
};
if (semver.pre) |pre| {
const dev_prefix = "dev.";
if (!std.mem.startsWith(u8, pre, dev_prefix)) {
std.log.err("invalid zig version '{s}', expected '-{s}' after major/minor/patch, but got '-{s}'", .{ version_str, dev_prefix, pre });
return error.InvalidZigVersion;
}
const commit_height_str = pre[dev_prefix.len..];
const build = semver.build orelse {
std.log.err("invalid zig version '{s}', has '-dev.COMMIT_HEIGHT' without a '+BUILD_REVISION'", .{version_str});
return error.InvalidZigVersion;
};
if (build.len > 40) {
std.log.err("invalid zig version '{s}', build revision is too long", .{version_str});
return error.InvalidZigVersion;
}
result.opt_dev = .{
.commit_height = std.fmt.parseInt(u32, commit_height_str, 10) catch |err| {
std.log.err("invalid zig version '{s}', invalid commit height '{s}': {s}", .{ version_str, commit_height_str, @errorName(err) });
return error.InvalidZigVersion;
},
.sha_buf = undefined,
.sha_len = @intCast(u8, build.len),
};
std.mem.copy(u8, result.opt_dev.?.sha_buf[0..build.len], build);
} else if (semver.build) |_| {
std.log.err("invalid zig version '{s}', has '+BUILD_REVISION' without '-dev.COMMIT_HEIGHT'", .{version_str});
return error.InvalidZigVersion;
}

return result;
}

pub const Order = enum {
eq,
lt,
gt,
/// Version and commit height are equal but hashes differ
commit_height_eq,
/// Version is equal but commit height is less than
commit_height_lt,
/// Version is equal but commit height is greater than
commit_height_gt,

pub fn inverse(self: Order) Order {
return switch (self) {
.eq => .eq,
.lt => .gt,
.gt => .lt,
.commit_height_eq => .commit_height_eq,
.commit_height_lt => .commit_height_gt,
.commit_height_gt => .commit_height_lt,
};
}
};
pub fn order(left: Version, right: Version) Order {
const left_dev = left.opt_dev orelse {
if (right.opt_dev) |_| return right.order(left).inverse();
return switch (left.version.order(right.version)) {
.eq => .eq, .lt => .lt, .gt => .gt
};

};
const right_dev = right.opt_dev orelse return switch(left.version.order(right.version)) {
// NOTE: .eq is supposed to map to .lt, the dev version is .lt if their semvers are equal
.eq => .lt, .lt => .lt, .gt => .gt,
};

switch (left.version.order(right.version)) {
.lt => return .lt,
.gt => return .gt,
.eq => {
if (left_dev.commit_height > right_dev.commit_height)
return .commit_height_gt;
if (left_dev.commit_height < right_dev.commit_height)
return .commit_height_lt;
if (!std.mem.eql(u8, left_dev.sha(), right_dev.sha()))
return .commit_height_eq;
return .eq;
},
}
}

pub fn format(
self: Version,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void {
_ = fmt;
_ = options;
try writer.print("{}.{}.{}", .{ self.version.major, self.version.minor, self.version.patch });
if (self.opt_dev) |dev| {
try writer.print("-dev.{d}+{s}", .{ dev.commit_height, dev.sha() });
}
}

pub const Dev = struct {
commit_height: u32,
sha_buf: [40]u8,
sha_len: u8,
pub fn sha(self: *const Dev) []const u8 {
return self.sha_buf[0..self.sha_len];
}
};
13 changes: 13 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3841,6 +3841,19 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi
};
child_argv.items[argv_index_build_file] = build_directory.path orelse cwd_path;

const zig_version = comptime try std.zig.Version.parse(build_options.version);
const build_config = try std.build.readConfig(build_directory.handle, build_zig_basename);
if (build_config.min_zig_version) |min_zig_version| {
switch (zig_version.order(min_zig_version)) {
.eq, .gt => {},
.commit_height_gt => {
// NOTE: having a larger commit height means we can't verify whether or not we are new enough
},
.lt, .commit_height_eq, .commit_height_lt =>
fatal("zig is too old, have {} but build.zig has min_zig_version {}", .{zig_version, min_zig_version}),
}
}

var build_pkg: Package = .{
.root_src_directory = build_directory,
.root_src_path = build_zig_basename,
Expand Down
1 change: 1 addition & 0 deletions test/standalone.zig
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub fn addCases(cases: *tests.StandaloneContext) void {
// https://github.com/ziglang/zig/issues/12419
cases.addBuildFile("test/standalone/issue_11595/build.zig", .{});
}
cases.addBuildFile("test/standalone/min_zig_version/build.zig", .{});
if (builtin.os.tag != .wasi) {
cases.addBuildFile("test/standalone/load_dynamic_library/build.zig", .{});
}
Expand Down
1 change: 1 addition & 0 deletions test/standalone/min_zig_version/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/equal-version-build.zig
52 changes: 52 additions & 0 deletions test/standalone/min_zig_version/build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const std = @import("std");

pub fn build(b: *std.build.Builder) void {
const test_step = b.step("test", "Run the test");

{
const run = testBuild(b, b.pathFromRoot("distant-future-dev-build.zig"));
run.expected_exit_code = 1;
run.stderr_action = .{ .expect_matches = &[_][]const u8 {
"error: zig is too old, have ",
" but build.zig has min_zig_version 999.999.999-dev.999999+5735ce39ae5a9fb2bc2ac9f5a722276c291b28bc\n",
}};
test_step.dependOn(&run.step);
}
{
const run = testBuild(b, b.pathFromRoot("distant-future-release-build.zig"));
run.expected_exit_code = 1;
run.stderr_action = .{ .expect_matches = &[_][]const u8 {
"error: zig is too old, have ",
" but build.zig has min_zig_version 999.999.999\n",
}};
test_step.dependOn(&run.step);
}
{
const build_zig = b.pathFromRoot("equal-version-build.zig");
const write_file = b.addWriteFile(
build_zig,
b.fmt(
\\//config min_zig_version {}
\\const std = @import("std");
\\pub fn build(b: *std.build.Builder) void {{ _ = b; }}
\\
, .{@import("builtin").zig_version},
),
);
const run = testBuild(b, build_zig);
run.stdout_action = .{ .expect_exact = "" };
run.stderr_action = .{ .expect_exact = "" };
run.step.dependOn(&write_file.step);
test_step.dependOn(&run.step);
}
}

fn testBuild(b: *std.build.Builder, build_file: []const u8) *std.build.RunStep {
const run = b.addSystemCommand(&[_][]const u8 {
b.zig_exe,
"build",
"--build-file",
build_file,
});
return run;
}
6 changes: 6 additions & 0 deletions test/standalone/min_zig_version/distant-future-dev-build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//config min_zig_version 999.999.999-dev.999999+5735ce39ae5a9fb2bc2ac9f5a722276c291b28bc
const std = @import("std");

pub fn build(b: *std.build.Builder) void {
_ = b;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//config min_zig_version 999.999.999
const std = @import("std");

pub fn build(b: *std.build.Builder) void {
_ = b;
}

0 comments on commit 5780064

Please sign in to comment.