summaryrefslogtreecommitdiff
path: root/build.zig
diff options
context:
space:
mode:
Diffstat (limited to 'build.zig')
-rw-r--r--build.zig246
1 files changed, 245 insertions, 1 deletions
diff --git a/build.zig b/build.zig
index a7e9edb..f85ce1f 100644
--- a/build.zig
+++ b/build.zig
@@ -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,