fix: harden AppImage certification checks
This commit is contained in:
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
# lavapipe: software Vulkan for the headless screenshot tests.
|
||||
# OCCT dev packages: headers + toolkits for the vernier-occt-sys facade.
|
||||
- run: apt-get update && apt-get install -y --no-install-recommends python3 fish jq mesa-vulkan-drivers libocct-foundation-dev libocct-modeling-data-dev libocct-modeling-algorithms-dev
|
||||
- run: apt-get update && apt-get install -y --no-install-recommends python3 fish jq patchelf mesa-vulkan-drivers libocct-foundation-dev libocct-modeling-data-dev libocct-modeling-algorithms-dev
|
||||
- run: rustup component add rustfmt clippy
|
||||
# The same exact manifest as scripts/check.fish. Its compiling Cargo
|
||||
# rows explicitly select system-occt; CI never starts a vendored build.
|
||||
|
||||
@@ -20,7 +20,7 @@ Branch: `codex/phased-next-steps`
|
||||
| 12. CI/software renderer | done | task12_ci_renderer | 064e6b7 | approved after review-fix pass; no remaining findings | shared 143-step adapter selection; Python 7/7 and focused golden green; 13.45s representative RADV positive/exact exit-1 control; lavapipe absent and explicitly unverified |
|
||||
| 13. C++ sanitizer evidence | done with explicit runtime boundary | task13_sanitizers | 3370872 | approved after one documentation correction | 14 generated cxxbridge warnings; no project-owned warning; one sanitizer link attempt failed before tests because runtimes were omitted under -nodefaultlibs; no sanitizer-clean claim or suppression |
|
||||
| 14. Vendored static OCCT | done | task14_static_occt | 9eae002 | approved after review-fix pass; no remaining findings | official V7_9_3 at a016080b; 26 static archives built once; app/worker have no libTK dynamic deps; 236-step RADV workflow deterministic with STEP/STL/reopen |
|
||||
| 15. AppImage | source approved; certification pending | task15_appimage | pending source commit | approved after review-fix pass; no remaining source findings | source contracts green; next exactly one detached build, structural smoke, dependency audit and native readback; driven workflows deferred to Task 16 |
|
||||
| 15. AppImage | built once; certification audit pending | task15_appimage | 0fb96da + pending auditor fix | auditor/integration fix approved; no remaining findings | artifact f02aea20 at 0fb96da; first smoke exposed path-normalization bug; source-only fix approved; no rebuild; corrected smoke/readback pending |
|
||||
| 16. Integrated verification | pending | controller | — | pending | — |
|
||||
|
||||
The MikroB dashboard was unavailable at `localhost:3420`; this branch-local ledger is the execution authority. No dashboard database is mutated directly.
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# Task 15 Stage B certification report
|
||||
|
||||
## Outcome
|
||||
|
||||
The one authorized AppImage build completed, but certification stopped at the
|
||||
first structural-smoke failure. The artifact is **built but not certified** and
|
||||
must not replace the managed local release.
|
||||
|
||||
No retry, second build, native readback, driven workflow, full gate,
|
||||
installation, launcher mutation, commit, or push was performed after the
|
||||
failure.
|
||||
|
||||
## Post-certification auditor fix
|
||||
|
||||
The narrow smoke-auditor fix is implemented in the main phased-next-steps
|
||||
worktree but remains uncommitted and uncertified. `scripts/smoke-appimage.sh`
|
||||
now canonicalizes both the extracted `usr/lib` root and each absolute path
|
||||
reported by `ldd` before deciding whether a non-base dependency came from the
|
||||
AppDir or the host.
|
||||
|
||||
A focused synthetic AppDir contract compiles a tiny exception-using C++ ELF with
|
||||
the already-required `c++` tool, then copies its `libstdc++.so.6` and
|
||||
`libgcc_s.so.1` dependencies and applies the production `$ORIGIN/../lib` RPATH.
|
||||
Before the fix it reproduced:
|
||||
|
||||
```text
|
||||
exit=1
|
||||
appimage smoke: non-base dependency resolved from host: libstdc++.so.6 => /tmp/vernier-appimage-smoke.vLUjNHrB/squashfs-root/usr/bin/../lib/libstdc++.so.6
|
||||
```
|
||||
|
||||
After the fix, the same `usr/bin/../lib` spelling is accepted. The contract then
|
||||
removes the AppDir copy of `libstdc++.so.6`, regenerates its manifest, and proves
|
||||
that a genuine host resolution is still refused. The focused source contract
|
||||
passes; no real AppImage smoke or native readback was rerun.
|
||||
|
||||
The packaging source check is now one exact `scope=all` row in the shared
|
||||
`scripts/acceptance-gate.json`; the direct pre-step was removed from
|
||||
`scripts/check.fish`, preserving the Task 12 single-manifest local/CI contract.
|
||||
The focused Python contract first failed on both the duplicate local pre-step and
|
||||
the absent manifest row, then passed after the integration change. CI now
|
||||
installs `patchelf`, which both the source contract and packaging toolchain use.
|
||||
The fixture no longer depends on an ambient `ninja` binary.
|
||||
|
||||
The current software-renderer evidence now records 144 selected commands for
|
||||
either adapter after adding the all-scope packaging row. Historical Task 12
|
||||
evidence remains unchanged at the 143-command state it measured.
|
||||
|
||||
The uncommitted source-fix file set is:
|
||||
|
||||
- `.gitea/workflows/ci.yaml`
|
||||
- `scripts/acceptance-gate.json`
|
||||
- `scripts/check.fish`
|
||||
- `scripts/smoke-appimage.sh`
|
||||
- `scripts/test-acceptance-gate.py`
|
||||
- `scripts/test-appimage-packaging.sh`
|
||||
- `docs/SOFTWARE_RENDERER_2026-09-14.md`
|
||||
- `.superpowers/sdd/task-15-certification-report.md` (this report; excluded
|
||||
from the source digest below)
|
||||
|
||||
The source/test digest (mode, SHA-256, path records sorted with `LC_ALL=C`) is:
|
||||
|
||||
```text
|
||||
9922a8d53bb0971ba4b0fa9624c58214c13be1ebb179b8433d6fb41e12ad0366
|
||||
```
|
||||
|
||||
Certification therefore remains pending a new immutable source commit, one
|
||||
new artifact build, structural smoke, and native readback. The artifact recorded
|
||||
below was built before this uncommitted auditor fix and remains uncertified.
|
||||
|
||||
## Immutable inputs
|
||||
|
||||
- Packaging-source revision:
|
||||
`0fb96da2668a7bf75b3044bb15f9e98bc373e8fa`
|
||||
- Detached certification worktree:
|
||||
`/home/nilsb/Documents/Projects/VernierCAD/target/worktrees/phased-next-steps/target/worktrees/task15-certification-0fb96da2`
|
||||
- Final worktree state: detached HEAD, zero status rows
|
||||
- Initialized OCCT submodule revision:
|
||||
`a016080bf6738d6aeae020badee4e888ad1540a5`
|
||||
- Reused prepared OCCT root:
|
||||
`/home/nilsb/Documents/Projects/VernierCAD/target/worktrees/phased-next-steps/target/vendored-occt`
|
||||
- Prepared OCCT validation: exact completion/source records, 17 direct toolkits,
|
||||
26 closure archives, platform list, six required resource files, two license
|
||||
files, and no shared `libTK*.so*` all passed before the build. OCCT was not
|
||||
rebuilt.
|
||||
|
||||
The explicit shared tool cache was absent before this run and populated by one
|
||||
fetch invocation:
|
||||
|
||||
```text
|
||||
appimagetool-x86_64.AppImage
|
||||
ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0
|
||||
|
||||
runtime-x86_64
|
||||
1cc49bcf1e2ccd593c379adb17c9f85a36d619088296504de95b1d06215aebbf
|
||||
```
|
||||
|
||||
Cache path:
|
||||
`/home/nilsb/Documents/Projects/VernierCAD/target/appimage-tools-task15`
|
||||
|
||||
## Single build evidence
|
||||
|
||||
The builder was invoked once with version `0.0.4-alpha`, revision equal to the
|
||||
detached HEAD, the validated prepared OCCT root, and the explicit shared tool
|
||||
cache. Cargo completed the release build in 40.55 seconds and appimagetool
|
||||
reported success. The optional AppStream metadata warning and the known generated
|
||||
`cxx` maybe-uninitialized warnings were present; neither stopped the build.
|
||||
|
||||
- AppImage:
|
||||
`/home/nilsb/Documents/Projects/VernierCAD/target/worktrees/phased-next-steps/target/worktrees/task15-certification-0fb96da2/target/appimage/VernierCAD-0.0.4-alpha-x86_64.AppImage`
|
||||
- AppImage SHA-256:
|
||||
`f02aea202b32c6aa91f4cc61712f662f3e036090848dc98184d4921cd53c1f35`
|
||||
- Sidecar row: exact same hash and basename
|
||||
`VernierCAD-0.0.4-alpha-x86_64.AppImage`
|
||||
- Size: `43,964,920` bytes
|
||||
- Mode: `0755`
|
||||
- File identity: x86-64 static-PIE AppImage runtime ELF
|
||||
- `vernier-app` SHA-256:
|
||||
`9dce882ddf746de123b47874e8ed16252e795e35ded265bd55567d07500a7ca6`
|
||||
- `vernier-worker` SHA-256:
|
||||
`d311a6a8ce01332a8f2768e3a711a5bd1e754d98bbf18fe310c7b10714b6beb0`
|
||||
- Build log:
|
||||
`/home/nilsb/Documents/Projects/VernierCAD/target/task15-appimage-build.log`
|
||||
- Build-log SHA-256:
|
||||
`27d2ed2320f74fbfce7491ecee187445d4b769acb30476bb933f449423194c8f`
|
||||
|
||||
## First failure and stop boundary
|
||||
|
||||
The structural smoke was invoked once with expected version `0.0.4-alpha` and
|
||||
the exact detached revision. It exited 1 with:
|
||||
|
||||
```text
|
||||
appimage smoke: non-base dependency resolved from host: libstdc++.so.6 => /tmp/vernier-appimage-smoke.uFtmpINb/squashfs-root/usr/bin/../lib/libstdc++.so.6
|
||||
```
|
||||
|
||||
Smoke log:
|
||||
`/home/nilsb/Documents/Projects/VernierCAD/target/task15-appimage-smoke.log`
|
||||
|
||||
Smoke-log SHA-256:
|
||||
`448717b686df678ade9fb956d69e55b2c24d9005f45180515ec7f7fb90904f7f`
|
||||
|
||||
The reported path lexically contains `usr/bin/../lib`; after normalization it
|
||||
would be inside the extracted AppDir. The smoke compares the unnormalized `ldd`
|
||||
spelling to the canonical AppDir library prefix, so it refused before completing
|
||||
the full app/worker dependency audit. This is evidence of the exact first
|
||||
failure, not a passing dependency claim.
|
||||
|
||||
Because the instructions required stopping at the first fetch/build/smoke
|
||||
failure, native `VERNIER_READBACK` was not attempted. Stage B therefore remains
|
||||
incomplete pending a separately authorized source fix and new immutable build.
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
`scripts/acceptance-gate.json` is the single `vernier.acceptance.v1` step
|
||||
list consumed by both `scripts/check.fish` and `.gitea/workflows/ci.yaml`.
|
||||
The manifest has 143 pre-existing gate commands plus four software variants
|
||||
of the golden-bearing workflows. A RADV selection and a software selection
|
||||
each execute 143 steps. All eight negative controls declare exact expected
|
||||
The manifest now selects 144 commands for either adapter. Four golden-bearing
|
||||
workflows have parallel RADV/software rows, and each adapter selects only its
|
||||
matching row. All eight negative controls declare exact expected
|
||||
exit 1; the runner treats every other non-zero status as an unexpected gate
|
||||
failure and retains each command's stdout, stderr, duration, and output tree.
|
||||
Runner evidence is written under `evidence/<step>/`; `{out}` expands to the
|
||||
|
||||
@@ -894,7 +894,7 @@ scripts/smoke-appimage.sh --version 0.0.4-alpha --revision "$release_sha" target
|
||||
|
||||
Expected: the artifact is absent and the smoke exits non-zero with a named missing-artifact error.
|
||||
|
||||
- [ ] **Step 3: Pin and fetch the packaging tools**
|
||||
- [x] **Step 3: Pin and fetch the packaging tools**
|
||||
|
||||
Record and verify exactly these two inputs in `packaging/appimage/tools.sha256`:
|
||||
|
||||
|
||||
@@ -24,6 +24,16 @@
|
||||
"adapter": "none",
|
||||
"artifact": "gate-step-contract"
|
||||
},
|
||||
{
|
||||
"id": "appimage-packaging-source-contract",
|
||||
"scope": "all",
|
||||
"argv": [
|
||||
"scripts/test-appimage-packaging.sh"
|
||||
],
|
||||
"expected_exit": 0,
|
||||
"adapter": "none",
|
||||
"artifact": "appimage-packaging-source-contract"
|
||||
},
|
||||
{
|
||||
"id": "clippy",
|
||||
"scope": "all",
|
||||
|
||||
@@ -2,6 +2,4 @@
|
||||
# Full release gate. The manifest is the single step list consumed locally
|
||||
# and by CI; its Cargo rows explicitly select the fast system-occt path.
|
||||
# Task 16 runs this complete selection once before installation.
|
||||
scripts/test-appimage-packaging.sh
|
||||
or exit $status
|
||||
python3 scripts/run-acceptance-gate.py --adapter RADV
|
||||
|
||||
@@ -67,6 +67,9 @@ trap 'rm -rf "$scratch"' EXIT
|
||||
) || die "extract-and-run extraction failed"
|
||||
root=$scratch/squashfs-root
|
||||
[[ -d "$root" ]] || die "extract-and-run did not create squashfs-root"
|
||||
root=$(readlink -f -- "$root")
|
||||
[[ -d "$root/usr/lib" ]] || die "missing AppDir usr/lib"
|
||||
app_lib=$(readlink -f -- "$root/usr/lib")
|
||||
|
||||
mapfile -t appruns < <(find "$root" -name AppRun -type f -perm /111 -print)
|
||||
(( ${#appruns[@]} == 1 )) || die "expected exactly one executable AppRun, found ${#appruns[@]}"
|
||||
@@ -157,7 +160,8 @@ for binary in "$app" "$worker"; do
|
||||
while IFS=$'\t' read -r soname resolved; do
|
||||
[[ -n "$soname" && -n "$resolved" ]] || continue
|
||||
is_base_runtime "$soname" && continue
|
||||
[[ "$resolved" == "$root/usr/lib/"* ]] || die "non-base dependency resolved from host: $soname => $resolved"
|
||||
resolved=$(readlink -f -- "$resolved") || die "could not canonicalize dependency path: $soname"
|
||||
[[ "$resolved" == "$app_lib/"* ]] || die "non-base dependency resolved from host: $soname => $resolved"
|
||||
printf '%s\n' "$soname" >>"$resolved_nonbase"
|
||||
done < <(awk '$2 == "=>" && $3 ~ /^\// { print $1 "\t" $3 }' <<<"$ldd_output")
|
||||
done
|
||||
|
||||
@@ -28,9 +28,31 @@ class AcceptanceManifestTests(unittest.TestCase):
|
||||
invocation = "scripts/run-acceptance-gate.py"
|
||||
self.assertIn(invocation, local)
|
||||
self.assertIn(invocation, ci)
|
||||
self.assertNotIn("scripts/test-appimage-packaging.sh", local)
|
||||
self.assertNotIn("target/release/vernier-drive scripts/drive/", local)
|
||||
self.assertNotIn("cargo run --locked -p vernier-drive", ci)
|
||||
|
||||
def test_appimage_source_contract_is_one_all_scope_manifest_step(self):
|
||||
expected = {
|
||||
"id": "appimage-packaging-source-contract",
|
||||
"scope": "all",
|
||||
"argv": ["scripts/test-appimage-packaging.sh"],
|
||||
"expected_exit": 0,
|
||||
"adapter": "none",
|
||||
"artifact": "appimage-packaging-source-contract",
|
||||
}
|
||||
matches = [
|
||||
step
|
||||
for step in self.manifest["steps"]
|
||||
if "scripts/test-appimage-packaging.sh" in step["argv"]
|
||||
]
|
||||
self.assertEqual(matches, [expected])
|
||||
|
||||
ci = (ROOT / ".gitea" / "workflows" / "ci.yaml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn(" patchelf ", ci)
|
||||
|
||||
def test_manifest_is_exact_and_fail_closed(self):
|
||||
self.assertEqual(set(self.manifest), {"schema", "steps"})
|
||||
self.assertEqual(self.manifest["schema"], "vernier.acceptance.v1")
|
||||
|
||||
@@ -121,8 +121,31 @@ fixture=$scratch/fixture
|
||||
mkdir -p "$fixture/usr/bin" "$fixture/usr/share/vernier"
|
||||
cp packaging/appimage/AppRun "$fixture/AppRun"
|
||||
cp packaging/appimage/vernier.desktop "$fixture/vernier.desktop"
|
||||
cp /usr/bin/true "$fixture/usr/bin/vernier-app"
|
||||
cp /usr/bin/true "$fixture/usr/bin/vernier-worker"
|
||||
cxx=$(command -v c++) || die "c++ is required for the synthetic dependency fixture"
|
||||
command -v patchelf >/dev/null || die "patchelf is required for the synthetic dependency fixture"
|
||||
cat >"$scratch/libstdcpp-fixture.cc" <<'EOF'
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
int main() {
|
||||
try {
|
||||
throw std::runtime_error("VernierCAD packaging fixture");
|
||||
} catch (const std::exception& error) {
|
||||
std::cout << error.what();
|
||||
}
|
||||
}
|
||||
EOF
|
||||
"$cxx" -O0 "$scratch/libstdcpp-fixture.cc" -o "$scratch/libstdcpp-fixture"
|
||||
cp "$scratch/libstdcpp-fixture" "$fixture/usr/bin/vernier-app"
|
||||
cp "$scratch/libstdcpp-fixture" "$fixture/usr/bin/vernier-worker"
|
||||
mkdir "$fixture/usr/lib"
|
||||
for soname in libstdc++.so.6 libgcc_s.so.1; do
|
||||
resolved=$(ldd "$scratch/libstdcpp-fixture" | awk -v soname="$soname" '$1 == soname { print $3 }')
|
||||
[[ -f "$resolved" ]] || die "could not resolve $soname for the synthetic dependency fixture"
|
||||
cp -L "$resolved" "$fixture/usr/lib/$soname"
|
||||
done
|
||||
patchelf --set-rpath '$ORIGIN/../lib' "$fixture/usr/bin/vernier-app"
|
||||
patchelf --set-rpath '$ORIGIN/../lib' "$fixture/usr/bin/vernier-worker"
|
||||
fixture_version=0.0.4-alpha
|
||||
fixture_revision=0123456789abcdef0123456789abcdef01234567
|
||||
fixture_artifact=$scratch/VernierCAD-$fixture_version-x86_64.AppImage
|
||||
@@ -177,6 +200,13 @@ expect_smoke_failure 'usage: scripts/smoke-appimage.sh --version VERSION --revis
|
||||
VERNIER_FAKE_APPDIR=$fixture scripts/smoke-appimage.sh \
|
||||
--version "$fixture_version" --revision "$fixture_revision" "$fixture_artifact" >/dev/null
|
||||
|
||||
mv "$fixture/usr/lib/libstdc++.so.6" "$scratch/libstdc++.so.6"
|
||||
make_fixture_manifest
|
||||
expect_smoke_failure 'appimage smoke: non-base dependency resolved from host: libstdc++.so.6 =>' \
|
||||
--version "$fixture_version" --revision "$fixture_revision" "$fixture_artifact"
|
||||
mv "$scratch/libstdc++.so.6" "$fixture/usr/lib/libstdc++.so.6"
|
||||
make_fixture_manifest
|
||||
|
||||
write_fixture_sidecar other.AppImage
|
||||
expect_smoke_failure 'appimage smoke: artifact checksum filename mismatch' \
|
||||
--version "$fixture_version" --revision "$fixture_revision" "$fixture_artifact"
|
||||
@@ -220,6 +250,5 @@ require_literal docs/PORTABLE_LINUX_RELEASE.md 'APPIMAGE_EXTRACT_AND_RUN=1'
|
||||
require_literal docs/PORTABLE_LINUX_RELEASE.md 'No FUSE'
|
||||
require_literal docs/PORTABLE_LINUX_RELEASE.md 'Task 16'
|
||||
require_literal docs/PORTABLE_LINUX_RELEASE.md '--version 0.0.4-alpha --revision "$release_sha"'
|
||||
require_literal scripts/check.fish 'scripts/test-appimage-packaging.sh'
|
||||
|
||||
echo "AppImage packaging source contracts passed"
|
||||
|
||||
Reference in New Issue
Block a user