summaryrefslogtreecommitdiff
path: root/build.zig
diff options
context:
space:
mode:
Diffstat (limited to 'build.zig')
-rw-r--r--build.zig1199
1 files changed, 614 insertions, 585 deletions
diff --git a/build.zig b/build.zig
index 398f02e..64968b9 100644
--- a/build.zig
+++ b/build.zig
@@ -5,10 +5,12 @@ const ipc = @import("src/ipc.zig");
const tests = @import("test/tests.zig");
const Build = compat.Build;
+const CompileStep = compat.build.CompileStep;
const Step = compat.build.Step;
const Child = std.process.Child;
const assert = std.debug.assert;
+const join = std.fs.path.join;
const print = std.debug.print;
pub const Exercise = struct {
@@ -29,21 +31,16 @@ pub const Exercise = struct {
/// Set this to true to check stdout instead.
check_stdout: bool = false,
- /// This exercise makes use of the async feature.
- /// We need to keep track of this, so we compile without the self hosted compiler
- @"async": bool = false,
-
/// This exercise makes use of C functions
/// We need to keep track of this, so we compile with libc
- C: bool = false,
+ link_libc: bool = false,
/// This exercise is not supported by the current Zig compiler.
skip: bool = false,
/// Returns the name of the main file with .zig stripped.
- pub fn baseName(self: Exercise) []const u8 {
- assert(std.mem.endsWith(u8, self.main_file, ".zig"));
- return self.main_file[0 .. self.main_file.len - 4];
+ pub fn name(self: Exercise) []const u8 {
+ return std.fs.path.stem(self.main_file);
}
/// Returns the key of the main file, the string before the '_' with
@@ -63,8 +60,606 @@ pub const Exercise = struct {
pub fn number(self: Exercise) usize {
return std.fmt.parseInt(usize, self.key(), 10) catch unreachable;
}
+
+ /// Returns the CompileStep for this exercise.
+ pub fn addExecutable(self: Exercise, b: *Build, work_path: []const u8) *CompileStep {
+ const file_path = join(b.allocator, &.{ work_path, self.main_file }) catch
+ @panic("OOM");
+
+ return b.addExecutable(.{
+ .name = self.name(),
+ .root_source_file = .{ .path = file_path },
+ .link_libc = self.link_libc,
+ });
+ }
+};
+
+pub fn build(b: *Build) !void {
+ if (!compat.is_compatible) compat.die();
+ if (!validate_exercises()) std.os.exit(1);
+
+ use_color_escapes = false;
+ if (std.io.getStdErr().supportsAnsiEscapeCodes()) {
+ use_color_escapes = true;
+ } else if (builtin.os.tag == .windows) {
+ const w32 = struct {
+ const WINAPI = std.os.windows.WINAPI;
+ const DWORD = std.os.windows.DWORD;
+ const ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
+ const STD_ERROR_HANDLE = @bitCast(DWORD, @as(i32, -12));
+ extern "kernel32" fn GetStdHandle(id: DWORD) callconv(WINAPI) ?*anyopaque;
+ extern "kernel32" fn GetConsoleMode(console: ?*anyopaque, out_mode: *DWORD) callconv(WINAPI) u32;
+ extern "kernel32" fn SetConsoleMode(console: ?*anyopaque, mode: DWORD) callconv(WINAPI) u32;
+ };
+ const handle = w32.GetStdHandle(w32.STD_ERROR_HANDLE);
+ var mode: w32.DWORD = 0;
+ if (w32.GetConsoleMode(handle, &mode) != 0) {
+ mode |= w32.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
+ use_color_escapes = w32.SetConsoleMode(handle, mode) != 0;
+ }
+ }
+
+ if (use_color_escapes) {
+ red_text = "\x1b[31m";
+ green_text = "\x1b[32m";
+ bold_text = "\x1b[1m";
+ reset_text = "\x1b[0m";
+ }
+
+ const logo =
+ \\
+ \\ _ _ _
+ \\ ___(_) __ _| (_)_ __ __ _ ___
+ \\ |_ | |/ _' | | | '_ \ / _' / __|
+ \\ / /| | (_| | | | | | | (_| \__ \
+ \\ /___|_|\__, |_|_|_| |_|\__, |___/
+ \\ |___/ |___/
+ \\
+ \\
+ ;
+
+ const use_healed = b.option(bool, "healed", "Run exercises from patches/healed") orelse false;
+ const exno: ?usize = b.option(usize, "n", "Select exercise");
+
+ const healed_path = "patches/healed";
+ const work_path = if (use_healed) healed_path else "exercises";
+
+ const header_step = PrintStep.create(b, logo);
+
+ if (exno) |n| {
+ if (n == 0 or n > exercises.len - 1) {
+ print("unknown exercise number: {}\n", .{n});
+ std.os.exit(1);
+ }
+
+ const ex = exercises[n - 1];
+
+ const build_step = ex.addExecutable(b, work_path);
+ b.installArtifact(build_step);
+
+ const run_step = b.addRunArtifact(build_step);
+
+ const test_step = b.step("test", b.fmt("Run {s} without checking output", .{ex.main_file}));
+ if (ex.skip) {
+ const skip_step = SkipStep.create(b, ex);
+ test_step.dependOn(&skip_step.step);
+ } else {
+ test_step.dependOn(&run_step.step);
+ }
+
+ const verify_step = ZiglingStep.create(b, ex, work_path);
+
+ const zigling_step = b.step("zigling", b.fmt("Check the solution of {s}", .{ex.main_file}));
+ zigling_step.dependOn(&verify_step.step);
+ b.default_step = zigling_step;
+
+ const start_step = b.step("start", b.fmt("Check all solutions starting at {s}", .{ex.main_file}));
+
+ var prev_step = verify_step;
+ for (exercises) |exn| {
+ const nth = exn.number();
+ if (nth > n) {
+ const verify_stepn = ZiglingStep.create(b, exn, work_path);
+ verify_stepn.step.dependOn(&prev_step.step);
+
+ prev_step = verify_stepn;
+ }
+ }
+ start_step.dependOn(&prev_step.step);
+
+ return;
+ } else if (use_healed and false) {
+ // Special case when healed by the eowyn script, where we can make the
+ // code more efficient.
+ //
+ // TODO: this branch is disabled because it prevents the normal case to
+ // be executed.
+ const test_step = b.step("test", "Test the healed exercises");
+ b.default_step = test_step;
+
+ for (exercises) |ex| {
+ const build_step = ex.addExecutable(b, healed_path);
+ b.installArtifact(build_step);
+
+ const run_step = b.addRunArtifact(build_step);
+ if (ex.skip) {
+ const skip_step = SkipStep.create(b, ex);
+ test_step.dependOn(&skip_step.step);
+ } else {
+ test_step.dependOn(&run_step.step);
+ }
+ }
+
+ return;
+ }
+
+ const ziglings_step = b.step("ziglings", "Check all ziglings");
+ b.default_step = ziglings_step;
+
+ // Don't use the "multi-object for loop" syntax, in order to avoid a syntax
+ // error with old Zig compilers.
+ var prev_step = &header_step.step;
+ for (exercises) |ex| {
+ const build_step = ex.addExecutable(b, "exercises");
+ b.installArtifact(build_step);
+
+ const verify_stepn = ZiglingStep.create(b, ex, work_path);
+ verify_stepn.step.dependOn(prev_step);
+
+ prev_step = &verify_stepn.step;
+ }
+ ziglings_step.dependOn(prev_step);
+
+ const test_step = b.step("test", "Run all the tests");
+ test_step.dependOn(tests.addCliTests(b, &exercises));
+}
+
+var use_color_escapes = false;
+var red_text: []const u8 = "";
+var green_text: []const u8 = "";
+var bold_text: []const u8 = "";
+var reset_text: []const u8 = "";
+
+const ZiglingStep = struct {
+ step: Step,
+ exercise: Exercise,
+ work_path: []const u8,
+
+ result_messages: []const u8 = "",
+ result_error_bundle: std.zig.ErrorBundle = std.zig.ErrorBundle.empty,
+
+ pub fn create(b: *Build, exercise: Exercise, work_path: []const u8) *ZiglingStep {
+ const self = b.allocator.create(ZiglingStep) catch @panic("OOM");
+ self.* = .{
+ .step = Step.init(.{
+ .id = .custom,
+ .name = exercise.main_file,
+ .owner = b,
+ .makeFn = make,
+ }),
+ .exercise = exercise,
+ .work_path = work_path,
+ };
+ return self;
+ }
+
+ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
+ const self = @fieldParentPtr(ZiglingStep, "step", step);
+
+ if (self.exercise.skip) {
+ print("Skipping {s}\n\n", .{self.exercise.main_file});
+
+ return;
+ }
+
+ const exe_path = self.compile(prog_node) catch {
+ if (self.exercise.hint.len > 0) {
+ print("\n{s}HINT: {s}{s}", .{
+ bold_text, self.exercise.hint, reset_text,
+ });
+ }
+
+ self.help();
+ std.os.exit(1);
+ };
+
+ self.run(exe_path, prog_node) catch {
+ if (self.exercise.hint.len > 0) {
+ print("\n{s}HINT: {s}{s}", .{
+ bold_text, self.exercise.hint, reset_text,
+ });
+ }
+
+ self.help();
+ std.os.exit(1);
+ };
+ }
+
+ fn run(self: *ZiglingStep, exe_path: []const u8, _: *std.Progress.Node) !void {
+ resetLine();
+ print("Checking {s}...\n", .{self.exercise.main_file});
+
+ const b = self.step.owner;
+
+ // Allow up to 1 MB of stdout capture.
+ const max_output_bytes = 1 * 1024 * 1024;
+
+ var result = Child.exec(.{
+ .allocator = b.allocator,
+ .argv = &.{exe_path},
+ .cwd = b.build_root.path.?,
+ .cwd_dir = b.build_root.handle,
+ .max_output_bytes = max_output_bytes,
+ }) catch |err| {
+ print("{s}Unable to spawn {s}: {s}{s}\n", .{
+ red_text, exe_path, @errorName(err), reset_text,
+ });
+ return err;
+ };
+
+ const raw_output = if (self.exercise.check_stdout)
+ result.stdout
+ else
+ result.stderr;
+
+ // Make sure it exited cleanly.
+ switch (result.term) {
+ .Exited => |code| {
+ if (code != 0) {
+ print("{s}{s} exited with error code {d} (expected {d}){s}\n", .{
+ red_text, self.exercise.main_file, code, 0, reset_text,
+ });
+ return error.BadExitCode;
+ }
+ },
+ else => {
+ print("{s}{s} terminated unexpectedly{s}\n", .{
+ red_text, self.exercise.main_file, reset_text,
+ });
+ return error.UnexpectedTermination;
+ },
+ }
+
+ // Validate the output.
+ const output = std.mem.trimRight(u8, raw_output, " \r\n");
+ const exercise_output = std.mem.trimRight(u8, self.exercise.output, " \r\n");
+ if (!std.mem.eql(u8, output, exercise_output)) {
+ const red = red_text;
+ const reset = reset_text;
+
+ print(
+ \\
+ \\{s}========= expected this output: =========={s}
+ \\{s}
+ \\{s}========= but found: ====================={s}
+ \\{s}
+ \\{s}=========================================={s}
+ \\
+ , .{ red, reset, exercise_output, red, reset, output, red, reset });
+ return error.InvalidOutput;
+ }
+
+ print("{s}PASSED:\n{s}{s}\n\n", .{ green_text, output, reset_text });
+ }
+
+ fn compile(self: *ZiglingStep, prog_node: *std.Progress.Node) ![]const u8 {
+ print("Compiling {s}...\n", .{self.exercise.main_file});
+
+ const b = self.step.owner;
+ const exercise_path = self.exercise.main_file;
+ const path = join(b.allocator, &.{ self.work_path, exercise_path }) catch
+ @panic("OOM");
+
+ var zig_args = std.ArrayList([]const u8).init(b.allocator);
+ defer zig_args.deinit();
+
+ zig_args.append(b.zig_exe) catch @panic("OOM");
+ zig_args.append("build-exe") catch @panic("OOM");
+
+ // Enable C support for exercises that use C functions.
+ if (self.exercise.link_libc) {
+ zig_args.append("-lc") catch @panic("OOM");
+ }
+
+ zig_args.append(b.pathFromRoot(path)) catch @panic("OOM");
+
+ zig_args.append("--cache-dir") catch @panic("OOM");
+ zig_args.append(b.pathFromRoot(b.cache_root.path.?)) catch @panic("OOM");
+
+ zig_args.append("--listen=-") catch @panic("OOM");
+
+ const argv = zig_args.items;
+ var code: u8 = undefined;
+ const exe_path = self.eval(argv, &code, prog_node) catch |err| {
+ self.printErrors();
+
+ switch (err) {
+ error.FileNotFound => {
+ print("{s}{s}: Unable to spawn the following command: file not found{s}\n", .{
+ red_text, self.exercise.main_file, reset_text,
+ });
+ for (argv) |v| print("{s} ", .{v});
+ print("\n", .{});
+ },
+ error.ExitCodeFailure => {
+ print("{s}{s}: The following command exited with error code {}:{s}\n", .{
+ red_text, self.exercise.main_file, code, reset_text,
+ });
+ for (argv) |v| print("{s} ", .{v});
+ print("\n", .{});
+ },
+ error.ProcessTerminated => {
+ print("{s}{s}: The following command terminated unexpectedly:{s}\n", .{
+ red_text, self.exercise.main_file, reset_text,
+ });
+ for (argv) |v| print("{s} ", .{v});
+ print("\n", .{});
+ },
+ error.ZigIPCError => {
+ print("{s}{s}: The following command failed to communicate the compilation result:{s}\n", .{
+ red_text, self.exercise.main_file, reset_text,
+ });
+ for (argv) |v| print("{s} ", .{v});
+ print("\n", .{});
+ },
+ else => {
+ print("{s}{s}: Unexpected error: {s}{s}\n", .{
+ red_text, self.exercise.main_file, @errorName(err), reset_text,
+ });
+ for (argv) |v| print("{s} ", .{v});
+ print("\n", .{});
+ },
+ }
+
+ return err;
+ };
+ self.printErrors();
+
+ return exe_path;
+ }
+
+ // Code adapted from `std.Build.execAllowFail and `std.Build.Step.evalZigProcess`.
+ pub fn eval(
+ self: *ZiglingStep,
+ argv: []const []const u8,
+ out_code: *u8,
+ prog_node: *std.Progress.Node,
+ ) ![]const u8 {
+ assert(argv.len != 0);
+ const b = self.step.owner;
+ const allocator = b.allocator;
+
+ var child = Child.init(argv, allocator);
+ child.env_map = b.env_map;
+ child.stdin_behavior = .Pipe;
+ child.stdout_behavior = .Pipe;
+ child.stderr_behavior = .Pipe;
+
+ try child.spawn();
+
+ var poller = std.io.poll(allocator, enum { stdout, stderr }, .{
+ .stdout = child.stdout.?,
+ .stderr = child.stderr.?,
+ });
+ defer poller.deinit();
+
+ try ipc.sendMessage(child.stdin.?, .update);
+ try ipc.sendMessage(child.stdin.?, .exit);
+
+ const Header = std.zig.Server.Message.Header;
+ var result: ?[]const u8 = null;
+
+ var node_name: std.ArrayListUnmanaged(u8) = .{};
+ defer node_name.deinit(allocator);
+ var sub_prog_node = prog_node.start("", 0);
+ defer sub_prog_node.end();
+
+ const stdout = poller.fifo(.stdout);
+
+ poll: while (true) {
+ while (stdout.readableLength() < @sizeOf(Header)) {
+ if (!(try poller.poll())) break :poll;
+ }
+ const header = stdout.reader().readStruct(Header) catch unreachable;
+ while (stdout.readableLength() < header.bytes_len) {
+ if (!(try poller.poll())) break :poll;
+ }
+ const body = stdout.readableSliceOfLen(header.bytes_len);
+
+ switch (header.tag) {
+ .zig_version => {
+ if (!std.mem.eql(u8, builtin.zig_version_string, body))
+ return error.ZigVersionMismatch;
+ },
+ .error_bundle => {
+ self.result_error_bundle = try ipc.parseErrorBundle(allocator, body);
+ },
+ .progress => {
+ node_name.clearRetainingCapacity();
+ try node_name.appendSlice(allocator, body);
+ sub_prog_node.setName(node_name.items);
+ },
+ .emit_bin_path => {
+ const emit_bin = try ipc.parseEmitBinPath(allocator, body);
+ result = emit_bin.path;
+ },
+ else => {}, // ignore other messages
+ }
+
+ stdout.discard(body.len);
+ }
+
+ const stderr = poller.fifo(.stderr);
+ if (stderr.readableLength() > 0) {
+ self.result_messages = try stderr.toOwnedSlice();
+ }
+
+ // Send EOF to stdin.
+ child.stdin.?.close();
+ child.stdin = null;
+
+ // Keep the errors compatible with std.Build.execAllowFail.
+ const term = try child.wait();
+ switch (term) {
+ .Exited => |code| {
+ if (code != 0) {
+ out_code.* = @truncate(u8, code);
+
+ return error.ExitCodeFailure;
+ }
+ },
+ .Signal, .Stopped, .Unknown => |code| {
+ out_code.* = @truncate(u8, code);
+
+ return error.ProcessTerminated;
+ },
+ }
+
+ return result orelse return error.ZigIPCError;
+ }
+
+ fn help(self: *ZiglingStep) void {
+ const path = self.exercise.main_file;
+ const key = self.exercise.key();
+
+ print("\n{s}Edit exercises/{s} and run this again.{s}", .{
+ red_text, path, reset_text,
+ });
+
+ const format =
+ \\
+ \\{s}To continue from this zigling, use this command:{s}
+ \\ {s}zig build -Dn={s}{s}
+ \\
+ ;
+ print(format, .{ red_text, reset_text, bold_text, key, reset_text });
+ }
+
+ fn printErrors(self: *ZiglingStep) void {
+ resetLine();
+
+ // Print the additional log and verbose messages.
+ // TODO: use colors?
+ if (self.result_messages.len > 0) print("{s}", .{self.result_messages});
+
+ // Print the compiler errors.
+ // TODO: use the same ttyconf from the builder.
+ const ttyconf: std.debug.TTY.Config = if (use_color_escapes)
+ .escape_codes
+ else
+ .no_color;
+ if (self.result_error_bundle.errorMessageCount() > 0) {
+ self.result_error_bundle.renderToStdErr(.{ .ttyconf = ttyconf });
+ }
+ }
};
+// Clear the entire line and move the cursor to column zero.
+// Used for clearing the compiler and build_runner progress messages.
+fn resetLine() void {
+ if (use_color_escapes) print("{s}", .{"\x1b[2K\r"});
+}
+
+// Print a message to stderr.
+const PrintStep = struct {
+ step: Step,
+ message: []const u8,
+
+ pub fn create(owner: *Build, message: []const u8) *PrintStep {
+ const self = owner.allocator.create(PrintStep) catch @panic("OOM");
+ self.* = .{
+ .step = Step.init(.{
+ .id = .custom,
+ .name = "print",
+ .owner = owner,
+ .makeFn = make,
+ }),
+ .message = message,
+ };
+
+ return self;
+ }
+
+ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
+ _ = prog_node;
+ const p = @fieldParentPtr(PrintStep, "step", step);
+
+ print("{s}", .{p.message});
+ }
+};
+
+// Skip an exercise.
+const SkipStep = struct {
+ step: Step,
+ exercise: Exercise,
+
+ pub fn create(owner: *Build, exercise: Exercise) *SkipStep {
+ const self = owner.allocator.create(SkipStep) catch @panic("OOM");
+ self.* = .{
+ .step = Step.init(.{
+ .id = .custom,
+ .name = owner.fmt("skip {s}", .{exercise.main_file}),
+ .owner = owner,
+ .makeFn = make,
+ }),
+ .exercise = exercise,
+ };
+
+ return self;
+ }
+
+ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
+ _ = prog_node;
+ const p = @fieldParentPtr(SkipStep, "step", step);
+
+ if (p.exercise.skip) {
+ print("{s} skipped\n", .{p.exercise.main_file});
+ }
+ }
+};
+
+// Check that each exercise number, excluding the last, forms the sequence
+// `[1, exercise.len)`.
+//
+// Additionally check that the output field does not contain trailing whitespace.
+fn validate_exercises() bool {
+ // Don't use the "multi-object for loop" syntax, in order to avoid a syntax
+ // error with old Zig compilers.
+ var i: usize = 0;
+ for (exercises[0..]) |ex| {
+ const exno = ex.number();
+ const last = 999;
+ i += 1;
+
+ if (exno != i and exno != last) {
+ print("exercise {s} has an incorrect number: expected {}, got {s}\n", .{
+ ex.main_file,
+ i,
+ ex.key(),
+ });
+
+ return false;
+ }
+
+ const output = std.mem.trimRight(u8, ex.output, " \r\n");
+ if (output.len != ex.output.len) {
+ print("exercise {s} output field has extra trailing whitespace\n", .{
+ ex.main_file,
+ });
+
+ return false;
+ }
+
+ if (!std.mem.endsWith(u8, ex.main_file, ".zig")) {
+ print("exercise {s} is not a zig source file\n", .{ex.main_file});
+
+ return false;
+ }
+ }
+
+ return true;
+}
+
const exercises = [_]Exercise{
.{
.main_file = "001_hello.zig",
@@ -439,49 +1034,41 @@ const exercises = [_]Exercise{
.main_file = "084_async.zig",
.output = "foo() A",
.hint = "Read the facts. Use the facts.",
- .@"async" = true,
.skip = true,
},
.{
.main_file = "085_async2.zig",
.output = "Hello async!",
- .@"async" = true,
.skip = true,
},
.{
.main_file = "086_async3.zig",
.output = "5 4 3 2 1",
- .@"async" = true,
.skip = true,
},
.{
.main_file = "087_async4.zig",
.output = "1 2 3 4 5",
- .@"async" = true,
.skip = true,
},
.{
.main_file = "088_async5.zig",
.output = "Example Title.",
- .@"async" = true,
.skip = true,
},
.{
.main_file = "089_async6.zig",
.output = ".com: Example Title, .org: Example Title.",
- .@"async" = true,
.skip = true,
},
.{
.main_file = "090_async7.zig",
.output = "beef? BEEF!",
- .@"async" = true,
.skip = true,
},
.{
.main_file = "091_async8.zig",
.output = "ABCDEF",
- .@"async" = true,
.skip = true,
},
@@ -492,15 +1079,15 @@ const exercises = [_]Exercise{
.{
.main_file = "093_hello_c.zig",
.output = "Hello C from Zig! - C result is 17 chars written.",
- .C = true,
+ .link_libc = true,
},
.{
.main_file = "094_c_math.zig",
.output = "The normalized angle of 765.2 degrees is 45.2 degrees.",
- .C = true,
+ .link_libc = true,
},
.{
- .main_file = "095_for_loops.zig",
+ .main_file = "095_for3.zig",
.output = "1 2 4 7 8 11 13 14 16 17 19",
},
.{
@@ -520,573 +1107,15 @@ const exercises = [_]Exercise{
.output = "\n X | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \n---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+\n 1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \n\n 2 | 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 \n\n 3 | 3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 \n\n 4 | 4 8 12 16 20 24 28 32 36 40 44 48 52 56 60 \n\n 5 | 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 \n\n 6 | 6 12 18 24 30 36 42 48 54 60 66 72 78 84 90 \n\n 7 | 7 14 21 28 35 42 49 56 63 70 77 84 91 98 105 \n\n 8 | 8 16 24 32 40 48 56 64 72 80 88 96 104 112 120 \n\n 9 | 9 18 27 36 45 54 63 72 81 90 99 108 117 126 135 \n\n10 | 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 \n\n11 | 11 22 33 44 55 66 77 88 99 110 121 132 143 154 165 \n\n12 | 12 24 36 48 60 72 84 96 108 120 132 144 156 168 180 \n\n13 | 13 26 39 52 65 78 91 104 117 130 143 156 169 182 195 \n\n14 | 14 28 42 56 70 84 98 112 126 140 154 168 182 196 210 \n\n15 | 15 30 45 60 75 90 105 120 135 150 165 180 195 210 225",
},
.{
+ .main_file = "100_for4.zig",
+ .output = "Arrays match!",
+ },
+ .{
+ .main_file = "101_for5.zig",
+ .output = "1. Wizard (Gold: 25, XP: 40)\n2. Bard (Gold: 11, XP: 17)\n3. Bard (Gold: 5, XP: 55)\n4. Warrior (Gold: 7392, XP: 21)",
+ },
+ .{
.main_file = "999_the_end.zig",
.output = "\nThis is the end for now!\nWe hope you had fun and were able to learn a lot, so visit us again when the next exercises are available.",
},
};
-
-pub fn build(b: *Build) !void {
- if (!compat.is_compatible) compat.die();
- if (!validate_exercises()) std.os.exit(1);
-
- use_color_escapes = false;
- if (std.io.getStdErr().supportsAnsiEscapeCodes()) {
- use_color_escapes = true;
- } else if (builtin.os.tag == .windows) {
- const w32 = struct {
- const WINAPI = std.os.windows.WINAPI;
- const DWORD = std.os.windows.DWORD;
- const ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
- const STD_ERROR_HANDLE = @bitCast(DWORD, @as(i32, -12));
- extern "kernel32" fn GetStdHandle(id: DWORD) callconv(WINAPI) ?*anyopaque;
- extern "kernel32" fn GetConsoleMode(console: ?*anyopaque, out_mode: *DWORD) callconv(WINAPI) u32;
- extern "kernel32" fn SetConsoleMode(console: ?*anyopaque, mode: DWORD) callconv(WINAPI) u32;
- };
- const handle = w32.GetStdHandle(w32.STD_ERROR_HANDLE);
- var mode: w32.DWORD = 0;
- if (w32.GetConsoleMode(handle, &mode) != 0) {
- mode |= w32.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
- use_color_escapes = w32.SetConsoleMode(handle, mode) != 0;
- }
- }
-
- if (use_color_escapes) {
- red_text = "\x1b[31m";
- green_text = "\x1b[32m";
- bold_text = "\x1b[1m";
- reset_text = "\x1b[0m";
- }
-
- const logo =
- \\
- \\ _ _ _
- \\ ___(_) __ _| (_)_ __ __ _ ___
- \\ |_ | |/ _' | | | '_ \ / _' / __|
- \\ / /| | (_| | | | | | | (_| \__ \
- \\ /___|_|\__, |_|_|_| |_|\__, |___/
- \\ |___/ |___/
- \\
- \\
- ;
-
- const use_healed = b.option(bool, "healed", "Run exercises from patches/healed") orelse false;
- const exno: ?usize = b.option(usize, "n", "Select exercise");
-
- const header_step = PrintStep.create(b, logo);
-
- if (exno) |n| {
- if (n == 0 or n > exercises.len - 1) {
- print("unknown exercise number: {}\n", .{n});
- std.os.exit(1);
- }
-
- const ex = exercises[n - 1];
- const base_name = ex.baseName();
- const file_path = std.fs.path.join(b.allocator, &[_][]const u8{
- if (use_healed) "patches/healed" else "exercises", ex.main_file,
- }) catch unreachable;
-
- const build_step = b.addExecutable(.{ .name = base_name, .root_source_file = .{ .path = file_path } });
- if (ex.C) {
- build_step.linkLibC();
- }
- b.installArtifact(build_step);
-
- const run_step = b.addRunArtifact(build_step);
-
- const test_step = b.step("test", b.fmt("Run {s} without checking output", .{ex.main_file}));
- if (ex.skip) {
- const skip_step = SkipStep.create(b, ex);
- test_step.dependOn(&skip_step.step);
- } else {
- test_step.dependOn(&run_step.step);
- }
-
- const install_step = b.step("install", b.fmt("Install {s} to prefix path", .{ex.main_file}));
- install_step.dependOn(b.getInstallStep());
-
- const uninstall_step = b.step("uninstall", b.fmt("Uninstall {s} from prefix path", .{ex.main_file}));
- uninstall_step.dependOn(b.getUninstallStep());
-
- const verify_step = ZiglingStep.create(b, ex, use_healed);
-
- const zigling_step = b.step("zigling", b.fmt("Check the solution of {s}", .{ex.main_file}));
- zigling_step.dependOn(&verify_step.step);
- b.default_step = zigling_step;
-
- const start_step = b.step("start", b.fmt("Check all solutions starting at {s}", .{ex.main_file}));
-
- var prev_step = verify_step;
- for (exercises) |exn| {
- const nth = exn.number();
- if (nth > n) {
- const verify_stepn = ZiglingStep.create(b, exn, use_healed);
- verify_stepn.step.dependOn(&prev_step.step);
-
- prev_step = verify_stepn;
- }
- }
- start_step.dependOn(&prev_step.step);
-
- return;
- } else if (use_healed and false) {
- const test_step = b.step("test", "Test the healed exercises");
- b.default_step = test_step;
-
- for (exercises) |ex| {
- const base_name = ex.baseName();
- const file_path = std.fs.path.join(b.allocator, &[_][]const u8{
- "patches/healed", ex.main_file,
- }) catch unreachable;
-
- const build_step = b.addExecutable(.{ .name = base_name, .root_source_file = .{ .path = file_path } });
- if (ex.C) {
- build_step.linkLibC();
- }
- b.installArtifact(build_step);
-
- const run_step = b.addRunArtifact(build_step);
- if (ex.skip) {
- const skip_step = SkipStep.create(b, ex);
- test_step.dependOn(&skip_step.step);
- } else {
- test_step.dependOn(&run_step.step);
- }
- }
-
- return;
- }
-
- const ziglings_step = b.step("ziglings", "Check all ziglings");
- b.default_step = ziglings_step;
-
- // Don't use the "multi-object for loop" syntax, in order to avoid a syntax
- // error with old Zig compilers.
- var prev_step = &header_step.step;
- for (exercises) |ex| {
- const base_name = ex.baseName();
- const file_path = std.fs.path.join(b.allocator, &[_][]const u8{
- "exercises", ex.main_file,
- }) catch unreachable;
-
- const build_step = b.addExecutable(.{ .name = base_name, .root_source_file = .{ .path = file_path } });
- b.installArtifact(build_step);
-
- const verify_stepn = ZiglingStep.create(b, ex, use_healed);
- verify_stepn.step.dependOn(prev_step);
-
- prev_step = &verify_stepn.step;
- }
- ziglings_step.dependOn(prev_step);
-
- const test_step = b.step("test", "Run all the tests");
- test_step.dependOn(tests.addCliTests(b, &exercises));
-}
-
-var use_color_escapes = false;
-var red_text: []const u8 = "";
-var green_text: []const u8 = "";
-var bold_text: []const u8 = "";
-var reset_text: []const u8 = "";
-
-const ZiglingStep = struct {
- step: Step,
- exercise: Exercise,
- builder: *Build,
- use_healed: bool,
-
- result_messages: []const u8 = "",
- result_error_bundle: std.zig.ErrorBundle = std.zig.ErrorBundle.empty,
-
- pub fn create(builder: *Build, exercise: Exercise, use_healed: bool) *@This() {
- const self = builder.allocator.create(@This()) catch unreachable;
- self.* = .{
- .step = Step.init(Step.Options{ .id = .custom, .name = exercise.main_file, .owner = builder, .makeFn = make }),
- .exercise = exercise,
- .builder = builder,
- .use_healed = use_healed,
- };
- return self;
- }
-
- fn make(step: *Step, prog_node: *std.Progress.Node) anyerror!void {
- const self = @fieldParentPtr(@This(), "step", step);
-
- if (self.exercise.skip) {
- print("Skipping {s}\n\n", .{self.exercise.main_file});
-
- return;
- }
- self.makeInternal(prog_node) catch {
- if (self.exercise.hint.len > 0) {
- print("\n{s}HINT: {s}{s}", .{ bold_text, self.exercise.hint, reset_text });
- }
-
- print("\n{s}Edit exercises/{s} and run this again.{s}", .{ red_text, self.exercise.main_file, reset_text });
- print("\n{s}To continue from this zigling, use this command:{s}\n {s}zig build -Dn={s}{s}\n", .{ red_text, reset_text, bold_text, self.exercise.key(), reset_text });
- std.os.exit(1);
- };
- }
-
- fn makeInternal(self: *@This(), prog_node: *std.Progress.Node) !void {
- print("Compiling {s}...\n", .{self.exercise.main_file});
-
- const exe_file = try self.doCompile(prog_node);
-
- resetLine();
- print("Checking {s}...\n", .{self.exercise.main_file});
-
- const cwd = self.builder.build_root.path.?;
-
- const argv = [_][]const u8{exe_file};
-
- var child = std.ChildProcess.init(&argv, self.builder.allocator);
-
- child.cwd = cwd;
- child.env_map = self.builder.env_map;
-
- child.stdin_behavior = .Inherit;
- if (self.exercise.check_stdout) {
- child.stdout_behavior = .Pipe;
- child.stderr_behavior = .Inherit;
- } else {
- child.stdout_behavior = .Inherit;
- child.stderr_behavior = .Pipe;
- }
-
- child.spawn() catch |err| {
- print("{s}Unable to spawn {s}: {s}{s}\n", .{ red_text, argv[0], @errorName(err), reset_text });
- return err;
- };
-
- // Allow up to 1 MB of stdout capture.
- const max_output_len = 1 * 1024 * 1024;
- const output = if (self.exercise.check_stdout)
- try child.stdout.?.reader().readAllAlloc(self.builder.allocator, max_output_len)
- else
- try child.stderr.?.reader().readAllAlloc(self.builder.allocator, max_output_len);
-
- // At this point stdout is closed, wait for the process to terminate.
- const term = child.wait() catch |err| {
- print("{s}Unable to spawn {s}: {s}{s}\n", .{ red_text, argv[0], @errorName(err), reset_text });
- return err;
- };
-
- // Make sure it exited cleanly.
- switch (term) {
- .Exited => |code| {
- if (code != 0) {
- print("{s}{s} exited with error code {d} (expected {d}){s}\n", .{ red_text, self.exercise.main_file, code, 0, reset_text });
- return error.BadExitCode;
- }
- },
- else => {
- print("{s}{s} terminated unexpectedly{s}\n", .{ red_text, self.exercise.main_file, reset_text });
- return error.UnexpectedTermination;
- },
- }
-
- // Validate the output.
- const trimOutput = std.mem.trimRight(u8, output, " \r\n");
- const trimExerciseOutput = std.mem.trimRight(u8, self.exercise.output, " \r\n");
- if (!std.mem.eql(u8, trimOutput, trimExerciseOutput)) {
- print(
- \\
- \\{s}----------- Expected this output -----------{s}
- \\"{s}"
- \\{s}----------- but found -----------{s}
- \\"{s}"
- \\{s}-----------{s}
- \\
- , .{ red_text, reset_text, trimExerciseOutput, red_text, reset_text, trimOutput, red_text, reset_text });
- return error.InvalidOutput;
- }
-
- print("{s}PASSED:\n{s}{s}\n\n", .{ green_text, trimOutput, reset_text });
- }
-
- // The normal compile step calls os.exit, so we can't use it as a library :(
- // This is a stripped down copy of std.build.LibExeObjStep.make.
- fn doCompile(self: *@This(), prog_node: *std.Progress.Node) ![]const u8 {
- const builder = self.builder;
-
- var zig_args = std.ArrayList([]const u8).init(builder.allocator);
- defer zig_args.deinit();
-
- zig_args.append(builder.zig_exe) catch unreachable;
- zig_args.append("build-exe") catch unreachable;
-
- // Enable the stage 1 compiler if using the async feature
- // disabled because of https://github.com/ratfactor/ziglings/issues/163
- // if (self.exercise.@"async") {
- // zig_args.append("-fstage1") catch unreachable;
- // }
-
- // Enable C support for exercises that use C functions
- if (self.exercise.C) {
- zig_args.append("-lc") catch unreachable;
- }
-
- const zig_file = std.fs.path.join(builder.allocator, &[_][]const u8{ if (self.use_healed) "patches/healed" else "exercises", self.exercise.main_file }) catch unreachable;
- zig_args.append(builder.pathFromRoot(zig_file)) catch unreachable;
-
- zig_args.append("--cache-dir") catch unreachable;
- zig_args.append(builder.pathFromRoot(builder.cache_root.path.?)) catch unreachable;
-
- zig_args.append("--listen=-") catch unreachable;
-
- const argv = zig_args.items;
- var code: u8 = undefined;
- const file_name = self.eval(argv, &code, prog_node) catch |err| {
- self.printErrors();
-
- switch (err) {
- error.FileNotFound => {
- print("{s}{s}: Unable to spawn the following command: file not found{s}\n", .{ red_text, self.exercise.main_file, reset_text });
- for (argv) |v| print("{s} ", .{v});
- print("\n", .{});
- },
- error.ExitCodeFailure => {
- print("{s}{s}: The following command exited with error code {}:{s}\n", .{ red_text, self.exercise.main_file, code, reset_text });
- for (argv) |v| print("{s} ", .{v});
- print("\n", .{});
- },
- error.ProcessTerminated => {
- print("{s}{s}: The following command terminated unexpectedly:{s}\n", .{ red_text, self.exercise.main_file, reset_text });
- for (argv) |v| print("{s} ", .{v});
- print("\n", .{});
- },
- error.ZigIPCError => {
- print("{s}{s}: The following command failed to communicate the compilation result:{s}\n", .{
- red_text,
- self.exercise.main_file,
- reset_text,
- });
- for (argv) |v| print("{s} ", .{v});
- print("\n", .{});
- },
- else => {},
- }
-
- return err;
- };
- self.printErrors();
-
- return file_name;
- }
-
- // Code adapted from `std.Build.execAllowFail and `std.Build.Step.evalZigProcess`.
- pub fn eval(
- self: *ZiglingStep,
- argv: []const []const u8,
- out_code: *u8,
- prog_node: *std.Progress.Node,
- ) ![]const u8 {
- assert(argv.len != 0);
- const b = self.step.owner;
- const allocator = b.allocator;
-
- var child = Child.init(argv, allocator);
- child.env_map = b.env_map;
- child.stdin_behavior = .Pipe;
- child.stdout_behavior = .Pipe;
- child.stderr_behavior = .Pipe;
-
- try child.spawn();
-
- var poller = std.io.poll(allocator, enum { stdout, stderr }, .{
- .stdout = child.stdout.?,
- .stderr = child.stderr.?,
- });
- defer poller.deinit();
-
- try ipc.sendMessage(child.stdin.?, .update);
- try ipc.sendMessage(child.stdin.?, .exit);
-
- const Header = std.zig.Server.Message.Header;
- var result: ?[]const u8 = null;
-
- var node_name: std.ArrayListUnmanaged(u8) = .{};
- defer node_name.deinit(allocator);
- var sub_prog_node = prog_node.start("", 0);
- defer sub_prog_node.end();
-
- const stdout = poller.fifo(.stdout);
-
- poll: while (true) {
- while (stdout.readableLength() < @sizeOf(Header)) {
- if (!(try poller.poll())) break :poll;
- }
- const header = stdout.reader().readStruct(Header) catch unreachable;
- while (stdout.readableLength() < header.bytes_len) {
- if (!(try poller.poll())) break :poll;
- }
- const body = stdout.readableSliceOfLen(header.bytes_len);
-
- switch (header.tag) {
- .zig_version => {
- if (!std.mem.eql(u8, builtin.zig_version_string, body))
- return error.ZigVersionMismatch;
- },
- .error_bundle => {
- self.result_error_bundle = try ipc.parseErrorBundle(allocator, body);
- },
- .progress => {
- node_name.clearRetainingCapacity();
- try node_name.appendSlice(allocator, body);
- sub_prog_node.setName(node_name.items);
- },
- .emit_bin_path => {
- const emit_bin = try ipc.parseEmitBinPath(allocator, body);
- result = emit_bin.path;
- },
- else => {}, // ignore other messages
- }
-
- stdout.discard(body.len);
- }
-
- const stderr = poller.fifo(.stderr);
- if (stderr.readableLength() > 0) {
- self.result_messages = try stderr.toOwnedSlice();
- }
-
- // Send EOF to stdin.
- child.stdin.?.close();
- child.stdin = null;
-
- // Keep the errors compatible with std.Build.execAllowFail.
- const term = try child.wait();
- switch (term) {
- .Exited => |code| {
- if (code != 0) {
- out_code.* = @truncate(u8, code);
-
- return error.ExitCodeFailure;
- }
- },
- .Signal, .Stopped, .Unknown => |code| {
- out_code.* = @truncate(u8, code);
-
- return error.ProcessTerminated;
- },
- }
-
- return result orelse return error.ZigIPCError;
- }
-
- fn printErrors(self: *ZiglingStep) void {
- resetLine();
-
- // Print the additional log and verbose messages.
- // TODO: use colors?
- if (self.result_messages.len > 0) print("{s}", .{self.result_messages});
-
- // Print the compiler errors.
- // TODO: use the same ttyconf from the builder.
- const ttyconf: std.debug.TTY.Config = if (use_color_escapes)
- .escape_codes
- else
- .no_color;
- if (self.result_error_bundle.errorMessageCount() > 0) {
- self.result_error_bundle.renderToStdErr(.{ .ttyconf = ttyconf });
- }
- }
-};
-
-// Clear the entire line and move the cursor to column zero.
-// Used for clearing the compiler and build_runner progress messages.
-fn resetLine() void {
- if (use_color_escapes) print("{s}", .{"\x1b[2K\r"});
-}
-
-// Print a message to stderr.
-const PrintStep = struct {
- step: Step,
- message: []const u8,
-
- pub fn create(owner: *Build, message: []const u8) *PrintStep {
- const self = owner.allocator.create(PrintStep) catch @panic("OOM");
- self.* = .{
- .step = Step.init(.{
- .id = .custom,
- .name = "print",
- .owner = owner,
- .makeFn = make,
- }),
- .message = message,
- };
-
- return self;
- }
-
- fn make(step: *Step, prog_node: *std.Progress.Node) !void {
- _ = prog_node;
- const p = @fieldParentPtr(PrintStep, "step", step);
-
- print("{s}", .{p.message});
- }
-};
-
-// Skip an exercise.
-const SkipStep = struct {
- step: Step,
- exercise: Exercise,
-
- pub fn create(owner: *Build, exercise: Exercise) *SkipStep {
- const self = owner.allocator.create(SkipStep) catch @panic("OOM");
- self.* = .{
- .step = Step.init(.{
- .id = .custom,
- .name = owner.fmt("skip {s}", .{exercise.main_file}),
- .owner = owner,
- .makeFn = make,
- }),
- .exercise = exercise,
- };
-
- return self;
- }
-
- fn make(step: *Step, prog_node: *std.Progress.Node) !void {
- _ = prog_node;
- const p = @fieldParentPtr(SkipStep, "step", step);
-
- if (p.exercise.skip) {
- print("{s} skipped\n", .{p.exercise.main_file});
- }
- }
-};
-
-// Check that each exercise number, excluding the last, forms the sequence
-// `[1, exercise.len)`.
-//
-// Additionally check that the output field does not contain trailing whitespace.
-fn validate_exercises() bool {
- // Don't use the "multi-object for loop" syntax, in order to avoid a syntax
- // error with old Zig compilers.
- var i: usize = 0;
- for (exercises[0 .. exercises.len - 1]) |ex| {
- i += 1;
- if (ex.number() != i) {
- print("exercise {s} has an incorrect number: expected {}, got {s}\n", .{
- ex.main_file,
- i,
- ex.key(),
- });
-
- return false;
- }
-
- const output = std.mem.trimRight(u8, ex.output, " \r\n");
- if (output.len != ex.output.len) {
- print("exercise {s} output field has extra trailing whitespace\n", .{
- ex.main_file,
- });
-
- return false;
- }
- }
-
- return true;
-}