diff options
Diffstat (limited to 'build.zig')
| -rw-r--r-- | build.zig | 246 |
1 files changed, 245 insertions, 1 deletions
@@ -257,6 +257,17 @@ pub fn build(b: *std.Build) void { }); app.root_module.addImport("heap", heap_mod); + // The input-rescue policy as a module, so `examples/selftest.zig` can run its checks ON THE DIE + // and not only on the host. Same file the firmware's UART uses. Added unconditionally, like + // `heap` above: an application that never imports it costs nothing, because an unreferenced + // module emits no code. + app.root_module.addImport("input_rescue", b.createModule(.{ + .root_source_file = b.path("src/pardes/input_rescue.zig"), + .target = target, + .optimize = optimize, + .single_threaded = true, + })); + if (pardes_app) { // The editor arrives as a linked OBJECT, not as a package dependency, and that is a // measurement rather than a preference. @@ -438,6 +449,58 @@ pub fn build(b: *std.Build) void { bench.addArgs(&.{ "--port", port_path }); bench.stdio = .inherit; bench.step.dependOn(&bench_install.step); + + // `zig build selftest` - its OWN application, image and flash chain, so it is one command with no + // flags to remember. Sharing the `-Dapp` pipeline would have meant `zig build selftest + // -Dapp=examples/selftest.zig`, which is the kind of incantation that turns a suite into + // something nobody runs. The modules are the ones its checks need and no more. + const selftest_exe = b.addExecutable(.{ + .name = "selftest", + .root_module = b.createModule(.{ + .root_source_file = b.path("examples/selftest.zig"), + .target = target, + .optimize = optimize, + .strip = true, + .single_threaded = true, + .unwind_tables = .none, + .omit_frame_pointer = true, + .error_tracing = false, + .imports = &.{ + .{ .name = "config", .module = config_mod }, + .{ .name = "soc", .module = soc_mod }, + .{ .name = "hal", .module = hal_mod }, + .{ .name = "mmio", .module = mmio_mod }, + .{ .name = "regs", .module = regs_mod }, + .{ .name = "heap", .module = heap_mod }, + .{ .name = "input_rescue", .module = b.createModule(.{ + .root_source_file = b.path("src/pardes/input_rescue.zig"), + .target = target, + .optimize = optimize, + .single_threaded = true, + }) }, + }, + }), + }); + selftest_exe.setLinkerScript(app.linker_script.?); + selftest_exe.link_function_sections = true; + selftest_exe.link_data_sections = true; + selftest_exe.entry = .{ .symbol_name = "_start" }; + // The app descriptor, without which the image has nothing at offset 0x20 for the bootloader to + // read and the board boots into silence - which is exactly how the first run of this step failed, + // and it looks identical to a suite that hung. + selftest_exe.root_module.addObject(appdesc_obj); + selftest_exe.step.dependOn(registers.census); + const selftest_img = ImageStep.create(b, selftest_exe, img.opts); + const selftest_flash = FlashStep.create(b, selftest_img, .{ + .port = flash.port, + .baud = flash.baud, + .verify = flash.verify, + .opts = img.opts, + }); + const selftest_run = SelftestStep.create(b, port_path, 20); + selftest_run.step.dependOn(&selftest_flash.step); + b.step("selftest", "flash the on-die test suite, run it, and fail the build if any check fails") + .dependOn(&selftest_run.step); b.step("bench", "measure the serial link: verified throughput each way, and latency") .dependOn(&bench.step); @@ -446,6 +509,9 @@ pub fn build(b: *std.Build) void { const size = SizeStep.create(b, img); b.step("size", "print the image layout byte by byte").dependOn(&size.step); + const layout_step = LayoutStep.create(b, app, img.opts); + b.step("layout", "print the ELF's image segments and the loader's verdict, without building an image") + .dependOn(&layout_step.step); // `zig build diff` - the hardware oracle. Builds the differential harness with ESP-IDF's own LL // functions linked in beside ours, flashes it, and prints the comparison. This is the project's @@ -1454,8 +1520,20 @@ const ImageStep = struct { // Validate before anything is written: a rejected image must never exist on disk under a // name the flash step - or a human with esptool - would pick up. - layout.validate(self.opts) catch |err| + // + // The TABLE goes out with the error, and that is not decoration. `NotTwoMappedSegments` says + // the count is wrong and says nothing about what the segments were, which is the only thing + // that tells you which section grew, shrank, or stopped being emitted at all. Diagnosing one + // of these by hand - readelf on an artifact that turned out to be a stale install, then + // guessing at the linker script - took an hour that this print makes unnecessary. It has to + // happen here because a rejected image is never written, so no later step can show it. + layout.validate(self.opts) catch |err| { + var buf: [4096]u8 = undefined; + var stderr = std.Io.File.stderr().writer(io, &buf); + describeLayout(&stderr.interface, layout, self.opts) catch {}; + stderr.interface.flush() catch {}; return step.fail("image violates a loader rule: {s}", .{@errorName(err)}); + }; const out_path = try b.cache_root.join(b.allocator, &.{ cache_dir, self.basename }); std.Io.Dir.cwd().writeFile(io, .{ .sub_path = out_path, .data = layout.bytes }) catch |err| @@ -1466,6 +1544,95 @@ const ImageStep = struct { } }; +/// The image's segment table, in one place because three callers want exactly this and disagreeing +/// about it would be its own bug: `size` on an image that built, the failure path above on one the +/// loader rejected, and `layout` on an ELF that never got as far as an image. +/// +/// The mapped count is spelled out because it is the subject of the rule that fails most often - the +/// bootloader asserts on exactly two - and counting rows by eye is how you misread it. +fn describeLayout(w: *std.Io.Writer, l: image.Layout, opts: image.Options) !void { + var mapped: usize = 0; + var loaded: usize = 0; + for (l.segments) |s| switch (s.kind) { + .mapped => mapped += 1, + else => loaded += 1, + }; + try w.print("image {d} B = {d} B segments + {d} B overhead, entry 0x{x}\n", .{ + l.bytes.len, l.payload, l.overhead, l.entry, + }); + var off: usize = 24; + for (l.segments) |s| { + const flash = opts.flash_offset + off + 8; + try w.print(" {s:<6} vaddr=0x{x:0>8} len={d:>6} flash=0x{x:0>6}{s}\n", .{ + @tagName(s.kind), s.addr, s.len, flash, + if (s.kind == .mapped and flash % opts.mmu_page == s.addr % opts.mmu_page) + " congruent" + else + "", + }); + off += 8 + s.len; + } + try w.print(" {d} mapped, {d} loaded", .{ mapped, loaded }); + if (mapped != 2) { + // The one that bites, and the two ways it happens, because the error name says neither. + try w.print(" <-- the bootloader asserts on exactly 2 mapped segments.\n", .{}); + try w.print(" .flash.rodata and .flash.text are what produce them; one of them is\n", .{}); + try w.print(" empty or merged. `zig build layout` on the ELF shows which.\n", .{}); + } else try w.print("\n", .{}); + try w.print(" 24 B header + {d} B segment headers + checksum pad + 32 B sha256\n", .{l.segments.len * 8}); +} + +/// `zig build layout` - the segment table for an ELF, WITHOUT validating it. +/// +/// `size` cannot do this job: it reads the finished image, so it only runs when the image built, and +/// the moment you actually need the table is when it did not. This one starts from the ELF and +/// reports the loader verdict as text instead of as a failed build, so an image the bootloader would +/// refuse can still be inspected. +const LayoutStep = struct { + step: std.Build.Step, + elf: std.Build.LazyPath, + opts: image.Options, + + fn create(b: *std.Build, app: *std.Build.Step.Compile, opts: image.Options) *LayoutStep { + const self = b.allocator.create(LayoutStep) catch @panic("OOM"); + self.* = .{ + .step = std.Build.Step.init(.{ .id = .custom, .name = "layout", .owner = b, .makeFn = make }), + .elf = app.getEmittedBin(), + .opts = opts, + }; + self.elf.addStepDependencies(&self.step); + return self; + } + + fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) anyerror!void { + const self: *LayoutStep = @fieldParentPtr("step", step); + const b = step.owner; + const io = b.graph.io; + const gpa = options.gpa; + + const elf_path = self.elf.getPath2(b, step); + const elf_bytes = std.Io.Dir.cwd().readFileAlloc(io, elf_path, gpa, .limited(8 << 20)) catch |err| + return step.fail("unable to read {s}: {s}", .{ elf_path, @errorName(err) }); + defer gpa.free(elf_bytes); + + var layout = image.fromElf(gpa, elf_bytes, self.opts) catch |err| + return step.fail("cannot derive segments from {s}: {s}", .{ elf_path, @errorName(err) }); + defer layout.deinit(gpa); + + var buf: [4096]u8 = undefined; + var stdout = std.Io.File.stdout().writer(io, &buf); + const w = &stdout.interface; + try w.print("{s}\n", .{elf_path}); + try describeLayout(w, layout, self.opts); + if (layout.validate(self.opts)) |_| { + try w.print(" verdict: the loader would accept this image\n", .{}); + } else |err| { + try w.print(" verdict: the loader would REFUSE this image: {s}\n", .{@errorName(err)}); + } + try w.flush(); + } +}; + /// Read a built image back off disk. Both the flash and size steps do exactly this, which is what /// lets them work on a cache hit, in any order, or on an image from a previous build. fn readImage(step: *std.Build.Step, gpa: std.mem.Allocator, path: std.Build.LazyPath, opts: image.Options) !image.Layout { @@ -1652,6 +1819,83 @@ const ResetStep = struct { } }; +/// `zig build selftest` - run the die's own test suite and make its verdict the build's. +/// +/// `monitor` already prints what the board says, so this exists for one reason: an exit code. A +/// suite whose result a human has to read out of a scrolling log is a suite that gets ignored the +/// first busy afternoon, and the whole point of putting tests on the board was that the interesting +/// failures are the ones a host cannot see. +/// +/// Reads until `MARK SELFTEST DONE pass=N fail=M`, echoing as it goes so a failing check is visible +/// in place rather than only as a count. Absent marker within the window is itself a failure: it +/// means the board never got there, which is worse than a failed check and must not read as a pass. +const SelftestStep = struct { + step: std.Build.Step, + port: []const u8, + seconds: u32, + + fn create(b: *std.Build, port: []const u8, seconds: u32) *SelftestStep { + const self = b.allocator.create(SelftestStep) catch @panic("OOM"); + self.* = .{ + .step = std.Build.Step.init(.{ .id = .custom, .name = "selftest", .owner = b, .makeFn = make }), + .port = port, + .seconds = seconds, + }; + return self; + } + + fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) anyerror!void { + const self: *SelftestStep = @fieldParentPtr("step", step); + const b = step.owner; + + port_lock.lockUncancelable(b.graph.io); + defer port_lock.unlock(b.graph.io); + + var port = serial.Port.open(self.port, .b115200) catch |err| + return failPort(step, self.port, err); + defer port.close(); + port.resetToRun(.{}) catch {}; + + var out_buf: [4096]u8 = undefined; + var stdout = std.Io.File.stdout().writer(b.graph.io, &out_buf); + var buf: [1024]u8 = undefined; + // The summary can straddle a read, so the tail of every read is kept. 128 is far more than + // the marker needs and costs nothing. + var tail: [128]u8 = undefined; + var tail_len: usize = 0; + const deadline = port.nowMs() + @as(i64, self.seconds) * 1000; + + while (port.nowMs() < deadline) { + const n = port.readTimeout(&buf, 200) catch break; + if (n == 0) continue; + try stdout.interface.writeAll(buf[0..n]); + try stdout.interface.flush(); + + const keep = @min(tail_len, tail.len - @min(n, tail.len)); + if (keep > 0) std.mem.copyForwards(u8, tail[0..keep], tail[tail_len - keep ..][0..keep]); + const take = @min(n, tail.len - keep); + @memcpy(tail[keep..][0..take], buf[n - take ..][0..take]); + tail_len = keep + take; + + if (std.mem.indexOf(u8, tail[0..tail_len], "SELFTEST DONE")) |at| { + const rest = tail[at..tail_len]; + const fail_at = std.mem.indexOf(u8, rest, "fail=") orelse continue; + var digits = rest[fail_at + 5 ..]; + var end: usize = 0; + while (end < digits.len and digits[end] >= '0' and digits[end] <= '9') end += 1; + if (end == 0) continue; + const failures = std.fmt.parseInt(u32, digits[0..end], 10) catch continue; + if (failures != 0) return step.fail("{d} on-board check(s) failed", .{failures}); + return; + } + } + return step.fail( + "the board never reported a result within {d}s - it did not reach the end of the suite", + .{self.seconds}, + ); + } +}; + const SizeStep = struct { step: std.Build.Step, bin: std.Build.LazyPath, |
