zig: upgrade from 0.10.x to 0.11.0
ci/woodpecker/push/woodpecker Pipeline was successful Details

while there's lots of changes and new features in zig v0.11.0, the most
important for this project at the moment is the HTTP client.
the client is most likely what will connect to lnd lightning node to
fetch stats and info for the UI to then visualize it on "lightning" tab,
similar to the bitcoind RPC client.

see all zig 0.11 release notes here:
https://ziglang.org/download/0.11.0/release-notes.html
pull/26/head
alex 1 year ago
parent e84489c345
commit 328df67c5d
Signed by: x1ddos
GPG Key ID: FDEFB4A63CBD8460

@ -9,19 +9,23 @@ clone:
recursive: false
pipeline:
lint:
image: git.qcode.ch/nakamochi/ci-zig0.10.1:v3
image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
commands:
- ./tools/fmt-check.sh
test:
image: git.qcode.ch/nakamochi/ci-zig0.10.1:v3
image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
commands:
- zig build test
sdl2:
image: git.qcode.ch/nakamochi/ci-zig0.10.1:v3
image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
commands:
- zig build -Ddriver=sdl2
aarch64:
image: git.qcode.ch/nakamochi/ci-zig0.10.1:v3
image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
commands:
- zig build -Ddriver=fbev -Dtarget=aarch64-linux-musl -Drelease-safe -Dstrip
- zig build -Ddriver=fbev -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe -Dstrip
- sha256sum zig-out/bin/nd zig-out/bin/ngui
playground:
image: git.qcode.ch/nakamochi/ci-zig0.11.0:v2
commands:
- zig build guiplay btcrpc

