omakit 0.5.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -45
- package/blocks/history.json +68 -0
- package/blocks/run/NOTICE +12 -0
- package/blocks/run/Run.qml +242 -0
- package/blocks/run/run-supervisor.py +522 -0
- package/blocks/store/NOTICE +12 -0
- package/blocks/store/Store.qml +157 -0
- package/blocks/store/store-helper.py +431 -0
- package/package.json +12 -5
- package/skills/omarchy-plugin-audit/SKILL.md +11 -5
- package/skills/omarchy-plugin-build/SKILL.md +164 -0
- package/skills/omarchy-plugin-check/SKILL.md +6 -3
- package/skills/omarchy-plugin-submit/SKILL.md +4 -1
- package/skills/omarchy-plugin-validation-watch/SKILL.md +5 -2
- package/skills/omarchy-plugin-weigh/SKILL.md +13 -2
- package/tests/fixtures/weigh/clean/Widget.qml +19 -0
- package/tests/fixtures/weigh/clean/manifest.json +9 -0
- package/tests/fixtures/weigh/clean/tests/harness.qml +7 -0
- package/tests/fixtures/weigh/idle-panel/Panel.qml +65 -0
- package/tests/fixtures/weigh/idle-panel/manifest.json +9 -0
- package/tests/fixtures/weigh/poller/Service.qml +50 -0
- package/tests/fixtures/weigh/poller/manifest.json +9 -0
- package/tests/fixtures/weigh/timer-180ms/Widget.qml +25 -0
- package/tests/fixtures/weigh/timer-180ms/manifest.json +9 -0
- package/tests/lab/run/harness/scenarios/controls.sh +6 -0
- package/tests/lab/run/harness/scenarios/envprobe.sh +10 -0
- package/tests/lab/run/harness/scenarios/forge.sh +11 -0
- package/tests/lab/run/harness/scenarios/holder.sh +5 -0
- package/tests/lab/run/harness/scenarios/orphan.sh +6 -0
- package/tests/lab/run/harness/scenarios/stall.sh +5 -0
- package/tests/lab/run/harness/scenarios/stubborn.sh +5 -0
- package/tests/lab/run/harness/scenarios/tree.sh +7 -0
- package/tests/lab/run/harness/shell.qml +84 -0
- package/tests/lab/run/report.py +217 -0
- package/tests/lab/run/suite.sh +106 -0
- package/tests/lab/store/harness/shell.qml +73 -0
- package/tests/lab/store/report.py +133 -0
- package/tests/lab/store/suite.sh +109 -0
- package/tests/parity/corpus.mjs +8 -3
- package/tests/parity/run.mjs +4 -4
- package/tools/audit/audit.mjs +17 -6
- package/tools/audit/git.mjs +3 -3
- package/tools/audit/report.mjs +31 -5
- package/tools/blocks/add.mjs +138 -0
- package/tools/blocks/commit.json +5 -0
- package/tools/blocks/record-commit.mjs +77 -0
- package/tools/blocks/registry.mjs +191 -0
- package/tools/blocks/stamp.mjs +61 -0
- package/tools/inspect/contract.mjs +36 -5
- package/tools/inspect/helpers.mjs +217 -0
- package/tools/inspect/inspect.mjs +68 -3
- package/tools/inspect/patterns.mjs +18 -3
- package/tools/inspect/processes.mjs +38 -5
- package/tools/inspect/report.mjs +18 -2
- package/tools/inspect/writes.mjs +22 -4
- package/tools/lab/guest.mjs +155 -0
- package/tools/lab/harness.sh +119 -0
- package/tools/lab/host.mjs +177 -0
- package/tools/lab/inspect.mjs +240 -0
- package/tools/lab/omarchy.gpg +13 -0
- package/tools/lab/patches/omarchy-iso-test.patch +351 -0
- package/tools/lab/paths.mjs +173 -0
- package/tools/lab/pin.json +42 -0
- package/tools/lab/pin.mjs +64 -0
- package/tools/lab/prune.mjs +68 -0
- package/tools/lab/qemu.mjs +153 -0
- package/tools/lab/qmp-cli.mjs +21 -0
- package/tools/lab/report.mjs +183 -0
- package/tools/lab/run.mjs +344 -0
- package/tools/lab/setup.mjs +430 -0
- package/tools/lab/suites/run.sh +35 -0
- package/tools/lab/suites/store.sh +41 -0
- package/tools/lab/suites/weigh.sh +196 -0
- package/tools/lab/suites.mjs +142 -0
- package/tools/lab/verify.mjs +134 -0
- package/tools/marketplace/README.md +38 -1
- package/tools/marketplace/banner.mjs +23 -2
- package/tools/marketplace/cli.mjs +449 -146
- package/tools/marketplace/completion-check.mjs +27 -1
- package/tools/marketplace/completion.mjs +32 -4
- package/tools/marketplace/doctor.mjs +47 -9
- package/tools/marketplace/github.mjs +52 -6
- package/tools/marketplace/local-transport.mjs +1 -1
- package/tools/marketplace/options.mjs +16 -5
- package/tools/marketplace/outcome.mjs +244 -0
- package/tools/marketplace/pin.mjs +178 -33
- package/tools/marketplace/setup.mjs +16 -15
- package/tools/marketplace/tree.mjs +1 -1
- package/tools/marketplace/upgrade.mjs +5 -5
- package/tools/marketplace/usage.mjs +116 -72
- package/tools/subject/resolve.mjs +19 -6
- package/tools/weigh/audit.mjs +47 -16
- package/tools/weigh/config.mjs +105 -24
- package/tools/weigh/list.mjs +10 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
diff --git c/bin/omarchy-iso-test w/bin/omarchy-iso-test
|
|
2
|
+
index 281cf56..be30127 100755
|
|
3
|
+
--- c/bin/omarchy-iso-test
|
|
4
|
+
+++ w/bin/omarchy-iso-test
|
|
5
|
+
@@ -21,10 +21,18 @@
|
|
6
|
+
# --sync-omarchy DIR Push DIR's test/acceptance into the guest before running
|
|
7
|
+
# (defaults to OMARCHY_PATH when the suite is present)
|
|
8
|
+
# --sync-all DIR Push all of DIR (bin/config/shell/test) into the guest
|
|
9
|
+
+# --dev-link With --sync-all, start a fresh graphical session linked
|
|
10
|
+
+# to the checkout so the whole desktop uses local source
|
|
11
|
+
+# --host-test FILE Source FILE after boot; it must define omarchy_host_test
|
|
12
|
+
+# and may use the harness QMP/SSH/artifact helpers
|
|
13
|
+
+# --host-test-only Run only --host-test after source activation, skipping
|
|
14
|
+
+# the broad desktop and in-guest acceptance suites
|
|
15
|
+
# --port PORT Host port forwarded to guest SSH (default 2222)
|
|
16
|
+
# --memory MB VM memory (default 8192)
|
|
17
|
+
# --timeout SECS Install timeout (default 2400)
|
|
18
|
+
# --keep-running Leave the acceptance VM running after the suite finishes
|
|
19
|
+
+# --discard-overlay Delete the disposable run disk after the VM stops
|
|
20
|
+
+# --keep-overlay Retain the run disk (overrides --discard-overlay)
|
|
21
|
+
# --no-preview Do not open the collected screenshots in imv
|
|
22
|
+
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
@@ -42,12 +50,17 @@ PROVISION=false
|
|
25
|
+
REUSE_BASE=false
|
|
26
|
+
INSTALL_ONLY=false
|
|
27
|
+
KEEP_RUNNING=false
|
|
28
|
+
+DISCARD_OVERLAY=false
|
|
29
|
+
NO_PREVIEW=false
|
|
30
|
+
SYNC_DIR=""
|
|
31
|
+
SYNC_ALL=false
|
|
32
|
+
+DEV_LINK=false
|
|
33
|
+
+HOST_TEST=""
|
|
34
|
+
+HOST_TEST_ONLY=false
|
|
35
|
+
|
|
36
|
+
GUEST_USER="omarchy"
|
|
37
|
+
GUEST_PASSWORD="omarchy"
|
|
38
|
+
+GREETER_PATTERN='Opinionated\|Beautiful, Fun & Agentic Linux by DHH'
|
|
39
|
+
GUEST_HOSTNAME="omarchy-test"
|
|
40
|
+
|
|
41
|
+
while (($#)); do
|
|
42
|
+
@@ -57,9 +70,14 @@ while (($#)); do
|
|
43
|
+
--reuse-base) REUSE_BASE=true ;;
|
|
44
|
+
--install-only) INSTALL_ONLY=true ;;
|
|
45
|
+
--keep-running) KEEP_RUNNING=true ;;
|
|
46
|
+
+ --discard-overlay) DISCARD_OVERLAY=true ;;
|
|
47
|
+
+ --keep-overlay) DISCARD_OVERLAY=false ;;
|
|
48
|
+
--no-preview) NO_PREVIEW=true ;;
|
|
49
|
+
--sync-omarchy) SYNC_DIR="$2"; shift ;;
|
|
50
|
+
--sync-all) SYNC_DIR="$2"; SYNC_ALL=true; shift ;;
|
|
51
|
+
+ --dev-link) DEV_LINK=true ;;
|
|
52
|
+
+ --host-test) HOST_TEST="$2"; shift ;;
|
|
53
|
+
+ --host-test-only) HOST_TEST_ONLY=true ;;
|
|
54
|
+
--port) SSH_PORT="$2"; shift ;;
|
|
55
|
+
--memory) MEMORY="$2"; shift ;;
|
|
56
|
+
--timeout) INSTALL_TIMEOUT="$2"; shift ;;
|
|
57
|
+
@@ -69,6 +87,23 @@ while (($#)); do
|
|
58
|
+
shift
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
+if $DEV_LINK && ! $SYNC_ALL; then
|
|
62
|
+
+ echo "--dev-link requires --sync-all DIR" >&2
|
|
63
|
+
+ exit 1
|
|
64
|
+
+fi
|
|
65
|
+
+
|
|
66
|
+
+if [[ -n $HOST_TEST ]]; then
|
|
67
|
+
+ HOST_TEST=$(realpath -e "$HOST_TEST") || {
|
|
68
|
+
+ echo "Host test not found: $HOST_TEST" >&2
|
|
69
|
+
+ exit 1
|
|
70
|
+
+ }
|
|
71
|
+
+fi
|
|
72
|
+
+
|
|
73
|
+
+if $HOST_TEST_ONLY && [[ -z $HOST_TEST ]]; then
|
|
74
|
+
+ echo "--host-test-only requires --host-test FILE" >&2
|
|
75
|
+
+ exit 1
|
|
76
|
+
+fi
|
|
77
|
+
+
|
|
78
|
+
if [[ -z $SYNC_DIR && -f ${OMARCHY_PATH:-}/test/acceptance ]]; then
|
|
79
|
+
SYNC_DIR="$OMARCHY_PATH"
|
|
80
|
+
fi
|
|
81
|
+
@@ -83,10 +118,21 @@ if [[ ! -f ${ISO:-} ]]; then
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
-omarchy-pkg-add qemu-full edk2-ovmf socat imagemagick tesseract tesseract-data-eng
|
|
86
|
+
+for required_command in qemu-system-x86_64 qemu-img socat magick tesseract; do
|
|
87
|
+
+ if ! command -v "$required_command" >/dev/null; then
|
|
88
|
+
+ echo "Missing host prerequisite: $required_command" >&2
|
|
89
|
+
+ exit 1
|
|
90
|
+
+ fi
|
|
91
|
+
+done
|
|
92
|
+
|
|
93
|
+
OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
|
|
94
|
+
OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
|
|
95
|
+
+for firmware in "$OVMF_CODE" "$OVMF_VARS_TEMPLATE"; do
|
|
96
|
+
+ if [[ ! -r $firmware ]]; then
|
|
97
|
+
+ echo "Missing readable UEFI firmware: $firmware" >&2
|
|
98
|
+
+ exit 1
|
|
99
|
+
+ fi
|
|
100
|
+
+done
|
|
101
|
+
|
|
102
|
+
BASE_NAME="$(basename "$ISO" .iso)"
|
|
103
|
+
$ENCRYPT && BASE_NAME+="-encrypted"
|
|
104
|
+
@@ -121,6 +167,52 @@ qmp() {
|
|
105
|
+
timeout 5 socat -t 2 - "UNIX-CONNECT:$QMP_SOCK" 2>/dev/null || true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
+qmp_pointer_move() {
|
|
109
|
+
+ local width="$1" height="$2" x="$3" y="$4" qx qy response
|
|
110
|
+
+
|
|
111
|
+
+ if ((width < 2 || height < 2 || x < 0 || y < 0 || x >= width || y >= height)); then
|
|
112
|
+
+ printf 'invalid QMP pointer geometry: viewport=%sx%s point=%s,%s\n' \
|
|
113
|
+
+ "$width" "$height" "$x" "$y" >&2
|
|
114
|
+
+ return 2
|
|
115
|
+
+ fi
|
|
116
|
+
+ qx=$((x * 32767 / (width - 1)))
|
|
117
|
+
+ qy=$((y * 32767 / (height - 1)))
|
|
118
|
+
+ response=$(qmp "\"input-send-event\", \"arguments\": {\"events\": [
|
|
119
|
+
+ {\"type\":\"abs\",\"data\":{\"axis\":\"x\",\"value\":$qx}},
|
|
120
|
+
+ {\"type\":\"abs\",\"data\":{\"axis\":\"y\",\"value\":$qy}}
|
|
121
|
+
+ ]}")
|
|
122
|
+
+ if grep -q '"error"' <<<"$response"; then
|
|
123
|
+
+ printf 'QMP pointer move failed: %s\n' "$response" >&2
|
|
124
|
+
+ return 1
|
|
125
|
+
+ fi
|
|
126
|
+
+}
|
|
127
|
+
+
|
|
128
|
+
+qmp_pointer_tap() {
|
|
129
|
+
+ local width="$1" height="$2" x="$3" y="$4" button="${5:-left}" response
|
|
130
|
+
+
|
|
131
|
+
+ case $button in
|
|
132
|
+
+ left | right | middle) ;;
|
|
133
|
+
+ *)
|
|
134
|
+
+ printf 'invalid QMP pointer button: %s\n' "$button" >&2
|
|
135
|
+
+ return 2
|
|
136
|
+
+ ;;
|
|
137
|
+
+ esac
|
|
138
|
+
+ qmp_pointer_move "$width" "$height" 1 1
|
|
139
|
+
+ sleep 0.1
|
|
140
|
+
+ qmp_pointer_move "$width" "$height" "$x" "$y"
|
|
141
|
+
+ sleep 0.1
|
|
142
|
+
+ response=$(qmp "\"input-send-event\", \"arguments\": {\"events\": [
|
|
143
|
+
+ {\"type\":\"btn\",\"data\":{\"down\":true,\"button\":\"$button\"}}
|
|
144
|
+
+ ]}")
|
|
145
|
+
+ grep -q '"error"' <<<"$response" && return 1
|
|
146
|
+
+ sleep 0.12
|
|
147
|
+
+ response=$(qmp "\"input-send-event\", \"arguments\": {\"events\": [
|
|
148
|
+
+ {\"type\":\"btn\",\"data\":{\"down\":false,\"button\":\"$button\"}}
|
|
149
|
+
+ ]}")
|
|
150
|
+
+ grep -q '"error"' <<<"$response" && return 1
|
|
151
|
+
+ sleep 0.4
|
|
152
|
+
+}
|
|
153
|
+
+
|
|
154
|
+
screendump() {
|
|
155
|
+
qmp "\"screendump\", \"arguments\": {\"filename\": \"$1\"}" >/dev/null
|
|
156
|
+
}
|
|
157
|
+
@@ -188,6 +280,15 @@ cleanup() {
|
|
158
|
+
if ! $KEEP_RUNNING; then
|
|
159
|
+
vm_running && log "Stopping test VM"
|
|
160
|
+
stop_vm
|
|
161
|
+
+
|
|
162
|
+
+ if $DISCARD_OVERLAY && [[ -f $RUN_DIR/run.qcow2 ]]; then
|
|
163
|
+
+ if vm_running; then
|
|
164
|
+
+ echo "WARN: VM did not stop; preserving its run overlay: $RUN_DIR/run.qcow2" >&2
|
|
165
|
+
+ else
|
|
166
|
+
+ rm -f -- "$RUN_DIR/run.qcow2"
|
|
167
|
+
+ log "Discarded the stopped VM overlay; run evidence remains in $RUN_DIR"
|
|
168
|
+
+ fi
|
|
169
|
+
+ fi
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
rm -f "$RUN_DIR/.screen.ppm" "$RUN_DIR/.screen.png"
|
|
173
|
+
@@ -355,7 +456,10 @@ wait_for_ssh() {
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
session_started() {
|
|
177
|
+
- ssh_guest "ls /run/user/1000/hypr 2>/dev/null | grep -q ." 2>/dev/null
|
|
178
|
+
+ # Hyprland leaves its runtime directory behind briefly after logout, while
|
|
179
|
+
+ # SDDM starts a different compositor as the sddm user. Only the compositor
|
|
180
|
+
+ # owned by the test user proves that the Omarchy session is actually ready.
|
|
181
|
+
+ ssh_guest "pgrep -u \$(id -u) -x Hyprland >/dev/null" 2>/dev/null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
ssh_session() {
|
|
185
|
+
@@ -367,7 +471,8 @@ ssh_session() {
|
|
186
|
+
export LANG=\${LANG:-C.UTF-8}; \
|
|
187
|
+
export HYPRLAND_INSTANCE_SIGNATURE=\$(ls -t \$XDG_RUNTIME_DIR/hypr | head -1); \
|
|
188
|
+
export WAYLAND_DISPLAY=\$(find \$XDG_RUNTIME_DIR -maxdepth 1 -name 'wayland-*' ! -name '*.lock' -printf '%f\\n' | head -1); \
|
|
189
|
+
- export OMARCHY_PATH=/usr/share/omarchy; \
|
|
190
|
+
+ [[ ! -f /etc/omarchy.conf ]] || source /etc/omarchy.conf; \
|
|
191
|
+
+ export OMARCHY_PATH=\${OMARCHY_PATH:-/usr/share/omarchy}; \
|
|
192
|
+
export PATH=\$OMARCHY_PATH/bin:\$PATH; \
|
|
193
|
+
$command" </dev/null
|
|
194
|
+
}
|
|
195
|
+
@@ -399,6 +504,20 @@ guest_layer_absent() {
|
|
196
|
+
! guest_layer_present "$1"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
+session_stopped() {
|
|
200
|
+
+ ! session_started
|
|
201
|
+
+}
|
|
202
|
+
+
|
|
203
|
+
+clear_startup_notifications() {
|
|
204
|
+
+ [[ -n $HOST_TEST ]] || return 0
|
|
205
|
+
+
|
|
206
|
+
+ wait_for_guest_state "notification service is ready" 15 \
|
|
207
|
+
+ ssh_session "omarchy-shell notifications ping >/dev/null" || return 1
|
|
208
|
+
+ ssh_session "omarchy-shell notifications dismissAll >/dev/null" || return 1
|
|
209
|
+
+ wait_for_guest_state "startup notifications are dismissed" 15 \
|
|
210
|
+
+ guest_layer_absent "omarchy-notifications"
|
|
211
|
+
+}
|
|
212
|
+
+
|
|
213
|
+
cleanup_shortcut_state() {
|
|
214
|
+
ssh_session 'for plugin in omarchy.launcher omarchy.menu omarchy.emojis omarchy.clipboard omarchy.audio; do
|
|
215
|
+
omarchy-shell shell hide "$plugin" >/dev/null 2>&1 || true
|
|
216
|
+
@@ -607,7 +726,10 @@ drive_configurator() {
|
|
217
|
+
log "Driving the installer wizard"
|
|
218
|
+
|
|
219
|
+
# A greeter (animated logo + tagline) opens the installer; Return begins.
|
|
220
|
+
- wait_for_screen "Opinionated" 300
|
|
221
|
+
+ # Released installers before v4.0.3 use the original slogan; v4.0.3 uses
|
|
222
|
+
+ # the exact replacement below. Keep this allowlist explicit so unrelated
|
|
223
|
+
+ # OCR text cannot be mistaken for an installer-ready signal.
|
|
224
|
+
+ wait_for_screen "$GREETER_PATTERN" 300
|
|
225
|
+
capture_console "success-installer-00-greeter"
|
|
226
|
+
press ret
|
|
227
|
+
|
|
228
|
+
@@ -631,7 +753,10 @@ drive_configurator() {
|
|
229
|
+
capture_console "success-installer-04-prepare-disk-warning-encrypted"
|
|
230
|
+
if ! $ENCRYPT; then
|
|
231
|
+
press ctrl-c
|
|
232
|
+
- wait_for_screen "without encryption" 30
|
|
233
|
+
+ # The selected confirmation label is light text on a light background,
|
|
234
|
+
+ # which Tesseract intentionally loses. The prompt itself stays put, so
|
|
235
|
+
+ # give the Ctrl+C state change time to render and capture it as proof.
|
|
236
|
+
+ sleep 2
|
|
237
|
+
capture_console "success-installer-05-prepare-disk-warning-unencrypted"
|
|
238
|
+
fi
|
|
239
|
+
press ret
|
|
240
|
+
@@ -695,7 +820,10 @@ drive_configurator() {
|
|
241
|
+
capture_console "success-installer-12-disk-warning-encrypted"
|
|
242
|
+
if ! $ENCRYPT; then
|
|
243
|
+
press ctrl-c # toggles the confirm to the unencrypted flow
|
|
244
|
+
- wait_for_screen "without encryption" 30
|
|
245
|
+
+ # The selected confirmation label is light text on a light background,
|
|
246
|
+
+ # which Tesseract intentionally loses. The prompt itself stays put, so
|
|
247
|
+
+ # give the Ctrl+C state change time to render and capture it as proof.
|
|
248
|
+
+ sleep 2
|
|
249
|
+
capture_console "success-installer-13-disk-warning-unencrypted"
|
|
250
|
+
fi
|
|
251
|
+
press ret
|
|
252
|
+
@@ -716,7 +844,7 @@ drive_provision_owner() {
|
|
253
|
+
# layout — UK leaves every letter identical to US, so the typed password (sent
|
|
254
|
+
# as raw keycodes) still matches the literal one the harness pipes to sudo.
|
|
255
|
+
# A greeter (animated logo + tagline) opens first boot; Return begins.
|
|
256
|
+
- wait_for_screen "Opinionated" 600
|
|
257
|
+
+ wait_for_screen "$GREETER_PATTERN" 600
|
|
258
|
+
capture_console "success-provision-00-greeter"
|
|
259
|
+
press ret
|
|
260
|
+
|
|
261
|
+
@@ -791,7 +919,7 @@ wait_for_install() {
|
|
262
|
+
# Deferred-provisioning installs skip the celebration and reboot on their own
|
|
263
|
+
# (no Reboot Now prompt); the first-boot greeter tagline appearing means the
|
|
264
|
+
# install finished and rebooted into setup.
|
|
265
|
+
- if $PROVISION && grep -qi "Opinionated" <<<"$text"; then
|
|
266
|
+
+ if $PROVISION && grep -qi "$GREETER_PATTERN" <<<"$text"; then
|
|
267
|
+
log "Install finished and auto-rebooted into first-boot setup."
|
|
268
|
+
capture_console "success-installer-15-autoreboot"
|
|
269
|
+
return 0
|
|
270
|
+
@@ -1063,6 +1191,49 @@ sync_omarchy() {
|
|
271
|
+
fi
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
+activate_dev_checkout() {
|
|
275
|
+
+ $DEV_LINK || return 0
|
|
276
|
+
+
|
|
277
|
+
+ local target=".local/share/omarchy"
|
|
278
|
+
+ log "Linking the disposable guest session to the synced Omarchy checkout"
|
|
279
|
+
+
|
|
280
|
+
+ ssh_guest "echo '$GUEST_PASSWORD' | sudo -S -v && $target/bin/omarchy-dev-link $target --no-reboot"
|
|
281
|
+
+
|
|
282
|
+
+ # omarchy-dev-link deliberately leaves the running UWSM environment alone.
|
|
283
|
+
+ # A fresh graphical login is enough to rebuild that environment from
|
|
284
|
+
+ # /etc/omarchy.conf. Avoid rebooting here: the acceptance disk is already a
|
|
285
|
+
+ # throwaway overlay, and some released unencrypted ISOs have a separate
|
|
286
|
+
+ # post-install Limine/resume defect that would obscure source-level tests.
|
|
287
|
+
+ ssh_guest "uwsm stop" >/dev/null 2>&1 || true
|
|
288
|
+
+
|
|
289
|
+
+ if ! wait_for_guest_state "linked graphical session stops" 60 session_stopped; then
|
|
290
|
+
+ return 1
|
|
291
|
+
+ fi
|
|
292
|
+
+
|
|
293
|
+
+ establish_session
|
|
294
|
+
+ ssh_guest "grep -Fq '$target' /etc/omarchy.conf"
|
|
295
|
+
+ log "Disposable guest is running entirely from the synced checkout"
|
|
296
|
+
+}
|
|
297
|
+
+
|
|
298
|
+
+run_host_test() {
|
|
299
|
+
+ [[ -n $HOST_TEST ]] || return 0
|
|
300
|
+
+
|
|
301
|
+
+ log "Running host-driven acceptance: $HOST_TEST"
|
|
302
|
+
+ # The host test is trusted local test code sourced deliberately so it can use
|
|
303
|
+
+ # press, ssh_guest, ssh_session, capture_console, wait_for_guest_state, and
|
|
304
|
+
+ # the current RUN_DIR without duplicating the QMP/SSH harness.
|
|
305
|
+
+ source "$HOST_TEST"
|
|
306
|
+
+ declare -F omarchy_host_test >/dev/null || {
|
|
307
|
+
+ echo "$HOST_TEST must define omarchy_host_test()" >&2
|
|
308
|
+
+ return 1
|
|
309
|
+
+ }
|
|
310
|
+
+ set +e
|
|
311
|
+
+ omarchy_host_test 2>&1 | tee "$RUN_DIR/host-test.log"
|
|
312
|
+
+ local status=${PIPESTATUS[0]}
|
|
313
|
+
+ set -e
|
|
314
|
+
+ return "$status"
|
|
315
|
+
+}
|
|
316
|
+
+
|
|
317
|
+
acceptance_phase() {
|
|
318
|
+
log "Booting acceptance VM from base image overlay"
|
|
319
|
+
|
|
320
|
+
@@ -1074,15 +1245,26 @@ acceptance_phase() {
|
|
321
|
+
|
|
322
|
+
establish_session
|
|
323
|
+
sync_omarchy
|
|
324
|
+
+ activate_dev_checkout
|
|
325
|
+
+ if ! clear_startup_notifications; then
|
|
326
|
+
+ capture_console "failure-startup-notifications"
|
|
327
|
+
+ return 1
|
|
328
|
+
+ fi
|
|
329
|
+
|
|
330
|
+
local status=0
|
|
331
|
+
- shortcut_smoke_phase || status=1
|
|
332
|
+
|
|
333
|
+
- log "Running acceptance suite"
|
|
334
|
+
- ssh_guest "OMARCHY_PATH=$guest_omarchy_path OMARCHY_ACCEPTANCE_DIR=/tmp/omarchy-acceptance bash .local/share/omarchy/test/acceptance" || status=$?
|
|
335
|
+
+ if $HOST_TEST_ONLY; then
|
|
336
|
+
+ run_host_test || status=1
|
|
337
|
+
+ else
|
|
338
|
+
+ shortcut_smoke_phase || status=1
|
|
339
|
+
+ run_host_test || status=1
|
|
340
|
+
|
|
341
|
+
- log "Collecting artifacts into $RUN_DIR"
|
|
342
|
+
- ssh_guest "tar -C /tmp -cf - omarchy-acceptance 2>/dev/null" | tar -C "$RUN_DIR" -xf - || true
|
|
343
|
+
+ log "Running acceptance suite"
|
|
344
|
+
+ ssh_guest "OMARCHY_PATH=$guest_omarchy_path OMARCHY_ACCEPTANCE_DIR=/tmp/omarchy-acceptance bash .local/share/omarchy/test/acceptance" || status=$?
|
|
345
|
+
+
|
|
346
|
+
+ log "Collecting artifacts into $RUN_DIR"
|
|
347
|
+
+ ssh_guest "tar -C /tmp -cf - omarchy-acceptance 2>/dev/null" | tar -C "$RUN_DIR" -xf - || true
|
|
348
|
+
+ fi
|
|
349
|
+
ssh_guest "echo $GUEST_PASSWORD | sudo -S cat /var/log/omarchy-install.log 2>/dev/null" >"$RUN_DIR/omarchy-install.log" 2>/dev/null || true
|
|
350
|
+
if ((status == 0)); then
|
|
351
|
+
capture_console "success-acceptance-final"
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Where the lab keeps its bytes, and the one guard every write goes
|
|
2
|
+
// through.
|
|
3
|
+
//
|
|
4
|
+
// Heavy files (the verified ISO, the base disk, a run's overlay) live
|
|
5
|
+
// under the user cache, $XDG_CACHE_HOME/omakit/lab or ~/.cache/omakit/lab;
|
|
6
|
+
// compact run records under the user state, $XDG_STATE_HOME/omakit/lab or
|
|
7
|
+
// ~/.local/state/omakit/lab. Nothing is written anywhere else: every path
|
|
8
|
+
// the lab writes to is built by `inLab`, which refuses a path outside the
|
|
9
|
+
// resolved root, and tests/unit/self-containment.test.mjs holds every
|
|
10
|
+
// write under tools/lab/ to that builder. Measured before this
|
|
11
|
+
// (docs/history/2026-09-18-lab-inventory.md P6): the base, the firmware
|
|
12
|
+
// variables and the SSH key lived inside the omarchy-iso checkout, named
|
|
13
|
+
// by the ISO's file name.
|
|
14
|
+
|
|
15
|
+
import { closeSync, createReadStream, createWriteStream, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs"
|
|
16
|
+
import { pipeline } from "node:stream/promises"
|
|
17
|
+
import { join, resolve, sep } from "node:path"
|
|
18
|
+
import { omakitCacheDir, omakitStateDir } from "../marketplace/paths.mjs"
|
|
19
|
+
|
|
20
|
+
/** The cache root: downloads, base, staging, the lock. */
|
|
21
|
+
export function labCacheDir(env = process.env) {
|
|
22
|
+
return omakitCacheDir("lab", env)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The state root: run records, one directory per run id. */
|
|
26
|
+
export function labStateDir(env = process.env) {
|
|
27
|
+
return omakitStateDir("lab", env)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The layout under the cache root. One base, not one per release
|
|
32
|
+
* (packaging/LAB_PLAN.md, the one-base rule): a pin update replaces it.
|
|
33
|
+
*/
|
|
34
|
+
export function labLayout(env = process.env) {
|
|
35
|
+
const cache = labCacheDir(env)
|
|
36
|
+
const state = labStateDir(env)
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
cache,
|
|
39
|
+
state,
|
|
40
|
+
downloads: join(cache, "downloads"),
|
|
41
|
+
base: join(cache, "base"),
|
|
42
|
+
staging: join(cache, "staging"),
|
|
43
|
+
plugins: join(cache, "plugins"),
|
|
44
|
+
lock: join(cache, "lab.lock"),
|
|
45
|
+
toolchain: join(cache, "toolchain.json"),
|
|
46
|
+
runs: join(state, "runs"),
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A path under one of the two lab roots, or a throw. Every write in
|
|
52
|
+
* tools/lab/ names its target through this, so a target outside the lab
|
|
53
|
+
* is a bug that fails before a byte moves, not a file somewhere else.
|
|
54
|
+
*/
|
|
55
|
+
export function inLab(root, ...parts) {
|
|
56
|
+
const target = resolve(root, ...parts)
|
|
57
|
+
const roots = [resolve(root)]
|
|
58
|
+
if (!roots.some((allowed) => target === allowed || target.startsWith(`${allowed}${sep}`))) {
|
|
59
|
+
throw new Error(`lab: ${target} is outside ${root}`)
|
|
60
|
+
}
|
|
61
|
+
return target
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A directory under the lab, created private (0700) with its parents. */
|
|
65
|
+
export function labDir(root, ...parts) {
|
|
66
|
+
const dir = inLab(root, ...parts)
|
|
67
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
68
|
+
return dir
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** JSON written whole and fsynced, so a manifest is either there or not. */
|
|
72
|
+
export function writeJson(root, relative, value) {
|
|
73
|
+
const file = inLab(root, relative)
|
|
74
|
+
const fd = openSync(file, "w", 0o600)
|
|
75
|
+
try {
|
|
76
|
+
writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`)
|
|
77
|
+
fsyncSync(fd)
|
|
78
|
+
} finally {
|
|
79
|
+
closeSync(fd)
|
|
80
|
+
}
|
|
81
|
+
return file
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readJson(file) {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(readFileSync(file, "utf8"))
|
|
87
|
+
} catch {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Move a file or directory within the lab: a rename when the two are on
|
|
94
|
+
* one filesystem, and for a file on another one a streamed copy followed
|
|
95
|
+
* by the removal of the source. The destination must not exist. This is
|
|
96
|
+
* the one place the lab renames anything, and tests/unit/self-containment
|
|
97
|
+
* holds the tree to it.
|
|
98
|
+
*/
|
|
99
|
+
export async function moveIntoLab(root, from, relative) {
|
|
100
|
+
const to = inLab(root, relative)
|
|
101
|
+
try {
|
|
102
|
+
statSync(to)
|
|
103
|
+
throw new Error(`lab: ${to} exists; nothing is moved over it`)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error.code !== "ENOENT") throw error
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
renameSync(from, to)
|
|
109
|
+
return to
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error.code !== "EXDEV") throw error
|
|
112
|
+
}
|
|
113
|
+
const source = statSync(from)
|
|
114
|
+
if (!source.isFile()) throw new Error(`lab: ${from} is on another filesystem and is not a file; move it by hand`)
|
|
115
|
+
await copyIntoLab(root, from, relative)
|
|
116
|
+
unlinkSync(from)
|
|
117
|
+
return to
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A streamed copy of one file into the lab, mode 0600, fsynced. The destination must not exist. */
|
|
121
|
+
export async function copyIntoLab(root, from, relative) {
|
|
122
|
+
const to = inLab(root, relative)
|
|
123
|
+
try {
|
|
124
|
+
statSync(to)
|
|
125
|
+
throw new Error(`lab: ${to} exists; nothing is copied over it`)
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error.code !== "ENOENT") throw error
|
|
128
|
+
}
|
|
129
|
+
const out = createWriteStream(to, { mode: 0o600, flags: "wx" })
|
|
130
|
+
await pipeline(createReadStream(from), out)
|
|
131
|
+
const fd = openSync(to, "r")
|
|
132
|
+
try {
|
|
133
|
+
fsyncSync(fd)
|
|
134
|
+
} finally {
|
|
135
|
+
closeSync(fd)
|
|
136
|
+
}
|
|
137
|
+
return to
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Remove something under the lab, and nothing outside it. */
|
|
141
|
+
export function removeFromLab(root, relative) {
|
|
142
|
+
rmSync(inLab(root, relative), { recursive: true, force: true })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** `20260918-150245`, the run id shape the toolchain used, kept so a run directory sorts by time. */
|
|
146
|
+
export function stampNow(date = new Date()) {
|
|
147
|
+
const pad = (n) => String(n).padStart(2, "0")
|
|
148
|
+
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Allocated bytes of a path, like `du -B1`: blocks times 512, so a sparse qcow2 is measured by what it takes. */
|
|
152
|
+
export function allocatedBytes(path) {
|
|
153
|
+
const walk = (p) => {
|
|
154
|
+
let st
|
|
155
|
+
try {
|
|
156
|
+
st = statSync(p)
|
|
157
|
+
} catch {
|
|
158
|
+
return 0
|
|
159
|
+
}
|
|
160
|
+
let total = st.blocks * 512
|
|
161
|
+
if (st.isDirectory()) {
|
|
162
|
+
let entries = []
|
|
163
|
+
try {
|
|
164
|
+
entries = readdirSync(p)
|
|
165
|
+
} catch {
|
|
166
|
+
return total
|
|
167
|
+
}
|
|
168
|
+
for (const entry of entries) total += walk(join(p, entry))
|
|
169
|
+
}
|
|
170
|
+
return total
|
|
171
|
+
}
|
|
172
|
+
return walk(path)
|
|
173
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"release": {
|
|
4
|
+
"name": "4.0.3",
|
|
5
|
+
"embeddedBuild": "2026.09.08",
|
|
6
|
+
"volume": "OMARCHY_202609",
|
|
7
|
+
"isoUrl": "https://iso.omarchy.org/omarchy-4.0.3.iso",
|
|
8
|
+
"checksumUrl": "https://iso.omarchy.org/omarchy-4.0.3.iso.sha256",
|
|
9
|
+
"signatureUrl": "https://iso.omarchy.org/omarchy-4.0.3.iso.sig",
|
|
10
|
+
"fileName": "omarchy-4.0.3.iso",
|
|
11
|
+
"bytes": 6260654080,
|
|
12
|
+
"sha256": "03d60bc74306dca51f96e1a84b690871d8d606826b260edd0208962da8507d14",
|
|
13
|
+
"signingFingerprint": "40DFB630FF42BCFFB047046CF0134EE680CAC571",
|
|
14
|
+
"signingKey": "omarchy.gpg",
|
|
15
|
+
"signingKeySha256": "15d6aac44df688165b2ea35fe0b23af239bbc66a6909c10a5c219e8d94b707de",
|
|
16
|
+
"expectedGuestVersion": "4.0.3-1"
|
|
17
|
+
},
|
|
18
|
+
"toolchain": {
|
|
19
|
+
"repository": "https://github.com/omacom-io/omarchy-iso",
|
|
20
|
+
"commit": "268bac16d351a21d867e37565738f458b11cb06c",
|
|
21
|
+
"harness": "bin/omarchy-iso-test",
|
|
22
|
+
"upstreamHarnessSha256": "fbc236a674464743c6ff8e4cc53be932dc36a16c9fcb159932006e9d7452439c",
|
|
23
|
+
"patch": "patches/omarchy-iso-test.patch",
|
|
24
|
+
"patchedHarnessSha256": "8637e8cc82413fb421daa17eb758e3136cf71099b108b6412326172993974e97"
|
|
25
|
+
},
|
|
26
|
+
"guest": {
|
|
27
|
+
"memoryMiB": 5120,
|
|
28
|
+
"user": "omarchy",
|
|
29
|
+
"password": "omarchy",
|
|
30
|
+
"diskGiB": 40
|
|
31
|
+
},
|
|
32
|
+
"measured": {
|
|
33
|
+
"on": "2026-09-18",
|
|
34
|
+
"buildMilliseconds": 357800,
|
|
35
|
+
"baseAllocatedBytes": 6181490688,
|
|
36
|
+
"baseDirectoryBytes": 6182264832,
|
|
37
|
+
"preparedLabBytes": 12442931200,
|
|
38
|
+
"overlayAfterRunBytes": 610734080,
|
|
39
|
+
"pluginsBytes": 6836224,
|
|
40
|
+
"source": "docs/MEASUREMENTS.md M14, measured by omakit lab on the reference host; packaging/LAB_PLAN.md M4 to M7 are the 2026-09-10 to 2026-09-13 observations it re-measured"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The release pin: the one Omarchy release the lab prepares, its exact
|
|
2
|
+
// digest, its signer, and the toolchain that builds a base from it.
|
|
3
|
+
//
|
|
4
|
+
// Read from tools/lab/pin.json, never from the network: the lab never
|
|
5
|
+
// resolves "latest" (docs/history/2026-09-18-lab-inventory.md P13: the
|
|
6
|
+
// old setup scraped omarchy.org for the first ISO link and verified it
|
|
7
|
+
// against a sidecar downloaded beside it, so a substituted pair passed).
|
|
8
|
+
// Updating the pin is a reviewed change, docs/LAB.md says how.
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs"
|
|
11
|
+
import { dirname, join } from "node:path"
|
|
12
|
+
import { fileURLToPath } from "node:url"
|
|
13
|
+
|
|
14
|
+
export const LAB_DIR = dirname(fileURLToPath(import.meta.url))
|
|
15
|
+
|
|
16
|
+
let cached = null
|
|
17
|
+
|
|
18
|
+
/** The pin, parsed once. `file` is injectable for tests that need a small release. */
|
|
19
|
+
export function labPin(file = join(LAB_DIR, "pin.json")) {
|
|
20
|
+
if (file === join(LAB_DIR, "pin.json") && cached) return cached
|
|
21
|
+
const pin = JSON.parse(readFileSync(file, "utf8"))
|
|
22
|
+
for (const key of ["release", "toolchain", "guest", "measured"]) {
|
|
23
|
+
if (!pin[key] || typeof pin[key] !== "object") throw new Error(`lab pin: no ${key} section in ${file}`)
|
|
24
|
+
}
|
|
25
|
+
if (!/^[0-9a-f]{64}$/.test(pin.release.sha256)) throw new Error("lab pin: the release sha256 is not 64 hex characters")
|
|
26
|
+
if (!/^[0-9A-F]{40}$/.test(pin.release.signingFingerprint)) throw new Error("lab pin: the signing fingerprint is not 40 hex characters")
|
|
27
|
+
if (!Number.isInteger(pin.release.bytes) || pin.release.bytes <= 0) throw new Error("lab pin: the release byte count is not a positive integer")
|
|
28
|
+
if (/latest/i.test(pin.release.isoUrl)) throw new Error("lab pin: the ISO URL resolves latest; the pin names one release")
|
|
29
|
+
if (file === join(LAB_DIR, "pin.json")) cached = pin
|
|
30
|
+
return pin
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Bytes for a person, in both units, so a size cannot be made to look
|
|
35
|
+
* smaller by choosing one: `6,260,654,080 B (6.261 GB / 5.831 GiB)`.
|
|
36
|
+
* Decimal GB is what the download page and the disk vendor say, binary
|
|
37
|
+
* GiB is what `df` and `du -h` say; both are printed every time.
|
|
38
|
+
*/
|
|
39
|
+
export function bytesBoth(bytes) {
|
|
40
|
+
const n = Number(bytes)
|
|
41
|
+
const gb = (n / 1e9).toFixed(3)
|
|
42
|
+
const gib = (n / 2 ** 30).toFixed(3)
|
|
43
|
+
return `${n.toLocaleString("en-US")} B (${gb} GB / ${gib} GiB)`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The two units without the byte count, for a breakdown whose total already printed it: `6.261 GB / 5.831 GiB`. */
|
|
47
|
+
export function gbBoth(bytes) {
|
|
48
|
+
const n = Number(bytes)
|
|
49
|
+
return `${(n / 1e9).toFixed(3)} GB / ${(n / 2 ** 30).toFixed(3)} GiB`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `4m 49.5s` from milliseconds, the way a person reads a build time. */
|
|
53
|
+
export function durationWords(ms) {
|
|
54
|
+
const seconds = Math.round(ms / 100) / 10
|
|
55
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`
|
|
56
|
+
const minutes = Math.floor(seconds / 60)
|
|
57
|
+
return `${minutes}m ${(seconds - minutes * 60).toFixed(1)}s`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The first eight and last eight characters of a digest or fingerprint, for a line that names it without being it. */
|
|
61
|
+
export function shortId(value) {
|
|
62
|
+
const s = String(value)
|
|
63
|
+
return s.length > 20 ? `${s.slice(0, 8)}...${s.slice(-8)}` : s
|
|
64
|
+
}
|