diff --git a/.woodpecker.yml b/.woodpecker.yml index c6d1252..4db927b 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,18 +1,27 @@ +clone: + git: + image: woodpeckerci/plugin-git + # https://woodpecker-ci.org/plugins/Git%20Clone + settings: + # tags are required for aarch64 release builds for semver + tags: true + lfs: false + recursive: false pipeline: lint: - image: git.qcode.ch/nakamochi/ci-zig0.10.1:v2 + image: git.qcode.ch/nakamochi/ci-zig0.10.1:v3 commands: - ./tools/fmt-check.sh test: - image: git.qcode.ch/nakamochi/ci-zig0.10.1:v2 + image: git.qcode.ch/nakamochi/ci-zig0.10.1:v3 commands: - zig build test sdl2: - image: git.qcode.ch/nakamochi/ci-zig0.10.1:v2 + image: git.qcode.ch/nakamochi/ci-zig0.10.1:v3 commands: - zig build -Ddriver=sdl2 aarch64: - image: git.qcode.ch/nakamochi/ci-zig0.10.1:v2 + image: git.qcode.ch/nakamochi/ci-zig0.10.1:v3 commands: - zig build -Ddriver=fbev -Dtarget=aarch64-linux-musl -Drelease-safe -Dstrip - sha256sum zig-out/bin/nd zig-out/bin/ngui diff --git a/build.zig b/build.zig index 10a825b..7732216 100644 --- a/build.zig +++ b/build.zig @@ -10,9 +10,11 @@ pub fn build(b: *std.build.Builder) void { const disp_horiz = b.option(u32, "horiz", "display horizontal pixels count; default: 800") orelse 800; const disp_vert = b.option(u32, "vert", "display vertical pixels count; default: 480") orelse 480; const lvgl_loglevel = b.option(LVGLLogLevel, "lvgl_loglevel", "LVGL lib logging level") orelse LVGLLogLevel.default(mode); + const inver = b.option([]const u8, "version", "semantic version of the build; must match git tag when available"); const buildopts = b.addOptions(); buildopts.addOption(DriverTarget, "driver", drv); + const semver_step = VersionStep.create(b, buildopts, inver); const common_cflags = .{ "-Wall", @@ -28,6 +30,7 @@ pub fn build(b: *std.build.Builder) void { ngui.setBuildMode(mode); ngui.pie = true; ngui.strip = strip; + ngui.step.dependOn(semver_step); ngui.addPackage(buildopts.getPackage("build_options")); ngui.addIncludePath("lib"); @@ -88,6 +91,7 @@ pub fn build(b: *std.build.Builder) void { nd.setBuildMode(mode); nd.pie = true; nd.strip = strip; + nd.step.dependOn(semver_step); nd.addPackage(buildopts.getPackage("build_options")); nifbuild.addPkg(b, nd, "lib/nif"); @@ -316,3 +320,75 @@ const LVGLLogLevel = enum { }; } }; + +/// VersionStep injects a release build semantic version into buildopts as "semver". +/// the make step fails if the inver input version and the one found in a git tag mismatch. +/// +/// while git-tagged versions are expected to be in vformat, input version +/// to match against is any format supported by std.SemanticVersion.parse. +/// input version is optional; if unset, make fn succeeds given a correctly formatted +/// git tag is found. +const VersionStep = struct { + inver: ?[]const u8, // input version in std.SemanticVersion.parse format + buildopts: *std.build.OptionsStep, // where to store the build version + + b: *std.build.Builder, + step: std.build.Step, + + fn create(b: *std.build.Builder, o: *std.build.OptionsStep, inver: ?[]const u8) *std.build.Step { + const vstep = b.allocator.create(VersionStep) catch unreachable; + vstep.* = VersionStep{ + .inver = inver, + .buildopts = o, + .b = b, + .step = std.build.Step.init(.custom, "VersionStep: ndg semver", b.allocator, make), + }; + return &vstep.step; + } + + fn make(step: *std.build.Step) anyerror!void { + const self = @fieldParentPtr(VersionStep, "step", step); + const semver = try self.eval(); + std.log.info("build version: {any}", .{semver}); + self.buildopts.addOption(std.SemanticVersion, "semver", semver); + } + + fn eval(self: *VersionStep) !std.SemanticVersion { + const repover = try self.gitver(); + if (self.inver) |v| { + const insem = std.SemanticVersion.parse(v) catch |err| { + std.log.err("invalid input semver '{s}': {any}", .{ v, err }); + return err; + }; + if (repover != null and insem.order(repover.?) != .eq) { + std.log.err("input and repo semver mismatch: {any} vs {any}", .{ insem, repover }); + return error.VersionMismatch; + } + return insem; + } + + if (repover == null) { + std.log.err("must supply build semver from command line.", .{}); + return error.MissingVersion; + } + return repover.?; + } + + fn gitver(self: *VersionStep) !?std.SemanticVersion { + if (!std.process.can_spawn) { + return null; + } + const git = self.b.findProgram(&[_][]const u8{"git"}, &[_][]const u8{}) catch return null; + + const prefix = "v"; // git tag prefix + const matchTag = self.b.fmt("{s}*.*.*", .{prefix}); + const cmd = [_][]const u8{ git, "-C", self.b.pathFromRoot("."), "describe", "--match", matchTag, "--tags", "--abbrev=8" }; + var code: u8 = undefined; + const git_describe = self.b.execAllowFail(&cmd, &code, .Ignore) catch return null; + const repotag = std.mem.trim(u8, git_describe, " \n\r")[prefix.len..]; + return std.SemanticVersion.parse(repotag) catch |err| ret: { + std.log.err("unparsable git tag semver '{s}': {any}", .{ repotag, err }); + break :ret err; + }; + } +}; diff --git a/src/nd.zig b/src/nd.zig index 1ce8f2f..fab09e9 100644 --- a/src/nd.zig +++ b/src/nd.zig @@ -1,3 +1,4 @@ +const buildopts = @import("build_options"); const std = @import("std"); const os = std.os; const sys = os.system; @@ -86,6 +87,9 @@ fn parseArgs(gpa: std.mem.Allocator) !NdArgs { if (std.mem.eql(u8, a, "-h") or std.mem.eql(u8, a, "-help") or std.mem.eql(u8, a, "--help")) { usage(prog) catch {}; std.process.exit(1); + } else if (std.mem.eql(u8, a, "-v")) { + try stderr.print("{any}\n", .{buildopts.semver}); + std.process.exit(0); } else if (std.mem.eql(u8, a, "-gui")) { lastarg = .gui; } else if (std.mem.eql(u8, a, "-gui-user")) { @@ -126,6 +130,7 @@ pub fn main() !void { // parse program args first thing and fail fast if invalid const args = try parseArgs(gpa); defer args.deinit(gpa); + logger.info("ndg version {any}", .{buildopts.semver}); // reset the screen backlight to normal power regardless // of its previous state. diff --git a/src/ngui.zig b/src/ngui.zig index 0ae7bf2..b2c7799 100644 --- a/src/ngui.zig +++ b/src/ngui.zig @@ -1,3 +1,4 @@ +const buildopts = @import("build_options"); const std = @import("std"); const time = std.time; @@ -16,6 +17,7 @@ pub const keep_sigpipe = true; const stdin = std.io.getStdIn().reader(); const stdout = std.io.getStdOut().writer(); +const stderr = std.io.getStdErr().writer(); const logger = std.log.scoped(.ngui); extern "c" fn ui_update_network_status(text: [*:0]const u8, wifi_list: ?[*:0]const u8) void; @@ -177,18 +179,59 @@ fn commThreadLoopCycle() !void { } } +/// prints messages in the same way std.fmt.format does and exits the process +/// with a non-zero code. +fn fatal(comptime fmt: []const u8, args: anytype) noreturn { + stderr.print(fmt, args) catch {}; + if (fmt[fmt.len - 1] != '\n') { + stderr.writeByte('\n') catch {}; + } + std.process.exit(1); +} + +fn parseArgs(alloc: std.mem.Allocator) !void { + var args = try std.process.ArgIterator.initWithAllocator(alloc); + defer args.deinit(); + const prog = args.next() orelse return error.NoProgName; + + while (args.next()) |a| { + if (std.mem.eql(u8, a, "-h") or std.mem.eql(u8, a, "-help") or std.mem.eql(u8, a, "--help")) { + usage(prog) catch {}; + std.process.exit(1); + } else if (std.mem.eql(u8, a, "-v")) { + try stderr.print("{any}\n", .{buildopts.semver}); + std.process.exit(0); + } else { + fatal("unknown arg name {s}", .{a}); + } + } +} + +/// prints usage help text to stderr. +fn usage(prog: []const u8) !void { + try stderr.print( + \\usage: {s} [-v] + \\ + \\ngui is nakamochi GUI interface. it communicates with nd, nakamochi daemon, + \\via stdio and is typically launched by the daemon as a child process. + \\ + , .{prog}); +} + /// nakamochi UI program entry point. pub fn main() anyerror!void { - // ensure timer is available on this platform before doing anything else; - // the UI is unusable otherwise. - tick_timer = try time.Timer.start(); - // main heap allocator used through the lifetime of nd var gpa_state = std.heap.GeneralPurposeAllocator(.{}){}; defer if (gpa_state.deinit()) { logger.err("memory leaks detected", .{}); }; gpa = gpa_state.allocator(); + try parseArgs(gpa); + logger.info("ndg version {any}", .{buildopts.semver}); + + // ensure timer is available on this platform before doing anything else; + // the UI is unusable otherwise. + tick_timer = try time.Timer.start(); // initalizes display, input driver and finally creates the user interface. ui.init() catch |err| { diff --git a/tools/ci-containerfile b/tools/ci-containerfile index 31babd9..036acaf 100644 --- a/tools/ci-containerfile +++ b/tools/ci-containerfile @@ -6,7 +6,7 @@ FROM alpine:3.17.1 ARG ZIGURL -RUN apk add --no-cache curl xz sdl2-dev clang15-extra-tools && \ +RUN apk add --no-cache git curl xz sdl2-dev clang15-extra-tools && \ mkdir -p /tools/zig && \ cd /tools/zig && \ curl -o zig.tar.xz $ZIGURL && \