add nix flake/fix release mode tests
All checks were successful
Build / Engine, CLI and TUI (push) Successful in 1m56s
Build / Android (push) Successful in 5m59s
Build / Sign (push) Successful in 3m38s
Build / Publish (push) Successful in 12s

This commit is contained in:
Emil Lerch 2026-10-04 15:01:16 -07:00
parent 95b2db5e02
commit 3c9ec72b88
Signed by: lobo
GPG key ID: A7B62D657EF764F8
7 changed files with 201 additions and 17 deletions

View file

@ -55,13 +55,27 @@ jobs:
# Engine, CLI, TUI, the C ABI from Zig, the JNI layer against a synthetic # Engine, CLI, TUI, the C ABI from Zig, the JNI layer against a synthetic
# function table, and the C ABI from C through the installed header. # function table, and the C ABI from C through the installed header.
run: zig build test --summary all run: zig build test --summary all
- name: Run tests in each release mode
# What ships is not Debug: the CLI and TUI are ReleaseSafe, the Android library
# ReleaseSmall. Debug fills undefined memory with 0xaa, so code that reads
# something it never wrote can pass there by luck and fail in a release build:
# the JNI tests did exactly that. ReleaseFast has no safety checks at all, which
# is where such a read shows up most readily, so it runs too.
run: |
for mode in ReleaseSafe ReleaseSmall ReleaseFast; do
echo "== $mode"
zig build test -Doptimize="$mode" --summary all
done
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: '21' java-version: '21'
- name: Prove the JNI table against a real JVM - name: Prove the JNI table against a real JVM
run: zig build jvm-test --summary all # Debug, and ReleaseSmall: the optimization the Android library ships with.
run: |
zig build jvm-test --summary all
zig build jvm-test -Doptimize=ReleaseSmall --summary all
- name: Package the CLI and TUI - name: Package the CLI and TUI
# One binary holds both. Static musl on Linux, so it runs on any distribution. # One binary holds both. Static musl on Linux, so it runs on any distribution.
run: | run: |

4
.gitignore vendored
View file

@ -2,6 +2,10 @@
.zig-cache/ .zig-cache/
zig-out/ zig-out/
# nix build output links
result
result-*
# Gradle and Android build artifacts # Gradle and Android build artifacts
android/.gradle/ android/.gradle/
android/.kotlin/ android/.kotlin/

View file

