3bc953e2
feat: the upgrade version rule — strictly newer or refused
a73x 2026-08-26 15:01
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -193,6 +193,11 @@ const mod_table = [_]ModSpec{ | |||
| 193 | // file and later the CLI all have to agree on — protocol and nothing | 193 | // file and later the CLI all have to agree on — protocol and nothing |
| 194 | // else, so its tests need no hub, no daemon and no socket. | 194 | // else, so its tests need no hub, no daemon and no socket. |
| 195 | .{ .name = "wall", .path = "src/wall.zig", .layer = 1, .imports = &.{"protocol"}, .test_imports = &.{"testtmp"} }, | 195 | .{ .name = "wall", .path = "src/wall.zig", .layer = 1, .imports = &.{"protocol"}, .test_imports = &.{"testtmp"} }, |
| 196 | // The upgrade vocabulary: the version skew rule and the manifest an | ||
| 197 | // exec-ing daemon leaves for its replacement. Protocol only — the | ||
| 198 | // manifest is a stranger's format (length-prefixed sections, unknown | ||
| 199 | // tags skipped), so it cannot depend on server types. | ||
| 200 | .{ .name = "upgrade", .path = "src/upgrade.zig", .layer = 1 }, | ||
| 196 | // Shell integration: the OSC 133 mark scripts and what a spawn must add | 201 | // Shell integration: the OSC 133 mark scripts and what a spawn must add |
| 197 | // to hand them to a shell. Near-leaf on purpose — it writes files and | 202 | // to hand them to a shell. Near-leaf on purpose — it writes files and |
| 198 | // reads the environment, and knows nothing of ptys, servers or the | 203 | // reads the environment, and knows nothing of ptys, servers or the |
| @@ -542,7 +547,7 @@ fn docGate(b: *std.Build, target: std.Build.ResolvedTarget, check_step: *std.Bui | |||
| 542 | /// parsers — a test that is never built is not a test (decisions.md). | 547 | /// parsers — a test that is never built is not a test (decisions.md). |
| 543 | const test_order = [_][]const u8{ | 548 | const test_order = [_][]const u8{ |
| 544 | "script", "select", "protocol", "client_core", "interact", "engine", "pty", | 549 | "script", "select", "protocol", "client_core", "interact", "engine", "pty", |
| 545 | "delta", "cmd", "wall", "shellint", "replica", "keymap", "webhub", | 550 | "delta", "cmd", "wall", "upgrade", "shellint", "replica", "keymap", "webhub", |
| 546 | "wallview", "sockpath", "muxa", "server", "client", "proxy", "mux", | 551 | "wallview", "sockpath", "muxa", "server", "client", "proxy", "mux", |
| 547 | "quic", "quic_server", "exe", "testtmp", "quic_client", "predict", "rawmode", | 552 | "quic", "quic_server", "exe", "testtmp", "quic_client", "predict", "rawmode", |
| 548 | "delaypipe", "xdg", "spawn", "handoff", "paint", "layout", "render", | 553 | "delaypipe", "xdg", "spawn", "handoff", "paint", "layout", "render", |
src/upgrade.zig
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,37 @@ | |||
| 1 | //! The upgrade vocabulary: the version rule and the manifest an exec-ing | ||
| 2 | //! daemon leaves for its replacement. The manifest crosses inside one | ||
| 3 | //! process (a memfd surviving execve), yet it is encoded as if for a | ||
| 4 | //! stranger — length-prefixed sections, unknown tags skipped — because the | ||
| 5 | //! reader IS a stranger: a newer binary, or on rollback an older one, and | ||
| 6 | //! neither may be held to this build's struct layout. | ||
| 7 | |||
| 8 | const std = @import("std"); | ||
| 9 | |||
| 10 | /// Skew rule: an upgrade candidate must outrank the running daemon. | ||
| 11 | /// SemanticVersion because our releases are `0.0.1-13`-shaped: the `-13` | ||
| 12 | /// is the prerelease field and numeric identifiers order numerically, | ||
| 13 | /// which is exactly the order the release sequence needs. | ||
| 14 | pub fn strictlyNewer(candidate: []const u8, mine: []const u8) error{BadVersion}!bool { | ||
| 15 | const c = std.SemanticVersion.parse(candidate) catch return error.BadVersion; | ||
| 16 | const m = std.SemanticVersion.parse(mine) catch return error.BadVersion; | ||
| 17 | return c.order(m) == .gt; | ||
| 18 | } | ||
| 19 | |||
| 20 | test "strictlyNewer: the next prerelease number is newer (0.0.1-14 over 0.0.1-13)" { | ||
| 21 | try std.testing.expect(try strictlyNewer("0.0.1-14", "0.0.1-13")); | ||
| 22 | } | ||
| 23 | test "strictlyNewer: prerelease numbers order numerically, not lexically (0.0.1-100 over 0.0.1-99)" { | ||
| 24 | try std.testing.expect(try strictlyNewer("0.0.1-100", "0.0.1-99")); | ||
| 25 | } | ||
| 26 | test "strictlyNewer: the same version is not newer" { | ||
| 27 | try std.testing.expect(!try strictlyNewer("0.0.1-13", "0.0.1-13")); | ||
| 28 | } | ||
| 29 | test "strictlyNewer: a downgrade is not newer" { | ||
| 30 | try std.testing.expect(!try strictlyNewer("0.0.1-12", "0.0.1-13")); | ||
| 31 | } | ||
| 32 | test "strictlyNewer: a release outranks its own prereleases" { | ||
| 33 | try std.testing.expect(try strictlyNewer("0.0.1", "0.0.1-13")); | ||
| 34 | } | ||
| 35 | test "strictlyNewer: garbage is an error, not a verdict" { | ||
| 36 | try std.testing.expectError(error.BadVersion, strictlyNewer("not-a-version", "0.0.1-13")); | ||
| 37 | } | ||