@ -2,13 +2,13 @@
build for rpi:
zig build -Dtarget=aarch64-linux-musl -Ddriver=fbev -Drelease-safe -Dstrip
zig build -Dtarget=aarch64-linux-musl -Ddriver=fbev -Doptimize=ReleaseSafe -Dstrip
otherwise just `zig build` on dev host
## local development
you'll need [zig v0.10.x](https://ziglang.org/download/).
you'll need [zig v0.11.x](https://ziglang.org/download/).
if working on the gui, also [SDL2](https://www.libsdl.org/).
note that compiling the daemon on macOS is currently unsupported since
@ -58,17 +58,17 @@ to make a new image and switch the CI to use it, first modify the
[ci-containerfile](tools/ci-containerfile) and produce the image locally:
podman build --rm -t ndg-ci -f ./tools/ci-containerfile \
--build-arg ZIGURL=https://ziglang.org/download/0.10.1/zig-linux-x86_64-0.10.1.tar.xz
--build-arg ZIGURL=https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz
then tag it with the target URL, for example:
podman tag localhost/ndg-ci git.qcode.ch/nakamochi/ci-zig0.10.1:v1
podman tag localhost/ndg-ci git.qcode.ch/nakamochi/ci-zig0.11.0:v2
generate an [access token](https://git.qcode.ch/user/settings/applications),
login to the container registry and push the image to remote:
podman login git.qcode.ch
podman push git.qcode.ch/nakamochi/ci-zig0.10.1:v1
podman push git.qcode.ch/nakamochi/ci-zig0.11.0:v2
the image will be available at
https://git.qcode.ch/nakamochi/-/packages/

@ -1,20 +1,26 @@
const std = @import("std");
const nifbuild = @import("lib/nif/build.zig");
pub fn build(b: *std.build.Builder) void {
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const mode = b.standardReleaseOptions();
const optimize = b.standardOptimizeOption(.{});
const strip = b.option(bool, "strip", "strip output binary; default: false") orelse false;
const drv = b.option(DriverTarget, "driver", "display and input drivers combo; default: sdl2") orelse .sdl2;
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 lvgl_loglevel = b.option(LVGLLogLevel, "lvgl_loglevel", "LVGL lib logging level") orelse LVGLLogLevel.default(optimize);
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);
buildopts.step.dependOn(semver_step);
// network interface (nif) standalone library used by the daemon and tests.
const libnif_dep = b.anonymousDependency("lib/nif", @import("lib/nif/build.zig"), .{
.target = target,
.optimize = optimize,
});
const libnif = libnif_dep.artifact("nif");
const common_cflags = .{
"-Wall",
@ -25,17 +31,18 @@ pub fn build(b: *std.build.Builder) void {
};
// gui build
const ngui = b.addExecutable("ngui", "src/ngui.zig");
ngui.setTarget(target);
ngui.setBuildMode(mode);
const ngui = b.addExecutable(.{
.name = "ngui",
.root_source_file = .{ .path = "src/ngui.zig" },
.target = target,
.optimize = optimize,
.link_libc = true,
});
ngui.pie = true;
ngui.strip = strip;
ngui.step.dependOn(semver_step);
ngui.addPackage(buildopts.getPackage("build_options"));
ngui.addIncludePath("lib");
ngui.addIncludePath("src/ui/c");
ngui.linkLibC();
ngui.addOptions("build_options", buildopts);
ngui.addIncludePath(.{ .path = "lib" });
ngui.addIncludePath(.{ .path = "src/ui/c" });
const lvgl_flags = .{
"-std=c11",
@ -68,14 +75,14 @@ pub fn build(b: *std.build.Builder) void {
switch (drv) {
.sdl2 => {
ngui.addCSourceFiles(lvgl_sdl2_src, &lvgl_flags);
ngui.addCSourceFile("src/ui/c/drv_sdl2.c", &ngui_cflags);
ngui.addCSourceFile(.{ .file = .{ .path = "src/ui/c/drv_sdl2.c" }, .flags = &ngui_cflags });
ngui.defineCMacro("NM_DRV_SDL2", null);
ngui.defineCMacro("USE_SDL", null);
ngui.linkSystemLibrary("SDL2");
},
.fbev => {
ngui.addCSourceFiles(lvgl_fbev_src, &lvgl_flags);
ngui.addCSourceFile("src/ui/c/drv_fbev.c", &ngui_cflags);
ngui.addCSourceFile(.{ .file = .{ .path = "src/ui/c/drv_fbev.c" }, .flags = &ngui_cflags });
ngui.defineCMacro("NM_DRV_FBEV", null);
ngui.defineCMacro("USE_FBDEV", null);
ngui.defineCMacro("USE_EVDEV", null);
@ -83,68 +90,77 @@ pub fn build(b: *std.build.Builder) void {
}
const ngui_build_step = b.step("ngui", "build ngui (nakamochi gui)");
ngui_build_step.dependOn(&b.addInstallArtifact(ngui).step);
ngui_build_step.dependOn(&b.addInstallArtifact(ngui, .{}).step);
// daemon build
const nd = b.addExecutable("nd", "src/nd.zig");
nd.setTarget(target);
nd.setBuildMode(mode);
const nd = b.addExecutable(.{
.name = "nd",
.root_source_file = .{ .path = "src/nd.zig" },
.target = target,
.optimize = optimize,
});
nd.pie = true;
nd.strip = strip;
nd.step.dependOn(semver_step);
nd.addPackage(buildopts.getPackage("build_options"));
nifbuild.addPkg(b, nd, "lib/nif");
const niflib = nifbuild.library(b, "lib/nif");
niflib.setTarget(target);
niflib.setBuildMode(mode);
nd.linkLibrary(niflib);
nd.addOptions("build_options", buildopts);
nd.addModule("nif", libnif_dep.module("nif"));
nd.linkLibrary(libnif);
const nd_build_step = b.step("nd", "build nd (nakamochi daemon)");
nd_build_step.dependOn(&b.addInstallArtifact(nd).step);
// default build
const build_all_step = b.step("all", "build nd and ngui");
build_all_step.dependOn(ngui_build_step);
build_all_step.dependOn(nd_build_step);
b.default_step.dependOn(build_all_step);
nd_build_step.dependOn(&b.addInstallArtifact(nd, .{}).step);
// automated tests
{
const tests = b.addTest("src/test.zig");
tests.setTarget(target);
tests.setBuildMode(mode);
tests.linkLibC();
tests.addPackage(buildopts.getPackage("build_options"));
nifbuild.addPkg(b, tests, "lib/nif");
const f = b.option([]const u8, "test-filter", "run tests matching the filter");
tests.setFilter(f);
const tests = b.addTest(.{
.root_source_file = .{ .path = "src/test.zig" },
.target = target,
.optimize = optimize,
.link_libc = true,
.filter = b.option([]const u8, "test-filter", "run tests matching the filter"),
});
tests.addOptions("build_options", buildopts);
tests.addModule("nif", libnif_dep.module("nif"));
tests.linkLibrary(libnif);
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "run tests");
test_step.dependOn(&tests.step);
test_step.dependOn(&run_tests.step);
}
// GUI playground
{
const guiplay = b.addExecutable("guiplay", "src/test/guiplay.zig");
guiplay.setTarget(target);
guiplay.setBuildMode(mode);
guiplay.step.dependOn(semver_step);
guiplay.addPackagePath("comm", "src/comm.zig");
const guiplay = b.addExecutable(.{
.name = "guiplay",
.root_source_file = .{ .path = "src/test/guiplay.zig" },
.target = target,
.optimize = optimize,
});
guiplay.addModule("comm", b.createModule(.{ .source_file = .{ .path = "src/comm.zig" } }));
const guiplay_build_step = b.step("guiplay", "build GUI playground");
guiplay_build_step.dependOn(&b.addInstallArtifact(guiplay).step);
guiplay_build_step.dependOn(&b.addInstallArtifact(guiplay, .{}).step);
guiplay_build_step.dependOn(ngui_build_step);
}
// bitcoind RPC client playground
{
const btcrpc = b.addExecutable("btcrpc", "src/test/btcrpc.zig");
btcrpc.setTarget(target);
btcrpc.setBuildMode(mode);
const btcrpc = b.addExecutable(.{
.name = "btcrpc",
.root_source_file = .{ .path = "src/test/btcrpc.zig" },
.target = target,
.optimize = optimize,
});
btcrpc.strip = strip;
btcrpc.addPackagePath("bitcoindrpc", "src/nd/bitcoindrpc.zig");
btcrpc.addModule("bitcoindrpc", b.createModule(.{ .source_file = .{ .path = "src/nd/bitcoindrpc.zig" } }));
const btcrpc_build_step = b.step("btcrpc", "bitcoind RPC client playground");
btcrpc_build_step.dependOn(&b.addInstallArtifact(btcrpc).step);
btcrpc_build_step.dependOn(&b.addInstallArtifact(btcrpc, .{}).step);
}
// default build step
const build_all_step = b.step("all", "build nd and ngui (default step)");
build_all_step.dependOn(ngui_build_step);
build_all_step.dependOn(nd_build_step);
b.default_step.dependOn(build_all_step);
}
const DriverTarget = enum {
@ -354,23 +370,28 @@ const LVGLLogLevel = enum {
/// 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
buildopts: *std.Build.Step.Options, // where to store the build version
b: *std.build.Builder,
step: std.build.Step,
b: *std.Build,
step: std.Build.Step,
fn create(b: *std.build.Builder, o: *std.build.OptionsStep, inver: ?[]const u8) *std.build.Step {
fn create(b: *std.Build, o: *std.Build.Step.Options, 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),
.step = std.Build.Step.init(.{
.id = .custom,
.name = "VersionStep: ndg semver",
.owner = b,
.makeFn = make,
}),
};
return &vstep.step;
}
fn make(step: *std.build.Step) anyerror!void {
fn make(step: *std.Build.Step, _: *std.Progress.Node) anyerror!void {
const self = @fieldParentPtr(VersionStep, "step", step);
const semver = try self.eval();
std.log.info("build version: {any}", .{semver});

@ -1,21 +1,23 @@
const build = @import("std").build;
const std = @import("std");
pub fn addPkg(b: *build.Builder, obj: *build.LibExeObjStep, prefix: []const u8) void {
obj.addPackagePath("nif", pkgPath(b, prefix));
}
pub fn pkgPath(b: *build.Builder, prefix: []const u8) []const u8 {
return b.pathJoin(&.{ prefix, "nif.zig" });
}
pub fn build(b: *std.Build) void {
_ = b.addModule("nif", .{ .source_file = .{ .path = "nif.zig" } });
pub fn library(b: *build.Builder, prefix: []const u8) *build.LibExeObjStep {
const lib = b.addStaticLibrary("nif", b.pathJoin(&.{ prefix, "nif.zig" }));
lib.addIncludePath(b.pathJoin(&.{ prefix, "wpa_supplicant" }));
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const lib = b.addStaticLibrary(.{
.name = "nif",
.root_source_file = .{ .path = "nif.zig" },
.target = target,
.optimize = optimize,
.link_libc = true,
});
lib.defineCMacro("CONFIG_CTRL_IFACE", null);
lib.defineCMacro("CONFIG_CTRL_IFACE_UNIX", null);
lib.addIncludePath(.{ .path = "wpa_supplicant" });
lib.addCSourceFiles(&.{
b.pathJoin(&.{ prefix, "wpa_supplicant/wpa_ctrl.c" }),
b.pathJoin(&.{ prefix, "wpa_supplicant/os_unix.c" }),
"wpa_supplicant/wpa_ctrl.c",
"wpa_supplicant/os_unix.c",
}, &.{
"-Wall",
"-Wextra",
@ -24,6 +26,5 @@ pub fn library(b: *build.Builder, prefix: []const u8) *build.LibExeObjStep {
"-Wunused-parameter",
"-Werror",
});
lib.linkLibC();
return lib;
b.installArtifact(lib);
}

@ -46,7 +46,7 @@ pub fn pubAddresses(allocator: mem.Allocator, ifname: ?[]const u8) ![]net.Addres
// skip loopbacks and those which are not "up"
continue;
}
const ipaddr = net.Address.initPosix(@alignCast(4, sa)); // initPosix makes a copy
const ipaddr = net.Address.initPosix(@alignCast(sa)); // initPosix makes a copy
if (ipaddr.any.family == os.AF.INET6 and ipaddr.in6.sa.scope_id > 0) {
// want only global, with 0 scope
// non-zero scopes make sense for link-local addr only.

@ -103,22 +103,37 @@ pub const MessageTag = enum(u16) {
// next: 0x0b
};
/// the return value type from `read` fn.
pub const ParsedMessage = struct {
value: Message,
arena: ?*std.heap.ArenaAllocator = null, // null for void message tags
/// releases all resources used by the message.
pub fn deinit(self: @This()) void {
if (self.arena) |a| {
const allocator = a.child_allocator;
a.deinit();
allocator.destroy(a);
}
}
};
/// reads and parses a single message from the input stream reader.
/// propagates reader errors as is. for example, a closed reader returns
/// error.EndOfStream.
///
/// callers must deallocate resources with Message.free when done.
pub fn read(allocator: mem.Allocator, reader: anytype) !Message {
/// callers must deallocate resources with ParsedMessage.deinit when done.
pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage {
// alternative is @intToEnum(reader.ReadIntLittle(u16)) but it may panic.
const tag = try reader.readEnum(MessageTag, .Little);
const len = try reader.readIntLittle(u64);
if (len == 0) {
return switch (tag) {
.ping => Message{ .ping = {} },
.pong => Message{ .pong = {} },
.poweroff => Message{ .poweroff = {} },
.standby => Message{ .standby = {} },
.wakeup => Message{ .wakeup = {} },
.ping => .{ .value = .{ .ping = {} } },
.pong => .{ .value = .{ .pong = {} } },
.poweroff => .{ .value = .{ .poweroff = {} } },
.standby => .{ .value = .{ .standby = {} } },
.wakeup => .{ .value = .{ .wakeup = {} } },
else => Error.CommReadZeroLenInNonVoidTag,
};
}
@ -126,24 +141,22 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !Message {
var bytes = try allocator.alloc(u8, len);
defer allocator.free(bytes);
try reader.readNoEof(bytes);
const jopt = json.ParseOptions{ .allocator = allocator, .ignore_unknown_fields = true };
var jstream = json.TokenStream.init(bytes);
return switch (tag) {
.ping, .pong, .poweroff, .standby, .wakeup => unreachable, // handled above
.wifi_connect => Message{
.wifi_connect = try json.parse(Message.WifiConnect, &jstream, jopt),
},
.network_report => Message{
.network_report = try json.parse(Message.NetworkReport, &jstream, jopt),
},
.get_network_report => Message{
.get_network_report = try json.parse(Message.GetNetworkReport, &jstream, jopt),
},
.poweroff_progress => Message{
.poweroff_progress = try json.parse(Message.PoweroffProgress, &jstream, jopt),
},
.bitcoind_report => Message{
.bitcoind_report = try json.parse(Message.BitcoindReport, &jstream, jopt),
inline else => |t| {
var arena = try allocator.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(allocator);
errdefer {
arena.deinit();
allocator.destroy(arena);
}
const jopt = std.json.ParseOptions{ .ignore_unknown_fields = true, .allocate = .alloc_always };
const v = try json.parseFromSliceLeaky(std.meta.TagPayload(Message, t), arena.allocator(), bytes, jopt);
const parsed = ParsedMessage{
.arena = arena,
.value = @unionInit(Message, @tagName(t), v),
};
return parsed;
},
};
}
@ -151,35 +164,25 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !Message {
/// outputs the message msg using writer.
/// all allocated resources are freed upon return.
pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void {
const jopt = .{ .whitespace = null };
var data = ByteArrayList.init(allocator);
defer data.deinit();
switch (msg) {
.ping, .pong, .poweroff, .standby, .wakeup => {}, // zero length payload
.wifi_connect => try json.stringify(msg.wifi_connect, jopt, data.writer()),
.network_report => try json.stringify(msg.network_report, jopt, data.writer()),
.get_network_report => try json.stringify(msg.get_network_report, jopt, data.writer()),
.poweroff_progress => try json.stringify(msg.poweroff_progress, jopt, data.writer()),
.bitcoind_report => try json.stringify(msg.bitcoind_report, jopt, data.writer()),
.wifi_connect => try json.stringify(msg.wifi_connect, .{}, data.writer()),
.network_report => try json.stringify(msg.network_report, .{}, data.writer()),
.get_network_report => try json.stringify(msg.get_network_report, .{}, data.writer()),
.poweroff_progress => try json.stringify(msg.poweroff_progress, .{}, data.writer()),
.bitcoind_report => try json.stringify(msg.bitcoind_report, .{}, data.writer()),
}
if (data.items.len > std.math.maxInt(u64)) {
return Error.CommWriteTooLarge;
}
try writer.writeIntLittle(u16, @enumToInt(msg));
try writer.writeIntLittle(u16, @intFromEnum(msg));
try writer.writeIntLittle(u64, data.items.len);
try writer.writeAll(data.items);
}
pub fn free(allocator: mem.Allocator, m: Message) void {
switch (m) {
.ping, .pong, .poweroff, .standby, .wakeup => {}, // zero length payload
else => |v| {
json.parseFree(@TypeOf(v), v, .{ .allocator = allocator });
},
}
}
// TODO: use fifo
//
// var buf = std.fifo.LinearFifo(u8, .Dynamic).init(t.allocator);
@ -206,16 +209,16 @@ test "read" {
var buf = std.ArrayList(u8).init(t.allocator);
defer buf.deinit();
try buf.writer().writeIntLittle(u16, @enumToInt(msg));
try buf.writer().writeIntLittle(u16, @intFromEnum(msg));
try buf.writer().writeIntLittle(u64, data.items.len);
try buf.writer().writeAll(data.items);
var bs = std.io.fixedBufferStream(buf.items);
const res = try read(t.allocator, bs.reader());
defer free(t.allocator, res);
defer res.deinit();
try t.expectEqualStrings(msg.wifi_connect.ssid, res.wifi_connect.ssid);
try t.expectEqualStrings(msg.wifi_connect.password, res.wifi_connect.password);
try t.expectEqualStrings(msg.wifi_connect.ssid, res.value.wifi_connect.ssid);
try t.expectEqualStrings(msg.wifi_connect.password, res.value.wifi_connect.password);
}
test "write" {
@ -229,7 +232,7 @@ test "write" {
const payload = "{\"ssid\":\"wlan\",\"password\":\"secret\"}";
var js = std.ArrayList(u8).init(t.allocator);
defer js.deinit();
try js.writer().writeIntLittle(u16, @enumToInt(msg));
try js.writer().writeIntLittle(u16, @intFromEnum(msg));
try js.writer().writeIntLittle(u64, payload.len);
try js.appendSlice(payload);
@ -255,8 +258,8 @@ test "write/read void tags" {
try write(t.allocator, buf.writer(), m);
var bs = std.io.fixedBufferStream(buf.items);
const res = try read(t.allocator, bs.reader());
free(t.allocator, res); // noop
try t.expectEqual(m, res);
res.deinit(); // noop due to void type
try t.expectEqual(m, res.value);
}
}
@ -283,7 +286,7 @@ test "msg sequence" {
var bs = std.io.fixedBufferStream(buf.items);
for (msgs) |m| {
const res = try read(t.allocator, bs.reader());
defer free(t.allocator, res);
try t.expectEqual(@as(MessageTag, m), @as(MessageTag, res));
defer res.deinit();
try t.expectEqual(@as(MessageTag, m), @as(MessageTag, res.value));
}
}

@ -126,7 +126,7 @@ fn sighandler(sig: c_int) callconv(.C) void {
pub fn main() !void {
// main heap allocator used throughout the lifetime of nd
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa_state.deinit()) {
defer if (gpa_state.deinit() == .leak) {
logger.err("memory leaks detected", .{});
};
const gpa = gpa_state.allocator();

@ -82,7 +82,7 @@ pub fn init(a: std.mem.Allocator, r: std.fs.File.Reader, w: std.fs.File.Writer,
.uiwriter = w,
.wpa_ctrl = try types.WpaControl.open(wpa),
.state = .stopped,
.services = svlist.toOwnedSlice(),
.services = try svlist.toOwnedSlice(),
// send a network report right at start without wifi scan to make it faster.
.want_network_report = true,
.want_wifi_scan = false,
@ -297,7 +297,7 @@ fn commThreadLoop(self: *Daemon) void {
std.atomic.spinLoopHint();
time.sleep(100 * time.ns_per_ms);
const msg = comm.read(self.allocator, self.uireader) catch |err| {
const res = comm.read(self.allocator, self.uireader) catch |err| {
self.mu.lock();
defer self.mu.unlock();
if (self.want_stop) {
@ -316,7 +316,9 @@ fn commThreadLoop(self: *Daemon) void {
},
}
};
defer res.deinit();
const msg = res.value;
logger.debug("got msg: {s}", .{@tagName(msg)});
switch (msg) {
.pong => {
@ -343,12 +345,12 @@ fn commThreadLoop(self: *Daemon) void {
},
else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}),
}
comm.free(self.allocator, msg);
self.mu.lock();
quit = self.want_stop;
self.mu.unlock();
}
logger.info("exiting comm thread loop", .{});
}
@ -356,8 +358,8 @@ fn commThreadLoop(self: *Daemon) void {
fn sendPoweroffReport(self: *Daemon) !void {
var svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.len);
defer self.allocator.free(svstat);
for (self.services) |*sv, i| {
svstat[i] = .{
for (self.services, svstat) |*sv, *stat| {
stat.* = .{
.name = sv.name,
.stopped = sv.status() == .stopped,
.err = if (sv.lastStopError()) |err| @errorName(err) else null,
@ -616,19 +618,19 @@ test "start-poweroff" {
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
.{ .name = "lnd", .stopped = false, .err = null },
.{ .name = "bitcoind", .stopped = false, .err = null },
} } }, msg1);
} } }, msg1.value);
const msg2 = try comm.read(arena, gui_reader);
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
.{ .name = "lnd", .stopped = true, .err = null },
.{ .name = "bitcoind", .stopped = false, .err = null },
} } }, msg2);
} } }, msg2.value);
const msg3 = try comm.read(arena, gui_reader);
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
.{ .name = "lnd", .stopped = true, .err = null },
.{ .name = "bitcoind", .stopped = true, .err = null },
} } }, msg3);
} } }, msg3.value);
// TODO: ensure "poweroff" was executed once custom runner is in a zig release;
// need custom runner to set up a global registry for child processes.

