85 Commits

Author SHA1 Message Date
cat 79342e3053 release: 0.4.4
Release / Create release (push) Successful in 37s
Test / Flake checks (push) Successful in 1m17s
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m49s
Test / ShareFS (push) Successful in 3m53s
Test / Hakurei (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 5m38s
Test / Hakurei (race detector) (push) Successful in 6m44s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-17 21:07:22 +09:00
cat 825bf24731 nix: read version from cmd/dist
Test / ShareFS (push) Successful in 36s
Test / Sandbox (race detector) (push) Successful in 38s
Test / Hakurei (push) Successful in 45s
Test / Sandbox (push) Successful in 42s
Test / Hakurei (race detector) (push) Successful in 48s
Test / Create distribution (push) Successful in 57s
Test / Flake checks (push) Successful in 1m15s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-17 21:06:03 +09:00
cat 08112f0b90 hst: optionally cover /run/ early
Test / Create distribution (push) Successful in 53s
Test / Sandbox (push) Successful in 2m44s
Test / ShareFS (push) Successful in 3m57s
Test / Hakurei (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 5m30s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m12s
This works around awkward root permissions.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-17 02:59:48 +09:00
cat e1a1e1e399 cmd/app: use ephemeral overlay root
Test / Create distribution (push) Successful in 56s
Test / Sandbox (push) Successful in 3m4s
Test / ShareFS (push) Successful in 3m50s
Test / Hakurei (push) Successful in 4m13s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m40s
Test / Flake checks (push) Successful in 1m14s
This replaces autoroot.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-17 02:32:46 +09:00
cat 92b61889a6 hst: support ephemeral overlay mounts
Test / Create distribution (push) Successful in 55s
Test / Sandbox (push) Successful in 2m41s
Test / ShareFS (push) Successful in 3m47s
Test / Hakurei (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m13s
This is useful for reusing a readonly template without autoroot.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-17 02:16:57 +09:00
cat 28e133c298 cmd/app: start application from configuration
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 3m3s
Test / ShareFS (push) Successful in 4m14s
Test / Hakurei (push) Successful in 4m28s
Test / Sandbox (race detector) (push) Successful in 5m48s
Test / Hakurei (race detector) (push) Successful in 7m1s
Test / Flake checks (push) Successful in 1m13s
This is currently not very usable due to hakurei immutable overlay mount limitations.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-17 01:54:29 +09:00
cat 323dcb2820 cmd/app: high-level app configuration syntax
Test / Create distribution (push) Successful in 55s
Test / Sandbox (push) Successful in 2m54s
Test / ShareFS (push) Successful in 4m8s
Test / Hakurei (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 5m45s
Test / Hakurei (race detector) (push) Successful in 7m0s
Test / Flake checks (push) Successful in 1m23s
This replaces the nixos module.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-17 01:50:53 +09:00
cat f46a0370a7 cmd/app: initial container template command
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m54s
Test / ShareFS (push) Successful in 3m59s
Test / Hakurei (push) Successful in 4m11s
Test / Sandbox (race detector) (push) Successful in 5m38s
Test / Hakurei (race detector) (push) Successful in 6m45s
Test / Flake checks (push) Successful in 1m13s
This program is an experimental frontend for cmd/hakurei. This serves as a short-term replacement of the nixos module and a testing environment to implement API useful to planterette.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-16 23:27:50 +09:00
cat 5ee66a86ee internal/rosa/python: respect toolchain opts
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 3m17s
Test / ShareFS (push) Successful in 4m34s
Test / Hakurei (push) Successful in 4m45s
Test / Sandbox (race detector) (push) Successful in 6m9s
Test / Hakurei (race detector) (push) Successful in 7m7s
Test / Flake checks (push) Successful in 1m16s
This was missed when migrating the pip helper.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-16 22:06:34 +09:00
cat d75b8b6e32 internal/rosa/package/tamago: 1.26.3 to 1.26.4
Test / Create distribution (push) Successful in 1m0s
Test / Sandbox (push) Successful in 4m14s
Test / Hakurei (push) Successful in 6m21s
Test / ShareFS (push) Successful in 4m44s
Test / Sandbox (race detector) (push) Successful in 6m35s
Test / Hakurei (race detector) (push) Successful in 8m57s
Test / Flake checks (push) Successful in 1m25s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-16 21:49:04 +09:00
cat 7c9481d38d internal/rosa/package/strace: 6.19 to 7.1
Test / Create distribution (push) Successful in 2m44s
Test / Sandbox (push) Successful in 9m27s
Test / ShareFS (push) Successful in 6m40s
Test / Hakurei (push) Successful in 11m5s
Test / Sandbox (race detector) (push) Successful in 10m36s
Test / Hakurei (race detector) (push) Successful in 13m43s
Test / Flake checks (push) Successful in 1m28s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-16 21:48:45 +09:00
cat 983ab4378f internal/rosa/package/nss: 3.124 to 3.125
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (race detector) (push) Successful in 11m39s
Test / ShareFS (push) Successful in 9m46s
Test / Hakurei (race detector) (push) Successful in 14m6s
Test / Sandbox (push) Successful in 1m44s
Test / Hakurei (push) Successful in 3m9s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-16 21:48:26 +09:00
cat 5b5c096d31 internal/rosa/package/iproute2: 7.0.0 to 7.1.0
Test / Create distribution (push) Successful in 1m32s
Test / Sandbox (race detector) (push) Successful in 12m33s
Test / ShareFS (push) Successful in 12m31s
Test / Hakurei (push) Successful in 13m42s
Test / Hakurei (race detector) (push) Successful in 15m15s
Test / Sandbox (push) Successful in 1m55s
Test / Flake checks (push) Successful in 1m30s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-16 21:48:04 +09:00
cat 5f49127120 internal/rosa/package/python: pytest 9.0.3 to 9.1.0
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 4m3s
Test / ShareFS (push) Successful in 7m28s
Test / Hakurei (push) Successful in 7m32s
Test / Sandbox (race detector) (push) Successful in 11m55s
Test / Hakurei (race detector) (push) Successful in 15m14s
Test / Flake checks (push) Successful in 1m41s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-16 21:47:40 +09:00
cat 164f8c873d internal/rosa/package/gcc: binutils 2.46.0 to 2.46.1
Test / Create distribution (push) Successful in 56s
Test / Sandbox (push) Successful in 3m47s
Test / ShareFS (push) Successful in 6m39s
Test / Hakurei (push) Successful in 6m44s
Test / Sandbox (race detector) (push) Successful in 12m17s
Test / Hakurei (race detector) (push) Successful in 15m36s
Test / Flake checks (push) Successful in 1m35s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-16 21:47:16 +09:00
cat 9274b642ec internal/rosa/package/kernel: 6.12.92 to 6.12.93
Test / Create distribution (push) Successful in 1m20s
Test / Sandbox (push) Successful in 3m12s
Test / Hakurei (push) Successful in 4m19s
Test / ShareFS (push) Successful in 4m21s
Test / Sandbox (race detector) (push) Successful in 5m53s
Test / Hakurei (race detector) (push) Successful in 7m8s
Test / Flake checks (push) Successful in 1m14s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-16 21:37:42 +09:00
cat 5992be3dd3 internal/rosa/package/python: add kernel-headers
Test / Create distribution (push) Successful in 58s
Test / ShareFS (push) Successful in 7m52s
Test / Sandbox (race detector) (push) Successful in 8m34s
Test / Hakurei (race detector) (push) Successful in 9m42s
Test / Sandbox (push) Successful in 1m28s
Test / Hakurei (push) Successful in 3m43s
Test / Flake checks (push) Successful in 2m46s
Missing kernel headers implicitly disables many useful things. This change also enables PGO and installs symlink, since a rebuild is unavoidable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-10 16:30:52 +09:00
cat c661a3b63a cmd/mbf: migrate shell to enter
Test / Create distribution (push) Successful in 57s
Test / Sandbox (push) Successful in 2m54s
Test / ShareFS (push) Successful in 3m56s
Test / Hakurei (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 5m39s
Test / Hakurei (race detector) (push) Successful in 6m40s
Test / Flake checks (push) Successful in 1m12s
This reduces duplicate code. This change also adds resolv.conf to the container.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-10 13:34:44 +09:00
cat df0bb877db internal/rosa: export etc native artifact
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m49s
Test / ShareFS (push) Successful in 3m59s
Test / Hakurei (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 5m35s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m12s
This is useful for external container tooling.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-10 13:26:38 +09:00
cat f333b8fbd6 cmd/mbf: register binfmt entry for shell
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 2m42s
Test / Hakurei (push) Successful in 3m56s
Test / ShareFS (push) Successful in 3m55s
Test / Sandbox (race detector) (push) Successful in 5m36s
Test / Hakurei (race detector) (push) Successful in 6m36s
Test / Flake checks (push) Successful in 1m16s
This fixes --arch for shell.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-10 12:51:09 +09:00
cat 928a9f61e9 cmd/hsu: remove parent check
Test / Create distribution (push) Successful in 23s
Test / ShareFS (push) Successful in 29s
Test / Sandbox (race detector) (push) Successful in 32s
Test / Sandbox (push) Successful in 35s
Test / Hakurei (push) Successful in 40s
Test / Hakurei (race detector) (push) Successful in 45s
Test / Flake checks (push) Successful in 1m11s
This check serves no real purpose and only makes it more difficult to start containers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-09 18:08:17 +09:00
cat ce06539eca internal/rosa: expose supported architectures
Test / Create distribution (push) Successful in 53s
Test / Sandbox (push) Successful in 2m57s
Test / ShareFS (push) Successful in 3m56s
Test / Hakurei (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m41s
Test / Flake checks (push) Successful in 1m12s
This information is useful to external tooling and makes a lot more sense in this package than cmd/mbf. This change also fixes non-native artifact resolution during clean.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-09 16:05:04 +09:00
cat 69908e5a41 internal/rosa: remove external toolchain reference
Test / Create distribution (push) Successful in 57s
Test / Sandbox (push) Successful in 2m53s
Test / ShareFS (push) Successful in 3m55s
Test / Hakurei (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 5m44s
Test / Hakurei (race detector) (push) Successful in 6m49s
Test / Flake checks (push) Successful in 1m27s
This should have used toolchain passed as the argument and was mistakenly never changed when migrating to late evaluation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-09 15:58:02 +09:00
cat b5445573a8 internal/rosa/package/ninja: work around test suite bug
Test / Create distribution (push) Successful in 51s
Test / Sandbox (push) Successful in 2m38s
Test / ShareFS (push) Successful in 3m46s
Test / Hakurei (push) Successful in 3m54s
Test / Sandbox (race detector) (push) Successful in 5m17s
Test / Hakurei (race detector) (push) Successful in 6m28s
Test / Flake checks (push) Successful in 1m6s
The test suite hard codes /bin/echo.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-08 18:43:42 +09:00
cat f869ff95a1 all: apply modernisers
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 2m48s
Test / ShareFS (push) Successful in 3m53s
Test / Hakurei (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 5m37s
Test / Hakurei (race detector) (push) Successful in 6m40s
Test / Flake checks (push) Successful in 1m12s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-08 14:58:24 +09:00
cat 725f2e0ef3 internal/rosa/package/wayland: wayland-protocols 1.48 to 1.49
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 2m57s
Test / ShareFS (push) Successful in 4m34s
Test / Hakurei (push) Successful in 4m40s
Test / Sandbox (race detector) (push) Successful in 5m57s
Test / Hakurei (race detector) (push) Successful in 7m5s
Test / Flake checks (push) Successful in 1m12s
This finally eliminates the backport patch.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-08 13:45:36 +09:00
cat 4ffa20cd3f cmd/mbf: custom azalea path
Test / Create distribution (push) Successful in 54s
Test / Sandbox (push) Successful in 2m53s
Test / Hakurei (push) Successful in 4m33s
Test / ShareFS (push) Successful in 4m38s
Test / Sandbox (race detector) (push) Successful in 6m14s
Test / Hakurei (race detector) (push) Successful in 7m38s
Test / Flake checks (push) Successful in 1m23s
This enables out-of-tree packaging.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-08 13:42:06 +09:00
cat 38450db74a internal/rosa: access backing storage through fs
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m44s
Test / Hakurei (push) Successful in 3m52s
Test / ShareFS (push) Successful in 3m53s
Test / Sandbox (race detector) (push) Successful in 5m32s
Test / Hakurei (race detector) (push) Successful in 6m30s
Test / Flake checks (push) Successful in 1m7s
This is more versatile than hardcoding the os.Root implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 22:08:57 +09:00
cat 9344f694c7 internal/pkg: defer directory permissions
Test / Create distribution (push) Successful in 54s
Test / Sandbox (push) Successful in 2m58s
Test / ShareFS (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 5m47s
Test / Hakurei (race detector) (push) Successful in 6m51s
Test / Hakurei (push) Successful in 2m23s
Test / Flake checks (push) Successful in 1m10s
This allows creation of directory structures with awkward permission bits.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 20:26:55 +09:00
cat e8bb5a622d internal/rosa: mirror service via external cache
Test / Create distribution (push) Successful in 51s
Test / Sandbox (push) Successful in 2m43s
Test / ShareFS (push) Successful in 3m58s
Test / Hakurei (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 5m29s
Test / Hakurei (race detector) (push) Successful in 6m43s
Test / Flake checks (push) Successful in 1m18s
This provides an authenticated implementation of the external cache.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 19:25:11 +09:00
cat 22e508fe17 internal/pkg: expose measured reader to extern status
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m50s
Test / ShareFS (push) Successful in 3m55s
Test / Hakurei (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 5m45s
Test / Hakurei (race detector) (push) Successful in 6m40s
Test / Flake checks (push) Successful in 1m12s
This is always required by the implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 16:14:54 +09:00
cat b18ecf5832 nix: work around systemd nondeterminism
Test / ShareFS (push) Successful in 27s
Test / Hakurei (push) Successful in 39s
Test / Hakurei (race detector) (push) Successful in 43s
Test / Create distribution (push) Successful in 54s
Test / Sandbox (push) Successful in 1m39s
Test / Sandbox (race detector) (push) Successful in 2m27s
Test / Flake checks (push) Successful in 1m7s
NixOS switched to systemd initramfs by default.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 14:10:06 +09:00
cat ec29e755fb internal/pkg: replace outcomes from external cache
Test / Create distribution (push) Successful in 57s
Test / ShareFS (push) Successful in 3m51s
Test / Hakurei (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m35s
Test / Sandbox (push) Successful in 1m27s
Test / Flake checks (push) Successful in 1m15s
This is primarily useful for implementing a mirror service. This change also works around the zero-dependency FloodArtifact edge case.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 13:52:26 +09:00
cat 9aaf160ff4 internal/pkg: do not hold up cures during status link
Test / Create distribution (push) Successful in 57s
Test / Sandbox (push) Successful in 2m48s
Test / ShareFS (push) Successful in 3m56s
Test / Hakurei (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 5m40s
Test / Hakurei (race detector) (push) Successful in 6m37s
Test / Flake checks (push) Successful in 1m8s
This is a bunch of string operation and a single syscall. Do not hold up queue here.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 02:28:45 +09:00
cat e192fca762 internal/pkg: check for unclean shutdown
Test / Create distribution (push) Successful in 57s
Test / Sandbox (push) Successful in 2m53s
Test / ShareFS (push) Successful in 3m54s
Test / Hakurei (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 5m41s
Test / Hakurei (race detector) (push) Successful in 6m34s
Test / Flake checks (push) Successful in 1m13s
This avoids running into nasty surprises opening a cache that suffered unclean shutdown due to power loss. All other parts of the cache are not prone to inconsistent state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-07 01:21:00 +09:00
cat 7eafc7b1e4 nix: update flake lock
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m52s
Test / ShareFS (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 5m45s
Test / Hakurei (race detector) (push) Successful in 7m0s
Test / Hakurei (push) Successful in 2m23s
Test / Flake checks (push) Successful in 1m14s
Was unfortunately not able to implement vm test suite before this release. Hopefully the last nixos update we have to follow.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-06 13:14:09 +09:00
cat 282462c2f0 internal/rosa/package/mesa: 26.1.1 to 26.1.2
Test / Sandbox (push) Successful in 3m34s
Test / Hakurei (push) Successful in 4m48s
Test / Sandbox (race detector) (push) Successful in 5m34s
Test / Hakurei (race detector) (push) Successful in 6m45s
Test / Create distribution (push) Successful in 1m4s
Test / ShareFS (push) Successful in 2m57s
Test / Flake checks (push) Successful in 1m32s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:23:40 +09:00
cat d9f522d648 internal/rosa/package/libinput: 1.31.2 to 1.31.3
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 3m16s
Test / ShareFS (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 5m42s
Test / Hakurei (race detector) (push) Successful in 6m47s
Test / Hakurei (push) Successful in 2m45s
Test / Flake checks (push) Successful in 2m4s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:23:11 +09:00
cat a0b0c7ecc9 internal/rosa/package/libdrm: 2.4.133 to 2.4.134
Test / Create distribution (push) Successful in 1m12s
Test / Sandbox (push) Successful in 3m16s
Test / Hakurei (push) Successful in 4m19s
Test / ShareFS (push) Successful in 4m23s
Test / Sandbox (race detector) (push) Successful in 5m59s
Test / Hakurei (race detector) (push) Successful in 6m53s
Test / Flake checks (push) Successful in 1m31s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:22:50 +09:00
cat 2b8809da7a internal/rosa/package/hwdata: 0.407 to 0.408
Test / Sandbox (push) Successful in 2m16s
Test / Create distribution (push) Successful in 1m14s
Test / Hakurei (push) Successful in 4m47s
Test / ShareFS (push) Successful in 3m22s
Test / Sandbox (race detector) (push) Successful in 5m41s
Test / Hakurei (race detector) (push) Successful in 7m4s
Test / Flake checks (push) Successful in 1m32s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:22:28 +09:00
cat 84cda19d63 internal/rosa/package/harfbuzz: 14.2.0 to 14.2.1
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 3m9s
Test / Hakurei (push) Successful in 4m35s
Test / ShareFS (push) Successful in 4m32s
Test / Sandbox (race detector) (push) Successful in 6m4s
Test / Hakurei (race detector) (push) Successful in 7m15s
Test / Flake checks (push) Successful in 1m34s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:22:08 +09:00
cat 154ab953c1 internal/rosa/package/fontconfig: 2.18.0 to 2.18.1
Test / Create distribution (push) Successful in 1m12s
Test / Sandbox (push) Successful in 3m11s
Test / ShareFS (push) Successful in 4m15s
Test / Hakurei (push) Successful in 4m19s
Test / Sandbox (race detector) (push) Successful in 5m53s
Test / Hakurei (race detector) (push) Successful in 6m58s
Test / Flake checks (push) Successful in 1m44s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:21:43 +09:00
cat 34e11dd312 internal/rosa/package/python: hatchling 1.16.5 to 1.17.0
Test / Create distribution (push) Successful in 1m22s
Test / Sandbox (push) Successful in 3m24s
Test / Hakurei (push) Successful in 4m35s
Test / ShareFS (push) Successful in 4m29s
Test / Sandbox (race detector) (push) Successful in 6m0s
Test / Hakurei (race detector) (push) Successful in 7m16s
Test / Flake checks (push) Successful in 1m43s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:21:10 +09:00
cat 140cb3cc47 internal/rosa/package/python: trove-classifiers 2026.5.22.10 to 2026.6.1.19
Test / Create distribution (push) Successful in 1m23s
Test / Sandbox (push) Successful in 3m13s
Test / ShareFS (push) Successful in 4m31s
Test / Hakurei (push) Successful in 5m34s
Test / Sandbox (race detector) (push) Successful in 6m3s
Test / Hakurei (race detector) (push) Successful in 7m10s
Test / Flake checks (push) Successful in 1m44s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:20:42 +09:00
cat 82c974d656 internal/rosa/package/xkbcommon: 1.13.1 to 1.13.2
Test / Create distribution (push) Successful in 1m8s
Test / Sandbox (push) Successful in 2m53s
Test / ShareFS (push) Successful in 4m17s
Test / Hakurei (push) Successful in 4m29s
Test / Sandbox (race detector) (push) Successful in 5m53s
Test / Hakurei (race detector) (push) Successful in 7m10s
Test / Flake checks (push) Successful in 1m38s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:20:12 +09:00
cat 8c17f201e5 internal/rosa/package/x: xwayland 24.1.11 to 24.1.12
Test / Create distribution (push) Successful in 1m7s
Test / Sandbox (push) Successful in 2m54s
Test / Hakurei (push) Successful in 3m57s
Test / ShareFS (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 5m40s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m26s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:19:46 +09:00
cat 63e0457538 internal/rosa/package/x: xserver 21.1.22 to 21.1.23
Test / Sandbox (race detector) (push) Successful in 53s
Test / Create distribution (push) Successful in 1m12s
Test / Sandbox (push) Successful in 3m9s
Test / Hakurei (race detector) (push) Successful in 4m4s
Test / Hakurei (push) Successful in 4m12s
Test / ShareFS (push) Successful in 4m13s
Test / Flake checks (push) Successful in 1m33s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:19:22 +09:00
cat ca93264c1f internal/rosa/package/dtc: 1.8.0 to 1.8.1
Test / Create distribution (push) Successful in 1m9s
Test / Sandbox (push) Successful in 3m5s
Test / Hakurei (push) Successful in 4m18s
Test / ShareFS (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 5m38s
Test / Hakurei (race detector) (push) Successful in 6m52s
Test / Flake checks (push) Successful in 1m28s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-05 09:18:53 +09:00
cat 94173eafff internal/rosa/llvm: 22.1.6 to 22.1.7
Test / Create distribution (push) Successful in 2m22s
Test / Sandbox (race detector) (push) Successful in 10m9s
Test / ShareFS (push) Successful in 10m43s
Test / Hakurei (race detector) (push) Successful in 14m43s
Test / Sandbox (push) Successful in 4m24s
Test / Hakurei (push) Successful in 5m50s
Test / Flake checks (push) Successful in 1m27s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 20:13:26 +09:00
cat e28c4aa3c0 internal/rosa/package/kernel: 6.12.91 to 6.12.92
Test / Create distribution (push) Successful in 1m44s
Test / ShareFS (push) Successful in 11m22s
Test / Sandbox (push) Successful in 2m51s
Test / Sandbox (race detector) (push) Successful in 4m0s
Test / Hakurei (push) Successful in 5m42s
Test / Hakurei (race detector) (push) Successful in 6m30s
Test / Flake checks (push) Successful in 1m54s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 20:10:27 +09:00
cat 8e8410ce38 internal/pkg: archive unpack artifact
Test / Create distribution (push) Successful in 4m0s
Test / Sandbox (push) Successful in 9m28s
Test / Sandbox (race detector) (push) Successful in 13m22s
Test / Hakurei (push) Successful in 13m27s
Test / ShareFS (push) Successful in 13m57s
Test / Hakurei (race detector) (push) Successful in 15m52s
Test / Flake checks (push) Successful in 2m37s
This unpacks an internal/pkg archive stream used in the upcoming mirror service.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 19:46:36 +09:00
cat 8fb6fdaa80 internal/pkg: prefix reporting names
Test / Create distribution (push) Successful in 3m40s
Test / Sandbox (push) Successful in 6m6s
Test / Hakurei (push) Successful in 7m37s
Test / ShareFS (push) Successful in 7m43s
Test / Sandbox (race detector) (push) Successful in 9m6s
Test / Hakurei (race detector) (push) Successful in 11m38s
Test / Flake checks (push) Successful in 5m27s
This reads better than suffixes.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 19:05:36 +09:00
cat db69dcf0be internal/pkg: remove tar built-in decompressor
Test / Create distribution (push) Successful in 1m30s
Test / Sandbox (push) Successful in 5m13s
Test / Hakurei (push) Successful in 7m56s
Test / ShareFS (push) Successful in 7m57s
Test / Hakurei (race detector) (push) Successful in 10m12s
Test / Sandbox (race detector) (push) Successful in 3m3s
Test / Flake checks (push) Successful in 1m47s
This is replaced by decompressArtifact and is no longer necessary.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 18:54:43 +09:00
cat 76c1fb84c8 internal/pkg: stream decompress artifact
Test / Create distribution (push) Successful in 2m56s
Test / Sandbox (push) Successful in 6m55s
Test / Hakurei (push) Successful in 9m46s
Test / ShareFS (push) Successful in 10m21s
Test / Sandbox (race detector) (push) Successful in 10m44s
Test / Hakurei (race detector) (push) Successful in 14m34s
Test / Flake checks (push) Successful in 3m14s
The tarArtifact predates FileArtifact pipelining. This migrates decompression and buffering into a standalone artifact implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 18:33:04 +09:00
cat 729be19af3 internal/pkg: rename archive checksum helpers
Test / Create distribution (push) Successful in 1m7s
Test / Sandbox (push) Successful in 2m50s
Test / ShareFS (push) Successful in 3m50s
Test / Hakurei (push) Successful in 3m54s
Test / Sandbox (race detector) (push) Successful in 5m41s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Flake checks (push) Successful in 1m41s
These names are more consistent with other helper names, so rename them while the API is still internal.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-04 14:13:20 +09:00
cat 4d04f86f4a internal/rosa/go: 1.26.3 to 1.26.4
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m56s
Test / ShareFS (push) Successful in 3m45s
Test / Hakurei (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 5m28s
Test / Hakurei (race detector) (push) Successful in 6m34s
Test / Flake checks (push) Successful in 1m14s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-03 16:18:04 +09:00
cat 42cea1e7c6 internal/pkg: streaming archive reader/writer
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m49s
Test / Hakurei (push) Successful in 3m53s
Test / ShareFS (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 5m35s
Test / Hakurei (race detector) (push) Successful in 6m32s
Test / Flake checks (push) Successful in 1m15s
This is much more robust and efficient than the simple buffering implementation for larger files. Allocations happen almost exclusively in WalkDir.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-03 16:00:36 +09:00
cat 83498b5a8a cmd/mbf: keep non-native entries alive
Test / Create distribution (push) Successful in 1m15s
Test / Sandbox (push) Successful in 2m54s
Test / Hakurei (push) Successful in 3m57s
Test / ShareFS (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 5m43s
Test / Hakurei (race detector) (push) Successful in 6m40s
Test / Flake checks (push) Successful in 1m16s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 16:06:04 +09:00
cat 9e824452bd internal/pkg: expose snapshot of binfmt entries
Test / Create distribution (push) Successful in 1m30s
Test / Sandbox (push) Successful in 2m54s
Test / Hakurei (push) Successful in 4m4s
Test / ShareFS (push) Successful in 4m4s
Test / Hakurei (race detector) (push) Successful in 6m46s
Test / Sandbox (race detector) (push) Successful in 8m30s
Test / Flake checks (push) Successful in 3m6s
This is otherwise not externally accessible. The resulting map can be safely mutated.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 16:05:09 +09:00
cat 56937ac396 internal/rosa: populate opts of cloned S
Test / Create distribution (push) Successful in 1m26s
Test / Sandbox (push) Successful in 2m39s
Test / Hakurei (push) Successful in 3m42s
Test / ShareFS (push) Successful in 3m39s
Test / Sandbox (race detector) (push) Successful in 8m31s
Test / Hakurei (race detector) (push) Successful in 11m26s
Test / Flake checks (push) Successful in 5m58s
This was missed when migrating opts into S.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 16:00:51 +09:00
cat 6c2f7089b6 internal/pkg: destroy unreachable status entries
Test / Create distribution (push) Successful in 1m28s
Test / Sandbox (push) Successful in 2m39s
Test / ShareFS (push) Successful in 3m39s
Test / Hakurei (push) Successful in 3m52s
Test / Sandbox (race detector) (push) Successful in 8m30s
Test / Hakurei (race detector) (push) Successful in 11m35s
Test / Flake checks (push) Successful in 8m54s
These must be destroyed alongside their corresponding identifier or substitute entries to avoid inconsistent state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 15:50:30 +09:00
cat 74c18390b4 internal/pkg: correctly scrub substitute status
Test / Create distribution (push) Successful in 1m6s
Test / Sandbox (push) Successful in 2m52s
Test / ShareFS (push) Successful in 3m44s
Test / Hakurei (push) Successful in 3m48s
Test / Sandbox (race detector) (push) Successful in 5m29s
Test / Hakurei (race detector) (push) Successful in 6m35s
Test / Flake checks (push) Successful in 1m14s
Scrubbing for status predates substitutes. This change fixes scrub handling of substitute status entries.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 15:37:55 +09:00
cat 1490b32387 cmd/mbf: garbage collection commands
Test / Create distribution (push) Successful in 1m29s
Test / Sandbox (push) Successful in 2m43s
Test / ShareFS (push) Successful in 3m55s
Test / Hakurei (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 8m34s
Test / Hakurei (race detector) (push) Successful in 11m35s
Test / Flake checks (push) Successful in 8m40s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 13:37:46 +09:00
cat fbd2329d50 internal/pkg: garbage collection
Test / Create distribution (push) Successful in 2m53s
Test / Sandbox (push) Successful in 7m2s
Test / ShareFS (push) Successful in 3m51s
Test / Hakurei (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 5m32s
Test / Hakurei (race detector) (push) Successful in 6m35s
Test / Flake checks (push) Successful in 2m13s
This destroys cache entries not referred to by user-specified artifacts and optionally their inputs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-02 13:28:24 +09:00
cat f398f71fa9 internal/pkg: input iterator via IR cache
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m51s
Test / ShareFS (push) Successful in 3m42s
Test / Hakurei (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 5m31s
Test / Hakurei (race detector) (push) Successful in 6m32s
Test / Flake checks (push) Successful in 1m22s
Primarily useful for garbage collection.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-31 14:19:27 +09:00
cat 4d017b1309 internal/rosa/package/rsync: annotate blocked update
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m52s
Test / ShareFS (push) Successful in 3m44s
Test / Hakurei (push) Successful in 3m50s
Test / Sandbox (race detector) (push) Successful in 5m32s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m21s
Despite rsync being practically feature complete, with minimal changes in many years, it recently had a large number of poor quality AI-generated changes, with the first release including them apparently being 3.4.2. Release 3.4.3 then introduced a dependency on kernel-headers, creating a dependency loop that prevented directly upgrading to it. This change annotates the blockage since it is not worth working around the dependency loop for AI-generated garbage.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-30 23:15:04 +09:00
cat cd1d447664 internal/rosa/package/rsync: 3.4.2 to 3.4.1
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m49s
Test / Hakurei (push) Successful in 3m47s
Test / ShareFS (push) Successful in 3m48s
Test / Sandbox (race detector) (push) Successful in 5m27s
Test / Hakurei (race detector) (push) Successful in 6m31s
Test / Flake checks (push) Successful in 1m22s
This appears to be the last release not to contain AI slop.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-30 19:10:40 +09:00
cat 8b87e0eb76 internal/rosa: annotate blocked updates
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m49s
Test / ShareFS (push) Successful in 3m47s
Test / Hakurei (push) Successful in 3m58s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Flake checks (push) Successful in 1m22s
These situations, while unfortunate, are inevitable at a larger scale. This change enables annotation and optional hiding of blocked updates.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-30 18:56:55 +09:00
cat f4215ddda5 internal/rosa/package/qemu: 11.0.0 to 11.0.1
Test / Create distribution (push) Successful in 1m11s
Test / Sandbox (push) Successful in 3m12s
Test / Hakurei (push) Successful in 4m8s
Test / ShareFS (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 5m40s
Test / Hakurei (race detector) (push) Successful in 6m42s
Test / Flake checks (push) Successful in 1m23s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-28 12:16:55 +09:00
cat 652b4c2ba0 internal/rosa/package/gnu: parallel 20260422 to 20260522
Test / Create distribution (push) Successful in 1m13s
Test / Sandbox (push) Successful in 3m6s
Test / ShareFS (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 5m46s
Test / Hakurei (push) Successful in 2m38s
Test / Hakurei (race detector) (push) Successful in 3m23s
Test / Flake checks (push) Successful in 1m16s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-28 12:16:05 +09:00
cat 75715f4590 cmd/earlyinit: mount /dev/shm
Test / Create distribution (push) Successful in 1m9s
Test / Sandbox (push) Successful in 2m52s
Test / ShareFS (push) Successful in 3m49s
Test / Hakurei (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 5m30s
Test / Hakurei (race detector) (push) Successful in 6m37s
Test / Flake checks (push) Successful in 1m22s
Required by many programs.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 23:59:48 +09:00
cat 87c3b3663d cmd/earlyinit: improve change message formatting
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m45s
Test / Hakurei (push) Successful in 3m47s
Test / ShareFS (push) Successful in 3m47s
Test / Sandbox (race detector) (push) Successful in 5m26s
Test / Hakurei (race detector) (push) Successful in 6m35s
Test / Flake checks (push) Successful in 1m22s
This is significantly more readable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 23:51:46 +09:00
cat 6d792023b2 internal/rosa/package: seatd
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m49s
Test / Hakurei (push) Successful in 3m47s
Test / ShareFS (push) Successful in 3m45s
Test / Sandbox (race detector) (push) Successful in 5m28s
Test / Hakurei (race detector) (push) Successful in 6m36s
Test / Flake checks (push) Successful in 1m22s
Required by wlroots sessions.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 23:17:33 +09:00
cat 3e62cf379f internal/rosa/package: libliftoff
Test / Create distribution (push) Successful in 1m6s
Test / Sandbox (push) Successful in 2m48s
Test / ShareFS (push) Successful in 3m47s
Test / Hakurei (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 5m54s
Test / Hakurei (race detector) (push) Successful in 7m0s
Test / Flake checks (push) Successful in 1m24s
Wanted by wlroots.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 23:07:53 +09:00
cat 6d991f2644 cmd/earlyinit: downgrade target error severity
Test / Create distribution (push) Successful in 1m8s
Test / Sandbox (push) Successful in 2m59s
Test / ShareFS (push) Successful in 4m6s
Test / Hakurei (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 5m36s
Test / Hakurei (race detector) (push) Successful in 6m47s
Test / Flake checks (push) Successful in 1m21s
This happens during coldboot, before reporting gains persistent state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 18:58:30 +09:00
cat a2cc28f53c cmd/earlyinit: load device drivers
Test / Create distribution (push) Successful in 1m11s
Test / Sandbox (push) Successful in 2m56s
Test / ShareFS (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 5m38s
Test / Hakurei (race detector) (push) Successful in 6m41s
Test / Hakurei (push) Successful in 2m32s
Test / Flake checks (push) Successful in 1m17s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 18:54:05 +09:00
cat 598c7aa30f internal/report: strict-exempt severity
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m47s
Test / Hakurei (push) Successful in 3m51s
Test / ShareFS (push) Successful in 3m49s
Test / Sandbox (race detector) (push) Successful in 5m30s
Test / Hakurei (race detector) (push) Successful in 6m29s
Test / Flake checks (push) Successful in 1m40s
For reportable but inconsequential errors like modprobe by MODALIAS failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 18:53:15 +09:00
cat b18f40d974 internal/kobject: pass action kind for range
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m52s
Test / ShareFS (push) Successful in 3m47s
Test / Hakurei (push) Successful in 3m52s
Test / Sandbox (race detector) (push) Successful in 5m24s
Test / Hakurei (race detector) (push) Successful in 6m33s
Test / Flake checks (push) Successful in 1m22s
Useful for handling most uevents.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 17:58:01 +09:00
cat 12a6061051 internal/rosa/package: sway
Test / Create distribution (push) Successful in 1m11s
Test / Sandbox (push) Successful in 3m7s
Test / Hakurei (push) Successful in 4m32s
Test / ShareFS (push) Successful in 4m32s
Test / Sandbox (race detector) (push) Successful in 6m6s
Test / Hakurei (race detector) (push) Successful in 7m4s
Test / Flake checks (push) Successful in 1m24s
Required by vm test suite.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 17:10:18 +09:00
cat 02c3823ed7 internal/rosa/package: libinput
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 3m28s
Test / ShareFS (push) Successful in 3m49s
Test / Sandbox (race detector) (push) Successful in 6m8s
Test / Hakurei (race detector) (push) Successful in 7m6s
Test / Hakurei (push) Successful in 2m33s
Test / Flake checks (push) Successful in 1m23s
Required by sway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 17:07:49 +09:00
cat cfbd8bd6f1 internal/rosa/package: libudev-zero
Test / Create distribution (push) Successful in 1m18s
Test / Sandbox (push) Successful in 3m25s
Test / Hakurei (push) Successful in 4m35s
Test / ShareFS (push) Successful in 4m39s
Test / Sandbox (race detector) (push) Successful in 6m2s
Test / Hakurei (race detector) (push) Successful in 7m4s
Test / Flake checks (push) Successful in 1m23s
Avoids systemd, required by many projects.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 17:07:35 +09:00
cat fc7a339ed2 internal/rosa/package/kernel: 6.12.90 to 6.12.91
Test / Create distribution (push) Successful in 1m8s
Test / Sandbox (push) Successful in 2m52s
Test / ShareFS (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 5m28s
Test / Hakurei (race detector) (push) Successful in 6m31s
Test / Hakurei (push) Successful in 2m28s
Test / Flake checks (push) Successful in 1m21s
This unfortunately updates headers linux/mii.h and linux/virtio_net.h.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 15:47:53 +09:00
cat 97fdd5db8b internal/rosa/package/dtc: 1.7.2 to 1.8.0
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m39s
Test / ShareFS (push) Successful in 3m42s
Test / Sandbox (race detector) (push) Successful in 5m17s
Test / Hakurei (race detector) (push) Successful in 6m24s
Test / Hakurei (push) Successful in 2m33s
Test / Flake checks (push) Successful in 1m21s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 15:46:58 +09:00
cat a5d9f76f50 internal/rosa/cmake: use DESTDIR instead of --prefix
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 2m58s
Test / ShareFS (push) Successful in 3m48s
Test / Hakurei (push) Successful in 3m54s
Test / Sandbox (race detector) (push) Successful in 5m27s
Test / Hakurei (race detector) (push) Successful in 6m30s
Test / Flake checks (push) Successful in 1m21s
Turns out --prefix is deeply broken, and DESTDIR works even when using ninja.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 15:45:58 +09:00
cat b313dfefb0 internal/rosa/package: libevdev
Required by sway.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-05-27 13:07:10 +09:00
130 changed files with 4334 additions and 1173 deletions
+8 -2
View File
@@ -20,8 +20,8 @@ func (e AbsoluteError) Error() string {
}
func (e AbsoluteError) Is(target error) bool {
var ce AbsoluteError
if !errors.As(target, &ce) {
ce, ok := errors.AsType[AbsoluteError](target)
if !ok {
return errors.Is(target, syscall.EINVAL)
}
return e == ce
@@ -31,6 +31,8 @@ func (e AbsoluteError) Is(target error) bool {
type Absolute struct{ pathname unique.Handle[string] }
var (
_ fmt.GoStringer = new(Absolute)
_ encoding.TextAppender = new(Absolute)
_ encoding.TextMarshaler = new(Absolute)
_ encoding.TextUnmarshaler = new(Absolute)
@@ -40,6 +42,10 @@ var (
_ encoding.BinaryUnmarshaler = new(Absolute)
)
func (a *Absolute) GoString() string {
return fmt.Sprintf("check.MustAbs(%q)", a.String())
}
// ok returns whether [Absolute] is not the zero value.
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
+264
View File
@@ -0,0 +1,264 @@
package main
import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/hst"
)
// parsePair parses a NUL-delimited quoted paths pair.
func parsePair(s string) (source, target *check.Absolute, err error) {
var p string
if p, err = strconv.Unquote(s); err != nil {
return
}
_source, _target, ok := strings.Cut(p, "\x00")
if source, err = check.NewAbs(_source); err != nil {
return
}
if !ok {
return
}
target, err = check.NewAbs(_target)
return
}
// parse decodes a high-level configuration stream and returns its
// corresponding [hst.Config].
func parse(id string, base *check.Absolute, r io.Reader) (*hst.Config, error) {
shell := fhs.AbsRoot.Append("bin", "zsh")
home := hst.AbsPrivateTmp.Append("home")
c := hst.Config{
ID: id,
Enablements: new(hst.Enablements),
SessionBus: &hst.BusConfig{
Own: []string{
id + ".*",
"org.mpris.MediaPlayer2." + id + ".*",
},
Filter: true,
},
SystemBus: &hst.BusConfig{Filter: true},
Container: &hst.ContainerConfig{
Env: make(map[string]string),
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSOverlay{
Target: fhs.AbsRoot,
Lower: []*check.Absolute{
base.Append("template", "initial"),
},
Upper: base.Append("template", "upper"),
}},
{FilesystemConfig: &hst.FSBind{
Target: home,
Source: base.Append("state", id),
Write: true,
Ensure: true,
}},
{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsVar.Append("tmp"),
Write: true,
Perm: 01777,
}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices")}},
},
Username: "chronos",
Shell: shell,
Home: home,
Path: shell,
Args: []string{"zsh", "-c"},
Flags: hst.FCoverRun,
},
}
s := bufio.NewScanner(r)
scanOnce := func() error {
if s.Scan() {
return nil
}
if err := s.Err(); err != nil {
return err
}
return io.ErrUnexpectedEOF
}
if err := scanOnce(); err != nil {
return nil, err
}
if v, err := strconv.Atoi(s.Text()); err != nil {
return nil, err
} else {
c.Identity = v
}
if err := scanOnce(); err != nil {
return nil, err
}
c.Container.Args = append(c.Container.Args, s.Text())
var flagGPU, flagSystemBus bool
flags := map[string]*bool{
"gpu": &flagGPU,
"system_bus": &flagSystemBus,
}
for s.Scan() {
key, value, ok := strings.Cut(s.Text(), " ")
if key != "" && key[0] == ';' {
continue
}
if !ok {
if key == "" {
continue
}
var p *bool
if p, ok = flags[key]; ok {
*p = true
continue
}
switch key {
case "wayland":
*c.Enablements |= hst.EWayland
case "x11":
*c.Enablements |= hst.EX11
case "dbus":
*c.Enablements |= hst.EDBus
case "pipewire":
*c.Enablements |= hst.EPipeWire
case "multiarch":
c.Container.Flags |= hst.FMultiarch
case "devel":
c.Container.Flags |= hst.FDevel
case "userns":
c.Container.Flags |= hst.FUserns
case "net":
c.Container.Flags |= hst.FHostNet
case "abstract":
c.Container.Flags |= hst.FHostAbstract
case "tty":
c.Container.Flags |= hst.FTty
case "mapuid":
c.Container.Flags |= hst.FMapRealUID
case "device":
c.Container.Flags |= hst.FDevice
case "share_runtime":
c.Container.Flags |= hst.FShareRuntime
case "share_tmpdir":
c.Container.Flags |= hst.FShareTmpdir
default:
return nil, fmt.Errorf("invalid flag %q", key)
}
continue
}
switch key {
case "group":
c.Groups = append(c.Groups, value)
continue
case "env":
if key, value, ok = strings.Cut(value, "="); !ok {
return nil, fmt.Errorf("invalid environment %q", key)
}
c.Container.Env[key] = value
continue
case "ro":
source, target, err := parsePair(value)
if err != nil {
return nil, err
}
c.Container.Filesystem = append(c.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: target,
Source: source,
}},
)
continue
case "rw":
source, target, err := parsePair(value)
if err != nil {
return nil, err
}
c.Container.Filesystem = append(c.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: target,
Source: source,
Write: true,
}},
)
continue
case "own":
c.SessionBus.Own = append(c.SessionBus.Own, value)
continue
case "own_system":
c.SystemBus.Own = append(c.SystemBus.Own, value)
continue
case "talk":
c.SessionBus.Talk = append(c.SessionBus.Talk, value)
continue
case "talk_system":
c.SystemBus.Talk = append(c.SystemBus.Talk, value)
continue
default:
return nil, fmt.Errorf("invalid key %q", key)
}
}
if err := s.Err(); err != nil {
return nil, err
}
if flagGPU {
c.Container.Filesystem = append(c.Container.Filesystem, []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("dri"),
Device: true,
Optional: true,
}},
}...)
}
if !flagSystemBus {
c.SystemBus = nil
}
if c.Container.Flags&hst.FShareTmpdir == 0 {
c.Container.Filesystem = append(c.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsTmp,
Write: true,
Perm: 01777,
}},
)
}
return &c, nil
}
+152
View File
@@ -0,0 +1,152 @@
package main
import (
"reflect"
"strings"
"testing"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/hst"
)
func TestParse(t *testing.T) {
t.Parallel()
base := fhs.AbsProc.Append("nonexistent")
testCases := []struct {
name string
data string
want *hst.Config
err error
}{
{"com.discordapp.Discord", `8
exec Discord --ozone-platform-hint=wayland
gpu
wayland
dbus
system_bus
pipewire
userns
net
mapuid
share_runtime
share_tmpdir
group media_rw
env ELECTRON_TRASH=gio
rw "/sdcard"
; remove before reusing
ro "/bin\x00/.hakurei/bin"
talk org.kde.StatusNotifierWatcher
talk com.canonical.AppMenu.Registrar
talk com.canonical.indicator.application
talk com.canonical.Unity
`, &hst.Config{
Identity: 8,
ID: "com.discordapp.Discord",
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire),
Groups: []string{"media_rw"},
SessionBus: &hst.BusConfig{
Talk: []string{
"org.kde.StatusNotifierWatcher",
"com.canonical.AppMenu.Registrar",
"com.canonical.indicator.application",
"com.canonical.Unity",
},
Own: []string{
"com.discordapp.Discord.*",
"org.mpris.MediaPlayer2.com.discordapp.Discord.*",
},
Filter: true,
},
SystemBus: &hst.BusConfig{Filter: true},
Container: &hst.ContainerConfig{
Env: map[string]string{
"ELECTRON_TRASH": "gio",
},
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSOverlay{
Target: fhs.AbsRoot,
Lower: []*check.Absolute{
base.Append("template", "initial"),
},
Upper: base.Append("template", "upper"),
}},
{FilesystemConfig: &hst.FSBind{
Target: hst.AbsPrivateTmp.Append("home"),
Source: base.Append("state", "com.discordapp.Discord"),
Write: true,
Ensure: true,
}},
{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsVar.Append("tmp"),
Write: true,
Perm: 01777,
}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices")}},
{FilesystemConfig: &hst.FSBind{
Source: check.MustAbs("/sdcard"),
Write: true,
}},
{FilesystemConfig: &hst.FSBind{
Target: check.MustAbs("/.hakurei/bin"),
Source: check.MustAbs("/bin"),
}},
{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("dri"),
Device: true,
Optional: true,
}},
},
Username: "chronos",
Shell: fhs.AbsRoot.Append("bin", "zsh"),
Home: hst.AbsPrivateTmp.Append("home"),
Path: fhs.AbsRoot.Append("bin", "zsh"),
Args: []string{
"zsh", "-c",
"exec Discord --ozone-platform-hint=wayland",
},
Flags: hst.FCoverRun | hst.FUserns | hst.FHostNet | hst.FMapRealUID |
hst.FShareRuntime | hst.FShareTmpdir,
},
}, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := parse(
tc.name,
base,
strings.NewReader(tc.data),
)
if !reflect.DeepEqual(err, tc.err) {
t.Errorf("parse: error = %v, want %v", err, tc.err)
}
if err != nil {
return
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("parse: %#v, want %#v", got, tc.want)
}
})
}
}
+170
View File
@@ -0,0 +1,170 @@
// The app program is a proof-of-concept frontend for cmd/hakurei.
//
// This program is not covered by the compatibility promise. The command line
// interface and configuration syntax may change at any time.
package main
import (
"context"
"errors"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"syscall"
"hakurei.app/check"
"hakurei.app/command"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/message"
)
func main() {
log.SetFlags(0)
log.SetPrefix("app: ")
msg := message.New(log.Default())
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer stop()
var (
flagVerbose bool
flagBase string
base, template, initial, upper, work *check.Absolute
)
c := command.New(os.Stderr, log.Printf, "app", func([]string) (err error) {
msg.SwapVerbose(flagVerbose)
flagBase = os.ExpandEnv(flagBase)
if flagBase == "" {
flagBase = "state"
}
if flagBase, err = filepath.Abs(flagBase); err != nil {
return
} else if base, err = check.NewAbs(flagBase); err != nil {
return
}
template = base.Append("template")
initial = template.Append("initial")
upper = template.Append("upper")
work = template.Append("work")
return
}).Flag(
&flagVerbose,
"v", command.BoolFlag(false),
"Increase log verbosity",
).Flag(
&flagBase,
"d", command.StringFlag("$HAKUREI_APP_PATH"),
"Configuration and state directory",
)
{
var (
flagShell string
flagHome string
)
c.NewCommand(
"enter", "Enter mutable state template",
func([]string) error {
config := hst.Config{
ID: "app.hakurei.mutable",
Container: &hst.ContainerConfig{
Hostname: "mutable",
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSOverlay{
Target: fhs.AbsRoot,
Lower: []*check.Absolute{initial},
Upper: upper,
Work: work,
}},
{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsTmp,
Write: true,
Perm: 0755,
}},
},
Username: "chronos",
Flags: hst.FMultiarch |
hst.FDevel |
hst.FUserns |
hst.FHostNet |
hst.FTty,
},
}
if a, err := check.NewAbs(flagShell); err != nil {
return err
} else {
config.Container.Shell = a
config.Container.Path = a
config.Container.Args = []string{
"-" + filepath.Base(flagShell),
}
}
if a, err := check.NewAbs(flagHome); err != nil {
return err
} else {
config.Container.Home = a
}
return run(ctx, msg, &config)
},
).Flag(
&flagShell,
"shell", command.StringFlag("/bin/zsh"),
"Shell program within container",
).Flag(
&flagHome,
"home", command.StringFlag("/home/chronos"),
"Home directory within container",
)
}
c.NewCommand(
"run", "Start the named application",
func(args []string) error {
if len(args) != 1 {
return errors.New("run requires 1 argument")
}
var config *hst.Config
f, err := os.Open(base.Append("app", args[0]).String())
if err != nil {
return err
}
config, err = parse(args[0], base, f)
if closeErr := f.Close(); err == nil {
err = closeErr
}
if err != nil {
return err
}
return run(ctx, msg, config)
},
)
c.MustParse(os.Args[1:], func(err error) {
if e, ok := errors.AsType[*exec.ExitError](err); ok && e != nil {
os.Exit(e.ExitCode())
}
if w, ok := err.(interface{ Unwrap() []error }); !ok {
log.Fatal(err)
} else {
errs := w.Unwrap()
for i, e := range errs {
if i == len(errs)-1 {
log.Fatal(e)
}
log.Println(e)
}
}
})
}
+51
View File
@@ -0,0 +1,51 @@
package main
import (
"context"
"encoding/json"
"os"
"os/exec"
"syscall"
"hakurei.app/hst"
"hakurei.app/message"
)
// run starts a container via cmd/hakurei and returns after it terminates.
func run(ctx context.Context, msg message.Msg, config *hst.Config) error {
c, cancel := context.WithCancel(ctx)
defer cancel()
cmd := exec.CommandContext(c, "hakurei")
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Cancel = func() error {
return cmd.Process.Signal(syscall.SIGINT)
}
if msg.IsVerbose() {
cmd.Args = append(cmd.Args, "-v")
}
cmd.Args = append(cmd.Args, "run", "3")
r, w, err := os.Pipe()
if err != nil {
return err
}
cmd.ExtraFiles = append(cmd.ExtraFiles, r)
if err = cmd.Start(); err != nil {
_, _ = r.Close(), w.Close()
return err
}
if err = r.Close(); err != nil {
_ = w.Close()
return err
} else if err = json.NewEncoder(w).Encode(&config); err != nil {
_ = w.Close()
return err
} else if err = w.Close(); err != nil {
return err
}
return cmd.Wait()
}
+1 -1
View File
@@ -1 +1 @@
v0.4.3
v0.4.4
+27 -3
View File
@@ -19,6 +19,7 @@ import (
"hakurei.app/internal/kobject"
"hakurei.app/internal/report"
"hakurei.app/internal/uevent"
"hakurei.app/message"
)
var r report.Reporter
@@ -79,6 +80,8 @@ const (
// optionSystem specifies devpath of the system device.
optionSystem = "system"
// flagVerbose increases output verbosity.
flagVerbose = "verbose"
// flagStrict sets [report.DStrict] on r.
flagStrict = "strict"
// flagNoRecover sets [report.DNoRecover] on r.
@@ -116,6 +119,9 @@ func main() {
r.SetFlags(flag)
}
msg := message.New(log.Default())
msg.SwapVerbose(slices.Contains(flags, flagVerbose))
mustSyscall("mount devtmpfs", Mount(
"devtmpfs",
"/dev/",
@@ -131,6 +137,14 @@ func main() {
MS_NOSUID|MS_NOEXEC,
"mode=620,ptmxmode=666",
))
must(os.Mkdir("/dev/shm/", 0))
mustSyscall("mount shm", Mount(
"shm",
"/dev/shm/",
"tmpfs",
MS_NOSUID|MS_NODEV,
"",
))
// The kernel might be unable to set up the console. When that happens,
// printk is called with "Warning: unable to open an initial console."
@@ -193,12 +207,21 @@ func main() {
must1(rand.Read(uuid[:]))
ctx, cancel := context.WithCancel(context.Background())
go consume(ctx, &r, conn, uuid, events)
go consume(ctx, msg, &r, conn, uuid, events)
s := kobject.New(uuid, func(o *kobject.Object, env map[string]string) {
log.Printf("change %s: %q", o.DevPath, env)
p := make([]string, 0, len(env))
for k, v := range env {
p = append(p, k+"="+v)
}
slices.Sort(p)
log.Printf("change %s: %s", o.DevPath, strings.Join(p, ", "))
}, func(err error) {
severity := report.Inconsistent
if e, ok := err.(kobject.EventError); ok && e.Kind == kobject.EBadTarget {
severity = report.Trivial
}
r.Dispatch(
report.Inconsistent,
severity,
"processed inconsistent uevent",
err,
)
@@ -236,5 +259,6 @@ func main() {
[]byte("/system/lib/firmware"),
0,
))
go dispatchModprobe(ctx, s)
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"os/exec"
"strings"
"hakurei.app/internal/kobject"
"hakurei.app/internal/report"
"hakurei.app/internal/uevent"
)
// ModprobeError describes an unsuccessful modprobe invocation.
type ModprobeError struct {
ModAlias string `json:"modalias"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
ExitCode int `json:"exit_code"`
}
var _ report.RepresentableError = ModprobeError{}
func (ModprobeError) Representable() {}
func (e ModprobeError) Error() string {
return fmt.Sprintf(
"modprobe exit status %d: %s",
e.ExitCode, strings.TrimSpace(e.Stderr),
)
}
// dispatchModprobe invokes modprobe for [uevent.KOBJ_ADD] events raising new
// MODALIAS strings.
func dispatchModprobe(
ctx context.Context,
s *kobject.State,
) {
aliases := make(chan string, 1<<8)
go func() {
defer close(aliases)
s.Range(ctx, func(o *kobject.Object, act uevent.KobjectAction) bool {
if act == uevent.KOBJ_ADD && o.Driver == "" && o.ModAlias != "" {
aliases <- o.ModAlias
}
return true
})
}()
for alias := range aliases {
stdout, err := exec.Command("/system/sbin/modprobe", alias).Output()
if err == nil {
if len(stdout) > 0 {
log.Println(string(stdout))
}
continue
}
exitError, ok := errors.AsType[*exec.ExitError](err)
if !ok || exitError == nil {
r.Dispatch(report.Degraded, "invoke modprobe", err)
continue
}
r.Dispatch(report.Trivial, "load device driver", ModprobeError{
ModAlias: alias,
Stdout: string(stdout),
Stderr: string(exitError.Stderr),
ExitCode: exitError.ExitCode(),
})
}
}
+4 -2
View File
@@ -12,6 +12,7 @@ import (
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/internal/kobject"
"hakurei.app/internal/uevent"
)
// mustMountSystem waits for and mounts a system device matching pattern.
@@ -26,8 +27,9 @@ func mustMountSystem(
for {
var matchErr error
var systemPath *check.Absolute
s.Range(c, func(o *kobject.Object) bool {
if o.Subsystem != "block" ||
s.Range(c, func(o *kobject.Object, act uevent.KobjectAction) bool {
if (act != uevent.KOBJ_ADD && act != uevent.KOBJ_CHANGE) ||
o.Subsystem != "block" ||
o.Env["DEVTYPE"] != "disk" {
return true
}
+3 -2
View File
@@ -2,12 +2,12 @@ package main
import (
"context"
"log"
"time"
"hakurei.app/fhs"
"hakurei.app/internal/report"
"hakurei.app/internal/uevent"
"hakurei.app/message"
)
// newRejectColdboot returns a function to be called on every subsequent pending
@@ -56,6 +56,7 @@ func newRejectColdboot() func() bool {
// consume continuously consumes events from conn with retries.
func consume(
ctx context.Context,
msg message.Msg,
r *report.Reporter,
conn *uevent.Conn,
uuid uevent.UUID,
@@ -67,7 +68,7 @@ func consume(
coldboot := true
retry:
if dispatchErr := conn.Consume(ctx, fhs.Sys, &uuid, events, coldboot, func(path string) {
log.Println("coldboot visited", path)
msg.Verbose("coldboot visited", path)
}, func(err error) bool {
if _, ok := err.(uevent.NeedsColdboot); ok && !nextColdboot() {
r.Dispatch(
+6 -5
View File
@@ -7,7 +7,8 @@ import (
"strconv"
)
// decodeJSON decodes json from r and stores it in v. A non-nil error results in a call to fatal.
// decodeJSON decodes json from r and stores it in v. A non-nil error results in
// a call to fatal.
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any) {
err := json.NewDecoder(r).Decode(v)
if err == nil {
@@ -47,14 +48,14 @@ func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any) {
}
if err := encoder.Encode(v); err != nil {
var marshalerError *json.MarshalerError
if errors.As(err, &marshalerError) && marshalerError != nil {
if e, ok := errors.AsType[*json.MarshalerError](err); ok && e != nil {
// this likely indicates an implementation error in hst
fatal("cannot encode json for " + marshalerError.Type.String() + ": " + marshalerError.Err.Error())
fatal("cannot encode json for " + e.Type.String() + ": " + e.Err.Error())
return
}
// UnsupportedTypeError, UnsupportedValueError: incorrect usage, does not need to be handled
// UnsupportedTypeError, UnsupportedValueError: incorrect usage, does
// not need to be handled
fatal("cannot write json: " + err.Error())
}
}
+5 -2
View File
@@ -64,7 +64,7 @@ func TestPrintShowInstance(t *testing.T) {
Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pipewire
Groups: video, dialout, plugdev
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, cover_run, runtime, tmpdir
Home: /data/data/org.chromium.Chromium
Hostname: localhost
Path: /run/current-system/sw/bin/chromium
@@ -161,7 +161,7 @@ App
Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pipewire
Groups: video, dialout, plugdev
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, cover_run, runtime, tmpdir
Home: /data/data/org.chromium.Chromium
Hostname: localhost
Path: /run/current-system/sw/bin/chromium
@@ -355,6 +355,7 @@ App
"multiarch": true,
"map_real_uid": true,
"device": true,
"cover_run": true,
"share_runtime": true,
"share_tmpdir": true
},
@@ -506,6 +507,7 @@ App
"multiarch": true,
"map_real_uid": true,
"device": true,
"cover_run": true,
"share_runtime": true,
"share_tmpdir": true
}
@@ -704,6 +706,7 @@ func TestPrintPs(t *testing.T) {
"multiarch": true,
"map_real_uid": true,
"device": true,
"cover_run": true,
"share_runtime": true,
"share_tmpdir": true
},
+1 -23
View File
@@ -21,15 +21,6 @@
// following paragraphs are considered an internal detail and not covered by the
// compatibility promise.
//
// After checking credentials, hsu checks via /proc/ the absolute pathname of
// its parent process, and fails if it does not match the hakurei pathname set
// at link time. This is not a security feature: the priv-side is considered
// trusted, and this feature makes no attempt to address the racy nature of
// querying /proc/, or debuggers attached to the parent process. Instead, this
// aims to discourage misuse and reduce confusion if the user accidentally
// stumbles upon this program. It also prevents accidental use of the incorrect
// installation of hsu in some environments.
//
// Since target container environment variables are set up in shim via the
// [container] infrastructure, the environment is used for parameters from the
// parent process.
@@ -62,7 +53,6 @@ import (
"runtime"
"slices"
"strconv"
"strings"
"syscall"
)
@@ -107,18 +97,6 @@ func main() {
return
}
var toolPath string
pexe := filepath.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") {
log.Fatal("hakurei executable has been deleted")
} else if p != hakureiPath {
log.Fatal("this program must be started by hakurei")
} else {
toolPath = p
}
// refuse to run if hsurc is not protected correctly
if s, err := os.Stat(hsuConfPath); err != nil {
log.Fatal(err)
@@ -205,7 +183,7 @@ func main() {
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
}
if err := syscall.Exec(toolPath, []string{
if err := syscall.Exec(hakureiPath, []string{
"hakurei",
"shim",
}, []string{
+21 -20
View File
@@ -2,13 +2,14 @@ package main
import (
"context"
"net/http"
"os"
"path/filepath"
"testing"
"hakurei.app/check"
"hakurei.app/container"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
"hakurei.app/message"
)
@@ -30,7 +31,7 @@ type cache struct {
// Loaded artifact of [rosa.QEMU].
qemu pkg.Artifact
base string
base, mirror string
}
// open opens the underlying [pkg.Cache].
@@ -86,6 +87,21 @@ func (cache *cache) open() (err error) {
}
done <- struct{}{}
if cache.mirror != "" {
var pub []byte
pub, err = os.ReadFile(base.Append("ed25519.pub").String())
if err != nil {
cache.c.Close()
return
}
var r rosa.Remote
if r, err = rosa.NewRemote(cache.mirror, pub, http.DefaultClient); err != nil {
cache.c.Close()
return err
}
cache.c.SetExternal(r)
}
if cache.qemu != nil {
var pathname *check.Absolute
pathname, _, err = cache.c.Cure(cache.qemu)
@@ -94,24 +110,9 @@ func (cache *cache) open() (err error) {
return
}
pkg.RegisterArch("riscv64", container.BinfmtEntry{
Offset: 0,
Magic: "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00",
Mask: "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
Interpreter: pathname.Append(
"system/bin",
"qemu-riscv64",
),
})
pkg.RegisterArch("arm64", container.BinfmtEntry{
Offset: 0,
Magic: "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00",
Mask: "\xff\xff\xff\xff\xff\xff\xff\xfc\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
Interpreter: pathname.Append(
"system/bin",
"qemu-aarch64",
),
})
for arch, entry := range rosa.Arches(pathname) {
pkg.RegisterArch(arch, entry)
}
}
return
+1 -1
View File
@@ -74,7 +74,7 @@ func (s *searchCache) clean() {
}
func indexsum(in [][]int) int {
sum := 0
for i := 0; i < len(in); i++ {
for i := range in {
sum += in[i][1] - in[i][0]
}
return sum
+228 -109
View File
@@ -14,6 +14,7 @@ package main
import (
"context"
"crypto/ed25519"
"crypto/sha512"
"errors"
"fmt"
@@ -31,12 +32,11 @@ import (
"syscall"
"time"
"unique"
"unsafe"
"hakurei.app/check"
"hakurei.app/command"
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/ext"
"hakurei.app/fhs"
"hakurei.app/internal/pkg"
@@ -47,6 +47,19 @@ import (
"hakurei.app/cmd/mbf/internal/pkgserver/ui"
)
// writeFileExcl is like [os.WriteFile], but sets [os.O_EXCL] instead.
func writeFileExcl(name string, data []byte, perm os.FileMode) error {
f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
if err != nil {
return err
}
_, err = f.Write(data)
if err1 := f.Close(); err1 != nil && err == nil {
err = err1
}
return err
}
func main() {
container.TryArgv0(nil)
@@ -86,6 +99,8 @@ func main() {
flagCheck bool
flagLTO bool
flagPT bool
flagDry bool
flagPath string
flagSourcePath string
flagCrossOverride int
@@ -107,6 +122,8 @@ func main() {
if cm.base == "" {
cm.base = "cache"
}
cm.mirror = os.ExpandEnv(cm.mirror)
azaleaPath := os.ExpandEnv(flagPath)
addr.Net = "unix"
addr.Name = os.ExpandEnv(addr.Name)
@@ -144,6 +161,17 @@ func main() {
}
}
if azaleaPath != "" {
var root *os.Root
if a, err := check.NewAbs(azaleaPath); err != nil {
return err
} else if root, err = os.OpenRoot(a.String()); err != nil {
return err
} else if err = rosa.Native().RegisterFS(root.FS()); err != nil {
return err
}
}
return nil
}).Flag(
&flagQuiet,
@@ -185,6 +213,10 @@ func main() {
&cm.base,
"d", command.StringFlag("$MBF_CACHE_DIR"),
"Directory to store cured artifacts",
).Flag(
&cm.mirror,
"r", command.StringFlag("$MBF_REMOTE"),
"URL of mirror service",
).Flag(
&cm.idle,
"sched-idle", command.BoolFlag(false),
@@ -204,10 +236,18 @@ func main() {
&flagPT,
"parse-time", command.BoolFlag(false),
"Print duration of the initial azalea parse",
).Flag(
&flagDry,
"dry", command.BoolFlag(false),
"Do not destroy cache entries",
).Flag(
&flagSourcePath,
"source", command.StringFlag(""),
"Override hakurei source tree",
).Flag(
&flagPath,
"p", command.StringFlag("$AZALEA_PATH"),
"Load additional azalea files",
)
c.NewCommand(
@@ -360,7 +400,10 @@ func main() {
)
{
var flagJobs int
var (
flagJobs int
flagNoBlock bool
)
c.NewCommand("updates", command.UsageInternal, func([]string) error {
var (
errsMu sync.Mutex
@@ -379,6 +422,11 @@ func main() {
continue
}
if !flagNoBlock && meta.Blocked != "" {
msg.Verbosef("%s is blocked: %s", meta.Name, meta.Blocked)
continue
}
v, err := meta.GetVersions(ctx)
if err != nil {
errsMu.Lock()
@@ -419,9 +467,23 @@ func main() {
&flagJobs,
"j", command.IntFlag(32),
"Maximum number of simultaneous connections",
).Flag(
&flagNoBlock,
"ignore-block", command.BoolFlag(false),
"Inhibit update blocking",
)
}
c.NewCommand("blocked", command.UsageInternal, func([]string) error {
for _, p := range rosa.Native().CollectAll() {
meta, _ := rosa.Native().Std().Load(p)
if meta.Blocked != "" {
fmt.Printf("%s: %s\n", meta.Name, meta.Blocked)
}
}
return nil
})
c.NewCommand(
"daemon",
"Service artifact IR with Rosa OS extensions",
@@ -435,6 +497,70 @@ func main() {
},
)
c.NewCommand(
"keygen",
"Create keypair for local cache",
func([]string) error {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
return err
}
return errors.Join(writeFileExcl(filepath.Join(
cm.base,
"ed25519.pub",
), pub, 0444), writeFileExcl(filepath.Join(
cm.base,
"ed25519",
), priv, 0400))
},
)
c.NewCommand(
"serve",
"Export local cache as mirror",
func(args []string) error {
const shutdownTimeout = 15 * time.Second
if len(args) != 1 {
return errors.New("serve requires 1 argument")
}
var key ed25519.PrivateKey
if p, err := os.ReadFile(filepath.Join(cm.base, "ed25519")); err != nil {
return err
} else if len(p) != ed25519.PrivateKeySize {
return errors.New("invalid private key")
} else {
key = p
}
var h http.Handler
if base, err := os.OpenRoot(cm.base); err != nil {
return err
} else {
h = rosa.NewMirror(msg, base.FS(), key)
}
server := http.Server{Addr: args[0], Handler: h}
go func() {
<-ctx.Done()
cc, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := server.Shutdown(cc); err != nil {
log.Fatal(err)
}
}()
msg.Verbosef("listening on %q", args[0])
err := server.ListenAndServe()
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
return err
},
)
{
var (
flagGentoo string
@@ -568,7 +694,7 @@ func main() {
0400,
); err != nil {
return err
} else if _, err = pkg.Flatten(
} else if err = pkg.Write(
os.DirFS(pathname.String()),
".",
f,
@@ -604,7 +730,7 @@ func main() {
return cache.EnterExec(
ctx,
a,
true, os.Stdin, os.Stdout, os.Stderr,
"", true, os.Stdin, os.Stdout, os.Stderr,
rosa.AbsSystem.Append("bin", "mksh"),
"sh",
)
@@ -714,8 +840,9 @@ func main() {
)
}
c.NewCommand(
"clear",
cleanC := c.New("clean", "Remove unused entries from the cache")
cleanC.NewCommand(
"fault",
"Remove all fault entries from the cache",
func([]string) error {
return cm.Do(func(*pkg.Cache) error {
@@ -736,6 +863,58 @@ func main() {
})
},
)
cleanC.NewCommand(
"checksum",
"Remove unreachable checksum entries",
func([]string) error {
return cm.Do(func(cache *pkg.Cache) error {
_, checksums, err := cache.Clean(flagDry, false)
log.Printf("destroyed %d entries", len(checksums))
return err
})
},
)
{
var flagDeep bool
cleanC.NewCommand(
"all",
"Remove identifiers not reachable by loaded packages",
func([]string) error {
return cm.Do(func(cache *pkg.Cache) error {
t := rosa.Native().Clone().Std()
handles := t.CollectAll()
flags := t.Flags()
a := t.Append(nil, handles...)
for arch := range rosa.Arches(nil) {
if arch == runtime.GOARCH {
continue
}
t.DropCaches(arch, rosa.OptLLVMNoLTO|rosa.OptSkipCheck)
a = t.Append(a, handles...)
t.DropCaches(arch, flags)
a = t.Append(a, handles...)
}
ids, checksums, err := cache.Clean(
flagDry,
!flagDeep,
a...,
)
log.Printf(
"destroyed %d identifier and %d checksum entries",
len(ids), len(checksums),
)
return err
})
},
).Flag(
&flagDeep,
"deep", command.BoolFlag(false),
"Include transitive inputs",
)
}
c.NewCommand(
"abort",
@@ -754,6 +933,17 @@ func main() {
"shell",
"Interactive shell in the specified Rosa OS environment",
func(args []string) error {
resolvconf := "nameserver 1.1.1.1\nnameserver 1.0.0.1\n"
if p, err := os.ReadFile(fhs.AbsEtc.Append(
"resolv.conf",
).String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err
}
} else {
resolvconf = unsafe.String(unsafe.SliceData(p), len(p))
}
handles := make([]rosa.ArtifactH, len(args), len(args)+3)
for i, arg := range args {
handles[i] = rosa.ArtifactH(unique.Make(arg))
@@ -773,112 +963,42 @@ func main() {
)
root := make(pkg.Collect, 0, 6+len(args))
root = append(root, rosa.NewEtc(false))
root = rosa.Native().Std().Append(root, handles...)
if err := cm.Do(func(cache *pkg.Cache) error {
_, _, err := cache.Cure(&root)
return err
}); err == nil {
return errors.New("unreachable")
} else if !pkg.IsCollected(err) {
return err
}
type cureRes struct {
pathname *check.Absolute
checksum unique.Handle[pkg.Checksum]
}
cured := make(map[pkg.Artifact]cureRes)
for _, a := range root {
if err := cm.Do(func(cache *pkg.Cache) error {
pathname, checksum, err := cache.Cure(a)
if err == nil {
cured[a] = cureRes{pathname, checksum}
}
return err
}); err != nil {
return err
}
}
// explicitly open for direct error-free use from this point
if cm.c == nil {
if err := cm.open(); err != nil {
return err
}
}
layers := pkg.PromoteLayers(root, func(a pkg.Artifact) (
*check.Absolute,
unique.Handle[pkg.Checksum],
) {
res := cured[a]
return res.pathname, res.checksum
}, func(i int, d pkg.Artifact) {
r := pkg.Encode(cm.c.Ident(d).Value())
if s, ok := d.(fmt.Stringer); ok {
if name := s.String(); name != "" {
r += "-" + name
}
}
msg.Verbosef("promoted layer %d as %s", i, r)
})
z := container.New(ctx, msg)
z.WaitDelay = 3 * time.Second
z.SeccompPresets = pkg.SeccompPresets
z.SeccompFlags |= seccomp.AllowMultiarch
z.ParentPerm = 0700
z.HostNet = flagNet
z.RetainSession = flagSession
z.Hostname = "localhost"
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
z.Quiet = !cm.verboseInit
if s, ok := os.LookupEnv("TERM"); ok {
z.Env = append(z.Env, "TERM="+s)
}
var tempdir *check.Absolute
if s, err := filepath.Abs(os.TempDir()); err != nil {
return err
} else if tempdir, err = check.NewAbs(s); err != nil {
return err
}
z.Dir = fhs.AbsRoot
z.Env = []string{
return cm.Do(func(cache *pkg.Cache) error {
return cache.EnterExec(
ctx,
pkg.NewExec(
"",
rosa.Native().Arch(),
new(pkg.Checksum),
1,
flagNet,
false,
fhs.AbsRoot,
[]string{
"SHELL=/system/bin/mksh",
"PATH=/system/bin",
"HOME=/",
}
z.Path = rosa.AbsSystem.Append("bin", "mksh")
z.Args = []string{"mksh"}
z.
OverlayEphemeral(fhs.AbsRoot, layers...).
Place(
fhs.AbsEtc.Append("hosts"),
[]byte("127.0.0.1 localhost\n"),
).
Place(
fhs.AbsEtc.Append("passwd"),
[]byte("media_rw:x:1023:1023::/:/system/bin/sh\n"+
"nobody:x:65534:65534::/proc/nonexistent:/system/bin/false\n"),
).
Place(
fhs.AbsEtc.Append("group"),
[]byte("media_rw:x:1023:\nnobody:x:65534:\n"),
).
Bind(tempdir, fhs.AbsTmp, std.BindWritable).
Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
if err := z.Start(); err != nil {
return err
}
if err := z.Serve(); err != nil {
return err
}
return z.Wait()
},
fhs.AbsProc.Append("nonexistent"),
nil,
pkg.Path(fhs.AbsRoot, true, root...),
pkg.Path(
fhs.AbsEtc.Append("resolv.conf"), false,
pkg.NewFile(
"resolv.conf",
unsafe.Slice(unsafe.StringData(resolvconf), len(resolvconf)),
),
),
),
"localhost",
flagSession, os.Stdin, os.Stdout, os.Stderr,
rosa.AbsSystem.Append("bin", "mksh"),
"sh",
)
})
},
).Flag(
&flagNet,
@@ -893,7 +1013,6 @@ func main() {
"with-toolchain", command.BoolFlag(false),
"Include the stage2 LLVM toolchain",
)
}
c.Command(
+2 -2
View File
@@ -508,8 +508,8 @@ func _main(s ...string) (exitCode int) {
if !z.AllowOrphan {
if err := z.Wait(); err != nil {
var exitError *exec.ExitError
if !errors.As(err, &exitError) || exitError == nil {
exitError, ok := errors.AsType[*exec.ExitError](err)
if !ok || exitError == nil {
log.Println(err)
return 5
}
+2 -2
View File
@@ -91,8 +91,8 @@ func (n *node) MustParse(arguments []string, handleError func(error)) {
case ErrEmptyTree:
os.Exit(1)
default:
var flagError FlagError
if !errors.As(err, &flagError) { // returned by HandlerFunc
flagError, ok := errors.AsType[FlagError](err)
if !ok { // returned by HandlerFunc
handleError(err)
os.Exit(1)
}
+2 -5
View File
@@ -154,11 +154,8 @@ func (e *StartError) Error() string {
return e.Step
}
{
var syscallError *os.SyscallError
if errors.As(e.Err, &syscallError) && syscallError != nil {
return e.Step + " " + syscallError.Error()
}
if se, ok := errors.AsType[*os.SyscallError](e.Err); ok && se != nil {
return e.Step + " " + se.Error()
}
return e.Step + ": " + e.Err.Error()
-2
View File
@@ -235,8 +235,6 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
})
}
func sliceAddr[S any](s []S) *[]S { return &s }
func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile {
f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr}
// check happens in Close, and cleanup is not guaranteed to run, so relying
+6 -8
View File
@@ -46,9 +46,8 @@ func messageFromError(err error) (m string, ok bool) {
// While this is usable for pointer errors, such use should be avoided as nil
// check is omitted.
func messagePrefix[T error](prefix string, err error) (string, bool) {
var targetError T
if errors.As(err, &targetError) {
return prefix + targetError.Error(), true
if e, ok := errors.AsType[T](err); ok {
return prefix + e.Error(), true
}
return zeroString, false
}
@@ -58,9 +57,8 @@ func messagePrefixP[V any, T interface {
*V
error
}](prefix string, err error) (string, bool) {
var targetError T
if errors.As(err, &targetError) && targetError != nil {
return prefix + targetError.Error(), true
if e, ok := errors.AsType[T](err); ok && e != nil {
return prefix + e.Error(), true
}
return zeroString, false
}
@@ -109,8 +107,8 @@ func optionalErrorUnwrap(err error) error {
// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback.
func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
var errno syscall.Errno
if !errors.As(err, &errno) {
errno, ok := errors.AsType[syscall.Errno](err)
if !ok {
return 0, &os.PathError{Op: op, Path: path, Err: err}
}
return errno, nil
+8 -8
View File
@@ -95,7 +95,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -123,7 +123,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -152,7 +152,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -182,7 +182,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -213,7 +213,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -245,7 +245,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -279,7 +279,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
@@ -315,7 +315,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16,
Gid: 1 << 15,
Hostname: "hakurei-check",
Ops: (*Ops)(sliceAddr(make(Ops, 1))),
Ops: new(make(Ops, 1)),
SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict,
RetainSession: true,
+1 -1
View File
@@ -39,7 +39,7 @@ func TestSyscall(t *testing.T) {
t.Errorf("Unmarshal: %v, want %v", got, tc.want)
}
})
if errors.As(tc.err, new(ext.SyscallNameError)) {
if _, ok := errors.AsType[ext.SyscallNameError](tc.err); ok {
return
}
Generated
+8 -8
View File
@@ -7,32 +7,32 @@
]
},
"locked": {
"lastModified": 1772985280,
"narHash": "sha256-FdrNykOoY9VStevU4zjSUdvsL9SzJTcXt4omdEDZDLk=",
"lastModified": 1780361225,
"narHash": "sha256-wnV9ttf4fPWNonBIQmvlrSlNpQYgx5HgWWd007mwIFA=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "8f736f007139d7f70752657dff6a401a585d6cbc",
"rev": "e28654b71096e08c019d4861ca26acb646f583d8",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-25.11",
"ref": "release-26.05",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1772822230,
"narHash": "sha256-yf3iYLGbGVlIthlQIk5/4/EQDZNNEmuqKZkQssMljuw=",
"lastModified": 1780453794,
"narHash": "sha256-bXMRa9VTsHSPXL4Cw8R6JJLQeY3Y/IP4+YJCYVmQ7FY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "71caefce12ba78d84fe618cf61644dce01cf3a96",
"rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.11",
"ref": "nixos-26.05",
"repo": "nixpkgs",
"type": "github"
}
+4 -4
View File
@@ -2,10 +2,10 @@
description = "hakurei container tool and nixos module";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
home-manager = {
url = "github:nix-community/home-manager/release-25.11";
url = "github:nix-community/home-manager/release-26.05";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@@ -37,7 +37,7 @@
inherit (pkgs)
runCommandLocal
callPackage
nixfmt-rfc-style
nixfmt
deadnix
statix
;
@@ -57,7 +57,7 @@
sharefs = callPackage ./cmd/sharefs/test { inherit system self; };
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt ]; } ''
cd ${./.}
echo "running nixfmt..."
+15
View File
@@ -2,6 +2,7 @@ package hst
import (
"encoding/json"
"fmt"
"strings"
"syscall"
"time"
@@ -68,6 +69,8 @@ const (
// FDevice mount /dev/ from the init mount namespace as is in the container
// mount namespace.
FDevice
// FCoverRun covers /run/ in the container mount namespace early.
FCoverRun
// FShareRuntime shares XDG_RUNTIME_DIR between containers under the same identity.
FShareRuntime
@@ -100,6 +103,8 @@ func (flags Flags) String() string {
return "mapuid"
case FDevice:
return "device"
case FCoverRun:
return "cover_run"
case FShareRuntime:
return "runtime"
case FShareTmpdir:
@@ -161,6 +166,10 @@ type ContainerConfig struct {
Flags Flags `json:"-"`
}
func (c *ContainerConfig) GoString() string {
return fmt.Sprintf("&%#v", *c)
}
// ContainerConfigF is [ContainerConfig] stripped of its methods.
//
// The [ContainerConfig.Flags] field does not survive a [json] round trip.
@@ -191,6 +200,8 @@ type containerConfigJSON = struct {
// Corresponds to [FDevice].
Device bool `json:"device,omitempty"`
// Corresponds to [FCoverRun].
CoverRun bool `json:"cover_run,omitempty"`
// Corresponds to [FShareRuntime].
ShareRuntime bool `json:"share_runtime,omitempty"`
@@ -214,6 +225,7 @@ func (c *ContainerConfig) MarshalJSON() ([]byte, error) {
Multiarch: c.Flags&FMultiarch != 0,
MapRealUID: c.Flags&FMapRealUID != 0,
Device: c.Flags&FDevice != 0,
CoverRun: c.Flags&FCoverRun != 0,
ShareRuntime: c.Flags&FShareRuntime != 0,
ShareTmpdir: c.Flags&FShareTmpdir != 0,
})
@@ -257,6 +269,9 @@ func (c *ContainerConfig) UnmarshalJSON(data []byte) error {
if v.Device {
c.Flags |= FDevice
}
if v.CoverRun {
c.Flags |= FCoverRun
}
if v.ShareRuntime {
c.Flags |= FShareRuntime
}
+3 -3
View File
@@ -21,8 +21,8 @@ func TestFlagsString(t *testing.T) {
}{
{"none", 0, "none"},
{"none high", hst.FAll + 1, "none"},
{"all", hst.FAll, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir"},
{"all high", math.MaxUint, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir"},
{"all", hst.FAll, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, cover_run, runtime, tmpdir"},
{"all high", math.MaxUint, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, cover_run, runtime, tmpdir"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@@ -53,7 +53,7 @@ func TestContainerConfig(t *testing.T) {
{"hostnet hostabstract mapuid", &hst.ContainerConfig{Flags: hst.FHostNet | hst.FHostAbstract | hst.FMapRealUID},
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"host_net":true,"host_abstract":true,"map_real_uid":true}`},
{"all", &hst.ContainerConfig{Flags: hst.FAll},
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"seccomp_compat":true,"devel":true,"userns":true,"host_net":true,"host_abstract":true,"tty":true,"multiarch":true,"map_real_uid":true,"device":true,"share_runtime":true,"share_tmpdir":true}`},
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"seccomp_compat":true,"devel":true,"userns":true,"host_net":true,"host_abstract":true,"tty":true,"multiarch":true,"map_real_uid":true,"device":true,"cover_run":true,"share_runtime":true,"share_tmpdir":true}`},
}
for _, tc := range testCases {
+5
View File
@@ -1,6 +1,7 @@
package hst
import (
"fmt"
"strconv"
"strings"
)
@@ -61,6 +62,10 @@ type BusConfig struct {
Filter bool `json:"filter"`
}
func (c *BusConfig) GoString() string {
return fmt.Sprintf("&%#v", *c)
}
// Interfaces iterates over all interface strings specified in [BusConfig].
func (c *BusConfig) Interfaces(yield func(string) bool) {
if c == nil {
+9 -6
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"reflect"
"strings"
"hakurei.app/check"
)
@@ -36,6 +37,8 @@ type Ops interface {
Bind(source, target *check.Absolute, flags int) Ops
// Overlay appends an op that mounts the overlay pseudo filesystem.
Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) Ops
// OverlayEphemeral appends a MountOverlayOp with an ephemeral upperdir and workdir.
OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) Ops
// OverlayReadonly appends an op that mounts the overlay pseudo filesystem readonly.
OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) Ops
@@ -78,17 +81,17 @@ type FSImplError struct{ Value FilesystemConfig }
func (f FSImplError) Error() string {
implType := reflect.TypeOf(f.Value)
var name string
for implType != nil && implType.Kind() == reflect.Ptr {
name += "*"
var buf strings.Builder
for implType != nil && implType.Kind() == reflect.Pointer {
buf.WriteByte('*')
implType = implType.Elem()
}
if implType != nil {
name += implType.Name()
buf.WriteString(implType.Name())
} else {
name += "nil"
buf.WriteString("nil")
}
return fmt.Sprintf("implementation %s not supported", name)
return "implementation " + buf.String() + " not supported"
}
// FilesystemConfigJSON is the [json] adapter for [FilesystemConfig].
+9 -4
View File
@@ -3,6 +3,7 @@ package hst_test
import (
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"strings"
@@ -103,7 +104,7 @@ func TestFilesystemConfigJSON(t *testing.T) {
t.Run("marshal", func(t *testing.T) {
t.Parallel()
wantErr := tc.wantErr
if errors.As(wantErr, new(hst.FSTypeError)) {
if _, ok := errors.AsType[hst.FSTypeError](wantErr); ok {
// for unsupported implementation tc
wantErr = hst.FSImplError{Value: stubFS{"cat"}}
}
@@ -139,7 +140,7 @@ func TestFilesystemConfigJSON(t *testing.T) {
t.Run("unmarshal", func(t *testing.T) {
t.Parallel()
if tc.data == "\x00" && tc.sData == "\x00" {
if errors.As(tc.wantErr, new(hst.FSImplError)) {
if _, ok := errors.AsType[hst.FSImplError](tc.wantErr); ok {
// this error is only returned on marshal
return
}
@@ -283,11 +284,11 @@ func checkFs(t *testing.T, testCases []fsTestCase) {
if !reflect.DeepEqual(ops, &tc.ops) {
gotString := new(strings.Builder)
for _, op := range *ops {
gotString.WriteString("\n" + op.String())
gotString.WriteString("\n" + fmt.Sprintf("%#v", op))
}
wantString := new(strings.Builder)
for _, op := range tc.ops {
wantString.WriteString("\n" + op.String())
wantString.WriteString("\n" + fmt.Sprintf("%#v", op))
}
t.Errorf("Apply: %s, want %s", gotString, wantString)
}
@@ -339,6 +340,10 @@ func (p opsAdapter) Overlay(target, state, work *check.Absolute, layers ...*chec
return opsAdapter{p.Ops.Overlay(target, state, work, layers...)}
}
func (p opsAdapter) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
return opsAdapter{p.Ops.OverlayEphemeral(target, layers...)}
}
func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
return opsAdapter{p.Ops.OverlayReadonly(target, layers...)}
}
+1 -6
View File
@@ -43,18 +43,13 @@ func (e *FSEphemeral) Apply(z *ApplyState) {
return
}
size := e.Size
if size < 0 {
size = 0
}
perm := e.Perm
if perm == 0 {
perm = fsEphemeralDefaultPerm
}
if e.Write {
z.Tmpfs(e.Target, size, perm)
z.Tmpfs(e.Target, max(e.Size, 0), perm)
} else {
z.Readonly(e.Target, perm)
}
+24 -6
View File
@@ -2,6 +2,7 @@ package hst
import (
"encoding/gob"
"slices"
"strings"
"hakurei.app/check"
@@ -40,7 +41,7 @@ func (o *FSOverlay) Valid() bool {
}
if o.Upper != nil { // rw
return o.Work != nil && len(o.Lower) > 0
return o.Work != nil || len(o.Lower) > 0
} else { // ro
return len(o.Lower) >= 2
}
@@ -58,8 +59,11 @@ func (o *FSOverlay) Host() []*check.Absolute {
return nil
}
p := make([]*check.Absolute, 0, 2+len(o.Lower))
if o.Upper != nil && o.Work != nil {
p = append(p, o.Upper, o.Work)
if o.Upper != nil {
p = append(p, o.Upper)
if o.Work != nil {
p = append(p, o.Work)
}
}
p = append(p, o.Lower...)
return p
@@ -70,11 +74,18 @@ func (o *FSOverlay) Apply(z *ApplyState) {
return
}
if o.Upper != nil && o.Work != nil {
z.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
if o.Upper != nil {
if o.Target.Is(fhs.AbsRoot) {
z.NoRemountRoot = true
}
if o.Work != nil {
z.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
} else {
z.OverlayEphemeral(o.Target, slices.Concat(
o.Lower,
[]*check.Absolute{o.Upper})...,
)
}
} else {
z.OverlayReadonly(o.Target, o.Lower...)
}
@@ -90,12 +101,19 @@ func (o *FSOverlay) String() string {
lower[i] = check.EscapeOverlayDataSegment(a.String())
}
if o.Upper != nil && o.Work != nil {
if o.Upper != nil {
if o.Work != nil {
return "w*" + strings.Join(append([]string{
check.EscapeOverlayDataSegment(o.Target.String()),
check.EscapeOverlayDataSegment(o.Upper.String()),
check.EscapeOverlayDataSegment(o.Work.String())},
lower...), check.SpecialOverlayPath)
}
return "e*" + strings.Join(append([]string{
check.EscapeOverlayDataSegment(o.Target.String()),
check.EscapeOverlayDataSegment(o.Upper.String())},
lower...), check.SpecialOverlayPath)
} else {
return "*" + strings.Join(append([]string{
check.EscapeOverlayDataSegment(o.Target.String())},
+13 -1
View File
@@ -5,6 +5,7 @@ import (
"hakurei.app/check"
"hakurei.app/container"
"hakurei.app/fhs"
"hakurei.app/hst"
)
@@ -14,7 +15,7 @@ func TestFSOverlay(t *testing.T) {
checkFs(t, []fsTestCase{
{"nil", (*hst.FSOverlay)(nil), false, nil, nil, nil, "<invalid>"},
{"nil lower", &hst.FSOverlay{Target: m("/etc"), Lower: []*check.Absolute{nil}}, false, nil, nil, nil, "<invalid>"},
{"zero lower", &hst.FSOverlay{Target: m("/etc"), Upper: m("/"), Work: m("/")}, false, nil, nil, nil, "<invalid>"},
{"zero lower", &hst.FSOverlay{Target: m("/etc"), Work: m("/")}, false, nil, nil, nil, "<invalid>"},
{"zero lower ro", &hst.FSOverlay{Target: m("/etc")}, false, nil, nil, nil, "<invalid>"},
{"short lower", &hst.FSOverlay{Target: m("/etc"), Lower: ms("/etc")}, false, nil, nil, nil, "<invalid>"},
@@ -62,5 +63,16 @@ func TestFSOverlay(t *testing.T) {
Work: m("/tmp/work"),
}}, m("/"), ms("/tmp/upper", "/tmp/work", "/tmp/.src0", "/tmp/.src1"),
"w*/:/tmp/upper:/tmp/work:/tmp/.src0:/tmp/.src1"},
{"ephemeral", &hst.FSOverlay{
Target: m("/"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
Upper: m("/tmp/upper"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/"),
Lower: ms("/tmp/.src0", "/tmp/.src1", "/tmp/upper"),
Upper: fhs.AbsRoot,
}}, m("/"), ms("/tmp/upper", "/tmp/.src0", "/tmp/.src1"),
"e*/:/tmp/upper:/tmp/.src0:/tmp/.src1"},
})
}
+1
View File
@@ -245,6 +245,7 @@ func TestTemplate(t *testing.T) {
"multiarch": true,
"map_real_uid": true,
"device": true,
"cover_run": true,
"share_runtime": true,
"share_tmpdir": true
}
+2 -2
View File
@@ -80,7 +80,7 @@ func unescapeValue(v []byte) (val []byte, errno ParseError) {
continue
}
if ib := bytes.IndexByte([]byte("-_/.\\*"), b); ib != -1 { // - // _/.\*
if found := bytes.Contains([]byte("-_/.\\*"), []byte{b}); found { // - // _/.\*
goto opt
} else if b >= '0' && b <= '9' { // 0-9
goto opt
@@ -101,7 +101,7 @@ func unescapeValue(v []byte) (val []byte, errno ParseError) {
break
}
if c, err := hex.Decode(val[i:i+1], v[iu+1:iu+3]); err != nil {
if errors.As(err, new(hex.InvalidByteError)) {
if _, ok := errors.AsType[hex.InvalidByteError](err); ok {
errno = ErrBadValHexByte
break
}
+18 -15
View File
@@ -101,7 +101,7 @@ func (o *Object) update(env map[string]string, strip bool) {
// A pendingIterator is a callback currently iterating through objects targeted
// by ongoing events.
type pendingIterator struct {
f func(o *Object) bool
f func(o *Object, act uevent.KobjectAction) bool
done chan<- struct{}
}
@@ -150,12 +150,12 @@ func (s *State) deleteIter(p *pendingIterator) {
}
// dispatchIter broadcasts an [Object] to all alive iterators.
func (s *State) dispatchIter(o *Object) {
func (s *State) dispatchIter(o *Object, act uevent.KobjectAction) {
s.iterMu.Lock()
defer s.iterMu.Unlock()
for _, p := range s.iter {
if !p.f(o) {
if !p.f(o, act) {
s.deleteIter(p)
close(p.done)
}
@@ -165,14 +165,17 @@ func (s *State) dispatchIter(o *Object) {
// Range calls f on all current and upcoming [Object] values tracked by s until
// f returns false or the context is cancelled. f must not retain o or modify
// the value it points to.
func (s *State) Range(ctx context.Context, f func(o *Object) bool) {
func (s *State) Range(
ctx context.Context,
f func(o *Object, act uevent.KobjectAction) bool,
) {
done := make(chan struct{})
p := pendingIterator{f, done}
s.iterMu.Lock()
s.ueventMu.RLock()
for _, o := range s.uevent {
if !f(o) {
if !f(o, uevent.KOBJ_ADD) {
s.ueventMu.RUnlock()
s.iterMu.Unlock()
return
@@ -296,13 +299,13 @@ func (s *State) processEvent(e *Event) {
return
}
switch e.Action {
switch act := e.Action; act {
case uevent.KOBJ_ADD:
if e.Synth == nil {
if o, ok := s.uevent[e.DevPath]; ok {
s.reportErr(e.NewError(EDuplicateAdd, o))
o.merge(e.Env)
s.dispatchIter(o)
s.dispatchIter(o, act)
return
}
}
@@ -312,7 +315,7 @@ func (s *State) processEvent(e *Event) {
}
o.merge(e.Env)
s.uevent[e.DevPath] = o
s.dispatchIter(o)
s.dispatchIter(o, act)
return
case uevent.KOBJ_REMOVE:
@@ -338,14 +341,14 @@ func (s *State) processEvent(e *Event) {
o = e.makeColdboot()
o.merge(e.Env)
s.uevent[e.DevPath] = o
s.dispatchIter(o)
s.dispatchIter(o, act)
return
}
o.update(e.Env, true)
if s.handleChange != nil {
s.handleChange(o, e.Env)
}
s.dispatchIter(o)
s.dispatchIter(o, act)
return
case uevent.KOBJ_MOVE:
@@ -368,7 +371,7 @@ func (s *State) processEvent(e *Event) {
o.merge(e.Env)
s.uevent[e.DevPath] = o
o.DevPath = e.DevPath
s.dispatchIter(o)
s.dispatchIter(o, act)
return
case uevent.KOBJ_ONLINE:
@@ -384,7 +387,7 @@ func (s *State) processEvent(e *Event) {
s.reportErr(e.NewError(EUnexpectedOffline, o))
}
o.Offline = false
s.dispatchIter(o)
s.dispatchIter(o, act)
return
case uevent.KOBJ_OFFLINE:
@@ -400,7 +403,7 @@ func (s *State) processEvent(e *Event) {
s.reportErr(e.NewError(EUnexpectedOffline, o))
}
o.Offline = true
s.dispatchIter(o)
s.dispatchIter(o, act)
return
case uevent.KOBJ_BIND:
@@ -416,7 +419,7 @@ func (s *State) processEvent(e *Event) {
}
o.State = StateBound
o.merge(e.Env)
s.dispatchIter(o)
s.dispatchIter(o, act)
return
case uevent.KOBJ_UNBIND:
@@ -432,7 +435,7 @@ func (s *State) processEvent(e *Event) {
}
o.State = StateNew
o.Driver = ""
s.dispatchIter(o)
s.dispatchIter(o, act)
return
default: // not reached
+9 -3
View File
@@ -388,7 +388,9 @@ func TestIter(t *testing.T) {
"SEQNUM=1",
}}
synctest.Wait()
s.Range(t.Context(), func(o *Object) bool { return false })
s.Range(t.Context(), func(*Object, uevent.KobjectAction) bool {
return false
})
var got []*Object
check := func(want []*Object) {
@@ -405,7 +407,7 @@ func TestIter(t *testing.T) {
defer cancel()
var done bool
wg.Go(func() {
s.Range(ctx, func(o *Object) bool {
s.Range(ctx, func(o *Object, _ uevent.KobjectAction) bool {
got = append(got, o.Clone())
return !done
})
@@ -437,7 +439,11 @@ func TestIter(t *testing.T) {
},
})
wg.Go(func() { s.Range(ctx, func(*Object) bool { return true }) })
wg.Go(func() {
s.Range(ctx, func(*Object, uevent.KobjectAction) bool {
return true
})
})
synctest.Wait()
iter := reflect.ValueOf(s).Elem().FieldByName("iter")
+1 -1
View File
@@ -40,7 +40,7 @@ func TestTransform(t *testing.T) {
const maxChunkWords = 8 << 10
buf := make([]byte, 2*maxChunkWords*8)
for i := uint64(0); i < 2*maxChunkWords; i++ {
for i := range uint64(2 * maxChunkWords) {
binary.LittleEndian.PutUint64(buf[i*8:], i)
}
if err := lockedfile.Write(path, bytes.NewReader(buf[:8]), 0666); err != nil {
+1 -2
View File
@@ -58,8 +58,7 @@ func (k *outcome) finalise(
supp := make([]string, len(config.Groups))
for i, name := range config.Groups {
if gid, err := k.lookupGroupId(name); err != nil {
var unknownGroupError user.UnknownGroupError
if errors.As(err, &unknownGroupError) {
if unknownGroupError, ok := errors.AsType[user.UnknownGroupError](err); ok {
return newWithMessageError(fmt.Sprintf("unknown group %q", name), unknownGroupError)
} else {
return &hst.AppError{Step: "look up group by name", Err: err, Msg: err.Error()}
+4 -6
View File
@@ -51,18 +51,16 @@ func (h *Hsu) ID() (int, error) {
cmd.Stderr = os.Stderr // pass through fatal messages
cmd.Env = make([]string, 0)
cmd.Dir = fhs.Root
var (
p []byte
exitError *exec.ExitError
)
var p []byte
const step = "obtain uid from hsu"
if p, h.idErr = h.k.cmdOutput(cmd); h.idErr == nil {
h.id, h.idErr = strconv.Atoi(string(p))
if h.idErr != nil {
h.idErr = &hst.AppError{Step: step, Err: h.idErr, Msg: "invalid uid string from hsu"}
}
} else if errors.As(h.idErr, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
} else if exitError, ok := errors.AsType[*exec.ExitError](h.idErr); ok &&
exitError != nil &&
exitError.ExitCode() == 1 {
// hsu prints an error message in this case
h.idErr = &hst.AppError{Step: step, Err: ErrHsuAccess}
} else if errors.Is(h.idErr, os.ErrNotExist) {
+3 -3
View File
@@ -328,11 +328,11 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
}
if err := k.sys.Revert((*system.Criteria)(&ec)); err != nil {
var joinError interface {
joinError, ok := errors.AsType[interface {
Unwrap() []error
error
}
if !errors.As(err, &joinError) || joinError == nil {
}](err)
if !ok || joinError == nil {
perror(err, "revert system setup")
} else {
for _, v := range joinError.Unwrap() {
+1
View File
@@ -136,6 +136,7 @@ func TestOutcomeRun(t *testing.T) {
Tmpfs(fhs.AbsDevShm, 0, 01777).
// spRuntimeOp
Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755).
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/1971"), std.BindWritable).
+2 -2
View File
@@ -390,8 +390,8 @@ func shimEntrypoint(k syscallDispatcher) {
if err := k.containerWait(z); err != nil {
sp.destroy()
var exitError *exec.ExitError
if !errors.As(err, &exitError) {
exitError, ok := errors.AsType[*exec.ExitError](err)
if !ok {
if errors.Is(err, context.Canceled) {
k.exit(hst.ExitCancel)
}
+1
View File
@@ -71,6 +71,7 @@ func TestShimEntrypoint(t *testing.T) {
Tmpfs(fhs.AbsDevShm, 0, 01777).
// spRuntimeOp
Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755).
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/tmp/hakurei.10/runtime/9999"), m("/run/user/1000"), std.BindWritable).
+4
View File
@@ -382,6 +382,10 @@ func (p opsAdapter) Overlay(target, state, work *check.Absolute, layers ...*chec
return opsAdapter{p.Ops.Overlay(target, state, work, layers...)}
}
func (p opsAdapter) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
return opsAdapter{p.Ops.OverlayEphemeral(target, layers...)}
}
func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
return opsAdapter{p.Ops.OverlayReadonly(target, layers...)}
}
+3
View File
@@ -113,6 +113,9 @@ func (s *spRuntimeOp) toContainer(state *outcomeStateParams) error {
}
if state.Container.Flags&hst.FCoverRun != 0 {
state.params.Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755)
}
state.params.Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755)
if state.Container.Flags&hst.FShareRuntime != 0 {
_, runtimeDirInst := s.commonPaths(state.outcomeState)
+4
View File
@@ -40,6 +40,7 @@ func TestSpRuntimeOp(t *testing.T) {
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755).
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
}, paramsWantEnv(config, map[string]string{
@@ -67,6 +68,7 @@ func TestSpRuntimeOp(t *testing.T) {
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755).
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
}, paramsWantEnv(config, map[string]string{
@@ -94,6 +96,7 @@ func TestSpRuntimeOp(t *testing.T) {
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755).
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
}, paramsWantEnv(config, map[string]string{
@@ -117,6 +120,7 @@ func TestSpRuntimeOp(t *testing.T) {
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Ops: new(container.Ops).
Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755).
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
}, paramsWantEnv(config, map[string]string{
+4 -4
View File
@@ -176,8 +176,8 @@ func marshalValueAppendRaw(data []byte, v reflect.Value) ([]byte, error) {
case reflect.Struct:
data = SPA_TYPE_Struct.append(data)
var err error
for i := 0; i < v.NumField(); i++ {
data, err = marshalValueAppend(data, v.Field(i))
for _, field := range v.Fields() {
data, err = marshalValueAppend(data, field)
if err != nil {
return data, err
}
@@ -370,8 +370,8 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error {
}
var fieldWireSize Word
for i := 0; i < v.NumField(); i++ {
if err := unmarshalValue(data, v.Field(i), &fieldWireSize); err != nil {
for _, field := range v.Fields() {
if err := unmarshalValue(data, field, &fieldWireSize); err != nil {
return err
}
// bounds check completed in successful call to unmarshalValue
+405
View File
@@ -0,0 +1,405 @@
package pkg
import (
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"unsafe"
"hakurei.app/check"
)
/*
| mode uint32 | path_sz uint32 |
| data_sz uint64 |
| path string |
| data []byte |
*/
// An ArchiveHeader represents a single header in an archive.
type ArchiveHeader struct {
Mode fs.FileMode // file mode bits
Path string // pathname of the file
Size uint64 // size of data segment
}
// Writer implements sequential writing of an archive. [Writer.WriteHeader]
// begins a new file with the provided [ArchiveHeader], and then Writer can be
// treated as an [io.Writer] to supply that file's data.
//
// It is the caller's responsibility to write entries in lexical order.
type Writer struct {
// Underlying writer.
w io.Writer
// Current header.
h ArchiveHeader
// Fixed-size header segment.
buf [wordSize * 2]byte
// Current position in data segment.
n uint64
}
// NewWriter returns the address of a new [Writer] writing to w.
func NewWriter(w io.Writer) *Writer { return &Writer{w: w} }
var zero [wordSize]byte
// padSize returns the padding size for aligning sz.
func padSize[T int | uint64](sz T) T {
return (wordSize - (sz)%wordSize) % wordSize
}
// flush concludes writing to the current file and writes padding.
func (aw *Writer) flush() error {
if aw.h.Size > aw.n {
return fmt.Errorf("missed writing %d bytes", aw.h.Size-aw.n)
} else if aw.h.Size < aw.n {
return fmt.Errorf("wrote %d bytes beyond end of file", aw.n-aw.h.Size)
}
if psz := padSize(aw.h.Size); psz != 0 {
if _, err := aw.w.Write(zero[:psz]); err != nil {
return err
}
}
aw.n = 0
return nil
}
// WriteHeader writes h and begins accepting its corresponding file.
func (aw *Writer) WriteHeader(h *ArchiveHeader) error {
if err := aw.flush(); err != nil {
return err
}
aw.h = *h
binary.LittleEndian.PutUint32(aw.buf[:], uint32(aw.h.Mode))
binary.LittleEndian.PutUint32(aw.buf[wordSize/2:], uint32(len(aw.h.Path)))
binary.LittleEndian.PutUint64(aw.buf[wordSize:], aw.h.Size)
if _, err := aw.w.Write(aw.buf[:]); err != nil {
return err
} else if _, err = aw.w.Write(
unsafe.Slice(unsafe.StringData(aw.h.Path), len(aw.h.Path)),
); err != nil {
return err
} else if psz := padSize(len(aw.h.Path)); psz != 0 {
if _, err = aw.w.Write(zero[:psz]); err != nil {
return err
}
}
return nil
}
// Write writes p to the underlying writer and records the new position. Invalid
// positions are reported by WriteHeader and Close.
func (aw *Writer) Write(p []byte) (n int, err error) {
n, err = aw.w.Write(p)
aw.n += uint64(n)
return
}
// Close concludes writing to the archive stream.
func (aw *Writer) Close() (err error) {
err = aw.flush()
aw.w = nil
return
}
// ErrInsecurePath is returned by [FlatEntry.Decode] if validation is requested
// and a nonlocal path is encountered in the stream.
var ErrInsecurePath = errors.New("insecure file path")
// Reader implements sequential reading of an archive. [Reader.Next] advances to
// the next file in the archive (including the first), and then Reader can be
// treated as an [io.Reader] to access the file's data.
type Reader struct {
// Underlying reader.
r io.Reader
// Fixed-size header segment.
buf [wordSize * 2]byte
// Remaining bytes in current data segment.
n, pad uint64
}
// NewReader returns the address of a new [Reader] reading from r.
func NewReader(r io.Reader) *Reader { return &Reader{r: r} }
// Next advances ar to the next entry. Remaining bytes of the current data
// segment are discarded. Advancing beyond the final entry returns [io.EOF].
func (ar *Reader) Next() (*ArchiveHeader, error) {
if dsz := int64(ar.n + ar.pad); dsz > 0 {
if n, err := io.CopyN(io.Discard, ar.r, dsz); err != nil {
if errors.Is(err, io.EOF) && n != dsz {
err = io.ErrUnexpectedEOF
}
return nil, err
}
}
if _, err := io.ReadFull(ar.r, ar.buf[:]); err != nil {
return nil, err
}
h := ArchiveHeader{
Mode: fs.FileMode(binary.LittleEndian.Uint32(ar.buf[:])),
Size: binary.LittleEndian.Uint64(ar.buf[wordSize:]),
}
pathSize := int(binary.LittleEndian.Uint32(ar.buf[wordSize/2:]))
pPathSize := alignSize(pathSize)
buf := make([]byte, pPathSize)
if _, err := io.ReadFull(ar.r, buf); err != nil {
if errors.Is(err, io.EOF) {
err = io.ErrUnexpectedEOF
}
return nil, err
}
h.Path = unsafe.String(unsafe.SliceData(buf), pathSize)
if !filepath.IsLocal(h.Path) {
return &h, ErrInsecurePath
}
ar.n = h.Size
ar.pad = padSize(h.Size)
return &h, nil
}
// Read implements [io.Reader] for the data segment of the current entry.
func (ar *Reader) Read(p []byte) (n int, err error) {
if uint64(len(p)) > ar.n {
p = p[:ar.n]
}
if len(p) > 0 {
n, err = ar.r.Read(p)
ar.n -= uint64(n)
}
switch err {
case io.EOF:
if ar.n > 0 {
return n, io.ErrUnexpectedEOF
}
case nil:
if ar.n == 0 {
return n, io.EOF
}
}
return
}
// Write writes a deterministic representation of the contents of fsys to w.
// The resulting data can be hashed to produce a deterministic checksum for the
// directory.
func Write(fsys fs.FS, root string, w io.Writer) error {
aw := NewWriter(w)
if err := fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
var fi fs.FileInfo
fi, err = d.Info()
if err != nil {
return err
}
h := ArchiveHeader{
Path: path,
Mode: fi.Mode(),
}
if h.Mode.IsRegular() {
h.Size = uint64(fi.Size())
if err = aw.WriteHeader(&h); err != nil {
return err
}
var r fs.File
r, err = fsys.Open(path)
if err != nil {
return err
}
_, err = io.Copy(aw, r)
if _err := r.Close(); err == nil {
err = _err
}
return err
} else if h.Mode&fs.ModeSymlink != 0 {
var newpath string
if newpath, err = fs.ReadLink(fsys, path); err != nil {
return err
}
h.Size = uint64(len(newpath))
if err = aw.WriteHeader(&h); err != nil {
return err
}
_, err = aw.Write(unsafe.Slice(unsafe.StringData(newpath), len(newpath)))
return err
} else if !h.Mode.IsDir() {
return InvalidFileModeError(h.Mode)
}
return aw.WriteHeader(&h)
}); err != nil {
return err
}
return aw.Close()
}
// SumFS saves checksum of the archive of fsys to the value pointed to by buf.
func SumFS(buf *Checksum, fsys fs.FS, root string) error {
h := sha512.New384()
if err := Write(fsys, root, h); err != nil {
return err
}
h.Sum(buf[:0])
return nil
}
// SumDir saves checksum of the archive of directory at pathname to the value
// pointed to by buf.
func SumDir(buf *Checksum, pathname *check.Absolute) error {
return SumFS(buf, os.DirFS(pathname.String()), ".")
}
// archiveArtifact is an [Artifact] unpacking an archive supported by [Reader]
// backed by a [FileArtifact].
type archiveArtifact struct {
// Caller-supplied backing archive.
f Artifact
}
// NewArchive returns a new [Artifact] backed by the supplied [Artifact]. The
// source [Artifact] must be a [FileArtifact] and produce a stream compatible
// with [Reader].
func NewArchive(a Artifact) Artifact {
return archiveArtifact{a}
}
// Kind returns the hardcoded [Kind] constant.
func (archiveArtifact) Kind() Kind { return KindArchive }
// Params is a noop.
func (archiveArtifact) Params(*IContext) {}
func init() {
register(KindArchive, func(r *IRReader) Artifact {
a := NewArchive(r.Next())
if _, ok := r.Finalise(); ok {
panic(ErrUnexpectedChecksum)
}
return a
})
}
// Dependencies returns a slice containing the backing file.
func (a archiveArtifact) Dependencies() []Artifact {
return []Artifact{a.f}
}
// IsExclusive returns false: [Reader] is fully sequential.
func (archiveArtifact) IsExclusive() bool { return false }
// Cure cures the [Artifact], producing a directory located at work.
func (a archiveArtifact) Cure(t *TContext) (err error) {
var r io.ReadCloser
if r, err = t.Open(a.f); err != nil {
return
}
defer func() {
closeErr := r.Close()
if err == nil {
err = closeErr
}
}()
type dirTargetPerm struct {
path string
mode fs.FileMode
}
var madeDirectories []dirTargetPerm
if err = os.MkdirAll(t.GetWorkDir().String(), 0700); err != nil {
return
}
var root *os.Root
if root, err = os.OpenRoot(t.GetWorkDir().String()); err != nil {
return
}
defer func() {
closeErr := root.Close()
if err == nil {
err = closeErr
}
}()
var header *ArchiveHeader
ar := NewReader(r)
for header, err = ar.Next(); err == nil; header, err = ar.Next() {
if header.Mode.IsRegular() {
var f *os.File
if f, err = root.OpenFile(
header.Path,
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
header.Mode.Perm(),
); err != nil {
return
}
if _, err = io.Copy(f, ar); err != nil {
_ = f.Close()
return
} else if err = f.Close(); err != nil {
return
}
} else if header.Mode&fs.ModeSymlink != 0 {
var p []byte
if p, err = io.ReadAll(ar); err != nil {
return
}
if err = root.Symlink(
unsafe.String(unsafe.SliceData(p), len(p)),
header.Path,
); err != nil {
return
}
} else if header.Mode.IsDir() {
if header.Path == "." {
continue
}
madeDirectories = append(madeDirectories, dirTargetPerm{
path: header.Path,
mode: header.Mode,
})
if err = root.Mkdir(header.Path, 0700); err != nil {
return
}
} else {
return InvalidFileModeError(header.Mode)
}
}
if errors.Is(err, io.EOF) {
err = nil
}
if err == nil {
for _, e := range madeDirectories {
if err = root.Chmod(e.path, e.mode.Perm()); err != nil {
return
}
}
} else {
return
}
return
}
+240
View File
@@ -0,0 +1,240 @@
package pkg_test
import (
"bytes"
"io"
"io/fs"
"maps"
"reflect"
"testing"
"testing/fstest"
"unsafe"
"hakurei.app/check"
"hakurei.app/internal/pkg"
)
func TestArchive(t *testing.T) {
t.Parallel()
type entry struct {
path string
mode fs.FileMode
data string
}
testCases := []struct {
name string
fsys fs.FS
entries []entry
sum pkg.Checksum
err error
}{
{"bad type", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"invalid": {Mode: fs.ModeCharDevice | 0400},
}, nil, pkg.Checksum{}, pkg.InvalidFileModeError(
fs.ModeCharDevice | 0400,
)},
{"coldboot", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"devices": {Mode: fs.ModeDir | 0700},
"devices/uevent": {Mode: 0600, Data: []byte("add")},
"devices/empty": {Mode: fs.ModeDir | 0700},
"devices/sub": {Mode: fs.ModeDir | 0700},
"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"block": {Mode: fs.ModeDir | 0700},
"block/uevent": {Mode: 0600},
}, []entry{
{".", fs.ModeDir | 0700, ""},
{"block", fs.ModeDir | 0700, ""},
{"block/uevent", 0600, ""},
{"devices", fs.ModeDir | 0700, ""},
{"devices/empty", fs.ModeDir | 0700, ""},
{"devices/sub", fs.ModeDir | 0700, ""},
{"devices/sub/uevent", 0600, "add"},
{"devices/uevent", 0600, "add"},
}, pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8"), nil},
{"empty", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"identifier": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []entry{
{".", fs.ModeDir | 0700, ""},
{"checksum", fs.ModeDir | 0700, ""},
{"identifier", fs.ModeDir | 0700, ""},
{"work", fs.ModeDir | 0700, ""},
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C"), nil},
{"sample directory step garbage", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500},
"lib": {Mode: fs.ModeDir | 0500},
"lib/check": {Mode: 0400},
"lib/pkgconfig": {Mode: fs.ModeDir | 0500},
}, []entry{
{".", fs.ModeDir | 0500, ""},
{"lib", fs.ModeDir | 0500, ""},
{"lib/check", 0400, ""},
{"lib/pkgconfig", fs.ModeDir | 0500, ""},
}, pkg.MustDecode("CUx-3hSbTWPsbMfDhgalG4Ni_GmR9TnVX8F99tY_P5GtkYvczg9RrF5zO0jX9XYT"), nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("roundtrip", func(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
if err := pkg.Write(
tc.fsys,
".",
&buf,
); !reflect.DeepEqual(err, tc.err) {
t.Fatalf("Flatten: error = %v, want %v", err, tc.err)
} else if tc.err != nil {
return
}
r := pkg.NewReader(bytes.NewReader(buf.Bytes()))
var got []entry
for {
h, err := r.Next()
if err != nil {
if err == io.EOF {
break
}
t.Fatalf("Next: error = %v", err)
}
var data []byte
if data, err = io.ReadAll(r); err != nil {
t.Fatalf("Read: error = %v", err)
}
got = append(got, entry{
path: h.Path,
mode: h.Mode,
data: unsafe.String(unsafe.SliceData(data), len(data)),
})
}
if !reflect.DeepEqual(got, tc.entries) {
t.Fatalf("Reader: %#v, want %#v", got, tc.entries)
}
})
if tc.err != nil {
return
}
t.Run("hash", func(t *testing.T) {
t.Parallel()
var got pkg.Checksum
if err := pkg.SumFS(&got, tc.fsys, "."); err != nil {
t.Fatalf("SumFS: error = %v", err)
} else if got != tc.sum {
t.Fatalf("SumFS: %v", &pkg.ChecksumMismatchError{
Got: got,
Want: tc.sum,
})
}
})
})
}
}
var archiveTestdata = fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"devices": {Mode: fs.ModeDir | 0700},
"devices/uevent": {Mode: 0600, Data: []byte("add")},
"devices/empty": {Mode: fs.ModeDir | 0700},
"devices/sub": {Mode: fs.ModeDir | 0700},
"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"block": {Mode: fs.ModeDir | 0700},
"block/uevent": {Mode: 0600},
}
func TestArchiveArtifact(t *testing.T) {
t.Parallel()
want := maps.Clone(archiveTestdata)
want["."].Mode = fs.ModeDir | 0500
checkWithCache(t, []cacheTestCase{
{"unpack", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
var buf bytes.Buffer
if err := pkg.Write(archiveTestdata, ".", &buf); err != nil {
t.Fatal(err)
}
cureMany(t, c, []cureStep{
{"sample", pkg.NewArchive(
pkg.NewFile("", buf.Bytes()),
), ignorePathname, expectsFS(want), nil},
})
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F": {Mode: fs.ModeDir | 0500},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/block": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/block/uevent": {Mode: 0600},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/empty": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/sub": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/uevent": {Mode: 0600, Data: []byte("add")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/3oYyAbRJ_we7AgWo1BRcRcnxXFk3mAQ0Qui2nGQMi8GIJNJQtvUC6P2IeoA5mbjD": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F")},
"substitute": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
})
}
func BenchmarkArchiveRead(b *testing.B) {
var buf bytes.Buffer
if err := pkg.Write(archiveTestdata, ".", &buf); err != nil {
b.Fatal(err)
}
testdata := buf.Bytes()
for b.Loop() {
r := pkg.NewReader(bytes.NewReader(testdata))
for {
_, err := r.Next()
if err != nil {
if err == io.EOF {
break
}
b.Fatal(err)
}
}
}
}
func BenchmarkArchiveWrite(b *testing.B) {
for b.Loop() {
if err := pkg.Write(archiveTestdata, ".", io.Discard); err != nil {
b.Fatal(err)
}
}
}
+152
View File
@@ -0,0 +1,152 @@
package pkg
import (
"errors"
"os"
"unique"
)
// Clean destroys checksum backing entries without any identifier or substitute
// entry referring to it. If at least one keep [Artifact] is specified,
// identifier and substitute entries not kept alive by them are destroyed first.
func (c *Cache) Clean(dry, inputs bool, keep ...Artifact) (
[]unique.Handle[ID],
[]unique.Handle[Checksum],
error,
) {
c.identMu.Lock()
defer c.identMu.Unlock()
c.checksumMu.Lock()
defer c.checksumMu.Unlock()
dents, err := os.ReadDir(c.base.Append(dirChecksum).String())
if err != nil {
return nil, nil, err
}
checksums := make(map[unique.Handle[Checksum]]string, len(dents))
var buf Checksum
for _, dent := range dents {
name := dent.Name()
if err = Decode(&buf, name); err != nil {
return nil, nil, err
}
checksums[unique.Make(buf)] = name
}
type identPair struct {
id unique.Handle[ID]
name string
}
dents, err = os.ReadDir(c.base.Append(dirIdentifier).String())
if err != nil {
return nil, nil, err
}
keepIdents := make(map[unique.Handle[ID]]struct{})
if inputs {
for _, id := range Inputs((*Collect)(&keep)) {
keepIdents[id] = struct{}{}
}
} else {
for _, a := range keep {
keepIdents[c.Ident(a)] = struct{}{}
}
}
idents := make([]identPair, 0, len(dents))
for _, dent := range dents {
name := dent.Name()
if err = Decode(&buf, name); err != nil {
return nil, nil, err
}
id := unique.Make(ID(buf))
if _, ok := keepIdents[id]; len(keep) == 0 || ok {
if err = readlinkChecksum(c.base.Append(
dirIdentifier,
name,
), &buf); err != nil {
return nil, nil, err
}
delete(checksums, unique.Make(buf))
continue
}
c.msg.Verbosef("arranging for destruction of %s...", name)
idents = append(idents, identPair{id, name})
}
destroyedIdents := make([]unique.Handle[ID], 0, len(idents))
for _, pair := range idents {
if !dry {
if err = os.Remove(c.base.Append(
dirStatus,
pair.name,
).String()); err != nil && !errors.Is(err, os.ErrNotExist) {
return destroyedIdents, nil, err
}
if err = os.Remove(c.base.Append(
dirIdentifier,
pair.name,
).String()); err != nil {
return destroyedIdents, nil, err
}
}
destroyedIdents = append(destroyedIdents, pair.id)
}
destroyedChecksums := make([]unique.Handle[Checksum], 0, len(checksums))
for checksum, name := range checksums {
if err = c.parent.Err(); err != nil {
return destroyedIdents, destroyedChecksums, err
}
c.msg.Verbosef("destroying checksum %s...", name)
if !dry {
if err = errors.Join(removeAll(c.base.Append(
dirChecksum,
name,
))); err != nil {
return destroyedIdents, destroyedChecksums, err
}
}
destroyedChecksums = append(destroyedChecksums, checksum)
}
dents, err = os.ReadDir(c.base.Append(dirSubstitute).String())
if err != nil {
return destroyedIdents, destroyedChecksums, err
}
for _, dent := range dents {
name := dent.Name()
if err = readlinkChecksum(c.base.Append(
dirSubstitute,
name,
), &buf); err != nil {
return destroyedIdents, destroyedChecksums, err
}
if _, ok := checksums[unique.Make(buf)]; !ok {
continue
}
c.msg.Verbosef("destroying substitute %s...", name)
if !dry {
if err = os.Remove(c.base.Append(
dirStatus,
name,
).String()); err != nil && !errors.Is(err, os.ErrNotExist) {
return destroyedIdents, nil, err
}
if err = os.Remove(c.base.Append(
dirSubstitute,
name,
).String()); err != nil {
return destroyedIdents, destroyedChecksums, err
}
}
}
return destroyedIdents, destroyedChecksums, nil
}
+293
View File
@@ -0,0 +1,293 @@
package pkg_test
import (
"bytes"
"crypto/sha512"
"io/fs"
"log"
"os"
"slices"
"strings"
"testing"
"unique"
"hakurei.app/internal/pkg"
"hakurei.app/message"
)
// formatHandles returns a user-facing string representing h.
func formatHandles[T pkg.ID | pkg.Checksum](handles ...unique.Handle[T]) string {
var buf strings.Builder
for _, h := range handles {
buf.WriteString(pkg.Encode(pkg.Checksum(h.Value())))
buf.WriteString(", ")
}
return strings.TrimSuffix(buf.String(), ", ")
}
func TestClean(t *testing.T) {
t.Parallel()
ic := pkg.NewIR()
testCases := []struct {
name string
a []pkg.Artifact
keep []pkg.Artifact
inputs bool
want expectsFS
wantIdents []unique.Handle[pkg.ID]
wantChecksums []unique.Handle[pkg.Checksum]
}{
{"simple", []pkg.Artifact{
pkg.NewFile("file", nil),
}, nil, false, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/pPRjw2XYgjB5k8dYedwxTBMgHh4_v2JM_G2Vd-skQbAGOOgPsl3CGSUbEF7om_MO": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
"lock": {Mode: 0644},
"variant": {Mode: 0400},
"status": {Mode: fs.ModeDir | 0700},
"substitute": {Mode: fs.ModeDir | 0700},
"fault": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, nil, nil},
{"keep", []pkg.Artifact{
pkg.NewFile("removed-file", []byte("removed file")),
}, []pkg.Artifact{
pkg.NewFile("file", []byte("\xfd")),
}, false, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/KgZ-FjbGuU-XP2QEHInpgv-2Zn0cTH5NqFMgTU0XrSdKmSwyC-3baVs1BMCP5spk": {Mode: 0400, Data: []byte("\xfd")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/FMwSBYw22KqM8jZryfY2ChHXpLuVDdWYyNOYdHvIVYk8ujY6UnGRm5brr2sTTfpD": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/KgZ-FjbGuU-XP2QEHInpgv-2Zn0cTH5NqFMgTU0XrSdKmSwyC-3baVs1BMCP5spk")},
"lock": {Mode: 0644},
"variant": {Mode: 0400},
"status": {Mode: fs.ModeDir | 0700},
"substitute": {Mode: fs.ModeDir | 0700},
"fault": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []unique.Handle[pkg.ID]{
ic.Ident(pkg.NewFile("removed-file", []byte("removed file"))),
}, []unique.Handle[pkg.Checksum]{
unique.Make(sha512.Sum384([]byte("removed file"))),
}},
{"inputs anchored substitute", []pkg.Artifact{
&stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
cure: func(f *pkg.FContext) error {
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), nil, 0444)
},
},
}, []pkg.Artifact{
&stubArtifactF{
kind: pkg.KindExec,
params: []byte("kept"),
deps: []pkg.Artifact{
pkg.NewFile("kept-input", []byte("kept")),
},
cure: func(f *pkg.FContext) error {
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), nil, 0444)
},
},
}, true, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv": {Mode: 0400, Data: []byte("kept")},
"checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE": {Mode: fs.ModeDir | 0500},
"checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE/result": {Mode: 0444},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/Ef8KX6s_rS_nLgze0rj90zKyrGAvOnzyU0DL7nrYQWEG_f4a9pmUKI6HBEMD8AE8": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv")},
"identifier/xoIGLemzLF227e-w_AJcf_1Sgqh2gs3KFgqvOIWUQE-9P_y2vHBMBytL4GRGQqTb": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"lock": {Mode: 0644},
"variant": {Mode: 0400},
"status": {Mode: fs.ModeDir | 0700},
"substitute": {Mode: fs.ModeDir | 0700},
"substitute/4bjS-QjGcSV4nth-W6Vg3-wolKmKgiq4Ld2oRIWcOfy6Wi41XXLAWPoo8FcDx6BH": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"substitute/dzO8FEY9lu4hwRT6BfRZOX-uYGsC_5XH4jEJ7sJyThcmG9J_w1ArOAaUCGfL8wAM": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"fault": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []unique.Handle[pkg.ID]{
ic.Ident(pkg.NewFile("destroyed-input", []byte("destroyed"))),
ic.Ident(&stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
}),
}, []unique.Handle[pkg.Checksum]{
unique.Make(sha512.Sum384([]byte("destroyed"))),
}},
{"inputs", []pkg.Artifact{
&stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
cure: func(f *pkg.FContext) error {
if w, err := f.GetStatusWriter(); err != nil {
return err
} else if _, err = w.Write([]byte("destroyed")); err != nil {
return err
}
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), nil, 0444)
},
},
}, []pkg.Artifact{
&stubArtifactF{
kind: pkg.KindExec,
params: []byte("kept"),
deps: []pkg.Artifact{
pkg.NewFile("kept-input", []byte("kept")),
},
cure: func(f *pkg.FContext) error {
if w, err := f.GetStatusWriter(); err != nil {
return err
} else if _, err = w.Write([]byte("kept")); err != nil {
return err
}
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), []byte{0}, 0444)
},
},
}, true, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W": {Mode: fs.ModeDir | 0500},
"checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W/result": {Mode: 0444, Data: []byte("\x00")},
"checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv": {Mode: 0400, Data: []byte("kept")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/Ef8KX6s_rS_nLgze0rj90zKyrGAvOnzyU0DL7nrYQWEG_f4a9pmUKI6HBEMD8AE8": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv")},
"identifier/xoIGLemzLF227e-w_AJcf_1Sgqh2gs3KFgqvOIWUQE-9P_y2vHBMBytL4GRGQqTb": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W")},
"lock": {Mode: 0644},
"variant": {Mode: 0400},
"status": {Mode: fs.ModeDir | 0700},
"status/4bjS-QjGcSV4nth-W6Vg3-wolKmKgiq4Ld2oRIWcOfy6Wi41XXLAWPoo8FcDx6BH": {Mode: 0400, Data: []byte(statusHeader + "kept")},
"status/xoIGLemzLF227e-w_AJcf_1Sgqh2gs3KFgqvOIWUQE-9P_y2vHBMBytL4GRGQqTb": {Mode: 0400, Data: []byte(statusHeader + "kept")},
"substitute": {Mode: fs.ModeDir | 0700},
"substitute/4bjS-QjGcSV4nth-W6Vg3-wolKmKgiq4Ld2oRIWcOfy6Wi41XXLAWPoo8FcDx6BH": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W")},
"fault": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []unique.Handle[pkg.ID]{
ic.Ident(pkg.NewFile("destroyed-input", []byte("destroyed"))),
ic.Ident(&stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
}),
}, []unique.Handle[pkg.Checksum]{
unique.Make(expectsFS{
".": {Mode: fs.ModeDir | 0500},
"result": {Mode: 0444},
}.hash()),
unique.Make(sha512.Sum384([]byte("destroyed"))),
}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
base := makeBase(t)
msg := message.New(log.New(os.Stderr, "clean: ", 0))
msg.SwapVerbose(testing.Verbose())
c, err := pkg.Open(t.Context(), msg, 0, 0, 0, base)
if err != nil {
t.Fatal(err)
}
t.Cleanup(c.Close)
all := pkg.Collect(slices.Concat(tc.a, tc.keep))
if _, _, err = c.Cure(&all); !pkg.IsCollected(err) {
t.Fatal(err)
}
var (
idents []unique.Handle[pkg.ID]
checksums []unique.Handle[pkg.Checksum]
)
idents, checksums, err = c.Clean(false, tc.inputs, tc.keep...)
if err != nil {
t.Fatalf("Clean: error = %v", err)
}
var buf [2]pkg.Checksum
slices.SortFunc(idents, func(a, b unique.Handle[pkg.ID]) int {
buf[0], buf[1] = a.Value(), b.Value()
return bytes.Compare(buf[0][:], buf[1][:])
})
slices.SortFunc(checksums, func(a, b unique.Handle[pkg.Checksum]) int {
buf[0], buf[1] = a.Value(), b.Value()
return bytes.Compare(buf[0][:], buf[1][:])
})
if !slices.Equal(idents, tc.wantIdents) {
t.Errorf(
"Clean: idents = %s, want %s",
formatHandles(idents...), formatHandles(tc.wantIdents...),
)
}
if !slices.Equal(checksums, tc.wantChecksums) {
t.Errorf(
"Clean: checksums = %s, want %s",
formatHandles(checksums...), formatHandles(tc.wantChecksums...),
)
}
want := tc.want.hash()
var checksum pkg.Checksum
if err = pkg.SumDir(&checksum, base); err != nil {
t.Fatalf("SumDir: error = %v", err)
} else if checksum != want {
t.Error(expectsFrom(base.String()))
}
})
}
}
+119
View File
@@ -0,0 +1,119 @@
package pkg
import (
"compress/bzip2"
"compress/gzip"
"fmt"
"io"
"os"
)
const (
// Gzip denotes a stream compressed via [gzip].
Gzip = iota
// Bzip2 denotes a stream compressed via [bzip2].
Bzip2
)
// A decompressArtifact is a [FileArtifact] decompressing a backing
// [FileArtifact] stream.
type decompressArtifact struct {
// Caller-supplied backing stream.
f Artifact
// Compression on top of the stream.
compress uint32
}
var _ FileArtifact = new(decompressArtifact)
// decompressArtifactNamed embeds decompressArtifact for a [fmt.Stringer] stream.
type decompressArtifactNamed struct {
decompressArtifact
// Copied from decompressArtifact.f.
name string
}
var _ fmt.Stringer = new(decompressArtifactNamed)
// NewDecompress returns a [FileArtifact] decompressing the supplied [Artifact].
func NewDecompress(a Artifact, compress uint32) FileArtifact {
da := decompressArtifact{a, compress}
if s, ok := a.(fmt.Stringer); ok {
if name := s.String(); name != "" {
return &decompressArtifactNamed{da, name}
}
}
return &da
}
// String returns the name of the underlying [Artifact] prefixed with decompress.
func (a *decompressArtifactNamed) String() string { return "decompress-" + a.name }
// Kind returns the hardcoded [Kind] constant.
func (a *decompressArtifact) Kind() Kind { return KindDecompress }
// Params writes value of compression enum.
func (a *decompressArtifact) Params(ctx *IContext) { ctx.WriteUint32(a.compress) }
func init() {
register(KindDecompress, func(r *IRReader) Artifact {
a := NewDecompress(r.Next(), r.ReadUint32())
if _, ok := r.Finalise(); ok {
panic(ErrUnexpectedChecksum)
}
return a
})
}
// Dependencies returns a slice containing the backing file.
func (a *decompressArtifact) Dependencies() []Artifact {
return []Artifact{a.f}
}
// IsExclusive returns false: decompressor is fully sequential.
func (a *decompressArtifact) IsExclusive() bool { return false }
// compoundCloser is an [io.ReadCloser] with an additional [io.Closer] attached.
type compoundCloser struct {
io.ReadCloser
c io.Closer
}
// Close closes [io.ReadCloser] and the additional [io.Closer]. It returns the
// non-nil error returned by the underlying [io.ReadCloser], otherwise it
// returns the error returned by the additional [io.Closer].
func (c compoundCloser) Close() error {
err := c.ReadCloser.Close()
if _err := c.c.Close(); err == nil {
err = _err
}
return err
}
// Cure returns a decompressor [io.ReadCloser].
func (a *decompressArtifact) Cure(r *RContext) (io.ReadCloser, error) {
sr, err := r.Open(a.f)
if err != nil {
return nil, err
}
br := r.cache.getReaderRC(sr)
var dr io.ReadCloser
switch a.compress {
case Gzip:
if dr, err = gzip.NewReader(br); err != nil {
_ = br.Close()
return nil, err
}
return compoundCloser{dr, br}, nil
case Bzip2:
return struct {
io.Reader
io.Closer
}{bzip2.NewReader(br), br}, nil
default:
return nil, os.ErrInvalid
}
}
+70
View File
@@ -0,0 +1,70 @@
package pkg_test
import (
"bytes"
"compress/gzip"
"crypto/sha512"
"io/fs"
"net/http"
"testing"
"testing/fstest"
"hakurei.app/check"
"hakurei.app/internal/pkg"
)
func TestDecompress(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
if _, err := gw.Write([]byte{0}); err != nil {
t.Fatal(err)
} else if err = gw.Close(); err != nil {
t.Fatal(err)
}
testdata := buf.String()
var transport http.Transport
client := http.Client{Transport: &transport}
transport.RegisterProtocol("file", http.NewFileTransportFS(fstest.MapFS{
"testdata": {Data: []byte(testdata), Mode: 0400},
}))
testdataChecksum := func() pkg.Checksum {
h := sha512.New384()
h.Write([]byte(testdata))
return (pkg.Checksum)(h.Sum(nil))
}()
checkWithCache(t, []cacheTestCase{
{"decompress", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
cureMany(t, c, []cureStep{
{"close", pkg.NewDecompress(pkg.NewHTTPGet(
&client,
"file:///testdata",
pkg.Checksum{0xfd},
), pkg.Gzip), nil, nil, &pkg.ChecksumMismatchError{
Got: testdataChecksum,
Want: pkg.Checksum{0xfd},
}},
{"gzip", pkg.NewDecompress(pkg.NewHTTPGet(
&client,
"file:///testdata",
testdataChecksum,
), pkg.Gzip), ignorePathname, expectsChecksum(sha512.Sum384([]byte{0})), nil},
})
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/" + pkg.Encode(sha512.Sum384([]byte{0})): {Mode: 0400, Data: []byte{0}},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/QpjkahDrz7pz-tv0eAGNXR6x9NAtTjWCK5Hr7G1cIZj9rT7bLYJWUQeLD4wamAlF": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX")},
"substitute": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
})
}
-203
View File
@@ -1,203 +0,0 @@
package pkg
import (
"crypto/sha512"
"encoding/binary"
"errors"
"io"
"io/fs"
"math"
"os"
"path/filepath"
"syscall"
"hakurei.app/check"
)
// FlatEntry is a directory entry to be encoded for [Flatten].
type FlatEntry struct {
Mode fs.FileMode // file mode bits
Path string // pathname of the file
Data []byte // file content or symlink destination
}
/*
| mode uint32 | path_sz uint32 |
| data_sz uint64 |
| path string |
| data []byte |
*/
// Encode encodes the entry for transmission or hashing.
func (ent *FlatEntry) Encode(w io.Writer) (n int, err error) {
pPathSize := alignSize(len(ent.Path))
if pPathSize > math.MaxUint32 {
return 0, syscall.E2BIG
}
pDataSize := alignSize(len(ent.Data))
payload := make([]byte, wordSize*2+pPathSize+pDataSize)
binary.LittleEndian.PutUint32(payload, uint32(ent.Mode))
binary.LittleEndian.PutUint32(payload[wordSize/2:], uint32(len(ent.Path)))
binary.LittleEndian.PutUint64(payload[wordSize:], uint64(len(ent.Data)))
copy(payload[wordSize*2:], ent.Path)
copy(payload[wordSize*2+pPathSize:], ent.Data)
return w.Write(payload)
}
// ErrInsecurePath is returned by [FlatEntry.Decode] if validation is requested
// and a nonlocal path is encountered in the stream.
var ErrInsecurePath = errors.New("insecure file path")
// Decode decodes the entry from its representation produced by Encode.
func (ent *FlatEntry) Decode(r io.Reader, validate bool) (n int, err error) {
var nr int
header := make([]byte, wordSize*2)
nr, err = r.Read(header)
n += nr
if err != nil {
if errors.Is(err, io.EOF) && n != 0 {
err = io.ErrUnexpectedEOF
}
return
}
ent.Mode = fs.FileMode(binary.LittleEndian.Uint32(header))
pathSize := int(binary.LittleEndian.Uint32(header[wordSize/2:]))
pPathSize := alignSize(pathSize)
dataSize := int(binary.LittleEndian.Uint64(header[wordSize:]))
pDataSize := alignSize(dataSize)
buf := make([]byte, pPathSize+pDataSize)
nr, err = r.Read(buf)
n += nr
if err != nil {
if errors.Is(err, io.EOF) {
if nr != len(buf) {
err = io.ErrUnexpectedEOF
return
}
} else {
return
}
}
ent.Path = string(buf[:pathSize])
if ent.Mode.IsDir() {
ent.Data = nil
} else {
ent.Data = buf[pPathSize : pPathSize+dataSize]
}
if validate && !filepath.IsLocal(ent.Path) {
err = ErrInsecurePath
}
return
}
// DirScanner provides an efficient interface for reading a stream of encoded
// [FlatEntry]. Successive calls to the Scan method will step through the
// entries in the stream.
type DirScanner struct {
// Underlying reader to scan [FlatEntry] representations from.
r io.Reader
// First non-EOF I/O error, returned by the Err method.
err error
// Entry to store results in. Its address is returned by the Entry method
// and is updated on every call to Scan.
ent FlatEntry
// Validate pathnames during decoding.
validate bool
}
// NewDirScanner returns the address of a new instance of [DirScanner] reading
// from r. The caller must no longer read from r after this function returns.
func NewDirScanner(r io.Reader, validate bool) *DirScanner {
return &DirScanner{r: r, validate: validate}
}
// Err returns the first non-EOF I/O error.
func (s *DirScanner) Err() error {
if errors.Is(s.err, io.EOF) {
return nil
}
return s.err
}
// Entry returns the address to the [FlatEntry] value storing the last result.
func (s *DirScanner) Entry() *FlatEntry { return &s.ent }
// Scan advances to the next [FlatEntry].
func (s *DirScanner) Scan() bool {
if s.err != nil {
return false
}
var n int
n, s.err = s.ent.Decode(s.r, s.validate)
if errors.Is(s.err, io.EOF) {
return n != 0
}
return s.err == nil
}
// Flatten writes a deterministic representation of the contents of fsys to w.
// The resulting data can be hashed to produce a deterministic checksum for the
// directory.
func Flatten(fsys fs.FS, root string, w io.Writer) (n int, err error) {
var nr int
err = fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
var fi fs.FileInfo
fi, err = d.Info()
if err != nil {
return err
}
ent := FlatEntry{
Path: path,
Mode: fi.Mode(),
}
if ent.Mode.IsRegular() {
if ent.Data, err = fs.ReadFile(fsys, path); err != nil {
return err
}
} else if ent.Mode&fs.ModeSymlink != 0 {
var newpath string
if newpath, err = fs.ReadLink(fsys, path); err != nil {
return err
}
ent.Data = []byte(newpath)
} else if !ent.Mode.IsDir() {
return InvalidFileModeError(ent.Mode)
}
nr, err = ent.Encode(w)
n += nr
return err
})
return
}
// HashFS returns a checksum produced by hashing the result of [Flatten].
func HashFS(buf *Checksum, fsys fs.FS, root string) error {
h := sha512.New384()
if _, err := Flatten(fsys, root, h); err != nil {
return err
}
h.Sum(buf[:0])
return nil
}
// HashDir returns a checksum produced by hashing the result of [Flatten].
func HashDir(buf *Checksum, pathname *check.Absolute) error {
return HashFS(buf, os.DirFS(pathname.String()), ".")
}
-134
View File
@@ -1,134 +0,0 @@
package pkg_test
import (
"bytes"
"io/fs"
"reflect"
"testing"
"testing/fstest"
"hakurei.app/internal/pkg"
)
func TestFlatten(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
fsys fs.FS
entries []pkg.FlatEntry
sum pkg.Checksum
err error
}{
{"bad type", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"invalid": {Mode: fs.ModeCharDevice | 0400},
}, nil, pkg.Checksum{}, pkg.InvalidFileModeError(
fs.ModeCharDevice | 0400,
)},
{"coldboot", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"devices": {Mode: fs.ModeDir | 0700},
"devices/uevent": {Mode: 0600, Data: []byte("add")},
"devices/empty": {Mode: fs.ModeDir | 0700},
"devices/sub": {Mode: fs.ModeDir | 0700},
"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"block": {Mode: fs.ModeDir | 0700},
"block/uevent": {Mode: 0600, Data: []byte{}},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "block"},
{Mode: 0600, Path: "block/uevent", Data: []byte{}},
{Mode: fs.ModeDir | 0700, Path: "devices"},
{Mode: fs.ModeDir | 0700, Path: "devices/empty"},
{Mode: fs.ModeDir | 0700, Path: "devices/sub"},
{Mode: 0600, Path: "devices/sub/uevent", Data: []byte("add")},
{Mode: 0600, Path: "devices/uevent", Data: []byte("add")},
}, pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8"), nil},
{"empty", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"identifier": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C"), nil},
{"sample directory step garbage", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500},
"lib": {Mode: fs.ModeDir | 0500},
"lib/check": {Mode: 0400, Data: []byte{}},
"lib/pkgconfig": {Mode: fs.ModeDir | 0500},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0500, Path: "."},
{Mode: fs.ModeDir | 0500, Path: "lib"},
{Mode: 0400, Path: "lib/check", Data: []byte{}},
{Mode: fs.ModeDir | 0500, Path: "lib/pkgconfig"},
}, pkg.MustDecode("CUx-3hSbTWPsbMfDhgalG4Ni_GmR9TnVX8F99tY_P5GtkYvczg9RrF5zO0jX9XYT"), nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("roundtrip", func(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
if _, err := pkg.Flatten(
tc.fsys,
".",
&buf,
); !reflect.DeepEqual(err, tc.err) {
t.Fatalf("Flatten: error = %v, want %v", err, tc.err)
} else if tc.err != nil {
return
}
s := pkg.NewDirScanner(bytes.NewReader(buf.Bytes()), true)
var got []pkg.FlatEntry
for s.Scan() {
got = append(got, *s.Entry())
}
if err := s.Err(); err != nil {
t.Fatalf("Err: error = %v", err)
}
if !reflect.DeepEqual(got, tc.entries) {
t.Fatalf("Scan: %#v, want %#v", got, tc.entries)
}
})
if tc.err != nil {
return
}
t.Run("hash", func(t *testing.T) {
t.Parallel()
var got pkg.Checksum
if err := pkg.HashFS(&got, tc.fsys, "."); err != nil {
t.Fatalf("HashFS: error = %v", err)
} else if got != tc.sum {
t.Fatalf("HashFS: %v", &pkg.ChecksumMismatchError{
Got: got,
Want: tc.sum,
})
}
})
})
}
}
+14
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"maps"
"os"
"os/exec"
"path/filepath"
@@ -122,6 +123,15 @@ func RegisterArch(arch string, e container.BinfmtEntry) {
binfmt[arch] = e
}
// Arch returns a snapshot of currently registered [KindExec] and [KindExecNet]
// binfmt entries.
func Arch() map[string]container.BinfmtEntry {
binfmtMu.RLock()
r := maps.Clone(binfmt)
binfmtMu.RUnlock()
return r
}
const (
// ExecTimeoutDefault replaces out of range [NewExec] timeout values.
ExecTimeoutDefault = 15 * time.Minute
@@ -581,6 +591,7 @@ var (
func (c *Cache) EnterExec(
ctx context.Context,
a Artifact,
hostname string,
retainSession bool,
stdin io.Reader,
stdout, stderr io.Writer,
@@ -661,6 +672,9 @@ func (c *Cache) EnterExec(
z.Env = append(z.Env, "TERM="+s)
}
}
if hostname != "" {
z.Hostname = hostname
}
if err = z.Start(); err != nil {
return
+14 -1
View File
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"io"
"iter"
"slices"
"strconv"
"sync"
@@ -20,7 +21,7 @@ import (
const wordSize = 8
// alignSize returns the padded size for aligning sz.
func alignSize(sz int) int {
func alignSize[T int | uint64](sz T) T {
return sz + (wordSize-(sz)%wordSize)%wordSize
}
@@ -65,6 +66,18 @@ func NewIR() *IRCache {
return &IRCache{zeroIRCache()}
}
// Inputs returns an iterator over direct and transitive inputs of an [Artifact]
// in randomised order.
func Inputs(a Artifact) iter.Seq2[Artifact, unique.Handle[ID]] {
ic := NewIR()
ic.Ident(a)
return func(yield func(Artifact, unique.Handle[ID]) bool) {
ic.artifact.Range(func(key, value any) bool {
return yield(key.(Artifact), value.(unique.Handle[ID]))
})
}
}
// IContext is passed to [Artifact.Params] and provides methods for writing
// values to the IR writer. It does not expose the underlying [io.Writer].
//
+17 -21
View File
@@ -27,16 +27,14 @@ func TestIRRoundtrip(t *testing.T) {
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
)},
{"http get tar", pkg.NewHTTPGetTar(
{"http get tar", pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet(
nil, "file:///testdata",
pkg.Checksum(bytes.Repeat([]byte{0xff}, len(pkg.Checksum{}))),
pkg.TarBzip2,
)},
{"http get tar unaligned", pkg.NewHTTPGetTar(
), pkg.Bzip2))},
{"http get tar unaligned", pkg.NewTar(pkg.NewHTTPGet(
nil, "https://hakurei.app",
pkg.Checksum(bytes.Repeat([]byte{0xfe}, len(pkg.Checksum{}))),
pkg.TarUncompressed,
)},
))},
{"exec offline", pkg.NewExec(
"exec-offline", "", nil, 0, false, false,
@@ -47,15 +45,13 @@ func TestIRRoundtrip(t *testing.T) {
pkg.MustPath("/file", false, pkg.NewFile("file", []byte(
"stub file",
))), pkg.MustPath("/.hakurei", false, pkg.NewHTTPGetTar(
))), pkg.MustPath("/.hakurei", false, pkg.NewTar(pkg.NewHTTPGet(
nil, "file:///hakurei.tar",
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
pkg.TarUncompressed,
)), pkg.MustPath("/opt", false, pkg.NewHTTPGetTar(
))), pkg.MustPath("/opt", false, pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet(
nil, "file:///testtool.tar.gz",
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
pkg.TarGzip,
)),
), pkg.Gzip))),
)},
{"exec net", pkg.NewExec(
@@ -69,15 +65,13 @@ func TestIRRoundtrip(t *testing.T) {
pkg.MustPath("/file", false, pkg.NewFile("file", []byte(
"stub file",
))), pkg.MustPath("/.hakurei", false, pkg.NewHTTPGetTar(
))), pkg.MustPath("/.hakurei", false, pkg.NewTar(pkg.NewHTTPGet(
nil, "file:///hakurei.tar",
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
pkg.TarUncompressed,
)), pkg.MustPath("/opt", false, pkg.NewHTTPGetTar(
))), pkg.MustPath("/opt", false, pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet(
nil, "file:///testtool.tar.gz",
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
pkg.TarGzip,
)),
), pkg.Gzip))),
)},
{"exec measured", pkg.NewExec(
@@ -91,19 +85,21 @@ func TestIRRoundtrip(t *testing.T) {
pkg.MustPath("/file", false, pkg.NewFile("file", []byte(
"stub file",
))), pkg.MustPath("/.hakurei", false, pkg.NewHTTPGetTar(
))), pkg.MustPath("/.hakurei", false, pkg.NewTar(pkg.NewHTTPGet(
nil, "file:///hakurei.tar",
pkg.Checksum(bytes.Repeat([]byte{0xfd}, len(pkg.Checksum{}))),
pkg.TarUncompressed,
)), pkg.MustPath("/opt", false, pkg.NewHTTPGetTar(
))), pkg.MustPath("/opt", false, pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet(
nil, "file:///testtool.tar.gz",
pkg.Checksum(bytes.Repeat([]byte{0xfd}, len(pkg.Checksum{}))),
pkg.TarGzip,
)),
), pkg.Gzip))),
)},
{"file anonymous", pkg.NewFile("", []byte{0})},
{"file", pkg.NewFile("stub", []byte("stub"))},
{"decompress", pkg.NewDecompress(pkg.NewFile("", []byte{0}), pkg.Bzip2)},
{"archive", pkg.NewArchive(pkg.NewFile("", []byte{0}))},
}
testCasesCache := make([]cacheTestCase, len(testCases))
for i, tc := range testCases {
+261 -38
View File
@@ -187,7 +187,7 @@ func makeStatusHeader(extension string) string {
var statusHeader = makeStatusHeader("")
// prepareStatus initialises the status file once.
func (t *TContext) prepareStatus() error {
func (t *TContext) prepareStatus(writeHeader bool) error {
if t.statusPath != nil || t.status != nil {
return t.statusErr
}
@@ -204,14 +204,16 @@ func (t *TContext) prepareStatus() error {
return t.statusErr
}
if writeHeader {
_, t.statusErr = t.status.WriteString(statusHeader)
}
return t.statusErr
}
// GetStatusWriter returns a [io.Writer] for build logs. The caller must not
// seek this writer before the position it was first returned in.
func (t *TContext) GetStatusWriter() (io.Writer, error) {
err := t.prepareStatus()
err := t.prepareStatus(true)
return t.status, err
}
@@ -236,8 +238,8 @@ func (t *TContext) destroy(errP *error) {
if chmodErr != nil || removeErr != nil {
*errP = errors.Join(*errP, chmodErr, removeErr)
} else if errors.Is(*errP, os.ErrExist) {
var linkError *os.LinkError
if errors.As(*errP, &linkError) && linkError != nil &&
if linkError, ok := errors.AsType[*os.LinkError](*errP); ok &&
linkError != nil &&
linkError.Op == "rename" {
// two artifacts may be backed by the same file
*errP = nil
@@ -332,6 +334,28 @@ type FContext struct {
deps map[Artifact]cureRes
}
// linkSubstitute links status for substitute if populated.
func (f *FContext) linkSubstitute(ids, substitutes string) (err error) {
if f.status == nil || ids == substitutes {
return
}
statusS := f.cache.base.Append(
dirStatus,
substitutes,
)
f.cache.checksumMu.Lock()
err = os.Link(f.cache.base.Append(
dirStatus,
ids,
).String(), statusS.String())
f.cache.checksumMu.Unlock()
if err == nil {
f.statusSPath = statusS
}
return
}
// InvalidLookupError is the identifier of non-dependency [Artifact] looked up
// via [FContext.GetArtifact] by a misbehaving [Artifact] implementation.
type InvalidLookupError ID
@@ -508,6 +532,10 @@ const (
KindExecNet
// KindFile is the kind of [Artifact] returned by [NewFile].
KindFile
// KindDecompress is the kind of [Artifact] returned by [NewDecompress].
KindDecompress
// KindArchive is the kind of [Artifact] returned by [NewArchive].
KindArchive
// _kindEnd is the total number of kinds and does not denote a kind.
_kindEnd
@@ -666,6 +694,20 @@ type pendingCure struct {
cancel context.CancelFunc
}
// An External cache provides prepared [Artifact] cure outcomes.
type External interface {
// Artifact returns the address of the [Checksum] of the cure outcome of
// an [Artifact] corresponding to id, or nil if this [Artifact] is not
// available in the external cache.
Artifact(ctx context.Context, id unique.Handle[ID]) (*Checksum, error)
// Checksum returns an [Artifact] producing the specified checksum.
Checksum(checksum unique.Handle[Checksum]) Artifact
// Status returns [io.ReadCloser] of the status file of an [Artifact]
// corresponding to id, or nil if this [Artifact] is not available or a
// status file is not present.
Status(r *RContext, id unique.Handle[ID]) (io.ReadCloser, error)
}
// Cache is a support layer that implementations of [Artifact] can use to store
// cured [Artifact] data in a content addressed fashion.
type Cache struct {
@@ -716,6 +758,11 @@ type Cache struct {
// Buffered I/O free list, must not be accessed directly.
brPool, bwPool sync.Pool
// Optional external cache implementation.
extern External
// Synchronises access to extern.
externMu sync.RWMutex
// Unlocks the on-filesystem cache. Must only be called from Close.
unlock func()
// Whether [Cache] is considered closed.
@@ -795,6 +842,38 @@ func (c *Cache) getReader(r io.Reader) *bufio.Reader {
// putReader adds br to brPool.
func (c *Cache) putReader(br *bufio.Reader) { c.brPool.Put(br) }
// bufioReadCloser is the concrete type of value returned by Cache.getReaderRC.
type bufioReadCloser struct {
// Saved close error.
closeErr error
// Synchronises calls to Close.
closeOnce sync.Once
// For backing freelist.
c *Cache
// Underlying reader.
r io.ReadCloser
// Allocated from c.
*bufio.Reader
}
// Close closes the underlying reader, saves its return value, and returns the
// [bufio.Reader] instance to the backing [Cache].
func (brc *bufioReadCloser) Close() error {
brc.closeOnce.Do(func() {
br := brc.Reader
brc.Reader = nil
brc.c.putReader(br)
brc.closeErr = brc.r.Close()
})
return brc.closeErr
}
// getReaderRC is like getReader, but returns an [io.ReadCloser].
func (c *Cache) getReaderRC(r io.ReadCloser) io.ReadCloser {
return &bufioReadCloser{c: c, r: r, Reader: c.getReader(r)}
}
// getWriter is like [bufio.NewWriter] but for bwPool.
func (c *Cache) getWriter(w io.Writer) *bufio.Writer {
bw := c.bwPool.Get().(*bufio.Writer)
@@ -816,6 +895,35 @@ func (e *ChecksumMismatchError) Error() string {
" instead of " + Encode(e.Want)
}
// LinknamePrefixError describes a malformed linkname to a [Checksum].
type LinknamePrefixError string
func (e LinknamePrefixError) Error() string {
return "linkname " + strconv.Quote(string(e)) + " missing prefix"
}
// readlinkChecksum reads a symbolic link to a dirChecksum entry and saves the
// decoded [Checksum] to the value pointed to by buf. The checksumLinknamePrefix
// is required.
func readlinkChecksum(a *check.Absolute, buf *Checksum) error {
linkname, err := os.Readlink(a.String())
if err != nil {
return nil
}
if !strings.HasPrefix(linkname, checksumLinknamePrefix) {
return LinknamePrefixError(linkname)
}
return Decode(buf, linkname[len(checksumLinknamePrefix):])
}
// SetExternal sets e as the [External] implementation of c.
func (c *Cache) SetExternal(e External) {
c.externMu.Lock()
c.extern = e
c.externMu.Unlock()
}
// ScrubError describes the outcome of a [Cache.Scrub] call where errors were
// found and removed from the underlying storage of [Cache].
type ScrubError struct {
@@ -866,36 +974,42 @@ func (e *ScrubError) Unwrap() []error {
// Error returns a multi-line representation of [ScrubError].
func (e *ScrubError) Error() string {
var segments []string
var buf strings.Builder
if len(e.ChecksumMismatches) > 0 {
s := "checksum mismatches:\n"
buf.Reset()
buf.WriteString("checksum mismatches:\n")
for _, m := range e.ChecksumMismatches {
s += m.Error() + "\n"
buf.WriteString(m.Error() + "\n")
}
segments = append(segments, s)
segments = append(segments, buf.String())
}
if len(e.DanglingIdentifiers) > 0 {
s := "dangling identifiers:\n"
buf.Reset()
buf.WriteString("dangling identifiers:\n")
for _, id := range e.DanglingIdentifiers {
s += Encode(id) + "\n"
buf.WriteString(Encode(id) + "\n")
}
segments = append(segments, s)
segments = append(segments, buf.String())
}
if len(e.DanglingStatus) > 0 {
s := "dangling status:\n"
buf.Reset()
buf.WriteString("dangling status:\n")
for _, id := range e.DanglingStatus {
s += Encode(id) + "\n"
buf.WriteString(Encode(id) + "\n")
}
segments = append(segments, s)
segments = append(segments, buf.String())
}
if len(e.Errs) > 0 {
s := "errors during scrub:\n"
buf.Reset()
buf.WriteString("errors during scrub:\n")
for pathname, errs := range e.errs {
s += " " + pathname.Value() + ":\n"
buf.WriteString(" " + pathname.Value() + ":\n")
for _, err := range errs {
s += " " + err.Error() + "\n"
buf.WriteString(" " + err.Error() + "\n")
}
}
segments = append(segments, s)
segments = append(segments, buf.String())
}
return strings.Join(segments, "\n")
}
@@ -985,7 +1099,7 @@ func (c *Cache) Scrub(checks int) error {
pathname := dir.Append(ent.Name())
if ent.IsDir() {
if err := HashDir(got, pathname); err != nil {
if err := SumDir(got, pathname); err != nil {
addErr(pathname, err)
return true
}
@@ -1085,19 +1199,28 @@ func (c *Cache) Scrub(checks int) error {
got := p.Get().(*Checksum)
defer p.Put(got)
if _, err := os.Stat(c.base.Append(
var ok bool
for _, name := range [...]string{
dirIdentifier,
dirSubstitute,
} {
if _, err := os.Stat(c.base.Append(
name,
ent.Name(),
).String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
addErr(dir.Append(ent.Name()), err)
}
continue
}
ok = true
}
if !ok {
seMu.Lock()
se.DanglingStatus = append(se.DanglingStatus, *want)
seMu.Unlock()
return false
}
return true
return ok
}}
}
wg.Wait()
@@ -1715,6 +1838,40 @@ func (r *RContext) NewMeasuredReader(
return r.cache.newMeasuredReader(rc, checksum)
}
// tryExtern attempts to obtain an [Artifact] outcome from extern.
func (c *Cache) tryExtern(ctx context.Context, id unique.Handle[ID]) (
unique.Handle[Checksum],
io.ReadCloser,
error,
) {
c.externMu.RLock()
defer c.externMu.RUnlock()
if c.extern == nil {
return zeroChecksum, nil, nil
}
v, err := c.extern.Artifact(ctx, id)
if err != nil {
return zeroChecksum, nil, err
}
if v == nil {
return zeroChecksum, nil, nil
}
checksum := unique.Make(*v)
var got unique.Handle[Checksum]
if _, got, err = c.Cure(c.extern.Checksum(checksum)); err != nil {
return checksum, nil, err
} else if got != checksum {
return zeroChecksum, nil, &ChecksumMismatchError{got.Value(), checksum.Value()}
}
var status io.ReadCloser
status, err = c.extern.Status(&RContext{common{ctx, c}}, id)
return checksum, status, err
}
// cure implements Cure without acquiring a read lock on abortMu. cure must not
// be entered during Abort.
func (c *Cache) cure(a Artifact, curesExempt bool) (
@@ -1781,7 +1938,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
err = zeroTimes(pathname.String())
}
if err == nil && alternative != nil {
if err == nil && alternative != nil && substitute != id {
c.substituteMu.Lock()
err = os.Symlink(
linkname,
@@ -2049,26 +2206,61 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
}
defer f.destroy(&err)
var (
externChecksum unique.Handle[Checksum]
externStatus io.ReadCloser
)
if externChecksum, externStatus, err = c.tryExtern(ctx, id); err != nil {
if c.msg.IsVerbose() {
c.msg.Verbosef("extern %s: %v", reportName(ca, id), err)
}
return
}
if externChecksum != zeroChecksum {
if checksum != zeroChecksum && externChecksum != checksum {
if externStatus != nil {
_ = externStatus.Close()
}
err = &ChecksumMismatchError{externChecksum.Value(), checksum.Value()}
if c.msg.IsVerbose() {
c.msg.Verbosef("extern %s: %v", reportName(ca, id), err)
}
return
}
checksum = externChecksum
checksums = Encode(checksum.Value())
checksumPathname = c.base.Append(
dirChecksum,
checksums,
)
if externStatus != nil {
if err = f.prepareStatus(false); err != nil {
_ = externStatus.Close()
return
} else if _, err = io.Copy(f.status, externStatus); err != nil {
_ = externStatus.Close()
return
} else if err = externStatus.Close(); err != nil {
return
} else if err = f.linkSubstitute(ids, substitutes); err != nil {
return
}
}
return
}
if err = c.enterCure(a, curesExempt); err != nil {
return
}
err = ca.Cure(&f)
if err == nil && f.status != nil {
statusS := c.base.Append(
dirStatus,
substitutes,
)
c.checksumMu.Lock()
err = os.Link(c.base.Append(
dirStatus,
ids,
).String(), statusS.String())
c.checksumMu.Unlock()
if err == nil {
f.statusSPath = statusS
}
}
c.exitCure(a, curesExempt)
if err == nil {
err = f.linkSubstitute(ids, substitutes)
}
if err != nil {
if c.msg.IsVerbose() {
c.msg.Verbosef("cure %s: %v", reportName(ca, id), err)
@@ -2101,7 +2293,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
}
var gotChecksum Checksum
if err = HashFS(
if err = SumFS(
&gotChecksum,
dotOverrideFS{os.DirFS(t.work.String()).(dirFS)},
".",
@@ -2376,6 +2568,37 @@ func open(
c.unlock = func() {}
}
for _, name := range []string{
dirWork,
dirTemp,
} {
dents, err := os.ReadDir(base.Append(name).String())
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
c.unlock()
return nil, err
}
if len(dents) != 0 {
c.unlock()
return nil, fmt.Errorf(
"%s is not empty, scrub likely required",
name,
)
}
}
if _, err := os.ReadDir(base.Append(
dirExecScratch,
).String()); !errors.Is(err, os.ErrNotExist) {
c.unlock()
if err != nil {
return nil, err
}
return nil, errors.New(dirExecScratch + " is present, scrub likely required")
}
variantPath := base.Append(fileVariant).String()
if p, err := os.ReadFile(variantPath); err != nil {
if !errors.Is(err, os.ErrNotExist) {
+299 -34
View File
@@ -61,6 +61,11 @@ var (
//
//go:linkname irArtifact hakurei.app/internal/pkg.irArtifact
irArtifact map[pkg.Kind]pkg.IRReadFunc
// statusHeader is the header written to all status files in dirStatus.
//
//go:linkname statusHeader hakurei.app/internal/pkg.statusHeader
statusHeader string
)
// newRContext returns the address of a new [pkg.RContext] unsafely created for
@@ -192,6 +197,35 @@ func newStubFile(
}
}
// stubExtern implements [External] with hardcoded prepared outcomes.
type stubExtern struct {
artifact map[unique.Handle[pkg.ID]]pkg.Checksum
checksum map[unique.Handle[pkg.Checksum]]fstest.MapFS
status map[unique.Handle[pkg.ID]]string
}
func (e stubExtern) Artifact(_ context.Context, id unique.Handle[pkg.ID]) (*pkg.Checksum, error) {
if checksum, ok := e.artifact[id]; ok {
return &checksum, nil
}
return nil, nil
}
func (e stubExtern) Checksum(checksum unique.Handle[pkg.Checksum]) pkg.Artifact {
var buf bytes.Buffer
if err := pkg.Write(e.checksum[checksum], ".", &buf); err != nil {
panic(err)
}
return pkg.NewArchive(pkg.NewFile("", buf.Bytes()))
}
func (e stubExtern) Status(_ *pkg.RContext, id unique.Handle[pkg.ID]) (io.ReadCloser, error) {
if status, ok := e.status[id]; ok {
return io.NopCloser(strings.NewReader(status)), nil
}
return nil, nil
}
// destroyArtifact removes all traces of an [Artifact] from the on-disk cache.
// Do not use this in a test case without a very good reason to do so.
func destroyArtifact(
@@ -288,15 +322,15 @@ func TestIdent(t *testing.T) {
a pkg.Artifact
want unique.Handle[pkg.ID]
}{
{"tar", &stubArtifact{
pkg.KindTar,
[]byte{pkg.TarGzip, 0, 0, 0, 0, 0, 0, 0},
{"decompress", &stubArtifact{
pkg.KindDecompress,
[]byte{pkg.Gzip, 0, 0, 0, 0, 0, 0, 0},
[]pkg.Artifact{
overrideIdent{pkg.ID{}, new(stubArtifact)},
},
nil,
}, unique.Make[pkg.ID](pkg.MustDecode(
"WKErnjTOVbuH2P9a0gM4OcAAO4p-CoX2HQu7CbZrg8ZOzApvWoO3-ISzPw6av_rN",
"97Y85QewssfPbNIN9cyNhzD4e6dLHcDTU8rb2c34k-aCrZfBNXFUc0duPiLFFcw_",
))},
}
@@ -352,7 +386,7 @@ type expectsFS fstest.MapFS
// hash computes the checksum of e.
func (e expectsFS) hash() (checksum pkg.Checksum) {
if err := pkg.HashFS(&checksum, fstest.MapFS(e), "."); err != nil {
if err := pkg.SumFS(&checksum, fstest.MapFS(e), "."); err != nil {
panic(err)
}
return
@@ -434,6 +468,31 @@ const (
checkDestroySubstitutes = 1 << (iota + 32)
)
// makeBase returns a [pkg.Cache] base directory created for tb.
func makeBase(tb testing.TB) (base *check.Absolute) {
tb.Helper()
base = check.MustAbs(tb.TempDir())
if err := os.Chmod(base.String(), 0700); err != nil {
tb.Fatal(err)
}
tb.Cleanup(func() {
if err := filepath.WalkDir(base.String(), func(path string, d fs.DirEntry, err error) error {
if err != nil {
tb.Error(err)
return nil
}
if !d.IsDir() {
return nil
}
return os.Chmod(path, 0700)
}); err != nil {
tb.Fatal(err)
}
})
return
}
// checkWithCache runs a slice of cacheTestCase.
func checkWithCache(t *testing.T, testCases []cacheTestCase) {
t.Helper()
@@ -443,25 +502,7 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
t.Helper()
t.Parallel()
base := check.MustAbs(t.TempDir())
if err := os.Chmod(base.String(), 0700); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := filepath.WalkDir(base.String(), func(path string, d fs.DirEntry, err error) error {
if err != nil {
t.Error(err)
return nil
}
if !d.IsDir() {
return nil
}
return os.Chmod(path, 0700)
}); err != nil {
t.Fatal(err)
}
})
base := makeBase(t)
msg := message.New(log.New(os.Stderr, "cache: ", 0))
msg.SwapVerbose(testing.Verbose())
@@ -486,7 +527,17 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
tc.early(t, base)
}
tc.f(t, base, c)
scrubFunc = func() error { return c.Scrub(1 << 7) }
scrubFunc = func() error {
err = c.Scrub(1 << 7)
idents, checksums, cleanErr := c.Clean(false, false)
if len(idents) > 0 {
t.Errorf("destroyed %d idents", len(idents))
}
if len(checksums) > 0 {
t.Errorf("destroyed %d checksums", len(checksums))
}
return errors.Join(err, cleanErr)
}
}
var restoreTemp bool
@@ -526,9 +577,10 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
// destroy empty status directory
if err := syscall.Rmdir(base.Append("status").String()); err != nil {
t.Error(expectsFrom(base.Append("status").String()))
if !errors.Is(err, syscall.ENOTEMPTY) {
t.Fatal(err)
}
}
// destroy empty fault directory
if err := os.Remove(base.Append("fault").String()); err != nil {
@@ -538,8 +590,8 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
want := tc.want.hash()
var checksum pkg.Checksum
if err := pkg.HashDir(&checksum, base); err != nil {
t.Fatalf("HashDir: error = %v", err)
if err := pkg.SumDir(&checksum, base); err != nil {
t.Fatalf("SumDir: error = %v", err)
} else if checksum != want {
t.Fatal(expectsFrom(base.String()))
}
@@ -558,13 +610,10 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
}
// validate again to make sure scrub did not condemn anything
if err := pkg.HashDir(&checksum, base); err != nil {
t.Fatalf("HashDir: error = %v", err)
if err := pkg.SumDir(&checksum, base); err != nil {
t.Fatalf("SumDir: error = %v", err)
} else if checksum != want {
t.Fatalf("(scrubbed) HashDir: %v", &pkg.ChecksumMismatchError{
Got: checksum,
Want: want,
})
t.Fatalf("(scrubbed) %s", expectsFrom(base.String()))
}
})
}
@@ -1399,6 +1448,179 @@ func TestCache(t *testing.T) {
"substitute": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
{"status substitute clean", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
destroyed := &stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
cure: func(f *pkg.FContext) error {
if w, err := f.GetStatusWriter(); err != nil {
return err
} else if _, err = w.Write([]byte("destroyed")); err != nil {
return err
}
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), nil, 0444)
},
}
substituted := new(*destroyed)
substituted.deps = []pkg.Artifact{
pkg.NewFile("destroyed-input-0", []byte("destroyed")),
}
substituted.cure = func(*pkg.FContext) error {
panic("substitutable cure reached")
}
cureMany(t, c, []cureStep{
{"destroyed", destroyed, base.Append(
"identifier",
pkg.Encode(c.Ident(destroyed).Value()),
), expectsFS{
".": {Mode: fs.ModeDir | 0500},
"result": {Mode: 0444},
}, nil},
{"substituted", substituted, base.Append(
"identifier",
pkg.Encode(c.Ident(substituted).Value()),
), expectsFS{
".": {Mode: fs.ModeDir | 0500},
"result": {Mode: 0444},
}, nil},
})
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE": {Mode: fs.ModeDir | 0500},
"checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE/result": {Mode: 0444},
"checksum/wILUy2izpj2sgKJVhGUGIAde1XVuqvp5BpFMIQHanT5Q8R6jK4QPVSrJsjZh-njV": {Mode: 0400, Data: []byte("destroyed")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/0Jmc-vGnNDlXTD3jxy-4DGxHW-2-2LtLZ9SXaJtDIqu4uyHfDwDbghNBQ2aYRpab": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"identifier/L-A8SK4ZX631eyealbJVH08u5pAVEf2NMk8RmLlxl7BVADkU4hNjWD6pi5H7uL1F": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/wILUy2izpj2sgKJVhGUGIAde1XVuqvp5BpFMIQHanT5Q8R6jK4QPVSrJsjZh-njV")},
"identifier/VbVV2dFVosCriHnG9t5vwfW4lbHvzOkV2eYwpGpPJZOgNBc0g1wZAhbdKEweLqmW": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"identifier/aSMwvPAwdsIF9e1spuLyRNEc8aTFA4HRVasoNqGjxdm1laSMs2h2teGsoSzLp_pR": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/wILUy2izpj2sgKJVhGUGIAde1XVuqvp5BpFMIQHanT5Q8R6jK4QPVSrJsjZh-njV")},
"status": {Mode: fs.ModeDir | 0700},
"status/0Jmc-vGnNDlXTD3jxy-4DGxHW-2-2LtLZ9SXaJtDIqu4uyHfDwDbghNBQ2aYRpab": {Mode: fs.ModeSymlink | 0777, Data: []byte("dzO8FEY9lu4hwRT6BfRZOX-uYGsC_5XH4jEJ7sJyThcmG9J_w1ArOAaUCGfL8wAM")},
"status/VbVV2dFVosCriHnG9t5vwfW4lbHvzOkV2eYwpGpPJZOgNBc0g1wZAhbdKEweLqmW": {Mode: 0400, Data: []byte(statusHeader + "destroyed")},
"status/dzO8FEY9lu4hwRT6BfRZOX-uYGsC_5XH4jEJ7sJyThcmG9J_w1ArOAaUCGfL8wAM": {Mode: 0400, Data: []byte(statusHeader + "destroyed")},
"substitute": {Mode: fs.ModeDir | 0700},
"substitute/dzO8FEY9lu4hwRT6BfRZOX-uYGsC_5XH4jEJ7sJyThcmG9J_w1ArOAaUCGfL8wAM": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"work": {Mode: fs.ModeDir | 0700},
}},
{"extern", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
a := &stubArtifactF{
kind: pkg.KindExec,
params: []byte("extern"),
}
wantIdent := c.Ident(a)
wantOutput := expectsFS{
".": {Mode: fs.ModeDir | 0500},
"result": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent")},
}
var wantChecksum pkg.Checksum
if err := pkg.SumFS(
&wantChecksum,
fstest.MapFS(wantOutput),
".",
); err != nil {
t.Fatal(err)
}
wantChecksumH := unique.Make(wantChecksum)
_a := &stubArtifactF{
kind: pkg.KindExec,
params: []byte("extern substitute"),
deps: []pkg.Artifact{pkg.NewFile("", nil)},
}
_wantIdent := c.Ident(_a)
_wantOutput := expectsFS{
".": {Mode: fs.ModeDir | 0500},
}
var _wantChecksum pkg.Checksum
if err := pkg.SumFS(
&_wantChecksum,
fstest.MapFS(_wantOutput),
".",
); err != nil {
t.Fatal(err)
}
_wantChecksumH := unique.Make(_wantChecksum)
kca := pkg.NewExec(
"", "",
new(pkg.Checksum), 0, false, false,
fhs.AbsRoot, nil, fhs.AbsRoot, nil,
)
kcIdent := c.Ident(kca)
c.SetExternal(stubExtern{
artifact: map[unique.Handle[pkg.ID]]pkg.Checksum{
wantIdent: wantChecksum,
_wantIdent: _wantChecksum,
kcIdent: wantChecksum,
},
checksum: map[unique.Handle[pkg.Checksum]]fstest.MapFS{
wantChecksumH: fstest.MapFS(wantOutput),
_wantChecksumH: fstest.MapFS(_wantOutput),
},
status: map[unique.Handle[pkg.ID]]string{
wantIdent: "\x00",
kcIdent: "unreachable",
},
})
cureMany(t, c, []cureStep{
{"extern", a, base.Append(
"identifier",
pkg.Encode(wantIdent.Value()),
), wantOutput, nil},
{"substitute", _a, base.Append(
"identifier",
pkg.Encode(_wantIdent.Value()),
), _wantOutput, nil},
{"mismatch", kca, nil, nil, &pkg.ChecksumMismatchError{
Got: wantChecksum,
}},
})
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400},
"checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl": {Mode: fs.ModeDir | 0500},
"checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl/result": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/4HqRo4uTwRQjfy3d2cujMoDC_pC4iv20h4a7NYlx0UdbVuky18o5iK78TFEfPX2U": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"identifier/7AZcJm58ghFyTVf_v2baSntgpsxkP5el7ti9dC77C29n8YTEqQW9jRW92KGNdYnz": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")},
"identifier/c4aCI00C-ZVyo_FQDQLl1OYK4U_kjzxwrLdFDiXMHnbMcZXCkXo_nxUWauScZ_4V": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"identifier/cNoG77frXGRCJa7fUi1INKUEQg7L4qrX5acsSv-wqZdGZT7dQwM93rD3at6kSFFF": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
"identifier/gvCqzexZVqXjF8B5lKMcP5onmq3jJ6AKqzOW_WN0Fl2yTr9NKhPt9l_ClD2EOSlS": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")},
"status": {Mode: fs.ModeDir | 0700},
"status/gvCqzexZVqXjF8B5lKMcP5onmq3jJ6AKqzOW_WN0Fl2yTr9NKhPt9l_ClD2EOSlS": {Mode: 0400, Data: []byte("\x00")},
"substitute": {Mode: fs.ModeDir | 0700},
"substitute/qOYrxy9ztKeOA96Os811_0Ox5sd8FBOxis6psJAnRJL5MLazFMaqmd4g7t7k1OHk": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"work": {Mode: fs.ModeDir | 0700},
}},
}
checkWithCache(t, testCases)
}
@@ -1761,6 +1983,49 @@ func TestOpen(t *testing.T) {
t.Errorf("Open: error = %#v, want %#v", err, wantErr)
}
})
t.Run("dirty", func(t *testing.T) {
t.Parallel()
tempDir := check.MustAbs(t.TempDir())
if err := os.MkdirAll(tempDir.Append(
"cache",
"work",
"dirty",
).String(), 0755); err != nil {
t.Fatal(err)
}
wantErr := errors.New("work is not empty, scrub likely required")
if _, err := pkg.Open(
t.Context(),
message.New(nil),
0, 0, 0, tempDir.Append("cache"),
); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Open: error = %#v, want %#v", err, wantErr)
}
})
t.Run("scratch", func(t *testing.T) {
t.Parallel()
tempDir := check.MustAbs(t.TempDir())
if err := os.MkdirAll(tempDir.Append(
"cache",
"scratch",
).String(), 0755); err != nil {
t.Fatal(err)
}
wantErr := errors.New("scratch is present, scrub likely required")
if _, err := pkg.Open(
t.Context(),
message.New(nil),
0, 0, 0, tempDir.Append("cache"),
); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Open: error = %#v, want %#v", err, wantErr)
}
})
}
func TestExtensionRegister(t *testing.T) {
+17 -66
View File
@@ -2,32 +2,18 @@ package pkg
import (
"archive/tar"
"compress/bzip2"
"compress/gzip"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
)
const (
// TarUncompressed denotes an uncompressed tarball.
TarUncompressed = iota
// TarGzip denotes a tarball compressed via [gzip].
TarGzip
// TarBzip2 denotes a tarball compressed via [bzip2].
TarBzip2
)
// A tarArtifact is an [Artifact] unpacking a tarball backed by a [FileArtifact].
type tarArtifact struct {
// Caller-supplied backing tarball.
f Artifact
// Compression on top of the tarball.
compression uint32
}
// tarArtifactNamed embeds tarArtifact for a [fmt.Stringer] tarball.
@@ -39,13 +25,13 @@ type tarArtifactNamed struct {
var _ fmt.Stringer = new(tarArtifactNamed)
// String returns the name of the underlying [Artifact] suffixed with unpack.
func (a *tarArtifactNamed) String() string { return a.name + "-unpack" }
// String returns the name of the underlying [Artifact] prefixed with unpack.
func (a *tarArtifactNamed) String() string { return "unpack-" + a.name }
// NewTar returns a new [Artifact] backed by the supplied [Artifact] and
// compression method. The source [Artifact] must be a [FileArtifact].
func NewTar(a Artifact, compression uint32) Artifact {
ta := tarArtifact{a, compression}
// NewTar returns a new [Artifact] unpacking the tar stream produced by the
// backing [Artifact]. The source [Artifact] must be a [FileArtifact].
func NewTar(a Artifact) Artifact {
ta := tarArtifact{a}
if s, ok := a.(fmt.Stringer); ok {
if name := s.String(); name != "" {
return &tarArtifactNamed{ta, name}
@@ -54,25 +40,15 @@ func NewTar(a Artifact, compression uint32) Artifact {
return &ta
}
// NewHTTPGetTar is abbreviation for NewHTTPGet passed to NewTar.
func NewHTTPGetTar(
hc *http.Client,
url string,
checksum Checksum,
compression uint32,
) Artifact {
return NewTar(NewHTTPGet(hc, url, checksum), compression)
}
// Kind returns the hardcoded [Kind] constant.
func (a *tarArtifact) Kind() Kind { return KindTar }
// Params writes compression encoded in little endian.
func (a *tarArtifact) Params(ctx *IContext) { ctx.WriteUint32(a.compression) }
// Params is a noop.
func (a *tarArtifact) Params(*IContext) {}
func init() {
register(KindTar, func(r *IRReader) Artifact {
a := NewTar(r.Next(), r.ReadUint32())
a := NewTar(r.Next())
if _, ok := r.Finalise(); ok {
panic(ErrUnexpectedChecksum)
}
@@ -98,42 +74,17 @@ func (e DisallowedTypeflagError) Error() string {
// Cure cures the [Artifact], producing a directory located at work.
func (a *tarArtifact) Cure(t *TContext) (err error) {
var tr io.ReadCloser
if tr, err = t.Open(a.f); err != nil {
var r io.ReadCloser
if r, err = t.Open(a.f); err != nil {
return
}
defer func(f io.ReadCloser) {
if err == nil {
err = tr.Close()
}
closeErr := f.Close()
defer func() {
closeErr := r.Close()
if err == nil {
err = closeErr
}
}(tr)
br := t.cache.getReader(tr)
defer t.cache.putReader(br)
tr = io.NopCloser(br)
switch a.compression {
case TarUncompressed:
break
case TarGzip:
if tr, err = gzip.NewReader(tr); err != nil {
return
}
break
case TarBzip2:
tr = io.NopCloser(bzip2.NewReader(tr))
break
default:
return os.ErrInvalid
}
}()
type dirTargetPerm struct {
path string
@@ -156,8 +107,8 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
}()
var header *tar.Header
r := tar.NewReader(tr)
for header, err = r.Next(); err == nil; header, err = r.Next() {
tr := tar.NewReader(r)
for header, err = tr.Next(); err == nil; header, err = tr.Next() {
typeflag := header.Typeflag
if typeflag == 0 {
if len(header.Name) > 0 && header.Name[len(header.Name)-1] == '/' {
@@ -183,7 +134,7 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
); err != nil {
return
}
if _, err = io.Copy(f, r); err != nil {
if _, err = io.Copy(f, tr); err != nil {
_ = f.Close()
return
} else if err = f.Close(); err != nil {
+19 -61
View File
@@ -80,8 +80,8 @@ func TestTar(t *testing.T) {
"checksum/" + wantEncode + "/work": {Mode: fs.ModeDir | 0500},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/W5S65DEhawz_WKaok5NjUKLmnD9dNl5RPauNJjcOVcB3VM4eGhSaLGmXbL8vZpiw": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)},
"identifier/rg7F1D5hwv6o4xctjD5zDq4i5MD0mArTsUIWfhUbik8xC6Bsyt3mjXXOm3goojTz": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)},
"identifier/T68YqeEW5moiwVe4J0wPvjERgyMUd4k_cKKxTcQx6AXqZaQAuuryu-Iv-qxDV6-T": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)},
"identifier/WbWIIUGl6bqDBsjU-UB69JqOWLjG5waiUJBUBGeeRp3V7FBbwaBe3sG1qm7DlPSk": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)},
"substitute": {Mode: fs.ModeDir | 0700},
@@ -104,8 +104,8 @@ func TestTar(t *testing.T) {
"checksum/" + wantExpandEncode + "/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/W5S65DEhawz_WKaok5NjUKLmnD9dNl5RPauNJjcOVcB3VM4eGhSaLGmXbL8vZpiw": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)},
"identifier/_v1blm2h-_KA-dVaawdpLas6MjHc6rbhhFS8JWwx8iJxZGUu8EBbRrhr5AaZ9PJL": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)},
"identifier/WbWIIUGl6bqDBsjU-UB69JqOWLjG5waiUJBUBGeeRp3V7FBbwaBe3sG1qm7DlPSk": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)},
"identifier/e3BECw_x_vDyTvj2P48MJVgwpgALeXQ90Ye2Y8gr6SwIPtMup6SR2LVcCTDRGpVi": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)},
"substitute": {Mode: fs.ModeDir | 0700},
@@ -156,49 +156,6 @@ func checkTarHTTP(
"testdata": {Data: []byte(testdata), Mode: 0400},
}))
wantIdent := func() pkg.ID {
h := sha512.New384()
// kind uint64
h.Write([]byte{byte(pkg.KindTar), 0, 0, 0, 0, 0, 0, 0})
// deps_sz uint64
h.Write([]byte{1, 0, 0, 0, 0, 0, 0, 0})
// kind uint64
h.Write([]byte{byte(pkg.KindHTTPGet), 0, 0, 0, 0, 0, 0, 0})
// ident ID
h0 := sha512.New384()
// kind uint64
h0.Write([]byte{byte(pkg.KindHTTPGet), 0, 0, 0, 0, 0, 0, 0})
// deps_sz uint64
h0.Write([]byte{0, 0, 0, 0, 0, 0, 0, 0})
// url string
h0.Write([]byte{byte(pkg.IRKindString), 0, 0, 0})
h0.Write([]byte{0x10, 0, 0, 0})
h0.Write([]byte("file:///testdata"))
// end(KnownChecksum)
h0.Write([]byte{byte(pkg.IRKindEnd), 0, 0, 0})
h0.Write([]byte{byte(pkg.IREndKnownChecksum), 0, 0, 0})
// checksum Checksum
h0.Write(testdataChecksum[:])
h.Write(h0.Sum(nil))
// compression uint32
h.Write([]byte{byte(pkg.IRKindUint32), 0, 0, 0})
h.Write([]byte{pkg.TarGzip, 0, 0, 0})
// end
h.Write([]byte{byte(pkg.IRKindEnd), 0, 0, 0})
h.Write([]byte{0, 0, 0, 0})
return pkg.ID(h.Sum(nil))
}()
a := pkg.NewHTTPGetTar(
&client,
"file:///testdata",
testdataChecksum,
pkg.TarGzip,
)
tarDir := stubArtifact{
kind: pkg.KindExec,
params: []byte("directory containing a single regular file"),
@@ -251,27 +208,28 @@ func checkTarHTTP(
defer newDestroyArtifactFunc(&tarDirType)(t, base, c)
cureMany(t, c, []cureStep{
{"file", a, base.Append(
"identifier",
pkg.Encode(wantIdent),
), want, nil},
{"file", pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet(
&client,
"file:///testdata",
testdataChecksum,
), pkg.Gzip)), ignorePathname, want, nil},
{"directory", pkg.NewTar(
{"directory", pkg.NewTar(pkg.NewDecompress(
&tarDir,
pkg.TarGzip,
), ignorePathname, want, nil},
pkg.Gzip,
)), ignorePathname, want, nil},
{"multiple entries", pkg.NewTar(
{"multiple entries", pkg.NewTar(pkg.NewDecompress(
&tarDirMulti,
pkg.TarGzip,
), nil, nil, errors.New(
pkg.Gzip,
)), nil, nil, errors.New(
"input directory does not contain a single regular file",
)},
{"bad type", pkg.NewTar(
{"bad type", pkg.NewTar(pkg.NewDecompress(
&tarDirType,
pkg.TarGzip,
), nil, nil, errors.New(
pkg.Gzip,
)), nil, nil, errors.New(
"input directory does not contain a single regular file",
)},
@@ -281,6 +239,6 @@ func checkTarHTTP(
cure: func(t *pkg.TContext) error {
return stub.UniqueError(0xcafe)
},
}, pkg.TarGzip), nil, nil, stub.UniqueError(0xcafe)},
}), nil, nil, stub.UniqueError(0xcafe)},
})
}
+8 -5
View File
@@ -15,8 +15,10 @@ import (
)
const (
// Trivial denotes an error that may happen frequently under normal operation.
Trivial = iota
// Inconsistent denotes an error diagnosed due to inconsistent state.
Inconsistent = iota
Inconsistent
// Degraded denotes an error condition causing a degraded state.
Degraded
// Fatal denotes an unrecoverable error. This is generally followed by
@@ -88,6 +90,8 @@ func (e Error) MarshalJSON() (data []byte, err error) {
}{Message: e.Message}
switch e.Severity {
case Trivial:
v.Severity = "trivial"
case Inconsistent:
v.Severity = "inconsistent"
case Degraded:
@@ -98,8 +102,7 @@ func (e Error) MarshalJSON() (data []byte, err error) {
v.Severity = e.Severity
}
var re RepresentableError
if errors.As(e.Err, &re) {
if re, ok := errors.AsType[RepresentableError](e.Err); ok {
v.Err, err = json.Marshal(re)
} else {
v.Err, err = json.Marshal(e.Err.Error())
@@ -162,11 +165,11 @@ func (r *Reporter) dispatch(severity int, message string, e error) (err error) {
err = _err
}
r.mu.Unlock()
} else if flag&DBypassEarly == 0 {
} else if flag&DBypassEarly == 0 && severity != Trivial {
panic(p)
}
if flag&DStrict != 0 {
if flag&DStrict != 0 && severity != Trivial {
panic(p)
}
+6 -5
View File
@@ -6,6 +6,7 @@ import (
"maps"
"reflect"
"slices"
"strings"
"unique"
)
@@ -107,8 +108,8 @@ func (e TypeError) Error() string {
}
func (e TypeError) Is(err error) bool {
var v TypeError
return errors.As(err, &v) &&
v, ok := errors.AsType[TypeError](err)
return ok &&
e.Asserted == v.Asserted &&
e.Concrete == v.Concrete
}
@@ -233,14 +234,14 @@ func evaluateAny(d PF, s []Frame, expr, rp any) bool {
return evaluateAny(d, s, e[0], rp)
}
}
var v string
var v strings.Builder
for i := range e {
var _r string
if evaluate(d, s, e[i], &_r) {
v += _r
v.WriteString(_r)
}
}
store(rp, v)
store(rp, v.String())
return true
case Array:
+1 -1
View File
@@ -214,7 +214,7 @@ func TestEvaluate(t *testing.T) {
}
var errEquals bool
if errors.As(err, new(TypeError)) {
if _, ok := errors.AsType[TypeError](err); ok {
errEquals = errors.Is(err, tc.err)
} else {
errEquals = reflect.DeepEqual(err, tc.err)
+10 -5
View File
@@ -21,8 +21,13 @@ const (
)
// newTar wraps [pkg.NewHTTPGetTar] with a simpler function signature.
func newTar(url, checksum string, compression uint32) pkg.Artifact {
return pkg.NewHTTPGetTar(nil, url, mustDecode(checksum), compression)
func newTar(url, checksum string, compress uint32) pkg.Artifact {
return pkg.NewTar(
pkg.NewDecompress(
pkg.NewHTTPGet(nil, url, mustDecode(checksum)),
compress,
),
)
}
// newFromCPAN is a helper for downloading release from CPAN.
@@ -32,7 +37,7 @@ func newFromCPAN(author, name, version, checksum string) pkg.Artifact {
author[:1]+"/"+author[:2]+"/"+author+"/"+
name+"-"+version+".tar.gz",
checksum,
pkg.TarGzip,
pkg.Gzip,
)
}
@@ -43,7 +48,7 @@ func newFromGitLab(domain, suffix, ref, checksum string) pkg.Artifact {
ref+"/"+path.Base(suffix)+"-"+
strings.ReplaceAll(ref, "/", "-")+".tar.bz2",
checksum,
pkg.TarBzip2,
pkg.Bzip2,
)
}
@@ -53,7 +58,7 @@ func newFromGitHub(suffix, tag, checksum string) pkg.Artifact {
"https://github.com/"+suffix+
"/archive/refs/tags/"+tag+".tar.gz",
checksum,
pkg.TarGzip,
pkg.Gzip,
)
}
+1 -1
View File
@@ -98,6 +98,6 @@ cmake -G ` + generate + ` \
-DCMAKE_INSTALL_PREFIX=/system \
'/usr/src/` + name + `/` + filepath.Join(attr.Append...) + `'
cmake --build . --parallel=` + jobsE + `
cmake --install . --prefix=/work/system
DESTDIR=/work cmake --install .
` + script
}
+22 -23
View File
@@ -15,8 +15,13 @@ import (
// [Toolchain]. This silences test suites expecting certain standard files to be
// available in /etc.
type cureEtc struct {
// Optional via newIANAEtc.
iana pkg.Artifact
// Whether to exclude ianaEtc.
minimal bool
}
// NewEtc returns a [pkg.Artifact] containing deterministic elements of /etc.
func NewEtc(minimal bool) pkg.Artifact {
return cureEtc{minimal}
}
// Cure writes hardcoded configuration to files under etc.
@@ -45,8 +50,8 @@ nobody:x:65534:
}
}
if a.iana != nil {
iana, _ := t.GetArtifact(a.iana)
if !a.minimal {
iana, _ := t.GetArtifact(ianaEtc)
buf := make([]byte, syscall.Getpagesize()<<3)
for _, name := range []string{
@@ -90,7 +95,7 @@ func (cureEtc) Kind() pkg.Kind { return kindEtc }
// Params writes whether iana-etc is populated.
func (a cureEtc) Params(ctx *pkg.IContext) {
if a.iana != nil {
if !a.minimal {
ctx.WriteUint32(1)
} else {
ctx.WriteUint32(0)
@@ -100,8 +105,8 @@ func (a cureEtc) Params(ctx *pkg.IContext) {
func init() {
pkg.Register(kindEtc, func(r *pkg.IRReader) pkg.Artifact {
a := cureEtc{}
if r.ReadUint32() != 0 {
a.iana = r.Next()
if r.ReadUint32() == 0 {
a.minimal = true
}
if _, ok := r.Finalise(); ok {
panic(pkg.ErrUnexpectedChecksum)
@@ -115,34 +120,28 @@ func (cureEtc) IsExclusive() bool { return false }
// Dependencies returns a slice containing the backing iana-etc release.
func (a cureEtc) Dependencies() []pkg.Artifact {
if a.iana != nil {
return []pkg.Artifact{a.iana}
if !a.minimal {
return []pkg.Artifact{ianaEtc}
}
return nil
}
// String returns a hardcoded reporting name.
func (a cureEtc) String() string {
if a.iana == nil {
if a.minimal {
return "cure-etc-minimal"
}
return "cure-etc"
}
// newIANAEtc returns an unpacked iana-etc release.
func newIANAEtc() pkg.Artifact {
const (
version = "20251215"
checksum = "kvKz0gW_rGG5QaNK9ZWmWu1IEgYAdmhj_wR7DYrh3axDfIql_clGRHmelP7525NJ"
)
return newFromGitHubRelease(
// ianaEtc is an unpacked iana-etc release.
var ianaEtc = newFromGitHubRelease(
"Mic92/iana-etc",
version,
"iana-etc-"+version+".tar.gz",
checksum,
pkg.TarGzip,
)
}
"20251215",
"iana-etc-20251215.tar.gz",
"kvKz0gW_rGG5QaNK9ZWmWu1IEgYAdmhj_wR7DYrh3axDfIql_clGRHmelP7525NJ",
pkg.Gzip,
)
var (
resolvconfPath pkg.ExecPath
+6 -6
View File
@@ -16,7 +16,7 @@ func (t Toolchain) newGo(
return t.NewPackage("go", version, newTar(
"https://go.dev/dl/go"+version+".src.tar.gz",
checksum,
pkg.TarGzip,
pkg.Gzip,
), &PackageAttr{
EnterSource: true,
Env: slices.Concat([]string{
@@ -56,8 +56,8 @@ ln -s \
func init() {
const (
version = "1.26.3"
checksum = "lEiFocZFnN5fKvZzmwVdqc9pYUjAuhzqZGbuiOqxUP4XdcY8yECisKcqsQ_eNn1N"
version = "1.26.4"
checksum = "fpqJlxa41wKRtbnviYNLk9VxgcL-5oIEDxsriF8svU6kNwQW70oRRA9gTz_ImVoB"
)
meta := Metadata{
Name: "go",
@@ -79,7 +79,7 @@ func init() {
bootstrapEarly = []pkg.Artifact{t.NewPackage("go", "1.4-bootstrap", newTar(
"https://dl.google.com/go/go1.4-bootstrap-20171003.tar.gz",
"8o9JL_ToiQKadCTb04nvBDkp8O1xiWOolAxVEqaTGodieNe4lOFEjlOxN3bwwe23",
pkg.TarGzip,
pkg.Gzip,
), &PackageAttr{
EnterSource: true,
Env: []string{
@@ -152,8 +152,8 @@ sed -i \
)
go125 := t.newGo(
"1.25.10",
"TwKwatkpwal-j9U2sDSRPEdM3YesI4Gm88YgGV59wtU-L85K9gA7UPy9SCxn6PMb",
"1.25.11",
"cnqAQ8wpVZXcM6nHAfSqFkIW4L3H73ALB9rBfKUBbfgg4_pOF3I9zOZARgYQ6MR2",
[]string{"CGO_ENABLED=0"}, `
sed -i \
's,/lib/ld-musl-`+t.linuxArch()+`.so.1,/system/bin/linker,' \
+2 -2
View File
@@ -190,8 +190,8 @@ ln -s \
})
const (
version = "22.1.6"
checksum = "vwMyqgc_09d__u49IlXGlxnp8eFlvpibhYQgVxQg-sGdoZlzyapMV4uX4UAjE7rA"
version = "22.1.7"
checksum = "GFjsoTzJ72YWQuAaNmlO67IIkoZ8Z12u3n0dOEMSpltmyXUJp8e3cccWDrscXILZ"
)
native.MustRegister("llvm-project", func(t Toolchain) (*Metadata, pkg.Artifact) {
+21
View File
@@ -0,0 +1,21 @@
package rosa_test
import (
"testing"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
)
func TestLLVMInputs(t *testing.T) {
const wantInputCount = 553
_, llvm := rosa.Native().Std().MustLoad(rosa.H("llvm"))
var n int
for range pkg.Inputs(llvm) {
n++
}
if n != wantInputCount {
t.Errorf("Inputs: %d, want %d", n, wantInputCount)
}
}
+367
View File
@@ -0,0 +1,367 @@
package rosa
import (
"compress/gzip"
"context"
"crypto/ed25519"
"crypto/sha512"
"errors"
"io"
"io/fs"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"unique"
"hakurei.app/internal/pkg"
"hakurei.app/message"
)
// Remote is an authenticated cache mirror.
type Remote struct {
// Mirror URL.
url *url.URL
// Trusted public key.
pub ed25519.PublicKey
// For requests to the mirror.
c *http.Client
}
// NewRemote returns a populated [Remote]
func NewRemote(base string, pub ed25519.PublicKey, c *http.Client) (Remote, error) {
u, err := url.Parse(base)
return Remote{u, pub, c}, err
}
// get makes a [http.MethodGet] request and returns the response, or nil if
// the response StatusCode is [http.StatusNotFound].
func (r Remote) get(ctx context.Context, elem ...string) (*http.Response, error) {
if r.url == nil || len(r.pub) != ed25519.PublicKeySize || r.c == nil {
return nil, os.ErrInvalid
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
r.url.JoinPath(elem...).String(),
nil,
)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "Rosa/1.1")
var resp *http.Response
if resp, err = r.c.Do(req); err != nil {
return nil, err
}
switch resp.StatusCode {
case http.StatusOK:
return resp, nil
case http.StatusNotFound:
return nil, resp.Body.Close()
default:
_ = resp.Body.Close()
return nil, pkg.ResponseStatusError(resp.StatusCode)
}
}
const (
// dirArtifact holds signed artifact outcome checksums.
dirArtifact = "artifact"
// dirOutcome holds outcome archives by their checksum.
dirOutcome = "outcome"
// dirStatus holds signed status files.
dirStatus = "status"
)
// An OutcomeBadSizeError describes a mirror outcome with unexpected size.
type OutcomeBadSizeError struct {
Ident unique.Handle[pkg.ID]
Size int64
}
func (e OutcomeBadSizeError) Error() string {
if e.Size < 0 {
return "remote did not return outcome size for " +
pkg.Encode(e.Ident.Value())
}
return "outcome size " + strconv.FormatInt(e.Size, 10) +
" invalid for " + pkg.Encode(e.Ident.Value())
}
// An OutcomeAuthError describes a mirror outcome with invalid signature.
type OutcomeAuthError unique.Handle[pkg.ID]
func (e OutcomeAuthError) Error() string {
return "invalid outcome signature for " +
pkg.Encode(unique.Handle[pkg.ID](e).Value())
}
// Artifact fetches and authenticates an outcome.
func (r Remote) Artifact(
ctx context.Context,
id unique.Handle[pkg.ID],
) (*pkg.Checksum, error) {
if len(r.pub) != ed25519.PublicKeySize || r.c == nil {
return nil, os.ErrInvalid
}
resp, err := r.get(ctx, dirArtifact, pkg.Encode(id.Value()))
if err != nil || resp == nil {
return nil, err
}
var buf [ed25519.SignatureSize + 2*len(pkg.Checksum{})]byte
if resp.ContentLength != int64(len(buf)) {
_ = resp.Body.Close()
return nil, OutcomeBadSizeError{id, resp.ContentLength}
}
if _, err = io.ReadFull(resp.Body, buf[:]); err != nil {
return nil, errors.Join(err, resp.Body.Close())
} else if err = resp.Body.Close(); err != nil {
return nil, err
}
if !ed25519.Verify(
r.pub,
buf[ed25519.SignatureSize:],
buf[:ed25519.SignatureSize],
) {
return nil, OutcomeAuthError(id)
} else if unique.Make((pkg.ID)(buf[ed25519.SignatureSize:])) != id {
return nil, OutcomeAuthError(id)
}
return (*pkg.Checksum)(buf[ed25519.SignatureSize+len(pkg.Checksum{}):]), nil
}
// Checksum returns an artifact satisfying checksum.
func (r Remote) Checksum(checksum unique.Handle[pkg.Checksum]) pkg.Artifact {
return pkg.NewArchive(pkg.NewHTTPGet(
r.c,
r.url.JoinPath(dirOutcome, pkg.Encode(checksum.Value())).String(),
checksum.Value(),
))
}
// A StatusBadSizeError describes a mirror status with unexpected size.
type StatusBadSizeError unique.Handle[pkg.ID]
func (e StatusBadSizeError) Error() string {
return "status payload too short for " +
pkg.Encode(unique.Handle[pkg.ID](e).Value())
}
// A StatusAuthError describes a mirror status with invalid signature.
type StatusAuthError unique.Handle[pkg.ID]
func (e StatusAuthError) Error() string {
return "invalid status signature for " +
pkg.Encode(unique.Handle[pkg.ID](e).Value())
}
// Status authenticates the checksum of a status file and returns its
// corresponding measured reader.
func (r Remote) Status(
ctx *pkg.RContext,
id unique.Handle[pkg.ID],
) (io.ReadCloser, error) {
resp, err := r.get(ctx.Unwrap(), dirStatus, pkg.Encode(id.Value()))
if err != nil || resp == nil {
return nil, err
}
var buf [ed25519.SignatureSize + 2*len(pkg.Checksum{})]byte
if _, err = io.ReadFull(resp.Body, buf[:]); err != nil {
if errors.Is(err, io.ErrUnexpectedEOF) {
err = StatusBadSizeError(id)
}
return nil, err
}
if !ed25519.Verify(
r.pub,
buf[ed25519.SignatureSize:],
buf[:ed25519.SignatureSize],
) {
_ = resp.Body.Close()
return nil, StatusAuthError(id)
} else if unique.Make((pkg.ID)(buf[ed25519.SignatureSize:])) != id {
return nil, StatusAuthError(id)
}
return ctx.NewMeasuredReader(
resp.Body,
unique.Make((pkg.Checksum)(buf[ed25519.SignatureSize+len(pkg.Checksum{}):])),
), nil
}
// NewMirror returns an [http.Handler] for servicing mirror requests.
func NewMirror(
msg message.Msg,
fsys fs.FS,
key ed25519.PrivateKey,
) http.Handler {
const identName = "ident"
var mux http.ServeMux
mux.HandleFunc("/"+dirArtifact+"/{"+identName+"}", func(
w http.ResponseWriter,
req *http.Request,
) {
var buf [2 * len(pkg.Checksum{})]byte
if err := pkg.Decode(
(*pkg.Checksum)(buf[:len(pkg.Checksum{})]),
req.PathValue(identName),
); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ids := pkg.Encode((pkg.Checksum)(buf[:len(pkg.Checksum{})]))
if linkname, err := fs.ReadLink(fsys, filepath.Join(
"identifier",
ids,
)); err != nil {
if errors.Is(err, os.ErrNotExist) {
w.WriteHeader(http.StatusNotFound)
return
}
msg.GetLogger().Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
} else if err = pkg.Decode(
(*pkg.Checksum)(buf[len(pkg.Checksum{}):]),
filepath.Base(linkname),
); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
msg.Verbosef("serving artifact %s", ids)
w.Header().Set(
"Content-Length",
strconv.Itoa(ed25519.SignatureSize+len(buf)),
)
if _, err := w.Write(append(
ed25519.Sign(key, buf[:]),
buf[:]...,
)); err != nil {
msg.Verbose(err)
}
})
mux.HandleFunc("/"+dirOutcome+"/{"+identName+"}", func(
w http.ResponseWriter,
req *http.Request,
) {
if !strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
w.WriteHeader(http.StatusNotAcceptable)
return
}
var buf pkg.Checksum
if err := pkg.Decode(
&buf,
req.PathValue(identName),
); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
checksums := pkg.Encode(buf)
rel := filepath.Join("checksum", checksums)
if _, err := fs.Lstat(fsys, rel); err != nil {
if errors.Is(err, os.ErrNotExist) {
w.WriteHeader(http.StatusNotFound)
return
}
msg.GetLogger().Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
msg.Verbosef("serving outcome %s", pkg.Encode(buf))
_fsys, err := fs.Sub(fsys, rel)
if err != nil {
msg.GetLogger().Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
var gw *gzip.Writer
if gw, err = gzip.NewWriterLevel(w, gzip.BestCompression); err != nil {
msg.GetLogger().Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Encoding", "gzip")
if err = pkg.Write(_fsys, ".", gw); err != nil {
msg.Verbose(err)
}
if err = gw.Close(); err != nil {
msg.GetLogger().Println(err)
}
})
mux.HandleFunc("/"+dirStatus+"/{"+identName+"}", func(
w http.ResponseWriter,
req *http.Request,
) {
var buf [2 * len(pkg.Checksum{})]byte
if err := pkg.Decode(
(*pkg.Checksum)(buf[:len(pkg.Checksum{})]),
req.PathValue(identName),
); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ids := pkg.Encode((pkg.Checksum)(buf[:len(pkg.Checksum{})]))
f, err := fsys.Open(filepath.Join(
"status",
ids,
))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
w.WriteHeader(http.StatusNotFound)
return
}
msg.GetLogger().Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
s, ok := f.(io.Seeker)
if !ok {
msg.GetLogger().Println("backing filesystem does not support seek")
w.WriteHeader(http.StatusInternalServerError)
return
}
msg.Verbosef("serving status %s", ids)
h := sha512.New384()
if _, err = io.Copy(h, f); err != nil {
_ = f.Close()
msg.Verbose(err)
w.WriteHeader(http.StatusInternalServerError)
}
h.Sum(buf[len(pkg.Checksum{}):len(pkg.Checksum{})])
if _, err = w.Write(append(ed25519.Sign(key, buf[:]), buf[:]...)); err != nil {
msg.Verbose(err)
return
} else if _, err = s.Seek(0, io.SeekStart); err != nil {
msg.GetLogger().Println(err)
return
} else if _, err = io.Copy(w, f); err != nil {
msg.Verbose(err)
return
}
})
return &mux
}
+77
View File
@@ -0,0 +1,77 @@
package rosa_test
import (
"crypto/ed25519"
"io/fs"
"log"
"net/http/httptest"
"os"
"testing"
"testing/fstest"
"unique"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
"hakurei.app/message"
)
func TestMirror(t *testing.T) {
t.Parallel()
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
base := check.MustAbs(t.TempDir())
msg := message.New(log.New(os.Stderr, "mirror: ", 0))
msg.SwapVerbose(testing.Verbose())
var c *pkg.Cache
c, err = pkg.Open(t.Context(), msg, 0, 0, 0, base)
if err != nil {
t.Fatal(err)
}
t.Cleanup(c.Close)
a := pkg.NewExec(
"", "",
nil, 0, false, false,
fhs.AbsRoot, nil, fhs.AbsRoot, nil,
)
id := c.Ident(a)
ids := pkg.Encode(id.Value())
var wantChecksum pkg.Checksum
if err = pkg.SumFS(&wantChecksum, fstest.MapFS{
".": {Mode: os.ModeDir | 0500},
}, "."); err != nil {
t.Fatal(err)
}
wantChecksums := pkg.Encode(wantChecksum)
server := httptest.NewServer(rosa.NewMirror(msg, fstest.MapFS{
"identifier/" + ids: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantChecksums)},
"checksum/" + wantChecksums: {Mode: os.ModeDir | 0500},
}, priv))
t.Cleanup(server.Close)
var extern pkg.External
extern, err = rosa.NewRemote("http://example.com:80", pub, server.Client())
if err != nil {
t.Fatal(err)
}
c.SetExternal(extern)
var got unique.Handle[pkg.Checksum]
if _, got, err = c.Cure(a); err != nil {
t.Fatal(err)
} else if got.Value() != wantChecksum {
t.Logf(
"Cure: checksum = %s, want %s",
pkg.Encode(got.Value()), pkg.Encode(wantChecksum),
)
}
}
+2 -3
View File
@@ -3,11 +3,11 @@ package dtc {
website = "https://git.kernel.org/pub/scm/utils/dtc/dtc.git";
anitya = 16911;
version# = "1.7.2";
version# = "1.8.1";
source = remoteTar {
url = "https://git.kernel.org/pub/scm/utils/dtc/dtc.git/snapshot/"+
"dtc-v"+version+".tar.gz";
checksum = "vUoiRynPyYRexTpS6USweT5p4SVHvvVJs8uqFkkVD-YnFjwf6v3elQ0-Etrh00Dt";
checksum = "sPsg0G4AeTvNQfUqu3bWsVNuMT0ZGvb5MP1CaO-sQYLP_rUTk5sHiHMQ_FfLPN92";
compress = gzip;
};
@@ -19,7 +19,6 @@ package dtc {
exec = meson {
setup = {
"Dyaml": "disabled";
"Dstatic-build": "true";
};
};
+2 -2
View File
@@ -3,12 +3,12 @@ package fontconfig {
website = "https://www.freedesktop.org/wiki/Software/fontconfig";
anitya = 827;
version# = "2.18.0";
version# = "2.18.1";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "fontconfig/fontconfig";
ref = version;
checksum = "Z-yA7pFiE7cRDxZm32EHUPmeRx-lth2X6uw51aoeLi4BXwNm4iOWT13IGp3YSbNW";
checksum = "S3FJfh-PeZ2_YgNrUqL0H2G4Wf_ilj5HpIGV2oURnkK672AQHpWk526bNtRHwyJt";
};
exec = make {
+2 -2
View File
@@ -3,10 +3,10 @@ package binutils {
website = "https://www.gnu.org/software/binutils";
anitya = 7981;
version# = "2.46.0";
version# = "2.46.1";
source = remoteTar {
url = "https://ftpmirror.gnu.org/gnu/binutils/binutils-"+version+".tar.bz2";
checksum = "4kK1_EXQipxSqqyvwD4LbiMLFKCUApjq6PeG4XJP4dzxYGqDeqXfh8zLuTyOuOVR";
checksum = "ihCVohBxX-URAR-YFDdVrL3inLYsEVX8KbbQmcbHaShHgqqmtsvUue1h3LXSjW-d";
compress = bzip2;
};
+2 -2
View File
@@ -504,10 +504,10 @@ package parallel {
website = "https://www.gnu.org/software/parallel";
anitya = 5448;
version# = "20260422";
version# = "20260522";
source = remoteTar {
url = "https://ftpmirror.gnu.org/gnu/parallel/parallel-"+version+".tar.bz2";
checksum = "eTsepxgqhXpMEhPd55qh-W5y4vjKn0x9TD2mzbJCNZYtFf4lT4Wzoqr74HGJYBEH";
checksum = "ezg3NZMlY-q-478YXmjWmDrDlaL55T7aBCuv3B7Z3DkAlfZRQEKO8eumqbcxTIy6";
compress = bzip2;
};
+2 -2
View File
@@ -3,11 +3,11 @@ package harfbuzz {
website = "https://harfbuzz.github.io";
anitya = 1299;
version# = "14.2.0";
version# = "14.2.1";
source = remoteGitHub {
suffix = "harfbuzz/harfbuzz";
tag = version;
checksum = "2VgjUmcPeIbleafZaGk5l7iGnag2qj0HTqrJ5770GzivebryZ2pbwVIha5j_24PH";
checksum = "gWl1fS1nXSH9Z9GpJSrh6w7Iv6AOjZbDL_4JZAZTiTYSyLCfxIFuCRLjYmqT2nbt";
};
exec = meson {
+2 -2
View File
@@ -3,11 +3,11 @@ package hwdata {
website = "https://github.com/vcrhonek/hwdata";
anitya = 5387;
version# = "0.407";
version# = "0.408";
source = remoteGitHub {
suffix = "vcrhonek/hwdata";
tag = "v"+version;
checksum = "6p1XD0CRuzt6hLfjv4ShKBW934BexmoPkRrmwxD4J63fBVCzVBRHyF8pVJdW_Xjm";
checksum = "1Rm52ovBb8sZRlA578EKgKSIea9QGgDVSBDETU872BisZTVosQy-OTKkWpzU_ddB";
};
writable = true;
+2 -2
View File
@@ -3,11 +3,11 @@ package iproute2 {
website = "https://wiki.linuxfoundation.org/networking/iproute2";
anitya = 1392;
version# = "7.0.0";
version# = "7.1.0";
source = remoteTar {
url = "https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/"+
"snapshot/iproute2-"+version+".tar.gz";
checksum = "6SNrMEKYMO-Nt4u_Ni-qOnBpzuokkf515ya-75l6APNbZwihXy8Utv1snbUpHx3T";
checksum = "yc6NdF6GtFUpKtKg6xwF_yxYGyj0eqFQW8RGsgBzKrOiVlE2bpVQlfeZf_ca5wJL";
compress = gzip;
};
+5 -5
View File
@@ -3,11 +3,11 @@ package kernel-source {
website = "https://kernel.org";
exclude = true;
version# = "6.12.90";
version# = "6.12.93";
output = remoteTar {
url = "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/"+
"snapshot/linux-"+version+".tar.gz";
checksum = "l9d_2veqbj3RVDp8L6vhV7j2DOLQtzQUqnzPFuJBR_VD8a3k4mvBQRR1-LE-SWZJ";
checksum = "cGFcgR-h4Vwv2BU78jV4HmU-3yU_ER8l8LyKF0MibEsB-kUbbrIgqxMedXZ1j8Xw";
compress = gzip;
};
}
@@ -45,9 +45,9 @@ cat \
checksum = arch {
default = "";
amd64 = "FkIf0_SD4g3mK_VWa9FJURjLS66YmknYTh49uMDlDSnoQpq6BqBymAm47joDwdWi";
arm64 = "bJ6ZLJhDR3K9So9zLh5s31hQPm_27gyl7XDVmCgdvyJGKDnffCKdtlE0HuvEbvye";
riscv64 = "bbZCVNS1ryP-XoW9Gwx3ugvhvOA6d5UuO6gE7-Gv82g_bQN1hk4GG0SXBo-otyqS";
amd64 = "Oy3soh5GNJr4JU7lk85J49LgNw-44SGpNB6NVlPp_mFUaXxvIGt1KLPnlpU3JYcB";
arm64 = "GKHHQepUpwQjsSiq47-HV1G_aRR12oasaO5dV2bjc3D-4UzE9-Mb92ptplQQhSEc";
riscv64 = "YwNZiSwm6EnTyCmKe-RMLhFHD7VuJoVjD6XnqywaB3e-Vk0rEWxsAxoNkCvxbXz9";
};
inputs = [ rsync ];
}
+2 -2
View File
@@ -3,12 +3,12 @@ package libdrm {
website = "https://dri.freedesktop.org";
anitya = 1596;
version# = "2.4.133";
version# = "2.4.134";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "mesa/libdrm";
ref = "libdrm-"+version;
checksum = "bfj296NcR9DndO11hqDbSRFPqaweSLMqRk3dlCPZpM6FONX1WZ9J4JdbTDMUd1rU";
checksum = "L81a0vjK3RBWDwLHPkyTvy-us37ICD_hD2GcIb5s8Lx_OhVWxqdElzeMCgzQ4yiK";
};
exec = meson {
+26
View File
@@ -0,0 +1,26 @@
package libevdev {
description = "wrapper library for evdev devices";
website = "https://www.freedesktop.org/software/libevdev";
anitya = 20540;
version# = "1.13.6";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "libevdev/libevdev";
ref = "libevdev-"+version;
checksum = "HJbLafbpqPaKvQitt0t484DiHPeg3t_CNv-45MluNUT66vYIdF7FSfrK0vSpUCdi";
};
exec = meson {
setup = {
"Ddocumentation": "disabled";
};
};
inputs = [
check,
bash,
kernel-headers,
];
}
+38
View File
@@ -0,0 +1,38 @@
package libinput {
description = "input device management and event handling library";
website = "https://www.freedesktop.org/wiki/Software/libinput";
anitya = 5781;
version# = "1.31.3";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "libinput/libinput";
ref = version;
checksum = "GxBGPN6YybQxrD2MDsIL8gdDYImXn4NAJi6EvTx_Hb_1jcbjwCrjeyjY2upUyTMi";
};
early = "ln -sf ../system/bin/bash /bin/\n";
exec = meson {
setup = {
"Dmtdev": "false";
"Dlibwacom": "false";
"Ddebug-gui": "false";
};
};
inputs = [
bash,
diffutils,
python-pytest,
libudev-zero,
libevdev,
kernel-headers,
];
runtime = [
libudev-zero,
libevdev,
];
}
+20
View File
@@ -0,0 +1,20 @@
package libliftoff {
description = "lightweight KMS plane library";
website = "https://gitlab.freedesktop.org/emersion/libliftoff";
anitya = 236274;
version# = "0.5.0";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "emersion/libliftoff";
ref = "v"+version;
checksum = "anxKfrW56iK7l6-bm6RGcaC_uk-Kc8x3_VqKaywmSjv5HLCs6A2sHyjGUbOC9xZv";
};
exec = meson {};
inputs = [
libdrm,
kernel-headers,
];
}
+27
View File
@@ -0,0 +1,27 @@
package libudev-zero {
description = "daemonless replacement for libudev";
website = "https://github.com/illiliti/libudev-zero";
anitya = 145201;
version# = "1.0.4";
source = remoteGitHub {
suffix = "illiliti/libudev-zero";
tag = version;
checksum = "MHETnJwIdXc62qvE1Ya9xb2Shb53gWxdXzr8FVCFEacXF1rc01yE9guP3eXlKhYw";
};
env = [ "CC=cc" ];
enterSource = true;
writable = true;
exec = make {
inPlace = true;
skipConfigure = true;
skipCheck = true;
make = [ "PREFIX=/system" ];
install = "make DESTDIR=/work PREFIX=/system install";
};
inputs = [ kernel-headers ];
}
+4 -2
View File
@@ -4,12 +4,12 @@ package mesa {
anitya = 1970;
latest = anityaFallback;
version# = "26.1.1";
version# = "26.1.2";
source = remoteGitLab {
domain = "gitlab.freedesktop.org";
suffix = "mesa/mesa";
ref = "mesa-"+version;
checksum = "OFCxGSTBe7qbnbVdazIJBNMFaQ-ylD5aRzo-JstSAdA0-hvVdRwsZiovMBm2rMzp";
checksum = "EcY_vsm4rjUzVj7jQraWb9i3y0I2F0oH3Tav01QszQMxNzjLbSWHrQYR1mPRU-J4";
};
exec = meson {
@@ -19,6 +19,7 @@ package mesa {
"Dglvnd": "enabled";
"Dgbm": "enabled";
"Degl": "enabled";
"Dgallium-drivers": join {
elems = [
@@ -123,6 +124,7 @@ package mesa {
];
runtime = [
llvm,
libdrm,
elfutils,
lm_sensors,
+4 -1
View File
@@ -24,7 +24,10 @@ python3 /usr/src/ninja/configure.py \
--gtest-source-dir=/usr/src/extra/googletest
./ninja ` + jobsFlagE + ` all`;
check = "\n./ninja_test";
check = `
chmod +w /bin/
ln -s ../system/bin/echo /bin/
./ninja_test`;
install = `
mkdir -p /work/system/bin/
+2 -2
View File
@@ -17,7 +17,7 @@ package nss {
website = "https://firefox-source-docs.mozilla.org/security/nss/index.html";
anitya = 2503;
version# = "3.124";
version# = "3.125";
source = remoteGitHub {
suffix = "nss-dev/nss";
tag = "NSS_"+join {
@@ -28,7 +28,7 @@ package nss {
};
sep = "_";
}+"_RTM";
checksum = "p_TFOmKxMVV-ZHRY0QwzEReUOxSRjEExpWIuoA3Bzxj50uNCS8EgqfzcpaiGAkr6";
checksum = "E4V_yPX6kh5I1xTfRLU01v_uNctjj8eFTyKQBGPSsqsArMPQhJd8rgIQT7YqTK9L";
};
extra = [ nspr ];
+2
View File
@@ -5,6 +5,8 @@ package openssl {
// strange malformed tags treated as pre-releases in Anitya
latest = anityaFallback;
block = "python";
version# = "3.6.2";
source = remoteGitHubRelease {
suffix = "openssl/openssl";
+10 -6
View File
@@ -48,7 +48,9 @@ package python {
];
exec = make {
configure = { "enable-optimizations"; };
check = [ "test" ];
postInstall = "ln -s python3 /work/system/bin/python";
};
inputs = [
@@ -59,6 +61,8 @@ package python {
pkg-config,
xz,
kernel-headers,
];
runtime = [
@@ -252,11 +256,11 @@ package python-trove-classifiers {
website = "https://pypi.org/p/trove-classifiers";
anitya = 88298;
version# = "2026.5.22.10";
version# = "2026.6.1.19";
source = remoteGitHub {
suffix = "pypa/trove-classifiers";
tag = version;
checksum = "PgFh58qkPCee5SIN62a_8s5kYRrydXODZEqeSgMJItiLrT5kaz8senRGn5B1Hv56";
checksum = "zdDI6XTfzuFnlhdhilnhplItbLhZ9f2_vSHZP8u5Zvhu5_rhTB73SFEzxT5gb1bO";
};
exec = pip {
@@ -298,11 +302,11 @@ package python-hatchling {
website = "https://hatch.pypa.io";
anitya = 16137;
version# = "1.16.5";
version# = "1.17.0";
source = remoteGitHub {
suffix = "pypa/hatch";
tag = "hatch-v"+version;
checksum = "V2eREtqZLZeV85yb4O-bfAJCUluHcQP76Qfs0QH5s7RF_Oc8xIP8jD0jl85qFyWk";
checksum = "fK85G542FXEXTP1QZNVhhfp8_Y8f9u1DAyeeFO3PWyv3JLTCPdPl9WTz8Ov6tfYp";
};
exec = pip {
@@ -378,11 +382,11 @@ package python-pytest {
website = "https://pytest.org";
anitya = 3765;
version# = "9.0.3";
version# = "9.1.0";
source = remoteGitHub {
suffix = "pytest-dev/pytest";
tag = version;
checksum = "qfLL_znWhbJCDbNJvrx9H3-orJ86z4ifhaW0bIn21jl2sDP-FVoX_1yieOypArQe";
checksum = "UNd_5ArXTfdGROVW5a0Z22FE4uOLfCMW4NeAnAp9SKHLGja9Db2Xc3BF48x7Hr_l";
};
env = [
+2 -2
View File
@@ -3,10 +3,10 @@ package qemu {
website = "https://www.qemu.org";
anitya = 13607;
version# = "11.0.0";
version# = "11.0.1";
source = remoteTar {
url = "https://download.qemu.org/qemu-"+version+".tar.bz2";
checksum = "C64gdi_Tkdg2fTwD9ERxtWGcf8vNn_6UvczW0c-x0KW1NZtd3NbEOIrlDhYGn15n";
checksum = "J3j3uNpiqxEoIEngBX2objV_1tzGfEgEphp5Ph86AJQvA_XMwYUakyvRH7YKEkwV";
compress = bzip2;
};
patches = [ "disable-mcast-test.patch" ];
+3 -2
View File
@@ -2,12 +2,13 @@ package rsync {
description = "an open source utility that provides fast incremental file transfer";
website = "https://rsync.samba.org";
anitya = 4217;
block = "kernel-headers circular dependency";
version# = "3.4.2";
version# = "3.4.1";
source = remoteTar {
url = "https://download.samba.org/pub/rsync/src/"+
"rsync-"+version+".tar.gz";
checksum = "t7PxS4WHXzefLMKKc_3hJgxUmlGG6KgHMZ8i4DZvCQAUAizxbclNKwfLyOHyq5BX";
checksum = "VBlTsBWd9z3r2-ex7GkWeWxkUc5OrlgDzikAC0pK7ufTjAJ0MbmC_N04oSVTGPiv";
compress = gzip;
};
+27
View File
@@ -0,0 +1,27 @@
package seatd {
description = "seat management daemon and library";
website = "https://sr.ht/~kennylevinsen/seatd";
anitya = 234932;
version# = "0.9.3";
source = remoteTar {
url = "https://git.sr.ht/~kennylevinsen/seatd/archive/"+version+".tar.gz";
checksum = "nKib7dZgUPJFr1EOF0OYElhzpFdgFPJiroAb3TScAJIAlr4p2NV1ZdS6erNA7Jwi";
compress = gzip;
};
env = [
// ../../usr/src/seatd/common/connection.c:154:55
"CFLAGS=-Wno-sign-compare",
];
exec = meson {
setup = {
"Dlibseat-logind": "disabled";
"Dlibseat-seatd": "enabled";
"Dlibseat-builtin": "enabled";
};
};
inputs = [ kernel-headers ];
}
@@ -57,7 +57,6 @@ package spirv-llvm-translator {
tag = "v"+version;
checksum = "JZAaV5ewYcm-35YA_U2BM2IcsQouZtX1BLZR0zh2vSlfEXMsT5OCtY4Gh5RJkcGy";
};
patches = [ "remove-early-prefix.patch" ];
// litArgs emits shell syntax
early = "\nexport LIT_OPTS=" + litArgs {
@@ -1,21 +0,0 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c000a77e..f18f3fde 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -164,7 +164,7 @@ install(
${LLVM_SPIRV_INCLUDE_DIRS}/LLVMSPIRVOpts.h
${LLVM_SPIRV_INCLUDE_DIRS}/LLVMSPIRVExtensions.inc
DESTINATION
- ${CMAKE_INSTALL_PREFIX}/include/LLVMSPIRVLib
+ include/LLVMSPIRVLib
)
configure_file(LLVMSPIRVLib.pc.in ${CMAKE_BINARY_DIR}/LLVMSPIRVLib.pc @ONLY)
@@ -172,5 +172,5 @@ install(
FILES
${CMAKE_BINARY_DIR}/LLVMSPIRVLib.pc
DESTINATION
- ${CMAKE_INSTALL_PREFIX}/lib${LLVM_LIBDIR_SUFFIX}/pkgconfig
+ lib${LLVM_LIBDIR_SUFFIX}/pkgconfig
)
;
+2 -2
View File
@@ -3,10 +3,10 @@ package strace {
website = "https://strace.io";
anitya = 4897;
version# = "6.19";
version# = "7.1";
source = remoteFile {
url = "https://strace.io/files/"+version+"/strace-"+version+".tar.xz";
checksum = "XJFJJ9XLh_1rHS3m_QNjLKzkkBAooE-QT9p9lJNNWowAmd54IJop_fI4-IFtjeeL";
checksum = "PR-JLYgVLyzRqRCFi2yvfip_LRP_Cksd04d7H2oAx820qt_H-vj7rbW5xyzdMTat";
};
early = `
+31
View File
@@ -0,0 +1,31 @@
package sway {
description = "i3-compatible Wayland compositor";
website = "https://swaywm.org";
anitya = 11497;
version# = "1.12";
source = remoteGitHub {
suffix = "swaywm/sway";
tag = version;
checksum = "g3WLHgFJBsl8u1sSmkjDIJY9KlIWe-e2JXthxMcLM9z0kYmWPJCAzJ_mb20ODMaD";
};
exec = meson {};
inputs = [
json-c,
pcre2,
pango,
libinput,
wlroots,
kernel-headers,
];
runtime = [
json-c,
pcre2,
pango,
libinput,
wlroots,
];
}
+2 -2
View File
@@ -3,11 +3,11 @@ package tamago {
website = "https://github.com/usbarmory/tamago-go";
anitya = 388872;
version# = "1.26.3";
version# = "1.26.4";
source = remoteGitHub {
suffix = "usbarmory/tamago-go";
tag = "tamago-go"+version;
checksum = "-nH3MjAzDDLTeJ2hRKYJcJwo5-Ikci4zOHfB8j1vKn7zrF9TS6zYaoLi8qohGwAE";
checksum = "3-VmJ2mRLQnJtbcXny2unpc7RMNzvVxajirl_M4o-JP7_oRrWOFuaJqUfE-yrZL-";
};
env = [

Some files were not shown because too many files have changed in this diff Show More