168 Commits

Author SHA1 Message Date
cat 4a42657d88 internal/rosa/package/gnu: disable sed flaky test
Test / Create distribution (push) Successful in 57s
Test / Sandbox (push) Successful in 2m45s
Test / ShareFS (push) Successful in 3m55s
Test / Hakurei (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 5m41s
Test / Hakurei (race detector) (push) Successful in 6m46s
Test / Flake checks (push) Successful in 3m1s
This is causing way too many spurious failures.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-05 23:02:02 +09:00
cat f0b6659865 internal/rosa/package/kernel: 6.18.37 to 6.18.38
Test / Sandbox (push) Successful in 3m55s
Test / Create distribution (push) Successful in 58s
Test / Hakurei (push) Successful in 5m46s
Test / Sandbox (race detector) (push) Successful in 4m54s
Test / ShareFS (push) Successful in 3m41s
Test / Hakurei (race detector) (push) Successful in 8m15s
Test / Flake checks (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-05 18:58:28 +09:00
cat a8ffaa20e0 internal/rosa/package/hwdata: 0.408 to 0.409
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 3m13s
Test / Hakurei (push) Successful in 5m24s
Test / ShareFS (push) Successful in 5m29s
Test / Sandbox (race detector) (push) Successful in 7m4s
Test / Hakurei (race detector) (push) Successful in 8m47s
Test / Flake checks (push) Successful in 1m10s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-05 18:57:40 +09:00
cat fb62ae00e4 internal/rosa/package/python: setuptools 82.0.1 to 83.0.0
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 3m8s
Test / Hakurei (push) Successful in 4m52s
Test / ShareFS (push) Successful in 4m58s
Test / Sandbox (race detector) (push) Successful in 7m16s
Test / Hakurei (race detector) (push) Successful in 8m56s
Test / Flake checks (push) Successful in 1m10s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-05 18:57:19 +09:00
cat 512d9b95bd internal/pkg: remove status acquisition from extern helper
Test / Create distribution (push) Successful in 1m0s
Test / Sandbox (push) Successful in 2m59s
Test / Hakurei (push) Successful in 4m2s
Test / ShareFS (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 5m42s
Test / Hakurei (race detector) (push) Successful in 6m45s
Test / Flake checks (push) Successful in 1m11s
Doing this together just makes it awkward to query status early, having to deal with cleaning up.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-04 23:46:50 +09:00
cat 5bf87561f5 internal/pkg: move concurrent cure implementation
Test / Create distribution (push) Successful in 57s
Test / Sandbox (push) Successful in 2m58s
Test / ShareFS (push) Successful in 3m59s
Test / Hakurei (push) Successful in 4m52s
Test / Sandbox (race detector) (push) Successful in 5m30s
Test / Hakurei (race detector) (push) Successful in 6m40s
Test / Flake checks (push) Successful in 1m13s
This is useful independent of cure and might replace the Collect hack.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-04 23:27:12 +09:00
cat dde69dde61 internal/pkg: rename inputs method
Test / Create distribution (push) Successful in 55s
Test / Sandbox (push) Successful in 2m57s
Test / ShareFS (push) Successful in 3m50s
Test / Hakurei (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 5m29s
Test / Hakurei (race detector) (push) Successful in 6m42s
Test / Flake checks (push) Successful in 1m12s
Inputs is more correct than dependencies in the current terminology.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-02 23:21:31 +09:00
cat 3ba6609444 internal/pkg: move outcome dereferencing
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 3m3s
Test / Hakurei (push) Successful in 4m0s
Test / ShareFS (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m40s
Test / Flake checks (push) Successful in 1m18s
This prepares substitute computation for shallow extern replacements.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-02 22:52:37 +09:00
cat 6dcc2fdc17 internal/rosa/package/mesa: 26.1.3 to 26.1.4
Test / Create distribution (push) Successful in 1m8s
Test / Sandbox (push) Successful in 3m28s
Test / ShareFS (push) Successful in 5m2s
Test / Hakurei (push) Successful in 5m4s
Test / Sandbox (race detector) (push) Successful in 6m18s
Test / Hakurei (race detector) (push) Successful in 7m30s
Test / Flake checks (push) Successful in 1m12s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-02 22:37:31 +09:00
cat c553b74807 internal/rosa/package/libva: 2.23.0 to 2.24.0
Test / Create distribution (push) Successful in 1m12s
Test / Sandbox (push) Successful in 3m11s
Test / ShareFS (push) Successful in 4m36s
Test / Hakurei (push) Successful in 4m41s
Test / Sandbox (race detector) (push) Successful in 6m19s
Test / Hakurei (race detector) (push) Successful in 7m39s
Test / Flake checks (push) Successful in 1m16s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-02 22:35:57 +09:00
cat 8c853c3eb6 internal/rosa/package/libseccomp: 2.6.0 to 2.6.1
Test / Create distribution (push) Successful in 1m0s
Test / Sandbox (push) Successful in 2m55s
Test / ShareFS (push) Successful in 3m59s
Test / Hakurei (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 5m57s
Test / Hakurei (race detector) (push) Successful in 7m9s
Test / Flake checks (push) Successful in 1m36s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-02 22:30:13 +09:00
cat d2f322e911 internal/rosa/package/gnutls: fix arm64-specific configure flag
Test / Create distribution (push) Successful in 1m0s
Test / Sandbox (push) Successful in 2m58s
Test / Hakurei (push) Successful in 4m3s
Test / ShareFS (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 5m29s
Test / Hakurei (race detector) (push) Successful in 6m45s
Test / Flake checks (push) Successful in 1m7s
Automatic prefix does not happen for the value string.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-02 20:58:23 +09:00
cat b538c69e01 internal/pkg: cache extern responses
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 2m24s
Test / ShareFS (push) Successful in 3m27s
Test / Hakurei (push) Successful in 3m46s
Test / Sandbox (race detector) (push) Successful in 5m1s
Test / Hakurei (race detector) (push) Successful in 6m15s
Test / Flake checks (push) Successful in 1m19s
This prepares substitute computation for shallow extern replacements.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-02 20:08:28 +09:00
cat c921ba4680 internal/rosa/package/qemu: set TIMEOUT_MULTIPLIER
Test / Create distribution (push) Successful in 1m2s
Test / ShareFS (push) Successful in 9m59s
Test / Sandbox (push) Successful in 1m40s
Test / Sandbox (race detector) (push) Successful in 2m29s
Test / Hakurei (push) Successful in 2m47s
Test / Hakurei (race detector) (push) Successful in 3m44s
Test / Flake checks (push) Successful in 1m13s
This uses the alpine linux value for now. The default timeouts make the test suite flaky under load.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-01 21:52:56 +09:00
cat 7404bf6025 internal/rosa/package/noto: 2026.06.01 to 2026.07.01
Test / Create distribution (push) Successful in 1m5s
Test / Sandbox (push) Successful in 2m59s
Test / ShareFS (push) Successful in 4m2s
Test / Hakurei (push) Successful in 4m15s
Test / Sandbox (race detector) (push) Successful in 5m42s
Test / Hakurei (race detector) (push) Successful in 6m49s
Test / Flake checks (push) Successful in 1m10s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-01 15:42:08 +09:00
cat 3975dd3e0f internal/rosa: create /bin symlinks via helper
Test / Create distribution (push) Successful in 3m19s
Test / Sandbox (push) Successful in 7m58s
Test / ShareFS (push) Successful in 10m31s
Test / Hakurei (push) Successful in 11m0s
Test / Sandbox (race detector) (push) Successful in 12m8s
Test / Hakurei (race detector) (push) Successful in 6m4s
Test / Flake checks (push) Successful in 2m47s
This centralises the workaround for surprisingly common buggy scripts with nonstandard hardcoded paths.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-01 15:03:26 +09:00
cat 3ab7116b29 internal/rosa/package/kernel: 6.12.94 to 6.18.37
Test / Create distribution (push) Successful in 1m44s
Test / ShareFS (push) Successful in 11m10s
Test / Sandbox (push) Successful in 1m52s
Test / Sandbox (race detector) (push) Successful in 2m50s
Test / Hakurei (push) Successful in 3m5s
Test / Hakurei (race detector) (push) Successful in 4m4s
Test / Flake checks (push) Successful in 1m15s
Finally jumping from 6.12 to 6.18, and the patches can be dropped.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-07-01 02:32:20 +09:00
cat 9be165d6ee internal/rosa/package/qemu: 11.0.1 to 11.0.2
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 3m15s
Test / ShareFS (push) Successful in 5m52s
Test / Sandbox (race detector) (push) Successful in 8m40s
Test / Hakurei (race detector) (push) Successful in 10m27s
Test / Hakurei (push) Successful in 2m34s
Test / Flake checks (push) Successful in 1m12s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-30 19:53:21 +09:00
cat 1c55e7c772 internal/rosa/package/python: vcs-versioning 2.2.0 to 2.2.2
Test / Create distribution (push) Successful in 55s
Test / Sandbox (push) Successful in 3m6s
Test / ShareFS (push) Successful in 4m20s
Test / Hakurei (push) Successful in 4m25s
Test / Sandbox (race detector) (push) Successful in 5m59s
Test / Hakurei (race detector) (push) Successful in 8m35s
Test / Flake checks (push) Successful in 3m58s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-30 19:51:02 +09:00
cat a981d185cf internal/rosa/package/git: 2.54.0 to 2.55.0
Test / Create distribution (push) Successful in 57s
Test / Sandbox (push) Successful in 2m53s
Test / ShareFS (push) Successful in 4m13s
Test / Hakurei (push) Successful in 4m17s
Test / Sandbox (race detector) (push) Successful in 6m2s
Test / Hakurei (race detector) (push) Successful in 3m29s
Test / Flake checks (push) Successful in 1m33s
For some reason rust is now forced on by default without any check whatsoever. No idea why the maintainers think it was such a good idea to be so in your face.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-30 19:43:25 +09:00
cat d91951ca97 internal/rosa/package/acl: 2.3.2 to 2.4.0
Test / Create distribution (push) Successful in 1m0s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 5m22s
Test / ShareFS (push) Successful in 4m20s
Test / Sandbox (race detector) (push) Successful in 5m23s
Test / Hakurei (race detector) (push) Successful in 8m3s
Test / Flake checks (push) Successful in 1m17s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-30 19:16:42 +09:00
cat 09a2ffc3bc internal/rosa/package/acl: attr 2.5.2 to 2.6.0
Test / Create distribution (push) Successful in 56s
Test / Sandbox (push) Successful in 3m3s
Test / Hakurei (push) Successful in 5m1s
Test / ShareFS (push) Successful in 5m4s
Test / Sandbox (race detector) (push) Successful in 7m2s
Test / Hakurei (race detector) (push) Successful in 8m14s
Test / Flake checks (push) Successful in 1m21s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-30 19:16:09 +09:00
cat a6444658da internal/rosa/package/json-c: 0.18-20240915 to 0.19-20260627
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 3m24s
Test / Hakurei (push) Successful in 5m0s
Test / ShareFS (push) Successful in 5m3s
Test / Sandbox (race detector) (push) Successful in 6m17s
Test / Hakurei (race detector) (push) Successful in 7m44s
Test / Flake checks (push) Successful in 1m10s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-30 19:06:41 +09:00
cat ed5615033c internal/rosa/package/gnu: parallel 20260522 to 20260622
Test / Create distribution (push) Successful in 1m0s
Test / Sandbox (push) Successful in 3m8s
Test / ShareFS (push) Successful in 4m46s
Test / Sandbox (race detector) (push) Successful in 6m30s
Test / Hakurei (race detector) (push) Successful in 7m55s
Test / Hakurei (push) Successful in 2m51s
Test / Flake checks (push) Successful in 1m14s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-30 19:06:13 +09:00
cat d1319a497c internal/rosa: handle nil source
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 3m15s
Test / Hakurei (push) Successful in 4m39s
Test / ShareFS (push) Successful in 4m52s
Test / Sandbox (race detector) (push) Successful in 6m7s
Test / Hakurei (race detector) (push) Successful in 7m23s
Test / Flake checks (push) Successful in 1m6s
Source is not always required. This improves flexibility.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-28 18:02:54 +09:00
cat cf7c34555c cmd/earlyinit: improve error messages
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m55s
Test / Hakurei (push) Successful in 4m20s
Test / ShareFS (push) Successful in 4m26s
Test / Sandbox (race detector) (push) Successful in 6m11s
Test / Hakurei (race detector) (push) Successful in 7m22s
Test / Flake checks (push) Successful in 1m12s
This improves readability, especially on a small display.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-28 18:01:51 +09:00
cat d96eecded0 internal/rosa/package/pango: 1.57.1 to 1.58.0
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 3m0s
Test / ShareFS (push) Successful in 3m52s
Test / Hakurei (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 5m31s
Test / Hakurei (race detector) (push) Successful in 6m43s
Test / Flake checks (push) Successful in 1m5s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-27 15:52:23 +09:00
cat 6863bcafd1 cmd/app: optional insecure options
Test / Create distribution (push) Successful in 55s
Test / Sandbox (push) Successful in 2m58s
Test / ShareFS (push) Successful in 3m52s
Test / Hakurei (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 5m35s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Flake checks (push) Successful in 1m11s
These are useful for very specific cases by the maintainer. No app should ever require this.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-26 21:56:07 +09:00
cat 39f023d0e5 internal/rosa/package/glib: 2.89.0 to 2.89.1
Test / Create distribution (push) Successful in 1m0s
Test / Sandbox (push) Successful in 3m5s
Test / ShareFS (push) Successful in 4m16s
Test / Hakurei (push) Successful in 4m22s
Test / Sandbox (race detector) (push) Successful in 5m56s
Test / Hakurei (race detector) (push) Successful in 6m53s
Test / Flake checks (push) Successful in 1m14s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-26 11:33:10 +09:00
cat a8a2f692e7 internal/rosa/package/libexpat: 2.8.1 to 2.8.2
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m56s
Test / ShareFS (push) Successful in 4m18s
Test / Hakurei (push) Successful in 4m22s
Test / Sandbox (race detector) (push) Successful in 5m48s
Test / Hakurei (race detector) (push) Successful in 7m3s
Test / Flake checks (push) Successful in 1m22s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-26 11:29:27 +09:00
cat 19f24c7206 internal/rosa/package/python: setuptools-scm 10.1.2 to 10.2.0
Test / Sandbox (push) Successful in 4m28s
Test / Create distribution (push) Successful in 1m3s
Test / ShareFS (push) Successful in 6m15s
Test / Hakurei (push) Successful in 6m18s
Test / Sandbox (race detector) (push) Successful in 7m33s
Test / Hakurei (race detector) (push) Successful in 8m52s
Test / Flake checks (push) Successful in 1m13s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-25 20:29:18 +09:00
cat 320432774a internal/rosa/package/python: vcs-versioning 2.1.2 to 2.2.0
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 4m41s
Test / Hakurei (push) Successful in 6m2s
Test / ShareFS (push) Successful in 6m7s
Test / Sandbox (race detector) (push) Successful in 7m46s
Test / Hakurei (race detector) (push) Successful in 9m12s
Test / Flake checks (push) Successful in 1m13s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-25 20:28:48 +09:00
cat 0721b0fe6d internal/rosa/package/curl: 8.20.0 to 8.21.0
Test / Create distribution (push) Successful in 1m16s
Test / Sandbox (push) Successful in 4m7s
Test / Hakurei (push) Successful in 6m28s
Test / ShareFS (push) Successful in 6m28s
Test / Sandbox (race detector) (push) Successful in 7m40s
Test / Hakurei (race detector) (push) Successful in 9m0s
Test / Flake checks (push) Successful in 1m7s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-25 14:08:07 +09:00
cat 418e4a874d internal/rosa/package/libpsl: 0.21.5 to 0.22.0
Test / Create distribution (push) Successful in 1m6s
Test / Sandbox (push) Successful in 4m5s
Test / Hakurei (push) Successful in 6m17s
Test / ShareFS (push) Successful in 5m55s
Test / Sandbox (race detector) (push) Successful in 7m21s
Test / Hakurei (race detector) (push) Successful in 8m58s
Test / Flake checks (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-25 14:07:38 +09:00
cat 8378e7a2c9 internal/rosa/package/vim: annotate blocked update
Test / Create distribution (push) Successful in 54s
Test / Sandbox (push) Successful in 3m3s
Test / Hakurei (push) Successful in 4m41s
Test / ShareFS (push) Successful in 4m50s
Test / Sandbox (race detector) (push) Successful in 6m46s
Test / Hakurei (race detector) (push) Successful in 8m52s
Test / Flake checks (push) Successful in 1m23s
Releases are unreasonably frequent, and the package is never exposed to the end user and never expected to run unconfined or consume untrusted input. Additionally, upstream is accepting AI slop.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-25 14:06:03 +09:00
cat 6210c9f272 internal/rosa/package: noto
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 2m42s
Test / ShareFS (push) Successful in 3m49s
Test / Hakurei (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m43s
Test / Flake checks (push) Successful in 1m7s
Internationalisation is required anyway, so just package the entire noto fonts.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-24 10:45:01 +09:00
cat c2038fa925 internal/rosa/package: rename
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m52s
Test / ShareFS (push) Successful in 3m56s
Test / Hakurei (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 5m34s
Test / Hakurei (race detector) (push) Successful in 6m45s
Test / Flake checks (push) Successful in 1m20s
Useful for packaging.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-24 10:39:38 +09:00
cat d797cca1f2 internal/rosa/package/python: vcs-versioning 2.1.1 to 2.1.2
Test / ShareFS (push) Successful in 32s
Test / Sandbox (race detector) (push) Successful in 36s
Test / Sandbox (push) Successful in 40s
Test / Create distribution (push) Successful in 58s
Test / Hakurei (race detector) (push) Successful in 1m13s
Test / Hakurei (push) Successful in 3m0s
Test / Flake checks (push) Successful in 1m16s
Another bug fix release already. Turns out upstream is using AI slop.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-24 09:52:59 +09:00
cat 2a51b433c8 cmd/app: exclude /tmp/ for X11 pathname socket
Test / Create distribution (push) Successful in 1m1s
Test / Sandbox (push) Successful in 2m57s
Test / ShareFS (push) Successful in 4m1s
Test / Hakurei (push) Successful in 4m3s
Test / Sandbox (race detector) (push) Successful in 5m41s
Test / Hakurei (race detector) (push) Successful in 6m37s
Test / Flake checks (push) Successful in 1m10s
This would otherwise cover the pathname socket.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-24 08:49:16 +09:00
cat e5ce36532b internal/rosa/package/toybox: 0.8.13 to 0.8.14
Test / Create distribution (push) Successful in 54s
Test / Sandbox (push) Successful in 2m56s
Test / ShareFS (push) Successful in 5m1s
Test / Hakurei (push) Successful in 5m11s
Test / Sandbox (race detector) (push) Successful in 7m10s
Test / Hakurei (race detector) (push) Successful in 7m54s
Test / Flake checks (push) Successful in 1m13s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-24 06:41:56 +09:00
cat 4c647388b0 internal/rosa/package/hakurei: 0.4.4 to 0.4.5
Test / Create distribution (push) Successful in 56s
Test / Sandbox (push) Successful in 3m1s
Test / ShareFS (push) Successful in 3m57s
Test / Hakurei (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 5m31s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Flake checks (push) Successful in 1m8s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-24 06:28:56 +09:00
cat 3a5f4af114 release: 0.4.5
Release / Create release (push) Successful in 1m0s
Test / Flake checks (push) Successful in 1m11s
Test / Create distribution (push) Successful in 30s
Test / ShareFS (push) Successful in 36s
Test / Hakurei (push) Successful in 43s
Test / Sandbox (race detector) (push) Successful in 43s
Test / Sandbox (push) Successful in 43s
Test / Hakurei (race detector) (push) Successful in 48s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-24 06:18:02 +09:00
cat 6195260480 internal/rosa/package/firmware: 20260519 to 20260622
Test / Create distribution (push) Successful in 1m2s
Test / Sandbox (push) Successful in 4m30s
Test / ShareFS (push) Successful in 6m7s
Test / Hakurei (push) Successful in 7m6s
Test / Sandbox (race detector) (push) Successful in 7m5s
Test / Hakurei (race detector) (push) Successful in 9m13s
Test / Flake checks (push) Successful in 1m15s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-24 06:11:20 +09:00
cat 21044d5a60 internal/rosa/package/vim: 9.2.0461 to 9.2.0707
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 3m27s
Test / Hakurei (push) Successful in 8m16s
Test / Sandbox (race detector) (push) Successful in 6m18s
Test / ShareFS (push) Successful in 6m45s
Test / Hakurei (race detector) (push) Successful in 10m16s
Test / Flake checks (push) Successful in 1m26s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-24 06:10:58 +09:00
cat 025810bf0f internal/rosa/package/python: setuptools-scm 10.0.5 to 10.1.2
Test / Create distribution (push) Successful in 1m35s
Test / Sandbox (push) Successful in 4m59s
Test / ShareFS (push) Successful in 7m47s
Test / Hakurei (push) Successful in 7m50s
Test / Sandbox (race detector) (push) Successful in 9m6s
Test / Hakurei (race detector) (push) Successful in 11m59s
Test / Flake checks (push) Successful in 1m27s
All in-between releases are broken. Not sure how that happened.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-24 06:10:32 +09:00
cat 20354c0411 internal/rosa/package/python: vcs-versioning 1.1.1 to 2.1.1
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 4m27s
Test / ShareFS (push) Successful in 6m52s
Test / Hakurei (push) Successful in 7m4s
Test / Sandbox (race detector) (push) Successful in 8m36s
Test / Hakurei (race detector) (push) Successful in 11m28s
Test / Flake checks (push) Successful in 1m28s
The environment variable changed for this package only, did not appear to affect other packages.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-24 06:09:42 +09:00
cat 9fbcd0daf2 internal/rosa/package/libarchive: 3.8.7 to 3.8.8
Test / Create distribution (push) Successful in 1m1s
Test / ShareFS (push) Successful in 5m35s
Test / Sandbox (race detector) (push) Successful in 7m51s
Test / Hakurei (race detector) (push) Successful in 10m28s
Test / Sandbox (push) Successful in 1m42s
Test / Hakurei (push) Successful in 3m7s
Test / Flake checks (push) Successful in 1m23s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-24 06:08:47 +09:00
cat 248f44a5a7 cmd/app: zero state buffer before reuse
Test / Create distribution (push) Successful in 54s
Test / Sandbox (push) Successful in 2m49s
Test / ShareFS (push) Successful in 3m49s
Test / Hakurei (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 5m24s
Test / Hakurei (race detector) (push) Successful in 8m34s
Test / Flake checks (push) Successful in 4m13s
Package internal/store expects a zero-initialised buffer.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-23 09:19:11 +09:00
cat 401dd57cbc cmd/app: display user-facing error message
Test / Create distribution (push) Successful in 59s
Test / Sandbox (push) Successful in 2m54s
Test / ShareFS (push) Successful in 4m0s
Test / Hakurei (push) Successful in 4m5s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m48s
Test / Flake checks (push) Successful in 1m9s
This is required for useful error messages for errors originating from internal/store.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-23 09:09:55 +09:00
cat 854bcc998b cmd/app: expose scheduling configuration
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 2m54s
Test / ShareFS (push) Successful in 4m2s
Test / Hakurei (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 5m44s
Test / Hakurei (race detector) (push) Successful in 6m45s
Test / Flake checks (push) Successful in 1m9s
Useful for the music player.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-23 06:33:27 +09:00
cat 358247be5b internal/rosa/package/x: xkeyboard-config 2.47 to 2.48
Test / Create distribution (push) Successful in 1m0s
Test / Sandbox (push) Successful in 3m26s
Test / Hakurei (push) Successful in 4m55s
Test / ShareFS (push) Successful in 4m59s
Test / Sandbox (race detector) (push) Successful in 6m6s
Test / Hakurei (race detector) (push) Successful in 7m24s
Test / Flake checks (push) Successful in 1m16s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-23 00:44:18 +09:00
cat f517a8ef07 internal/rosa/package/libffi: 3.5.2 to 3.6.0
Test / Create distribution (push) Successful in 56s
Test / Sandbox (push) Successful in 2m59s
Test / ShareFS (push) Successful in 4m21s
Test / Hakurei (push) Successful in 4m29s
Test / Sandbox (race detector) (push) Successful in 6m2s
Test / Hakurei (race detector) (push) Successful in 7m21s
Test / Flake checks (push) Successful in 1m15s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-23 00:43:24 +09:00
cat 973218f91f internal/rosa/package/qemu: disable netdev-socket test
Test / Create distribution (push) Successful in 55s
Test / Sandbox (push) Successful in 2m58s
Test / ShareFS (push) Successful in 3m55s
Test / Hakurei (push) Successful in 4m8s
Test / Sandbox (race detector) (push) Successful in 5m32s
Test / Hakurei (race detector) (push) Successful in 6m44s
Test / Flake checks (push) Successful in 1m13s
This fails with ipv6 disabled.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-22 01:21:09 +09:00
cat 0ea195837b internal/rosa/package/glib: disable gio suite
Test / Create distribution (push) Successful in 57s
Test / Sandbox (push) Successful in 4m18s
Test / ShareFS (push) Successful in 5m41s
Test / Sandbox (race detector) (push) Successful in 7m7s
Test / Hakurei (push) Successful in 2m42s
Test / Hakurei (race detector) (push) Successful in 3m31s
Test / Flake checks (push) Successful in 1m12s
This fails with ipv6 disabled.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-22 00:53:40 +09:00
cat 1348991634 internal/rosa/meson: skip specific test suites
Test / Create distribution (push) Successful in 55s
Test / ShareFS (push) Successful in 5m32s
Test / Sandbox (race detector) (push) Successful in 7m9s
Test / Hakurei (race detector) (push) Successful in 8m28s
Test / Sandbox (push) Successful in 1m30s
Test / Hakurei (push) Successful in 2m32s
Test / Flake checks (push) Successful in 1m7s
For disabling specific broken or flaky tests.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-22 00:53:17 +09:00
cat 14b445fde5 internal/rosa/package/openssl: disable test_bio_dgram
Test / Create distribution (push) Successful in 1m5s
Test / ShareFS (push) Successful in 9m39s
Test / Sandbox (push) Successful in 2m16s
Test / Sandbox (race detector) (push) Successful in 3m23s
Test / Hakurei (push) Successful in 4m8s
Test / Hakurei (race detector) (push) Successful in 4m57s
Test / Flake checks (push) Successful in 2m27s
This fails when ipv6 is disabled.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-21 21:43:46 +09:00
cat 218f7fa345 cmd/app: enforce mutable instance exclusion
Test / Create distribution (push) Successful in 55s
Test / Sandbox (push) Successful in 2m47s
Test / ShareFS (push) Successful in 3m46s
Test / Hakurei (push) Successful in 3m57s
Test / Sandbox (race detector) (push) Successful in 5m31s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Flake checks (push) Successful in 1m7s
This avoids invoking undefined behaviour in the underlying overlay filesystem implementation.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-21 02:09:01 +09:00
cat cd493fd95f cmd/app: centralise workdir
Test / Create distribution (push) Successful in 51s
Test / Sandbox (push) Successful in 2m47s
Test / ShareFS (push) Successful in 3m48s
Test / Hakurei (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 5m19s
Test / Hakurei (race detector) (push) Successful in 6m31s
Test / Flake checks (push) Successful in 1m7s
This makes the directory structure significantly more manageable.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-21 00:48:18 +09:00
cat 9db70c83e3 cmd/app: configure username and hostname
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m42s
Test / ShareFS (push) Successful in 3m46s
Test / Hakurei (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m42s
Test / Flake checks (push) Successful in 1m9s
These no longer need to be hardcoded since this is not subject to the limitations of home-manager.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 22:15:49 +09:00
cat 4e09241e5f cmd/app: optional interactive shell
Test / Create distribution (push) Successful in 55s
Test / Sandbox (push) Successful in 3m4s
Test / Hakurei (push) Successful in 4m20s
Test / ShareFS (push) Successful in 4m19s
Test / Sandbox (race detector) (push) Successful in 5m36s
Test / Hakurei (race detector) (push) Successful in 6m51s
Test / Flake checks (push) Successful in 1m8s
Enabling this unconditionally causes the new configuration prompt to be shown when started from a terminal, and is generally less robust than not reading zshrc unless explicitly required.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 19:08:47 +09:00
cat bd4b300ea6 internal/rosa/package/cmake: 4.3.3 to 4.3.4
Test / Create distribution (push) Successful in 58s
Test / Sandbox (push) Successful in 3m22s
Test / ShareFS (push) Successful in 4m38s
Test / Sandbox (race detector) (push) Successful in 6m34s
Test / Hakurei (race detector) (push) Successful in 7m57s
Test / Hakurei (push) Successful in 2m58s
Test / Flake checks (push) Successful in 1m26s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 17:56:09 +09:00
cat 6dc8214a1a internal/rosa/package/kernel: 6.12.93 to 6.12.94
Test / Create distribution (push) Successful in 51s
Test / Sandbox (push) Successful in 3m26s
Test / Hakurei (push) Successful in 6m11s
Test / ShareFS (push) Successful in 5m46s
Test / Sandbox (race detector) (push) Successful in 7m3s
Test / Hakurei (race detector) (push) Successful in 9m8s
Test / Flake checks (push) Successful in 1m20s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 17:55:49 +09:00
cat cada5a46ad internal/rosa/package/mesa: 26.1.2 to 26.1.3
Test / Create distribution (push) Successful in 1m3s
Test / Sandbox (push) Successful in 3m29s
Test / Hakurei (push) Successful in 5m43s
Test / ShareFS (push) Successful in 5m47s
Test / Sandbox (race detector) (push) Successful in 7m37s
Test / Hakurei (race detector) (push) Successful in 9m31s
Test / Flake checks (push) Successful in 1m20s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 17:55:08 +09:00
cat e747942829 internal/rosa/package/python: pytest 9.1.0 to 9.1.1
Test / Create distribution (push) Successful in 1m9s
Test / Sandbox (push) Successful in 3m24s
Test / ShareFS (push) Successful in 5m32s
Test / Sandbox (race detector) (push) Successful in 7m48s
Test / Hakurei (race detector) (push) Successful in 9m26s
Test / Hakurei (push) Successful in 3m0s
Test / Flake checks (push) Successful in 1m25s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 17:54:38 +09:00
cat 33b855123e internal/rosa/package/spirv: spirv-headers 1.4.350.0 to 1.4.350.1
Test / Create distribution (push) Successful in 51s
Test / Sandbox (push) Successful in 2m43s
Test / ShareFS (push) Successful in 3m43s
Test / Sandbox (race detector) (push) Successful in 5m46s
Test / Hakurei (race detector) (push) Successful in 7m0s
Test / Hakurei (push) Successful in 2m35s
Test / Flake checks (push) Successful in 1m38s
This unfortunately does not unblock SPIRV-LLVM-Translator.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 04:43:04 +09:00
cat 58ce134718 internal/outcome: attempt nscd path-hiding if present
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m46s
Test / ShareFS (push) Successful in 3m56s
Test / Hakurei (push) Successful in 4m9s
Test / Sandbox (race detector) (push) Successful in 5m42s
Test / Hakurei (race detector) (push) Successful in 6m50s
Test / Flake checks (push) Successful in 1m9s
This avoids creating the mount point on musl setups which accomplishes nothing and can run into permission problems.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 02:42:35 +09:00
cat 2066093343 cmd/app: remove sysfs bind mounts
Test / Create distribution (push) Successful in 51s
Test / Sandbox (push) Successful in 2m57s
Test / ShareFS (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 5m26s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Hakurei (push) Successful in 2m46s
Test / Flake checks (push) Successful in 1m9s
This should be in common instead.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 01:57:55 +09:00
cat 07509b3ba2 cmd/app: additional bind types
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m58s
Test / ShareFS (push) Successful in 3m54s
Test / Hakurei (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 5m27s
Test / Hakurei (race detector) (push) Successful in 6m43s
Test / Flake checks (push) Successful in 1m25s
This adds optional and device mount points.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 01:51:57 +09:00
cat a7485d587a cmd/app: pass user-specified arguments
Test / Create distribution (push) Successful in 50s
Test / Sandbox (push) Successful in 2m52s
Test / ShareFS (push) Successful in 3m48s
Test / Sandbox (race detector) (push) Successful in 5m20s
Test / Hakurei (race detector) (push) Successful in 6m37s
Test / Hakurei (push) Successful in 2m40s
Test / Flake checks (push) Successful in 1m26s
An extra argument is added to pad out argv0.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 01:44:47 +09:00
cat 4892beefc1 cmd/app: optionally override configured command
Test / Create distribution (push) Successful in 50s
Test / Sandbox (push) Successful in 2m45s
Test / ShareFS (push) Successful in 3m50s
Test / Hakurei (push) Successful in 4m1s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m35s
Test / Flake checks (push) Successful in 1m9s
Useful for multiple applications sharing state.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 01:31:07 +09:00
cat 7ab54b8c94 internal/rosa: read overridden version string from source
Test / Create distribution (push) Successful in 54s
Test / Sandbox (push) Successful in 3m2s
Test / ShareFS (push) Successful in 4m2s
Test / Hakurei (push) Successful in 4m10s
Test / Sandbox (race detector) (push) Successful in 5m31s
Test / Hakurei (race detector) (push) Successful in 6m42s
Test / Flake checks (push) Successful in 1m7s
This is more correct than the hardcoded string and is generally more robust against relative paths.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 01:18:12 +09:00
cat a4fab67811 internal/pkg: optionally exempt implementations from cures counter
Test / Create distribution (push) Successful in 51s
Test / Sandbox (push) Successful in 2m49s
Test / ShareFS (push) Successful in 3m48s
Test / Hakurei (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 5m23s
Test / Hakurei (race detector) (push) Successful in 6m39s
Test / Flake checks (push) Successful in 1m26s
This avoids holding up many slots with a long pipeline.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 01:03:28 +09:00
cat ed5cdd38a4 cmd/dist: build hsu separately
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m41s
Test / ShareFS (push) Successful in 3m47s
Test / Hakurei (push) Successful in 4m0s
Test / Sandbox (race detector) (push) Successful in 5m25s
Test / Hakurei (race detector) (push) Successful in 6m36s
Test / Flake checks (push) Successful in 1m8s
This program must be built with cgo disabled, and was missed when migrating build script.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 00:59:47 +09:00
cat f6318304ee hst: fix ephemeral overlay order
Test / Create distribution (push) Successful in 1m0s
Test / Sandbox (push) Successful in 2m55s
Test / ShareFS (push) Successful in 4m13s
Test / Hakurei (push) Successful in 4m19s
Test / Sandbox (race detector) (push) Successful in 5m58s
Test / Hakurei (race detector) (push) Successful in 6m53s
Test / Flake checks (push) Successful in 1m11s
This is quite counterintuitive otherwise.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 00:20:31 +09:00
cat cb618093d5 hst: optionally disable file placement
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m39s
Test / ShareFS (push) Successful in 3m46s
Test / Sandbox (race detector) (push) Successful in 5m28s
Test / Hakurei (race detector) (push) Successful in 6m37s
Test / Hakurei (push) Successful in 2m46s
Test / Flake checks (push) Successful in 1m17s
This works around stubborn package managers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-20 00:16:35 +09:00
cat b0b2471c0c cmd/app: include template name in container metadata
Test / Create distribution (push) Successful in 55s
Test / Sandbox (push) Successful in 3m3s
Test / ShareFS (push) Successful in 3m46s
Test / Hakurei (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 5m27s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m10s
This helps disambiguate active mutable containers.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-19 23:36:31 +09:00
cat 344d2b8207 cmd/app: use ROSA_ prefix
Test / Create distribution (push) Successful in 54s
Test / Sandbox (push) Successful in 2m52s
Test / ShareFS (push) Successful in 3m51s
Test / Hakurei (push) Successful in 3m59s
Test / Sandbox (race detector) (push) Successful in 5m29s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m24s
This avoids awkward case-insensitive zsh completion.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-19 23:29:51 +09:00
cat 3938e8bce5 cmd/app: multiple template uppers
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m44s
Test / ShareFS (push) Successful in 3m53s
Test / Hakurei (push) Successful in 4m6s
Test / Sandbox (race detector) (push) Successful in 5m27s
Test / Hakurei (race detector) (push) Successful in 6m40s
Test / Flake checks (push) Successful in 1m12s
Having multiple environments is useful, and this was trivial to implement.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-18 03:51:49 +09:00
cat aee15b4f2a cmd/app: common configuration file
Test / Create distribution (push) Successful in 1m36s
Test / Sandbox (push) Successful in 3m28s
Test / ShareFS (push) Successful in 4m33s
Test / Hakurei (push) Successful in 4m41s
Test / Sandbox (race detector) (push) Successful in 6m45s
Test / Hakurei (race detector) (push) Successful in 7m46s
Test / Flake checks (push) Successful in 1m33s
Generally useful for shared storage and environment.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-18 02:35:56 +09:00
cat 18b1103fdc internal/rosa/llvm: 22.1.7 to 22.1.8
Test / Create distribution (push) Successful in 1m13s
Test / Sandbox (push) Successful in 3m20s
Test / ShareFS (push) Successful in 8m47s
Test / Hakurei (push) Successful in 8m51s
Test / Sandbox (race detector) (push) Successful in 2m54s
Test / Hakurei (race detector) (push) Successful in 4m22s
Test / Flake checks (push) Successful in 1m18s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-17 21:49:38 +09:00
cat c5a02da0f0 internal/rosa/package/python: 3.14.5 to 3.14.6
Test / Create distribution (push) Successful in 1m8s
Test / Sandbox (push) Successful in 3m14s
Test / ShareFS (push) Successful in 6m39s
Test / Hakurei (push) Successful in 7m35s
Test / Sandbox (race detector) (push) Successful in 2m53s
Test / Hakurei (race detector) (push) Successful in 4m27s
Test / Flake checks (push) Successful in 1m19s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-17 21:49:07 +09:00
cat c0c2f3233a internal/rosa/package/util-linux: 2.42.1 to 2.42.2
Test / Create distribution (push) Successful in 55s
Test / Sandbox (push) Successful in 2m58s
Test / ShareFS (push) Successful in 3m57s
Test / Hakurei (push) Successful in 4m7s
Test / Sandbox (race detector) (push) Successful in 5m47s
Test / Hakurei (race detector) (push) Successful in 7m4s
Test / Flake checks (push) Successful in 1m34s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-17 21:44:47 +09:00
cat bda00ac90e internal/rosa/package/hakurei: 0.4.3 to 0.4.4
Test / Create distribution (push) Successful in 52s
Test / Sandbox (push) Successful in 2m41s
Test / ShareFS (push) Successful in 3m48s
Test / Hakurei (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 5m33s
Test / Hakurei (race detector) (push) Successful in 6m42s
Test / Flake checks (push) Successful in 1m23s
Signed-off-by: Ophestra <cat@gensokyo.uk>
2026-06-17 21:40:04 +09:00
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
181 changed files with 8001 additions and 3718 deletions
+1
View File
@@ -1,4 +1,5 @@
# produced by tools and text editors
*.swp
*.qcow2
*.test
*.out
+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{}) }
+337
View File
@@ -0,0 +1,337 @@
package main
import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
"hakurei.app/check"
"hakurei.app/ext"
"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,
templateP *string,
) (*hst.Config, error) {
shell := fhs.AbsRoot.Append("bin", "zsh")
home := hst.AbsPrivateTmp.Append("home")
root := hst.FSOverlay{
Target: fhs.AbsRoot,
Lower: []*check.Absolute{base.Append("initial")},
}
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: &root},
{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,
}},
},
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 template, identity, ok := strings.Cut(s.Text(), ":"); !ok {
return nil, io.ErrUnexpectedEOF
} else if v, err := strconv.Atoi(identity); err != nil {
return nil, err
} else {
if templateP != nil {
*templateP = template
}
c.Identity = v
root.Upper = base.Append("template", template)
}
if err := scanOnce(); err != nil {
return nil, err
}
c.Container.Args = append(c.Container.Args, s.Text(), "")
var flagInteractive, flagGPU, flagSystemBus bool
flags := map[string]*bool{
"interactive": &flagInteractive,
"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 "username":
c.Container.Username = value
continue
case "hostname":
c.Container.Hostname = value
continue
case "group":
c.Groups = append(c.Groups, value)
continue
case "sched_policy":
if err := c.SchedPolicy.UnmarshalText([]byte(value)); err != nil {
return nil, err
}
continue
case "sched_priority":
v, err := strconv.Atoi(value)
if err != nil {
return nil, err
}
c.SchedPriority = ext.Int(v)
continue
case "insecure":
switch value {
case "pipewire":
*c.Enablements |= hst.EPipeWire
c.DirectPipeWire = true
continue
default:
return nil, fmt.Errorf("invalid insecure flag %q", value)
}
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 "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,
Optional: true,
}},
)
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 "dev":
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,
Device: 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 flagInteractive {
c.Container.Args[1] += "i"
}
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.Enablements.Unwrap()&hst.EX11 == 0 ||
c.Container.Flags&(hst.FHostNet|hst.FHostAbstract) ==
hst.FHostNet|hst.FHostAbstract) {
c.Container.Filesystem = append(c.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsTmp,
Write: true,
Perm: 01777,
}},
)
}
return &c, nil
}
+148
View File
@@ -0,0 +1,148 @@
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", `nonfree: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("initial"),
},
Upper: base.Append("template", "nonfree"),
}},
{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: 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),
nil,
)
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)
}
})
}
}
+112
View File
@@ -0,0 +1,112 @@
package main
import (
"errors"
"log"
"os"
"strconv"
"strings"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/env"
"hakurei.app/internal/lockedfile"
"hakurei.app/internal/outcome"
)
// MutationConflictError describes an active mutable instance.
type MutationConflictError string
func (e MutationConflictError) Error() string {
return "mutable instance active at " + string(e)
}
// informTemplate guards intention of a template or its derivatives.
func informTemplate(base *check.Absolute, name string, mutable bool) (func() error, error) {
mu := lockedfile.MutexAt(base.Append("lock", name).String())
if unlock, err := mu.Lock(); err != nil {
return nil, err
} else {
defer unlock()
}
marker := base.Append("lock", "."+name)
if p, err := os.ReadFile(marker.String()); err == nil {
if _, err = os.Stat(fhs.AbsProc.Append(string(p)).String()); err == nil {
return nil, MutationConflictError(p)
} else if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
log.Printf("removing stale marker by %s", string(p))
if err = os.Remove(marker.String()); err != nil {
return nil, err
}
} else if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
if !mutable {
return nil, nil
}
var active []hst.ID
var sc hst.Paths
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
entries, copyError := outcome.NewStore(&sc).All()
var s hst.State
for eh := range entries {
s = hst.State{}
if _, err := eh.Load(&s); err != nil {
return nil, err
}
if s.Validate(0) != nil || len(s.Container.Filesystem) < 1 {
continue
}
root, ok := s.Container.Filesystem[0].FilesystemConfig.(*hst.FSOverlay)
if !ok || root == nil {
continue
}
if !root.Target.Is(fhs.AbsRoot) ||
len(root.Lower) != 1 ||
!root.Lower[0].Is(base.Append("initial")) ||
!root.Upper.Is(base.Append("template", name)) ||
root.Work != nil {
continue
}
active = append(active, s.ID)
}
if err := copyError(); err != nil {
return nil, err
}
if len(active) != 0 {
var buf strings.Builder
buf.WriteString("derivative instances still active:")
for _, id := range active {
buf.WriteString("\n\t")
buf.WriteString(id.String())
}
return nil, errors.New(buf.String())
}
return func() error { return os.RemoveAll(marker.String()) }, os.WriteFile(
marker.String(),
[]byte(strconv.Itoa(os.Getpid())),
0400,
)
}
// acquireTemplate obtains exclusivity of a template.
func acquireTemplate(base *check.Absolute, name string) (remove func() error, err error) {
return informTemplate(base, name, true)
}
// enterTemplate checks against exclusivity of a template.
func enterTemplate(base *check.Absolute, name string) error {
_, err := informTemplate(base, name, false)
return err
}
+247
View File
@@ -0,0 +1,247 @@
// 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"
"fmt"
"io"
"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
flagInsecure bool
base, template, initial *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 = base.Append("initial")
return
}).Flag(
&flagVerbose,
"v", command.BoolFlag(false),
"Increase log verbosity",
).Flag(
&flagBase,
"d", command.StringFlag("$ROSA_APP_PATH"),
"Configuration and state directory",
).Flag(
&flagInsecure,
"insecure", command.BoolFlag(false),
"Allow use of insecure compatibility options",
)
{
var (
flagShell string
flagHome string
)
c.NewCommand(
"enter", "Enter mutable state template",
func(args []string) error {
if len(args) != 1 {
dents, err := os.ReadDir(template.String())
if err != nil {
return err
}
for _, dent := range dents {
if !dent.IsDir() {
continue
}
fmt.Println(dent.Name())
}
return nil
}
config := hst.Config{
ID: "app.hakurei.mutable." + args[0],
Container: &hst.ContainerConfig{
Hostname: args[0] + "-mutable",
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSOverlay{
Target: fhs.AbsRoot,
Lower: []*check.Absolute{initial},
Upper: template.Append(args[0]),
Work: base.Append("work", args[0]),
}},
{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsTmp,
Write: true,
Perm: 0755,
}},
},
Username: "chronos",
Flags: hst.FNoPlace |
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
}
remove, err := acquireTemplate(base, args[0])
if err != nil {
return err
}
err = run(ctx, msg, false, &config)
return errors.Join(err, remove())
},
).Flag(
&flagShell,
"shell", command.StringFlag("/bin/zsh"),
"Shell program within container",
).Flag(
&flagHome,
"home", command.StringFlag("/home/chronos"),
"Home directory within container",
)
}
{
var (
flagCommand string
)
c.NewCommand(
"run", "Start the named application",
func(args []string) error {
if len(args) < 1 {
dents, err := os.ReadDir(base.Append("app").String())
if err != nil {
return err
}
for _, dent := range dents {
if dent.IsDir() {
continue
}
fmt.Println(dent.Name())
}
return nil
}
var config *hst.Config
var r io.Reader
f, err := os.Open(base.Append("app", args[0]).String())
if err != nil {
return err
}
r = f
var common *os.File
if common, err = os.Open(base.Append("common").String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
_ = f.Close()
return err
}
} else {
r = io.MultiReader(f, common)
}
var name string
config, err = parse(args[0], base, r, &name)
if closeErr := f.Close(); err == nil {
err = closeErr
}
if common != nil {
if closeErr := common.Close(); err == nil {
err = closeErr
}
}
if err != nil {
return err
}
if flagCommand != "" {
config.Container.Args[2] = flagCommand
}
if err = enterTemplate(base, name); err != nil {
return err
}
return run(ctx, msg, flagInsecure, config, args[1:]...)
},
).
Flag(
&flagCommand,
"command", command.StringFlag(""),
"Override configured command",
)
}
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 {
var m string
m, ok = message.GetMessage(err)
if !ok {
log.Fatal(err)
return
}
log.Fatal(m)
} else {
errs := w.Unwrap()
for i, e := range errs {
if i == len(errs)-1 {
log.Fatal(e)
}
log.Println(e)
}
}
})
}
+61
View File
@@ -0,0 +1,61 @@
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,
insecure bool,
config *hst.Config,
args ...string,
) 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")
}
if insecure {
cmd.Args = append(cmd.Args, "--insecure")
}
cmd.Args = append(cmd.Args, "run", "3")
cmd.Args = append(cmd.Args, args...)
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.5
+24 -9
View File
@@ -35,8 +35,11 @@ func getenv(key, fallback string) string {
// mustRun runs a command with the current process's environment and panics
// on error or non-zero exit code.
func mustRun(ctx context.Context, name string, arg ...string) {
func mustRun(ctx context.Context, env []string, name string, arg ...string) {
cmd := exec.CommandContext(ctx, name, arg...)
if env != nil {
cmd.Env = append(cmd.Environ(), env...)
}
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
@@ -49,6 +52,7 @@ var comp []byte
func main() {
log.SetFlags(0)
log.SetPrefix("")
log.SetOutput(os.Stdout)
verbose := os.Getenv("VERBOSE") != ""
runTests := os.Getenv("HAKUREI_DIST_MAKE") == ""
@@ -91,26 +95,37 @@ func main() {
verboseFlag = "-buildvcs=false"
}
log.Printf("Building hakurei for %s/%s.", runtime.GOOS, runtime.GOARCH)
mustRun(ctx, "go", "generate", "./...")
log.Printf("Building hakurei %s for %s/%s.", version, runtime.GOOS, runtime.GOARCH)
mustRun(ctx, nil, "go", "generate", "./...")
mustRun(
ctx, "go", "build",
ctx, nil, "go", "build",
"-trimpath",
verboseFlag, "-o", s,
"-ldflags=-s -w "+
"-buildid= -linkmode external -extldflags=-static "+
"-X hakurei.app/internal/info.buildVersion="+version+" "+
"-X hakurei.app/internal/info.hakureiPath="+prefix+"/bin/hakurei "+
"-X hakurei.app/internal/info.hsuPath="+prefix+"/bin/hsu "+
"-X main.hakureiPath="+prefix+"/bin/hakurei",
"./...",
"-X hakurei.app/internal/info.hsuPath="+prefix+"/bin/hsu",
"./cmd/hakurei",
"./cmd/sharefs",
)
log.Println()
log.Printf("Building cmd/hsu for %s/%s.", runtime.GOOS, runtime.GOARCH)
mustRun(
ctx, []string{"CGO_ENABLED=0"}, "go", "build",
"-trimpath",
verboseFlag, "-o", s,
"-ldflags=-s -w "+
"-buildid= "+
"-X main.hakureiPath="+prefix+"/bin/hakurei",
"./cmd/hsu",
)
log.Println()
if runTests {
log.Println("##### Testing Hakurei.")
mustRun(
ctx, "go", "test",
ctx, nil, "go", "test",
"-ldflags=-buildid= -linkmode external -extldflags=-static",
"./...",
)
+30 -4
View File
@@ -7,6 +7,7 @@ package main
import (
"context"
"crypto/rand"
"io"
"log"
"os"
"os/signal"
@@ -19,6 +20,7 @@ import (
"hakurei.app/internal/kobject"
"hakurei.app/internal/report"
"hakurei.app/internal/uevent"
"hakurei.app/message"
)
var r report.Reporter
@@ -50,13 +52,14 @@ func init() {
func fatal(v ...any) {
log.Println(v...)
log.Println("unable to continue, please reboot and resolve the problem manually")
log.SetOutput(io.Discard)
select {}
}
// must calls fatal with err if it is non-nil.
func must(err error) {
if err != nil {
log.Println(err)
fatal(err)
select {}
}
}
@@ -79,6 +82,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 +121,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 +139,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 +209,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 +261,6 @@ func main() {
[]byte("/system/lib/firmware"),
0,
))
go dispatchModprobe(ctx, s)
}
+74
View File
@@ -0,0 +1,74 @@
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(
"%s (exit status %d)",
strings.TrimPrefix(strings.TrimSpace(e.Stderr), "modprobe: "),
e.ExitCode,
)
}
// 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())
}
}
+8 -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, noplace, 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, noplace, device, cover_run, runtime, tmpdir
Home: /data/data/org.chromium.Chromium
Hostname: localhost
Path: /run/current-system/sw/bin/chromium
@@ -354,7 +354,9 @@ App
"tty": true,
"multiarch": true,
"map_real_uid": true,
"noplace": true,
"device": true,
"cover_run": true,
"share_runtime": true,
"share_tmpdir": true
},
@@ -505,7 +507,9 @@ App
"tty": true,
"multiarch": true,
"map_real_uid": true,
"noplace": true,
"device": true,
"cover_run": true,
"share_runtime": true,
"share_tmpdir": true
}
@@ -703,7 +707,9 @@ func TestPrintPs(t *testing.T) {
"tty": true,
"multiarch": true,
"map_real_uid": true,
"noplace": 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
+230 -111
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)
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=/",
},
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",
)
})
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{
"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()
},
).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..."
+25
View File
@@ -2,6 +2,7 @@ package hst
import (
"encoding/json"
"fmt"
"strings"
"syscall"
"time"
@@ -64,10 +65,14 @@ const (
// Some programs fail to connect to dbus session running as a different uid,
// this option works around it by mapping priv-side caller uid in container.
FMapRealUID
// FNoPlace disables placement of /etc/passwd and /etc/group.
FNoPlace
// 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
@@ -98,8 +103,12 @@ func (flags Flags) String() string {
return "tty"
case FMapRealUID:
return "mapuid"
case FNoPlace:
return "noplace"
case FDevice:
return "device"
case FCoverRun:
return "cover_run"
case FShareRuntime:
return "runtime"
case FShareTmpdir:
@@ -161,6 +170,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.
@@ -188,9 +201,13 @@ type containerConfigJSON = struct {
// Corresponds to [FMapRealUID].
MapRealUID bool `json:"map_real_uid"`
// Corresponds to [FNoPlace].
NoPlace bool `json:"noplace,omitempty"`
// 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"`
@@ -213,7 +230,9 @@ func (c *ContainerConfig) MarshalJSON() ([]byte, error) {
Tty: c.Flags&FTty != 0,
Multiarch: c.Flags&FMultiarch != 0,
MapRealUID: c.Flags&FMapRealUID != 0,
NoPlace: c.Flags&FNoPlace != 0,
Device: c.Flags&FDevice != 0,
CoverRun: c.Flags&FCoverRun != 0,
ShareRuntime: c.Flags&FShareRuntime != 0,
ShareTmpdir: c.Flags&FShareTmpdir != 0,
})
@@ -254,9 +273,15 @@ func (c *ContainerConfig) UnmarshalJSON(data []byte) error {
if v.MapRealUID {
c.Flags |= FMapRealUID
}
if v.NoPlace {
c.Flags |= FNoPlace
}
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, noplace, device, cover_run, runtime, tmpdir"},
{"all high", math.MaxUint, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, noplace, 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,"noplace":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)
}
+27 -9
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(
[]*check.Absolute{o.Upper},
o.Lower,
)...)
}
} 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 {
return "w*" + strings.Join(append([]string{
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()),
check.EscapeOverlayDataSegment(o.Work.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/upper", "/tmp/.src0", "/tmp/.src1"),
Upper: fhs.AbsRoot,
}}, m("/"), ms("/tmp/upper", "/tmp/.src0", "/tmp/.src1"),
"e*/:/tmp/upper:/tmp/.src0:/tmp/.src1"},
})
}
+2
View File
@@ -244,7 +244,9 @@ func TestTemplate(t *testing.T) {
"tty": true,
"multiarch": true,
"map_real_uid": true,
"noplace": 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
}
+24 -4
View File
@@ -1,21 +1,29 @@
// Package env provides the [Paths] struct for efficiently building paths from the environment.
// Package env provides the [Paths] struct for efficiently building paths from
// the environment.
package env
import (
"errors"
"io/fs"
"log"
"os"
"strconv"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/hst"
)
const VarRunNscd = fhs.Var + "run/nscd"
// Paths holds paths copied from the environment and is used to create [hst.Paths].
type Paths struct {
// TempDir is returned by [os.TempDir].
TempDir *check.Absolute
// RuntimePath is copied from $XDG_RUNTIME_DIR.
RuntimePath *check.Absolute
// Whether [VarRunNscd] is a directory.
HasNscd bool
}
// Copy expands [Paths] into [hst.Paths].
@@ -37,14 +45,17 @@ func (env *Paths) Copy(v *hst.Paths, userid int) {
}
// CopyPaths returns a populated [Paths].
func CopyPaths() *Paths { return CopyPathsFunc(log.Fatalf, os.TempDir, os.Getenv) }
func CopyPaths() *Paths {
return CopyPathsFunc(log.Fatalf, os.TempDir, os.Getenv, os.Stat)
}
// CopyPathsFunc returns a populated [Paths],
// using the provided [log.Fatalf], [os.TempDir], [os.Getenv] functions.
// CopyPathsFunc returns a populated [Paths], using the provided [log.Fatalf],
// [os.TempDir], [os.Getenv] functions.
func CopyPathsFunc(
fatalf func(format string, v ...any),
tempdir func() string,
getenv func(key string) string,
stat func(name string) (fs.FileInfo, error),
) *Paths {
const xdgRuntimeDir = "XDG_RUNTIME_DIR"
@@ -61,5 +72,14 @@ func CopyPathsFunc(
env.RuntimePath = a
}
if fi, err := stat(VarRunNscd); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
fatalf("%v", err)
panic("unreachable")
}
} else {
env.HasNscd = fi.IsDir()
}
return &env
}
+4 -1
View File
@@ -2,6 +2,7 @@ package env_test
import (
"fmt"
"io/fs"
"reflect"
"testing"
@@ -104,7 +105,9 @@ func TestCopyPaths(t *testing.T) {
t.Fatalf("fatalf: %q, want %q", got, tc.fatal)
}
panic(stub.PanicExit)
}, func() string { return tc.tmp }, func(key string) string { return tc.env[key] })
}, func() string { return tc.tmp }, func(key string) string { return tc.env[key] }, func(name string) (fs.FileInfo, error) {
return nil, fs.ErrNotExist
})
if tc.fatal != "" {
t.Fatalf("copyPaths: expected fatal %q", tc.fatal)
+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 {
+2
View File
@@ -23,6 +23,7 @@ import (
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/hst"
"hakurei.app/internal/env"
"hakurei.app/internal/stub"
"hakurei.app/internal/system"
"hakurei.app/message"
@@ -174,6 +175,7 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
call("cmdOutput", stub.ExpectArgs{container.Nonexistent, os.Stderr, []string{}, "/"}, []byte("0"), nil),
call("tempdir", stub.ExpectArgs{}, container.Nonexistent+"/tmp", nil),
call("lookupEnv", stub.ExpectArgs{"XDG_RUNTIME_DIR"}, wantRuntimePath, nil),
call("stat", stub.ExpectArgs{env.VarRunNscd}, stubFileInfoIsDir(true), nil),
call("getuid", stub.ExpectArgs{}, 1000, nil),
call("getgid", stub.ExpectArgs{}, 100, 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) {
+1 -1
View File
@@ -110,7 +110,7 @@ func newOutcomeState(k syscallDispatcher, msg message.Msg, id *hst.ID, config *h
Paths: env.CopyPathsFunc(k.fatalf, k.tempdir, func(key string) string {
v, _ := k.lookupEnv(key)
return v
}),
}, k.stat),
Container: config.Container,
}
+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() {
+3 -8
View File
@@ -136,16 +136,13 @@ 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).
// spTmpdirOp
Bind(m("/tmp/hakurei.0/tmpdir/9"), fhs.AbsTmp, std.BindWritable).
// spAccountOp
Place(m("/etc/passwd"), []byte("chronos:x:1971:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
// spWaylandOp
Bind(m("/tmp/hakurei.0/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1971/wayland-0"), 0).
@@ -452,7 +449,7 @@ func TestOutcomeRun(t *testing.T) {
Path: m("/nix/store/yqivzpzzn7z5x0lq9hmbzygh45d8rhqd-chromium-start"),
Flags: hst.FUserns | hst.FHostNet | hst.FMapRealUID | hst.FShareRuntime | hst.FShareTmpdir,
Flags: hst.FUserns | hst.FHostNet | hst.FMapRealUID | hst.FNoPlace | hst.FShareRuntime | hst.FShareTmpdir,
},
SystemBus: &hst.BusConfig{
Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"},
@@ -547,8 +544,6 @@ func TestOutcomeRun(t *testing.T) {
Tmpfs(m("/run/user/"), xdgRuntimeDirSize, 0755).
Bind(m("/tmp/hakurei.0/runtime/1"), m("/run/user/1971"), std.BindWritable).
Bind(m("/tmp/hakurei.0/tmpdir/1"), m("/tmp/"), std.BindWritable).
Place(m("/etc/passwd"), []byte("u0_a1:x:1971:100:Hakurei:/var/lib/persist/module/hakurei/0/1:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
Bind(m("/run/user/1971/wayland-0"), m("/run/user/1971/wayland-0"), 0).
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/pipewire"), m("/run/user/1971/pipewire-0"), 0).
Bind(m("/tmp/hakurei.0/8e2c76b066dabe574cf073bdb46eb5c1/bus"), m("/run/user/1971/bus"), 0).
@@ -715,7 +710,7 @@ func (k *stubNixOS) lookupEnv(key string) (string, bool) {
func (k *stubNixOS) stat(name string) (fs.FileInfo, error) {
switch name {
case "/var/run/nscd":
return nil, nil
return stubFileInfoIsDir(true), nil
case "/run/user/1971/pulse":
return nil, nil
case "/run/user/1971/pulse/native":
+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 -4
View File
@@ -71,16 +71,13 @@ 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).
// spTmpdirOp
Bind(m("/tmp/hakurei.10/tmpdir/9999"), fhs.AbsTmp, std.BindWritable).
// spAccountOp
Place(m("/etc/passwd"), []byte("chronos:x:1000:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:100:\n")).
// spWaylandOp
Bind(m("/tmp/hakurei.10/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/wayland"), m("/run/user/1000/wayland-0"), 0).
+13 -10
View File
@@ -6,6 +6,7 @@ import (
"syscall"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/validate"
)
@@ -41,16 +42,18 @@ func (s spAccountOp) toContainer(state *outcomeStateParams) error {
state.env["USER"] = username
state.env["SHELL"] = state.Container.Shell.String()
state.params.
Place(fhs.AbsEtc.Append("passwd"),
[]byte(username+":x:"+
state.mapuid.String()+":"+
state.mapgid.String()+
":Hakurei:"+
state.Container.Home.String()+":"+
state.Container.Shell.String()+"\n")).
Place(fhs.AbsEtc.Append("group"),
[]byte("hakurei:x:"+state.mapgid.String()+":\n"))
if state.Container.Flags&hst.FNoPlace == 0 {
state.params.
Place(fhs.AbsEtc.Append("passwd"),
[]byte(username+":x:"+
state.mapuid.String()+":"+
state.mapgid.String()+
":Hakurei:"+
state.Container.Home.String()+":"+
state.Container.Shell.String()+"\n")).
Place(fhs.AbsEtc.Append("group"),
[]byte("hakurei:x:"+state.mapgid.String()+":\n"))
}
return nil
}
+2 -3
View File
@@ -38,6 +38,7 @@ func TestSpAccountOp(t *testing.T) {
{"success fallback username", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config {
c := hst.Template()
c.Container.Username = ""
c.Container.Flags = hst.FMapRealUID
return c
}, nil, []stub.Call{
// this op performs basic validation and does not make calls during toSystem
@@ -60,9 +61,7 @@ func TestSpAccountOp(t *testing.T) {
// this op configures the container state and does not make calls during toContainer
}, &container.Params{
Dir: config.Container.Home,
Ops: new(container.Ops).
Place(m("/etc/passwd"), []byte("chronos:x:1000:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")).
Place(m("/etc/group"), []byte("hakurei:x:100:\n")),
Ops: new(container.Ops),
}, paramsWantEnv(config, map[string]string{
"HOME": config.Container.Home.String(),
"USER": config.Container.Username,
+18 -9
View File
@@ -18,13 +18,12 @@ import (
"hakurei.app/hst"
"hakurei.app/internal/acl"
"hakurei.app/internal/dbus"
"hakurei.app/internal/env"
"hakurei.app/internal/system"
"hakurei.app/internal/validate"
"hakurei.app/message"
)
const varRunNscd = fhs.Var + "run/nscd"
func init() { gob.Register(new(spParamsOp)) }
// spParamsOp initialises unordered fields of [container.Params] and the
@@ -136,17 +135,23 @@ type spFilesystemOp struct {
}
func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error {
/* retrieve paths and hide them if they're made available in the sandbox;
this feature tries to improve user experience of permissive defaults, and
to warn about issues in custom configuration; it is NOT a security feature
and should not be treated as such, ALWAYS be careful with what you bind */
// retrieve paths and hide them if they're made available in the sandbox
//
// this feature tries to improve user experience of permissive defaults, and
// to warn about issues in custom configuration; it is NOT a security feature
// and should not be treated as such, ALWAYS be careful with what you bind
hidePaths := []string{
state.sc.RuntimePath.String(),
state.sc.SharePath.String(),
}
// this causes emulated passwd database to be bypassed on some /etc/ setups
varRunNscd,
if state.Paths == nil || state.HasNscd {
hidePaths = append(hidePaths,
// this causes emulated passwd database to be bypassed on some /etc/
// setups, made optional to avoid needlessly creating it on
// non-glibc systems when invoking permissive defaults
env.VarRunNscd,
)
}
// dbus.Address does not go through syscallDispatcher
@@ -382,6 +387,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
+411
View File
@@ -0,0 +1,411 @@
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
}
var _ CuresExempt = archiveArtifact{}
// 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
})
}
// Inputs returns a slice containing the backing file.
func (a archiveArtifact) Inputs() []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
}
// CuresExempt exempts the cheap [KindArchive] implementation often found at
// the end of a [FileArtifact] pipeline.
func (archiveArtifact) CuresExempt() {}
+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()))
}
})
}
}
+124
View File
@@ -0,0 +1,124 @@
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)
var _ CuresExempt = 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
})
}
// Inputs returns a slice containing the backing file.
func (a *decompressArtifact) Inputs() []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
}
}
// CuresExempt exempts the cheap [KindDecompress] implementation often part of
// a [FileArtifact] pipeline.
func (*decompressArtifact) CuresExempt() {}
+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,
})
}
})
})
}
}
+17 -3
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
@@ -397,9 +407,9 @@ func init() {
func(r *IRReader) Artifact { return readExecArtifact(r, true) })
}
// Dependencies returns a slice of all artifacts collected from caller-supplied
// Inputs returns a slice of all artifacts collected from caller-supplied
// [ExecPath].
func (a *execArtifact) Dependencies() []Artifact {
func (a *execArtifact) Inputs() []Artifact {
artifacts := make([][]Artifact, 0, len(a.paths))
for _, p := range a.paths {
artifacts = append(artifacts, p.A)
@@ -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,
@@ -606,7 +617,7 @@ func (c *Cache) EnterExec(
return ErrNotExec
}
deps := Collect(a.Dependencies())
deps := Collect(a.Inputs())
if _, _, err = c.Cure(&deps); err == nil {
return errors.New("unreachable")
} else if !IsCollected(err) {
@@ -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
+6 -2
View File
@@ -11,6 +11,7 @@ import (
type fileArtifact []byte
var _ KnownChecksum = new(fileArtifact)
var _ CuresExempt = new(fileArtifact)
// fileArtifactNamed embeds fileArtifact alongside a caller-supplied name.
type fileArtifactNamed struct {
@@ -62,8 +63,8 @@ func init() {
})
}
// Dependencies returns a nil slice.
func (*fileArtifact) Dependencies() []Artifact { return nil }
// Inputs returns a nil slice.
func (*fileArtifact) Inputs() []Artifact { return nil }
// IsExclusive returns false: Cure returns a prepopulated buffer.
func (*fileArtifact) IsExclusive() bool { return false }
@@ -79,3 +80,6 @@ func (a *fileArtifact) Checksum() Checksum {
func (a *fileArtifact) Cure(*RContext) (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(*a)), nil
}
// CuresExempt exempts the cheap [KindFile] implementation.
func (*fileArtifact) CuresExempt() {}
+16 -3
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].
//
@@ -228,7 +241,7 @@ func (ic *irCache) encode(
a Artifact,
inputs map[Artifact]cureRes,
) (err error) {
deps := a.Dependencies()
deps := a.Inputs()
idents := make([]*extIdent, len(deps))
if inputs == nil {
for i, d := range deps {
@@ -317,7 +330,7 @@ func (ic *irCache) encodeAll(
return
}
for _, d := range a.Dependencies() {
for _, d := range a.Inputs() {
if err = ic.encodeAll(w, d, encoded); err != nil {
return
}
+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 {
+2 -2
View File
@@ -54,8 +54,8 @@ func init() {
})
}
// Dependencies returns a nil slice.
func (*httpArtifact) Dependencies() []Artifact { return nil }
// Inputs returns a nil slice.
func (*httpArtifact) Inputs() []Artifact { return nil }
// IsExclusive returns false: Cure returns as soon as a response is received.
func (*httpArtifact) IsExclusive() bool { return false }
+351 -86
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
}
_, t.statusErr = t.status.WriteString(statusHeader)
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
@@ -328,8 +330,30 @@ func (c *common) Open(a Artifact) (r io.ReadCloser, err error) {
type FContext struct {
TContext
// Cured top-level dependencies looked up by Pathname.
deps map[Artifact]cureRes
// Cured top-level inputs looked up by Pathname.
inputs 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
@@ -349,7 +373,7 @@ func (f *FContext) GetArtifact(a Artifact) (
pathname *check.Absolute,
checksum unique.Handle[Checksum],
) {
if res, ok := f.deps[a]; ok {
if res, ok := f.inputs[a]; ok {
return res.pathname, res.checksum
}
panic(InvalidLookupError(f.cache.Ident(a).Value()))
@@ -381,13 +405,13 @@ type Artifact interface {
// Result must remain identical across multiple invocations.
Params(ctx *IContext)
// Dependencies returns a slice of [Artifact] that the current instance
// depends on to produce its contents.
// Inputs returns a slice of [Artifact] the current instance has access to
// while producing its output.
//
// Callers must not modify the retuned slice.
//
// Result must remain identical across multiple invocations.
Dependencies() []Artifact
Inputs() []Artifact
// IsExclusive returns whether the [Artifact] is exclusive. Exclusive
// artifacts might not run in parallel with each other, and are still
@@ -461,6 +485,16 @@ type KnownChecksum interface {
Checksum() Checksum
}
// CuresExempt is optionally implemented for an artifact exempt to the
// cache-wide cures counter and limit.
type CuresExempt interface {
Artifact
// CuresExempt is a no-op function but serves to distinguish implementations
// that are cures-exempt.
CuresExempt()
}
// FileArtifact refers to an [Artifact] backed by a single file.
//
// FileArtifact does not support fine-grained cancellation. Its context is
@@ -508,6 +542,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 +704,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 +768,13 @@ type Cache struct {
// Buffered I/O free list, must not be accessed directly.
brPool, bwPool sync.Pool
// Optional external cache implementation.
extern External
// Caches responses from extern.
externCache map[unique.Handle[ID]]unique.Handle[Checksum]
// 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 +854,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 +907,36 @@ 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.externCache = make(map[unique.Handle[ID]]unique.Handle[Checksum])
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 +987,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 +1112,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 +1212,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,
ent.Name(),
).String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
addErr(dir.Append(ent.Name()), err)
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 +1851,90 @@ func (r *RContext) NewMeasuredReader(
return r.cache.newMeasuredReader(rc, checksum)
}
// tryChecksum dereferences a symlink to a cure outcome.
func (c *Cache) tryChecksum(pathname *check.Absolute) (
checksum unique.Handle[Checksum],
err error,
) {
_, err = os.Lstat(pathname.String())
if err == nil {
var name string
if name, err = os.Readlink(pathname.String()); err != nil {
return
}
buf := c.getIdentBuf()
err = Decode((*Checksum)(buf[:]), filepath.Base(name))
if err == nil {
checksum = unique.Make(Checksum(buf[:]))
}
c.putIdentBuf(buf)
}
return
}
// tryLocal attempts to obtain an [Artifact] outcome from the filesystem.
func (c *Cache) tryLocal(id unique.Handle[ID]) (unique.Handle[Checksum], error) {
return c.tryChecksum(c.base.Append(
dirIdentifier,
Encode(id.Value()),
))
}
// tryExtern attempts to obtain an [Artifact] outcome from extern.
func (c *Cache) tryExtern(ctx context.Context, id unique.Handle[ID]) (
unique.Handle[Checksum],
error,
) {
c.externMu.RLock()
defer c.externMu.RUnlock()
checksum, ok := c.externCache[id]
if !ok {
if c.extern == nil {
return zeroChecksum, nil
}
v, err := c.extern.Artifact(ctx, id)
if err != nil {
return zeroChecksum, err
}
if v == nil {
return zeroChecksum, nil
}
checksum = unique.Make(*v)
var got unique.Handle[Checksum]
if _, got, err = c.Cure(c.extern.Checksum(checksum)); err != nil {
return checksum, err
} else if got != checksum {
return zeroChecksum, &ChecksumMismatchError{got.Value(), checksum.Value()}
}
}
return checksum, nil
}
// cureMany concurrently collects outcome of multiple [Artifact].
func (c *Cache) cureMany(inputs []Artifact, r map[Artifact]cureRes) error {
var wg sync.WaitGroup
wg.Add(len(inputs))
res := make([]cureRes, len(inputs))
errs := make(DependencyCureError, 0, len(inputs))
var errsMu sync.Mutex
for i, d := range inputs {
pending := pendingArtifactDep{d, &res[i], &errs, &errsMu, &wg}
go pending.cure(c)
}
wg.Wait()
if len(errs) > 0 {
return &errs
}
for i, p := range res {
r[inputs[i]] = p
}
return nil
}
// 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) (
@@ -1735,6 +1955,10 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
}
}()
if _, ok := a.(CuresExempt); ok {
curesExempt = true
}
var (
ctx context.Context
done chan<- struct{}
@@ -1746,21 +1970,8 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
defer func() { c.finaliseIdent(done, id, checksum, err) }()
}
_, err = os.Lstat(pathname.String())
if err == nil {
var name string
if name, err = os.Readlink(pathname.String()); err != nil {
return
}
buf := c.getIdentBuf()
err = Decode((*Checksum)(buf[:]), filepath.Base(name))
if err == nil {
checksum = unique.Make(Checksum(buf[:]))
}
c.putIdentBuf(buf)
return
}
if !errors.Is(err, os.ErrNotExist) {
checksum, err = c.tryChecksum(pathname)
if err == nil || !errors.Is(err, os.ErrNotExist) {
return
}
@@ -1781,7 +1992,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,
@@ -1982,30 +2193,14 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
break
case FloodArtifact:
deps := a.Dependencies()
f := FContext{t, make(map[Artifact]cureRes, len(deps))}
var wg sync.WaitGroup
wg.Add(len(deps))
res := make([]cureRes, len(deps))
errs := make(DependencyCureError, 0, len(deps))
var errsMu sync.Mutex
for i, d := range deps {
pending := pendingArtifactDep{d, &res[i], &errs, &errsMu, &wg}
go pending.cure(c)
}
wg.Wait()
if len(errs) > 0 {
err = &errs
inputs := a.Inputs()
f := FContext{t, make(map[Artifact]cureRes, len(inputs))}
if err = c.cureMany(inputs, f.inputs); err != nil {
return
}
for i, p := range res {
f.deps[deps[i]] = p
}
sh := sha512.New384()
err = c.encode(sh, a, f.deps)
err = c.encode(sh, a, f.inputs)
if err != nil {
return
}
@@ -2049,26 +2244,63 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
}
defer f.destroy(&err)
var externChecksum unique.Handle[Checksum]
if externChecksum, 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 {
err = &ChecksumMismatchError{externChecksum.Value(), checksum.Value()}
if c.msg.IsVerbose() {
c.msg.Verbosef("extern %s: %v", reportName(ca, id), err)
}
return
}
var externStatus io.ReadCloser
c.externMu.RLock()
externStatus, err = c.extern.Status(&RContext{common{ctx, c}}, id)
c.externMu.RUnlock()
if err != nil {
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 +2333,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 +2608,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) {
@@ -2433,6 +2696,8 @@ func IsCollected(err error) bool { return errors.As(err, new(Collected)) }
// [pkg.Artifact]. It returns [Collected].
type Collect []Artifact
var _ Artifact = new(Collect)
// Cure returns [Collected].
func (*Collect) Cure(*FContext) error { return Collected{} }
@@ -2442,8 +2707,8 @@ func (*Collect) Kind() Kind { return kindCollection }
// Params is a noop: dependencies are already represented in the header.
func (*Collect) Params(*IContext) {}
// Dependencies returns [Collect] as is.
func (c *Collect) Dependencies() []Artifact { return *c }
// Inputs returns [Collect] as is.
func (c *Collect) Inputs() []Artifact { return *c }
// IsExclusive returns false: Cure is a noop.
func (*Collect) IsExclusive() bool { return false }
+311 -46
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
@@ -135,11 +140,11 @@ type stubArtifact struct {
cure func(t *pkg.TContext) error
}
func (a *stubArtifact) Kind() pkg.Kind { return a.kind }
func (a *stubArtifact) Params(ctx *pkg.IContext) { ctx.Write(a.params) }
func (a *stubArtifact) Dependencies() []pkg.Artifact { return a.deps }
func (a *stubArtifact) Cure(t *pkg.TContext) error { return a.cure(t) }
func (*stubArtifact) IsExclusive() bool { return false }
func (a *stubArtifact) Kind() pkg.Kind { return a.kind }
func (a *stubArtifact) Params(ctx *pkg.IContext) { ctx.Write(a.params) }
func (a *stubArtifact) Inputs() []pkg.Artifact { return a.deps }
func (a *stubArtifact) Cure(t *pkg.TContext) error { return a.cure(t) }
func (*stubArtifact) IsExclusive() bool { return false }
// A stubArtifactF implements [FloodArtifact] with hardcoded behaviour.
type stubArtifactF struct {
@@ -151,11 +156,11 @@ type stubArtifactF struct {
cure func(f *pkg.FContext) error
}
func (a *stubArtifactF) Kind() pkg.Kind { return a.kind }
func (a *stubArtifactF) Params(ctx *pkg.IContext) { ctx.Write(a.params) }
func (a *stubArtifactF) Dependencies() []pkg.Artifact { return a.deps }
func (a *stubArtifactF) Cure(f *pkg.FContext) error { return a.cure(f) }
func (a *stubArtifactF) IsExclusive() bool { return a.excl }
func (a *stubArtifactF) Kind() pkg.Kind { return a.kind }
func (a *stubArtifactF) Params(ctx *pkg.IContext) { ctx.Write(a.params) }
func (a *stubArtifactF) Inputs() []pkg.Artifact { return a.deps }
func (a *stubArtifactF) Cure(f *pkg.FContext) error { return a.cure(f) }
func (a *stubArtifactF) IsExclusive() bool { return a.excl }
// A stubFile implements [FileArtifact] with hardcoded behaviour.
type stubFile struct {
@@ -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,8 +577,9 @@ 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()))
t.Fatal(err)
if !errors.Is(err, syscall.ENOTEMPTY) {
t.Fatal(err)
}
}
// destroy empty fault directory
@@ -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)
}
@@ -1661,7 +1883,7 @@ func (earlyFailureF) Kind() pkg.Kind { return pkg.KindExec }
func (earlyFailureF) Params(*pkg.IContext) {}
func (earlyFailureF) IsExclusive() bool { return false }
func (a earlyFailureF) Dependencies() []pkg.Artifact {
func (a earlyFailureF) Inputs() []pkg.Artifact {
deps := make([]pkg.Artifact, a)
for i := range deps {
deps[i] = a - 1
@@ -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) {
+25 -68
View File
@@ -2,34 +2,22 @@ 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
}
var _ CuresExempt = new(tarArtifact)
// tarArtifactNamed embeds tarArtifact for a [fmt.Stringer] tarball.
type tarArtifactNamed struct {
tarArtifact
@@ -39,13 +27,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 +42,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)
}
@@ -80,8 +58,8 @@ func init() {
})
}
// Dependencies returns a slice containing the backing file.
func (a *tarArtifact) Dependencies() []Artifact {
// Inputs returns a slice containing the backing file.
func (a *tarArtifact) Inputs() []Artifact {
return []Artifact{a.f}
}
@@ -98,42 +76,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 +109,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 +136,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 {
@@ -260,3 +213,7 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
}
return
}
// CuresExempt exempts the cheap [KindTar] implementation often at the end of a
// [FileArtifact] pipeline.
func (*tarArtifact) CuresExempt() {}
+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,
)
}
+2 -2
View File
@@ -26,8 +26,8 @@ func (a busyboxBin) Params(*pkg.IContext) {}
// IsExclusive returns false: Cure performs a trivial filesystem write.
func (busyboxBin) IsExclusive() bool { return false }
// Dependencies returns the underlying busybox [pkg.FileArtifact].
func (a busyboxBin) Dependencies() []pkg.Artifact {
// Inputs returns the underlying busybox [pkg.FileArtifact].
func (a busyboxBin) Inputs() []pkg.Artifact {
return []pkg.Artifact{a.bin}
}
+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
}
+25 -26
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)
@@ -113,36 +118,30 @@ func init() {
// IsExclusive returns false: Cure performs a few trivial filesystem writes.
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}
// Inputs returns a slice containing the backing iana-etc release.
func (a cureEtc) Inputs() []pkg.Artifact {
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(
"Mic92/iana-etc",
version,
"iana-etc-"+version+".tar.gz",
checksum,
pkg.TarGzip,
)
}
// ianaEtc is an unpacked iana-etc release.
var ianaEtc = newFromGitHubRelease(
"Mic92/iana-etc",
"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,' \
+10 -12
View File
@@ -190,8 +190,8 @@ ln -s \
})
const (
version = "22.1.6"
checksum = "vwMyqgc_09d__u49IlXGlxnp8eFlvpibhYQgVxQg-sGdoZlzyapMV4uX4UAjE7rA"
version = "22.1.8"
checksum = "_QzoDE0W6cv0vfIWeLPDvqG_vKhz6IGWl1nqUUvhlKWiRzf5dMGqeC3tyS2XgI6j"
)
native.MustRegister("llvm-project", func(t Toolchain) (*Metadata, pkg.Artifact) {
@@ -341,6 +341,13 @@ ln -s \
return &meta, t.NewPackage("llvm", meta.Version, source, &PackageAttr{
Flag: TExclusive,
Bin: []string{
"chmod",
"mkdir",
"rm",
"tr",
"awk",
},
}, &CMakeHelper{
Append: []string{"llvm"},
@@ -358,16 +365,7 @@ ln -s clang++ /work/system/bin/c++
// on 3-stage determinism to test later stages.
SkipTest: t.stage.isStage0(),
Test: `
chmod +w /bin && ln -s \
../system/bin/chmod \
../system/bin/mkdir \
../system/bin/rm \
../system/bin/tr \
../system/bin/awk \
/bin
ninja ` + jobsFlagE + ` check-all
`,
Test: "ninja " + jobsFlagE + " check-all",
},
_python,
_perl,
+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)
}
}
+5
View File
@@ -18,6 +18,8 @@ type MesonHelper struct {
// Flags passed to the setup command.
Setup []KV
// Test suites to skip.
SkipTests []string
// Whether to skip meson test.
SkipTest bool
// Run tests with interactive input/output.
@@ -61,6 +63,9 @@ meson test \
scriptTest += ` \
--interactive`
}
for _, suite := range attr.SkipTests {
scriptTest += " \\\n\t--no-suite='" + suite + "'"
}
}
return `
+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),
)
}
}
@@ -1,26 +0,0 @@
From 8a80d895dfd779373363c3a4b62ecce5a549efb2 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Sat, 30 Mar 2024 10:17:10 +0100
Subject: tools/attr.c: Add missing libgen.h include for basename(3)
Fixes compilation issue with musl and modern C99 compilers.
See: https://bugs.gentoo.org/926294
---
tools/attr.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/tools/attr.c b/tools/attr.c
index f12e4af..6a3c1e9 100644
--- a/tools/attr.c
+++ b/tools/attr.c
@@ -28,6 +28,7 @@
#include <errno.h>
#include <string.h>
#include <locale.h>
+#include <libgen.h>
#include <attr/attributes.h>
--
cgit v1.1
+4 -4
View File
@@ -1,7 +1,7 @@
diff --git a/test/attr.test b/test/attr.test
index 6ce2f9b..e9bde92 100644
--- a/test/attr.test
+++ b/test/attr.test
diff --git a/test/attr.run b/test/attr.run
index 27d17f6..a4d60c0 100644
--- a/test/attr.run
+++ b/test/attr.run
@@ -11,7 +11,7 @@ Try various valid and invalid names
$ touch f
+13 -13
View File
@@ -3,25 +3,25 @@ package attr {
website = "https://savannah.nongnu.org/projects/attr";
anitya = 137;
version# = "2.5.2";
version# = "2.6.0";
source = remoteTar {
url = "https://download.savannah.nongnu.org/releases/attr/"+
"attr-"+version+".tar.gz";
checksum = "YWEphrz6vg1sUMmHHVr1CRo53pFXRhq_pjN-AlG8UgwZK1y6m7zuDhxqJhD0SV0l";
checksum = "pp-NvD1cMIwZycNwZGW2ez-PbTEpHxrRnVH27csj9QdN4oEBkEbJOZX1IIvKnTWS";
compress = gzip;
};
patches = [
"libgen-basename.patch",
"musl-errno.patch",
];
early = `
ln -s ../../system/bin/perl /usr/bin
`;
patches = [ "musl-errno.patch" ];
bin = [ "perl" ];
populateUsrBin = true;
exec = make {};
inputs = [ perl ];
inputs = [
perl,
kernel-headers,
];
}
package acl {
@@ -29,11 +29,11 @@ package acl {
website = "https://savannah.nongnu.org/projects/acl";
anitya = 16;
version# = "2.3.2";
version# = "2.4.0";
source = remoteTar {
url = "https://download.savannah.nongnu.org/releases/acl/"+
"acl-"+version+".tar.gz";
checksum = "-fY5nwH4K8ZHBCRXrzLdguPkqjKI6WIiGu4dBtrZ1o0t6AIU73w8wwJz_UyjIS0P";
checksum = "U2eaAsWrhhzEfhyprwHQQG55bFPZxtsHk1Usnd9Jb_g-H__pJod4H3RTrL4ipXDw";
compress = gzip;
};
@@ -42,6 +42,6 @@ package acl {
skipCheck = true;
};
inputs = [ attr ];
inputs = [ attr, kernel-headers ];
runtime = [ attr ];
}
+2 -2
View File
@@ -3,12 +3,12 @@ package cmake {
website = "https://cmake.org";
anitya = 306;
version# = "4.3.3";
version# = "4.3.4";
source = remoteGitHubRelease {
suffix = "Kitware/CMake";
tag = "v"+version;
name = "cmake-"+version+".tar.gz";
checksum = "VS-b6cN4S9hfNv3JOUAbAfI9nh3EeuVwY_IVgUdgq6VKwvfchhXwvvFAUcpZG6Ez";
checksum = "6A50pqarKDXKOCv7ffKMvWJWmNTVp_ep8KoaWwNdg-RYQRddgKbxayHveTo6jZ_7";
compress = gzip;
};
patches = [
+2 -2
View File
@@ -3,10 +3,10 @@ package curl {
website = "https://curl.se";
anitya = 381;
version# = "8.20.0";
version# = "8.21.0";
source = remoteTar {
url = "https://curl.se/download/curl-"+version+".tar.bz2";
checksum = "xyHXwrngIRGMasuzhn-I5MSCOhktwINbsWt1f_LuR-5jRVvyx_g6U1EQfDLEbr9r";
checksum = "lJSm8bVjS0OmsarEdbvejdQdvXsb7yGarlr6oMtA9FW1EXOga8zZxa1LPtfaq_qX";
compress = bzip2;
};
+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 firmware {
website = "https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git";
anitya = 141464;
version# = "20260519";
version# = "20260622";
source = remoteGitLab {
domain = "gitlab.com";
suffix = "kernel-firmware/linux-firmware";
ref = version;
checksum = "l-wBRTWclYnJsgV4qtUV1-UL5Y4nknAPre8CMe0dH7PxtAqbaeudEIM_Fnuj0TCV";
checksum = "gpytkDM58EVjaUuryOekcekh_rWsfuv3S7LQxopHlNlVGmMsuqNwfug4BjgTgizE";
};
// dedup creates temporary file
+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;
};
+6 -4
View File
@@ -3,24 +3,25 @@ package git {
website = "https://www.git-scm.com";
anitya = 5350;
version# = "2.54.0";
version# = "2.55.0";
source = remoteTar {
url = "https://www.kernel.org/pub/software/scm/git/"+
"git-"+version+".tar.gz";
checksum = "7vGKtFOJGqY8DO4e8UMRax7dLgImXKQz5MMalec6MlgYrsarffSJjgOughwRFpSH";
checksum = "lOer6jb8vZQk6Nd1rLZIgsYys8whLvjJq8XbmCdTATcPFAYuQcK1Rgj1Jj6a00W8";
compress = gzip;
};
early = `
ln -s ../../system/bin/perl /usr/bin/ || true
# test suite assumes apache
rm -f /system/bin/httpd
`;
// uses source tree as scratch space
enterSource = true;
env = [ "NO_RUST=YesPlease" ];
bin = [ "perl" ];
populateUsrBin = true;
exec = make {
inPlace = true;
generate = "make configure";
@@ -85,6 +86,7 @@ disable_test t5515-fetch-merge-logic
zlib,
curl,
libexpat,
kernel-headers,
];
runtime = [
+4 -2
View File
@@ -3,11 +3,11 @@ package glib {
website = "https://developer.gnome.org/glib";
anitya = 10024;
version# = "2.89.0";
version# = "2.89.1";
source = remoteGit {
url = "https://gitlab.gnome.org/GNOME/glib.git";
tag = version;
checksum = "4FXKhdS3pC98LevYa_h7piRylG86cZ_c9zAtGr78oHodU1ob8rBxGU0hoIZ4nzcA";
checksum = "9_6Eew2KIwa1AHopjU7CqC13_nur5FPJMu-iGUd7sD_1gAM1pa_HVUuAtqExJoYU";
};
files = {
@@ -19,6 +19,8 @@ package glib {
setup = {
"Ddefault_library": "both";
};
// fails with ipv6 disabled
skipTests = [ "gio" ];
};
inputs = [
+10 -6
View File
@@ -155,6 +155,13 @@ package sed {
compress = gzip;
};
writable = true;
early = `
test_disable() { chmod +w "$2" && echo "$1" > "$2"; }
test_disable '#!/bin/sh' testsuite/read-error-stale-errno.sh
`;
exec = make {};
inputs = [
@@ -504,17 +511,14 @@ package parallel {
website = "https://www.gnu.org/software/parallel";
anitya = 5448;
version# = "20260422";
version# = "20260622";
source = remoteTar {
url = "https://ftpmirror.gnu.org/gnu/parallel/parallel-"+version+".tar.bz2";
checksum = "eTsepxgqhXpMEhPd55qh-W5y4vjKn0x9TD2mzbJCNZYtFf4lT4Wzoqr74HGJYBEH";
checksum = "pEkZ_J18AvKCMoPfxEprC0Smw7zP6qXASmC1uFf_HVD_jc95H06LvzGi40-mtCUo";
compress = bzip2;
};
early = `
ln -s ../system/bin/bash /bin/
`;
bin = [ "bash" ];
exec = make {};
inputs = [
+2 -1
View File
@@ -34,7 +34,8 @@ echo 'int main(){return 0;}' > tests/mini-dtls-fragments.c
"with-zstd": "link";
"": arch {
arm64 = "disable-hardware-acceleration";
// miscompilation on arm64
arm64 = "--disable-hardware-acceleration";
};
};
};

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