@ -55,16 +55,7 @@ pub const Client = struct {
};
pub fn Result(comptime m: Method) type {
return struct {
value: ResultValue(m),
arena: *ArenaAllocator,
pub fn deinit(self: @This()) void {
const allocator = self.arena.child_allocator;
self.arena.deinit();
allocator.destroy(self.arena);
}
};
return std.json.Parsed(ResultValue(m));
}
pub fn ResultValue(comptime m: Method) type {
@ -142,27 +133,16 @@ pub const Client = struct {
}
fn parseResponse(self: Client, comptime m: Method, b: []const u8) !Result(m) {
var result = Result(m){
.value = undefined,
.arena = try self.allocator.create(ArenaAllocator),
};
errdefer self.allocator.destroy(result.arena);
result.arena.* = ArenaAllocator.init(self.allocator);
var jstream = std.json.TokenStream.init(b);
const jopt = std.json.ParseOptions{ .allocator = result.arena.allocator(), .ignore_unknown_fields = true };
const resp = try std.json.parse(RpcResponse(m), &jstream, jopt);
errdefer result.arena.deinit();
if (resp.@"error") |errfield| {
const jopt = std.json.ParseOptions{ .ignore_unknown_fields = true, .allocate = .alloc_always };
const resp = try std.json.parseFromSlice(RpcResponse(m), self.allocator, b, jopt);
errdefer resp.deinit();
if (resp.value.@"error") |errfield| {
return rpcErrorFromCode(errfield.code) orelse error.UnknownError;
}
if (resp.result == null) {
if (resp.value.result == null) {
return error.NullResult;
}
result.value = resp.result.?;
return result;
return .{ .value = resp.value.result.?, .arena = resp.arena };
}
fn formatreq(self: *Client, comptime m: Method, args: MethodArgs(m)) ![]const u8 {
@ -189,7 +169,7 @@ pub const Client = struct {
try w.print("Content-Length: {d}\r\n", .{jreq.items.len});
try w.writeAll("\r\n");
try w.writeAll(jreq.items);
return bytes.toOwnedSlice();
return try bytes.toOwnedSlice();
}
fn getAuthBase64(self: Client) ![]const u8 {

@ -62,40 +62,34 @@ pub fn addWifi(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl, ssid: []const u8
/// reports network status to the writer w in `comm.Message.NetworkReport` format.
pub fn sendReport(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl, w: anytype) !void {
var arena_state = std.heap.ArenaAllocator.init(gpa);
defer arena_state.deinit();
const arena = arena_state.allocator();
var report = comm.Message.NetworkReport{
.ipaddrs = undefined,
.wifi_ssid = null,
.wifi_scan_networks = undefined,
.wifi_scan_networks = &.{},
};
// fetch all public IP addresses using getifaddrs
const pubaddr = try nif.pubAddresses(gpa, null);
defer gpa.free(pubaddr);
//var addrs = std.ArrayList([]).init(t.allocator);
var ipaddrs = try gpa.alloc([]const u8, pubaddr.len);
for (pubaddr) |a, i| {
ipaddrs[i] = try std.fmt.allocPrint(gpa, "{s}", .{a});
const pubaddr = try nif.pubAddresses(arena, null);
var ipaddr = try std.ArrayList([]const u8).initCapacity(arena, pubaddr.len);
for (pubaddr) |apub| {
try ipaddr.append(try std.fmt.allocPrint(arena, "{}", .{apub}));
}
defer {
for (ipaddrs) |a| gpa.free(a);
gpa.free(ipaddrs);
}
report.ipaddrs = ipaddrs;
report.ipaddrs = try ipaddr.toOwnedSlice();
// get currently connected SSID, if any, from WPA ctrl
const ssid = queryWifiSSID(gpa, wpa_ctrl) catch |err| blk: {
report.wifi_ssid = queryWifiSSID(arena, wpa_ctrl) catch |err| blk: {
logger.err("queryWifiSsid: {any}", .{err});
break :blk null;
};
defer if (ssid) |v| gpa.free(v);
report.wifi_ssid = ssid;
// fetch available wifi networks from scan results using WPA ctrl
var wifi_networks: ?types.StringList = if (queryWifiScanResults(gpa, wpa_ctrl)) |v| v else |err| blk: {
var wifi_networks: ?types.StringList = if (queryWifiScanResults(arena, wpa_ctrl)) |v| v else |err| blk: {
logger.err("queryWifiScanResults: {any}", .{err});
break :blk null;
};
defer if (wifi_networks) |*list| list.deinit();
if (wifi_networks) |list| {
report.wifi_scan_networks = list.items();
}
@ -179,5 +173,5 @@ fn queryWifiNetworksList(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl, filter
}
list.append(id) catch {}; // grab anything we can
}
return list.toOwnedSlice();
return try list.toOwnedSlice();
}

@ -10,12 +10,6 @@ const lvgl = @import("ui/lvgl.zig");
const screen = @import("ui/screen.zig");
const symbol = @import("ui/symbol.zig");
/// SIGPIPE is triggered when a process attempts to write to a broken pipe.
/// by default, SIGPIPE terminates the process without invoking a panic handler.
/// this declaration makes such writes result in EPIPE (error.BrokenPipe) to let
/// the program can handle it.
pub const keep_sigpipe = true;
const logger = std.log.scoped(.ngui);
// these are auto-closed as soon as main fn terminates.
@ -46,39 +40,40 @@ var state: enum {
/// while deinit and replace handle concurrency, field access requires holding mu.
var last_report: struct {
mu: std.Thread.Mutex = .{},
network: ?comm.Message.NetworkReport = null,
bitcoind: ?comm.Message.BitcoindReport = null,
network: ?comm.ParsedMessage = null, // NetworkReport
bitcoind: ?comm.ParsedMessage = null, // BitcoinReport
fn deinit(self: *@This()) void {
self.mu.lock();
defer self.mu.unlock();
if (self.network) |v| {
comm.free(gpa, .{ .network_report = v });
v.deinit();
self.network = null;
}
if (self.bitcoind) |v| {
comm.free(gpa, .{ .bitcoind_report = v });
v.deinit();
self.bitcoind = null;
}
}
fn replace(self: *@This(), new: anytype) void {
fn replace(self: *@This(), new: comm.ParsedMessage) void {
self.mu.lock();
defer self.mu.unlock();
switch (@TypeOf(new)) {
comm.Message.NetworkReport => {
const tag: comm.MessageTag = new.value;
switch (tag) {
.network_report => {
if (self.network) |old| {
comm.free(gpa, .{ .network_report = old });
old.deinit();
}
self.network = new;
},
comm.Message.BitcoindReport => {
.bitcoind_report => {
if (self.bitcoind) |old| {
comm.free(gpa, .{ .bitcoind_report = old });
old.deinit();
}
self.bitcoind = new;
},
else => @compileError("unhandled type: " ++ @typeName(@TypeOf(new))),
else => |t| logger.err("last_report: replace: unhandled tag {}", .{t}),
}
}
} = .{};
@ -103,9 +98,9 @@ export fn nm_get_curr_tick() u32 {
const ms = tick_timer.read() / time.ns_per_ms;
const over = ms >> 32;
if (over > 0) {
return @truncate(u32, over); // LVGL deals with overflow correctly
return @truncate(over); // LVGL deals with overflow correctly
}
return @truncate(u32, ms);
return @truncate(ms);
}
export fn nm_check_idle_time(_: *lvgl.LvTimer) void {
@ -223,27 +218,27 @@ fn commThreadLoopCycle() !void {
ui_mutex.lock(); // guards the state and all UI calls below
defer ui_mutex.unlock();
switch (state) {
.standby => switch (msg) {
.standby => switch (msg.value) {
.ping => try comm.write(gpa, stdout, comm.Message.pong),
.network_report => |v| last_report.replace(v),
.bitcoind_report => |v| last_report.replace(v),
else => logger.debug("ignoring {s}: in standby", .{@tagName(msg)}),
.network_report => last_report.replace(msg),
.bitcoind_report => last_report.replace(msg),
else => logger.debug("ignoring {s}: in standby", .{@tagName(msg.value)}),
},
.active, .alert => switch (msg) {
.active, .alert => switch (msg.value) {
.ping => try comm.write(gpa, stdout, comm.Message.pong),
.poweroff_progress => |rep| {
ui.poweroff.updateStatus(rep) catch |err| logger.err("poweroff.updateStatus: {any}", .{err});
comm.free(gpa, msg);
msg.deinit();
},
.network_report => |rep| {
updateNetworkStatus(rep) catch |err| logger.err("updateNetworkStatus: {any}", .{err});
last_report.replace(rep);
last_report.replace(msg);
},
.bitcoind_report => |rep| {
ui.bitcoin.updateTabPanel(rep) catch |err| logger.err("bitcoin.updateTabPanel: {any}", .{err});
last_report.replace(rep);
last_report.replace(msg);
},
else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}),
else => logger.warn("unhandled msg tag {s}", .{@tagName(msg.value)}),
},
}
}
@ -281,11 +276,15 @@ fn uiThreadLoop() void {
last_report.mu.lock();
defer last_report.mu.unlock();
if (last_report.network) |rep| {
updateNetworkStatus(rep) catch |err| logger.err("updateNetworkStatus: {any}", .{err});
if (last_report.network) |msg| {
updateNetworkStatus(msg.value.network_report) catch |err| {
logger.err("updateNetworkStatus: {any}", .{err});
};
}
if (last_report.bitcoind) |rep| {
ui.bitcoin.updateTabPanel(rep) catch |err| logger.err("bitcoin.updateTabPanel: {any}", .{err});
if (last_report.bitcoind) |msg| {
ui.bitcoin.updateTabPanel(msg.value.bitcoind_report) catch |err| {
logger.err("bitcoin.updateTabPanel: {any}", .{err});
};
}
}
continue;
@ -342,7 +341,7 @@ fn sighandler(sig: c_int) callconv(.C) void {
pub fn main() anyerror!void {
// main heap allocator used through the lifetime of nd
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa_state.deinit()) {
defer if (gpa_state.deinit() == .leak) {
logger.err("memory leaks detected", .{});
};
gpa = gpa_state.allocator();

@ -55,8 +55,8 @@ pub const TestChildProcess = struct {
pub fn init(argv: []const []const u8, allocator: std.mem.Allocator) TestChildProcess {
var adup = allocator.alloc([]u8, argv.len) catch unreachable;
for (argv) |v, i| {
adup[i] = allocator.dupe(u8, v) catch unreachable;
for (argv, adup) |v, *dup| {
dup.* = allocator.dupe(u8, v) catch unreachable;
}
return .{
.allocator = allocator,
@ -167,10 +167,10 @@ pub const TestWpaControl = struct {
}
};
/// similar to std.testing.expectEqual but compares slices with expectEqualSlices
/// similar to std.testing.expectEqualDeep but compares slices with expectEqualSlices
/// or expectEqualStrings where slice element is a u8.
/// unhandled types are passed to std.testing.expectEqualDeep.
pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void {
const t = std.testing;
switch (@typeInfo(@TypeOf(actual))) {
.Pointer => |p| {
switch (p.size) {
@ -185,7 +185,7 @@ pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void {
}
break :blk null;
};
const n = std.math.min(expected.len, actual.len);
const n = @min(expected.len, actual.len);
var i: usize = 0;
while (i < n) : (i += 1) {
expectDeepEqual(expected[i], actual[i]) catch |e| {
@ -199,14 +199,14 @@ pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void {
},
else => {
if (p.child == u8) {
try t.expectEqualStrings(expected, actual);
try std.testing.expectEqualStrings(expected, actual);
} else {
try t.expectEqualSlices(p.child, expected, actual);
try std.testing.expectEqualSlices(p.child, expected, actual);
}
},
}
},
else => try t.expectEqual(expected, actual),
else => try std.testing.expectEqualDeep(expected, actual),
}
},
.Struct => |st| {
@ -238,17 +238,15 @@ pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void {
}
const Tag = std.meta.Tag(@TypeOf(expected));
const atag = @as(Tag, actual);
try t.expectEqual(@as(Tag, expected), atag);
inline for (u.fields) |f| {
if (std.mem.eql(u8, f.name, @tagName(atag))) {
try expectDeepEqual(@field(expected, f.name), @field(actual, f.name));
return;
}
try std.testing.expectEqual(@as(Tag, expected), atag);
switch (expected) {
inline else => |x, tag| {
try expectDeepEqual(x, @field(actual, @tagName(tag)));
},
}
unreachable;
},
else => {
try t.expectEqual(expected, actual);
try std.testing.expectEqualDeep(expected, actual);
},
}
}

@ -4,7 +4,7 @@ const bitcoinrpc = @import("bitcoindrpc");
pub fn main() !void {
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa_state.deinit()) {
defer if (gpa_state.deinit() == .leak) {
std.debug.print("memory leaks detected!", .{});
};
const gpa = gpa_state.allocator();

@ -87,9 +87,10 @@ fn commReadThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
logger.err("comm.read: {any}", .{err});
continue;
};
defer msg.deinit();
logger.debug("got ui msg tagged {s}", .{@tagName(msg)});
switch (msg) {
logger.debug("got msg: {s}", .{@tagName(msg.value)});
switch (msg.value) {
.pong => {
logger.info("received pong from ngui", .{});
},
@ -131,18 +132,18 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
while (true) {
time.sleep(time.ns_per_s);
if (sectimer.read() < time.ns_per_s) {
if (sectimer.read() < 3 * time.ns_per_s) {
continue;
}
sectimer.reset();
block_count += 1;
const now = time.timestamp();
const btcrep: comm.Message.BitcoindReport = .{
.blocks = block_count,
.headers = block_count,
.timestamp = @intCast(u64, now),
.timestamp = @intCast(now),
.hash = "00000000000000000002bf8029f6be4e40b4a3e0e161b6a1044ddaf9eb126504",
.ibd = false,
.verifyprogress = 100,
@ -155,7 +156,7 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
.mempool = .{
.loaded = true,
.txcount = 100000 + block_count,
.usage = std.math.min(200123456 + block_count * 10, 300000000),
.usage = @min(200123456 + block_count * 10, 300000000),
.max = 300000000,
.totalfee = 2.23049932,
.minfee = 0.00004155,
@ -168,7 +169,7 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
pub fn main() !void {
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa_state.deinit()) {
defer if (gpa_state.deinit() == .leak) {
logger.err("memory leaks detected", .{});
};
const gpa = gpa_state.allocator();

@ -106,10 +106,10 @@ pub fn updateTabPanel(rep: comm.Message.BitcoindReport) !void {
if (rep.mempool.max == 0) {
break :pct 0;
}
const v = @intToFloat(f64, rep.mempool.usage) / @intToFloat(f64, rep.mempool.max);
break :pct @floatCast(f32, v * 100);
const v = @as(f64, @floatFromInt(rep.mempool.usage)) / @as(f64, @floatFromInt(rep.mempool.max));
break :pct @floatCast(v * 100);
};
tab.mempool.usage_bar.setValue(@floatToInt(i32, @round(mempool_pct)));
tab.mempool.usage_bar.setValue(@as(i32, @intFromFloat(@round(mempool_pct))));
try tab.mempool.usage_lab.setTextFmt(&buf, "{:.1} " ++ cmark ++ "out of# {:.1} ({d:.1}%)", .{
fmt.fmtIntSizeBin(rep.mempool.usage),
fmt.fmtIntSizeBin(rep.mempool.max),

@ -203,7 +203,7 @@ pub const LvStyle = opaque {
/// produces an int value suitable for lv_xxx functions.
fn value(self: Selector) c.lv_style_selector_t {
return @enumToInt(self.part) | @enumToInt(self.state);
return @intFromEnum(self.part) | @intFromEnum(self.state);
}
};
};
@ -249,11 +249,11 @@ const RGB16 = packed struct {
/// rgb produces a Color value base on the red, green and blue components.
pub inline fn rgb(r: u8, g: u8, b: u8) Color {
const c16 = RGB16{
.b = @truncate(u5, b >> 3),
.g = @truncate(u6, g >> 2),
.r = @truncate(u5, r >> 3),
.b = @truncate(b >> 3),
.g = @truncate(g >> 2),
.r = @truncate(r >> 3),
};
return @bitCast(Color, c16);
return @bitCast(c16);
}
/// black color
@ -295,19 +295,19 @@ pub const Palette = enum(c.lv_palette_t) {
/// returns main color from the predefined palette.
pub inline fn main(p: Palette) Color {
return lv_palette_main(@enumToInt(p));
return lv_palette_main(@intFromEnum(p));
}
/// makes the main color from the predefined palette lighter according to the
/// specified level.
pub inline fn lighten(p: Palette, l: ModLevel) Color {
return lv_palette_lighten(@enumToInt(p), @enumToInt(l));
return lv_palette_lighten(@intFromEnum(p), @intFromEnum(l));
}
/// makes the main color from the predefined palette darker according to the
/// specified level.
pub inline fn darken(p: Palette, l: ModLevel) Color {
return lv_palette_darken(@enumToInt(p), @enumToInt(l));
return lv_palette_darken(@intFromEnum(p), @intFromEnum(l));
}
};
@ -326,16 +326,16 @@ pub const BaseObjMethods = struct {
/// sets or clears an object flag.
pub fn setFlag(self: anytype, v: LvObj.Flag) void {
lv_obj_add_flag(self.lvobj, @enumToInt(v));
lv_obj_add_flag(self.lvobj, @intFromEnum(v));
}
pub fn clearFlag(self: anytype, v: LvObj.Flag) void {
lv_obj_clear_flag(self.lvobj, @enumToInt(v));
lv_obj_clear_flag(self.lvobj, @intFromEnum(v));
}
/// reports whether the object has v flag set.
pub fn hasFlag(self: anytype, v: LvObj.Flag) bool {
return lv_obj_has_flag(self.lvobj, @enumToInt(v));
return lv_obj_has_flag(self.lvobj, @intFromEnum(v));
}
/// returns a user data pointer associated with the object.
@ -409,7 +409,7 @@ pub const WidgetMethods = struct {
/// aligns object position. the offset is relative to the specified alignment a.
pub fn posAlign(self: anytype, a: PosAlign, xoffset: Coord, yoffset: Coord) void {
lv_obj_align(self.lvobj, @enumToInt(a), xoffset, yoffset);
lv_obj_align(self.lvobj, @intFromEnum(a), xoffset, yoffset);
}
/// sets flex layout growth property; same meaning as in CSS flex.
@ -548,24 +548,24 @@ pub const FlexLayout = struct {
}
fn adopt(obj: *LvObj, flow: Flow, opt: AlignOpt) FlexLayout {
lv_obj_set_flex_flow(obj, @enumToInt(flow));
lv_obj_set_flex_flow(obj, @intFromEnum(flow));
if (opt.all) |a| {
const v = @enumToInt(a);
const v = @intFromEnum(a);
lv_obj_set_flex_align(obj, v, v, v);
} else {
lv_obj_set_flex_align(obj, @enumToInt(opt.main), @enumToInt(opt.cross), @enumToInt(opt.track));
lv_obj_set_flex_align(obj, @intFromEnum(opt.main), @intFromEnum(opt.cross), @intFromEnum(opt.track));
}
return .{ .lvobj = obj };
}
/// sets flex layout flow on the object.
pub fn setFlow(self: FlexLayout, ff: Flow) void {
lv_obj_set_flex_flow(self.lvobj, @enumToInt(ff));
lv_obj_set_flex_flow(self.lvobj, @intFromEnum(ff));
}
/// sets flex layout alignments.
pub fn setAlign(self: FlexLayout, main: Align, cross: AlignCross, track: Align) void {
lv_obj_set_flex_align(self.lvobj, @enumToInt(main), @enumToInt(cross), @enumToInt(track));
lv_obj_set_flex_align(self.lvobj, @intFromEnum(main), @intFromEnum(cross), @intFromEnum(track));
}
/// same as setPad .column but using a default constant to make flex layouts consistent.
@ -645,10 +645,10 @@ pub const Label = struct {
lv_label_set_text(lv_label, text);
//lv_obj_set_height(lb, sizeContent); // default
if (opt.long_mode) |m| {
lv_label_set_long_mode(lv_label, @enumToInt(m));
lv_label_set_long_mode(lv_label, @intFromEnum(m));
}
if (opt.pos) |p| {
lv_obj_align(lv_label, @enumToInt(p), 0, 0);
lv_obj_align(lv_label, @intFromEnum(p), 0, 0);
}
if (opt.recolor) {
lv_label_set_recolor(lv_label, true);

@ -67,12 +67,12 @@ pub fn modal(title: [*:0]const u8, text: [*:0]const u8, btns: []const [*:0]const
btncont.setHeightToContent();
// leave 5% as an extra spacing.
const btnwidth = lvgl.sizePercent(try std.math.divFloor(i16, 95, @truncate(u8, btns.len)));
for (btns) |btext, i| {
const btnwidth = lvgl.sizePercent(try std.math.divFloor(i16, 95, @as(u8, @truncate(btns.len))));
for (btns, 0..) |btext, i| {
const btn = try lvgl.TextButton.new(btncont, btext);
btn.setFlag(.event_bubble);
btn.setFlag(.user1); // .user1 indicates actionable button in callback
btn.setUserdata(@intToPtr(?*anyopaque, i)); // button index in callback
btn.setUserdata(@ptrFromInt(i)); // button index in callback
btn.setWidth(btnwidth);
if (i == 0) {
btn.addStyle(lvgl.nm_style_btn_red(), .{});
@ -88,9 +88,9 @@ export fn nm_modal_callback(e: *lvgl.LvEvent) void {
return;
}
const btn_index = @ptrToInt(target.userdata());
const win = lvgl.Window{ .lvobj = @ptrCast(*lvgl.LvObj, edata) };
const cb = @ptrCast(ModalButtonCallbackFn, @alignCast(@alignOf(ModalButtonCallbackFn), win.userdata()));
const btn_index = @intFromPtr(target.userdata());
const win = lvgl.Window{ .lvobj = @ptrCast(edata) };
const cb: ModalButtonCallbackFn = @alignCast(@ptrCast(win.userdata()));
win.destroy();
cb(btn_index);
}

@ -14,7 +14,7 @@ fn formatUnix(sec: u64, comptime fmt: []const u8, opts: std.fmt.FormatOptions, w
if (sec > std.math.maxInt(u47)) {
// EpochSeconds.getEpochDay trucates to u47 which results in a "truncated bits"
// panic for too big numbers. so, just print raw digits.
return std.fmt.format(w, "{d}", .{sec});
return std.fmt.formatInt(sec, 10, .lower, .{}, w);
}
const epoch: std.time.epoch.EpochSeconds = .{ .secs = sec };
const daysec = epoch.getDaySeconds();

@ -1,12 +1,12 @@
# ci container file for compiling and testing zig projects.
# requires a ZIGURL build arg. for instance:
# podman build --rm -t ci-zig0.10.1 -f ci-containerfile \
# --build-arg ZIGURL=https://ziglang.org/download/0.10.1/zig-linux-x86_64-0.10.1.tar.xz
# podman build --rm -t ci-zig0.11.0 -f ci-containerfile \
# --build-arg ZIGURL=https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz
FROM alpine:3.17.1
FROM alpine:3.18.3
ARG ZIGURL
RUN apk add --no-cache git curl xz sdl2-dev clang15-extra-tools && \
RUN apk add --no-cache git curl xz libc-dev sdl2-dev clang16-extra-tools && \
mkdir -p /tools/zig && \
cd /tools/zig && \
curl -o zig.tar.xz $ZIGURL && \