build: add semantic versioning support

both nd and ngui now acquire semantic versioning recorded at the build
time. they also report the version at startup and -v flag.
this is useful for a release process and to avoid potential
compatibility issues in the future.

in a regular build flow, the version is taken from a git tag using the
following command:

    git -C . describe --match 'v*.*.*' --tags

in a non-standard scenario where git isn't available, the version can
be provided on the command line during build like so:

    zig build -Dversion=1.2.3

if both git and command line supplied versions are available, they must
match.
alex 1 year ago
parent 53812cbd37
commit 8bccc87029
Signed by: x1ddos
GPG Key ID: FDEFB4A63CBD8460

@ -1,3 +1,10 @@
clone:
git:
image: woodpeckerci/plugin-git
# https://woodpecker-ci.org/plugins/Git%20Clone
settings:
# tags are required for aarch64 release builds
tags: true
pipeline:
lint:
image: git.qcode.ch/nakamochi/ci-zig0.10.1:v2

@ -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 v<semver>format, 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" };
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;
};
}
};

@ -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.nd_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("version {any}", .{buildopts.nd_semver});
// reset the screen backlight to normal power regardless
// of its previous state.

@ -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.ngui_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("version {any}", .{buildopts.ngui_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| {