@ -104,6 +104,35 @@ Android ties an app to its signing key permanently, so a published APK cannot up
copy you built yourself (which is signed with your debug key), and the reverse. Uninstall copy you built yourself (which is signed with your debug key), and the reverse. Uninstall
one before installing the other; the app's saved state goes with it. one before installing the other; the app's saved state goes with it.
### With nix
This repository is a flake, for the CLI and TUI (the Android app stays with gradle). To
add it to a `buildEnv`-style home profile:
```nix
inputs = {
tally.url = "git+https://git.lerch.org/lobo/tally.git";
# Optional: reuse your own nixpkgs instead of instantiating another one.
tally.inputs.nixpkgs.follows = "nixpkgs";
};
```
then add `tally.packages.${system}.default` to your package list. Or run it directly:
```
nix run git+https://git.lerch.org/lobo/tally.git -- '2^100 + 1'
```
The package is the `tally` binary, plus the engine as `lib/libtally-engine.a`,
`lib/libtally.so` and `include/tally.h`. `nix flake update tally` picks up a new version.
The build runs `zig build test` in the sandbox, in ReleaseSafe like the binary it
installs, so a version that fails its tests will not install. `nix develop` gives a shell
with Zig 0.16 and zls.
Changing the dependencies in `build.zig.zon` invalidates the `zigDeps` hash in
`nix/package.nix`. To refresh it: set the hash to `lib.fakeHash`, build, and paste the
hash nix reports.
## Building ## Building
Every tool comes from [mise](https://mise.jdx.dev), pinned in [.mise.toml](.mise.toml): Every tool comes from [mise](https://mise.jdx.dev), pinned in [.mise.toml](.mise.toml):
@ -186,9 +215,13 @@ A few decisions shape everything else, and the design document explains each:
- `zig build test`: about a thousand Zig tests across the engine, CLI, TUI (rendering - `zig build test`: about a thousand Zig tests across the engine, CLI, TUI (rendering
real frames and reading the cells back), the C ABI, and the JNI layer against a real frames and reading the cells back), the C ABI, and the JNI layer against a
synthetic function table; plus a C program that links the shared library through synthetic function table; plus a C program that links the shared library through
`tally.h`, which catches a header that disagrees with the library. `tally.h`, which catches a header that disagrees with the library. CI also runs it with
`-Doptimize=ReleaseSafe`, `ReleaseSmall` and `ReleaseFast`: what ships is not Debug,
and Debug's 0xaa fill of undefined memory can hide a read of something never written
(it hid one in the JNI tests).
- `zig build jvm-test`: the JNI entry points from Java, which is the only way to check - `zig build jvm-test`: the JNI entry points from Java, which is the only way to check
the function-table indices without a device. the function-table indices without a device. CI runs it in Debug and in ReleaseSmall,
the optimization the Android library ships with.
- `cd android && gradle testDebugUnitTest`: JVM tests of the app's rules (the converter, - `cd android && gradle testDebugUnitTest`: JVM tests of the app's rules (the converter,
programmer mode, saved state), no device needed. programmer mode, saved state), no device needed.
- `cd android && gradle connectedAndroidTest`: the engine through the real app on a - `cd android && gradle connectedAndroidTest`: the engine through the real app on a
@ -203,12 +236,12 @@ punctuation: the source is ASCII.
[.forgejo/workflows/build.yaml](.forgejo/workflows/build.yaml) runs on every push: [.forgejo/workflows/build.yaml](.forgejo/workflows/build.yaml) runs on every push:
| Job | Runner | What | | Job | Runner | What |
|---------------------|--------------------------|---------------------------------------------------------------------------------------------------------------------------------------------| |---------------------|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Engine, CLI and TUI | `ubuntu-latest` | `zig fmt --check`, zlint, build, `zig build test`, `zig build jvm-test`, then release builds for x86_64 and aarch64 Linux and aarch64 macOS | | Engine, CLI and TUI | `ubuntu-latest` | `zig fmt --check`, zlint, build, `zig build test` in Debug and each release mode, `zig build jvm-test` in Debug and ReleaseSmall, then release builds for x86_64 and aarch64 Linux and aarch64 macOS |
| Android | `ubuntu-latest` | `zig build android`, the library audit, the JVM unit tests, debug and unsigned release APKs (with lint's release checks) | | Android | `ubuntu-latest` | `zig build android`, the library audit, the JVM unit tests, debug and unsigned release APKs (with lint's release checks) |
| Sign | `ubuntu-latest-with-hsm` | master only: signs the APK with the HSM, then every release file with a detached signature logged to sigstore | | Sign | `ubuntu-latest-with-hsm` | master only: signs the APK with the HSM, then every release file with a detached signature logged to sigstore |
| Publish | `ubuntu-latest` | master only: uploads the signed files to the generic package registry under the short SHA and `latest` | | Publish | `ubuntu-latest` | master only: uploads the signed files to the generic package registry under the short SHA and `latest` |
Every job reports to ntfy. Zlint and Gradle are downloaded at pinned versions and checked Every job reports to ntfy. Zlint and Gradle are downloaded at pinned versions and checked
against pinned SHA-256 digests; Zig's version comes from `build.zig.zon`. against pinned SHA-256 digests; Zig's version comes from `build.zig.zon`.

View file

@ -508,20 +508,24 @@ const FakeJvm = struct {
var current: ?*FakeJvm = null; var current: ?*FakeJvm = null;
/// Puts every field in its starting state, defaults included: the tests declare the
/// fake `undefined` and call this, so a field set nowhere here would be whatever the
/// stack held. It was: `misplace_new_string` was never reset, Debug's 0xaa fill
/// happened to read as false, and release builds, which do not fill, failed the JNI
/// tests whenever the leftover byte was odd.
fn init(self: *FakeJvm) void { fn init(self: *FakeJvm) void {
self.table = .{ .entries = @splat(null) }; self.* = .{
.table = .{ .entries = @splat(null) },
.vm_table = .{ .entries = @splat(null) },
// Addresses inside `self`, which is where this value is being written.
.env = .{ .functions = &self.table },
.vm = .{ .functions = &self.vm_table },
};
self.table.entries[@intFromEnum(EnvIndex.get_version)] = @ptrCast(&getVersion); self.table.entries[@intFromEnum(EnvIndex.get_version)] = @ptrCast(&getVersion);
self.table.entries[@intFromEnum(EnvIndex.new_string_utf)] = @ptrCast(&newStringUtf); self.table.entries[@intFromEnum(EnvIndex.new_string_utf)] = @ptrCast(&newStringUtf);
self.table.entries[@intFromEnum(EnvIndex.get_string_utf_chars)] = @ptrCast(&getStringUtfChars); self.table.entries[@intFromEnum(EnvIndex.get_string_utf_chars)] = @ptrCast(&getStringUtfChars);
self.table.entries[@intFromEnum(EnvIndex.release_string_utf_chars)] = @ptrCast(&releaseStringUtfChars); self.table.entries[@intFromEnum(EnvIndex.release_string_utf_chars)] = @ptrCast(&releaseStringUtfChars);
self.env = .{ .functions = &self.table };
self.vm_table = .{ .entries = @splat(null) };
self.vm_table.entries[@intFromEnum(VmIndex.get_env)] = @ptrCast(&getEnv); self.vm_table.entries[@intFromEnum(VmIndex.get_env)] = @ptrCast(&getEnv);
self.vm = .{ .functions = &self.vm_table };
self.made_len = 0;
self.releases = 0;
current = self; current = self;
} }

26
flake.lock generated Normal file
View file

@ -0,0 +1,26 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1791142733,
"narHash": "sha256-x1jawtfPqMTQYpt1d6H+9rxXweqTzU5hYhdfz0T5MPM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "466eaf945dddfd32abd006bd94cc55660fdb56c8",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "master",
"type": "indirect"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

46
flake.nix Normal file
View file

@ -0,0 +1,46 @@
{
description = "Calculator with exact arithmetic, unit conversion and a programmer mode, as a CLI and TUI";
inputs.nixpkgs.url = "nixpkgs/master";
outputs = { self, nixpkgs }:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
# Helper to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }'.
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
in
{
packages = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
tally = pkgs.callPackage ./nix/package.nix {
# Pinned deliberately rather than tracking `pkgs.zig`: build.zig.zon
# declares 0.16.0 as the minimum, and 0.16 was a large standard
# library refactor, so a newer default would not build unchanged.
zig = pkgs.zig_0_16;
# The git-tracked tree, which keeps .zig-cache, zig-out, zig-pkg and
# the Android build outputs out of the derivation.
src = self;
};
in
{
inherit tally;
default = tally;
});
devShells = forAllSystems (system:
let pkgs = nixpkgs.legacyPackages.${system};
in
{
# `nix develop` for the CLI/TUI without mise. The versions here are the
# same ones .mise.toml pins; the Android toolchain stays with mise.
default = pkgs.mkShell {
packages = [
pkgs.zig_0_16
pkgs.zls
];
};
});
};
}

57
nix/package.nix Normal file
View file

@ -0,0 +1,57 @@
{ lib, stdenv, zig, src }:
stdenv.mkDerivation (finalAttrs: {
pname = "tally";
# Kept in step with build.zig.zon.
version = "0.1.0";
inherit src;
__structuredAttrs = true;
strictDeps = true;
# Zig resolves build.zig.zon dependencies (libvaxis) over the network, which the
# build sandbox forbids. `fetchDeps` runs `zig build --fetch` inside a fixed-output
# derivation, so the fetch happens once against a known hash and the real build
# stays offline.
#
# This hash changes whenever build.zig.zon's dependencies change. To update it: set
# `lib.fakeHash`, run the build, and paste the hash nix reports.
zigDeps = zig.fetchDeps {
inherit (finalAttrs) pname version src;
fetchAll = true;
hash = "sha256-Wy/H3z2dBsF/WTF2LrCofOuE15pbgdwEJLSUrKCOQGI=";
};
postConfigure = ''
# A writable copy rather than a symlink into the store: Zig writes cache
# metadata alongside the fetched packages while it verifies them.
cp -rLT ${finalAttrs.zigDeps} "$ZIG_GLOBAL_CACHE_DIR/p"
chmod -R u+w "$ZIG_GLOBAL_CACHE_DIR/p"
'';
nativeBuildInputs = [ zig ];
# The zig setup hook's check phase runs `zig build test`, with the same flags as the
# build (ReleaseSafe): the engine, CLI, TUI, C ABI and JNI tests, and the C program
# linked through tally.h. None of them touch the network or anything outside the
# build directory, so every rebuild is a test run. (`jvm-test`, `coverage` and
# `android` are separate steps, not run here.)
doCheck = true;
meta = {
description = "Calculator with exact arithmetic, unit conversion and a programmer mode, as a CLI and TUI";
longDescription = ''
One Zig engine behind a command line, a terminal UI and an Android app:
exact rational arithmetic where the operations allow it, programmer mode
(8 to 128-bit integers in every base), unit conversion as part of the
expression language, and financial functions. This package is the CLI/TUI
binary, plus the engine as a static library and as a shared library with
its C header (tally.h).
'';
homepage = "https://git.lerch.org/lobo/tally";
license = lib.licenses.mit;
mainProgram = "tally";
platforms = lib.platforms.unix;
};
})