mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(appimage): anchor sharun library paths to AppDir (#5189)
This commit is contained in:
@@ -172,6 +172,8 @@ jobs:
|
||||
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||
- name: Install Tauri dependencies (ubuntu only)
|
||||
if: startsWith(matrix.settings.platform, 'ubuntu-')
|
||||
env:
|
||||
MATRIX_TARGET: ${{ matrix.settings.target }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
@@ -180,7 +182,12 @@ jobs:
|
||||
libevdev-dev libssl-dev libclang-dev desktop-file-utils \
|
||||
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
|
||||
libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
|
||||
libgbm1 libpango-1.0-0 libcairo2 libatspi2.0-0 libxshmfence1 libu2f-udev
|
||||
libgbm1 libpango-1.0-0 libcairo2 libatspi2.0-0 libxshmfence1 libu2f-udev \
|
||||
xvfb
|
||||
if [ "$MATRIX_TARGET" = "x86_64-unknown-linux-gnu" ] \
|
||||
&& ! command -v apparmor_parser >/dev/null 2>&1; then
|
||||
sudo apt-get install -y apparmor
|
||||
fi
|
||||
# NOTE: The post-build dump lives further down (after `cargo tauri build`)
|
||||
# so the binary actually exists when ldd runs. Running it here pre-build
|
||||
# silently no-op'd and gave us no signal during the
|
||||
@@ -528,19 +535,19 @@ jobs:
|
||||
done
|
||||
|
||||
# Strip host graphics libraries (Mesa/libdrm/libva/libgbm) that lib4bin
|
||||
# ldd-walks into the AppImage. Those drivers must come from the user's
|
||||
# host system — even on a current Ubuntu runner the bundled Mesa can
|
||||
# fail to initialize newer GPUs (RDNA3, Intel Arc, Lovelace) and
|
||||
# crashes CEF on launch because the bundled libs shadow the host's
|
||||
# working ones on LD_LIBRARY_PATH. CEF's own SwiftShader subdir is
|
||||
# untouched. Re-signs the AppImage + updater tarball when signing
|
||||
# is enabled.
|
||||
- name: Strip host graphics libs from AppImage
|
||||
# ldd-walks into the AppImage. Final-artifact validation runs before
|
||||
# re-signing: amd64 also gets a bounded Xvfb launch from a foreign CWD,
|
||||
# while arm64 receives static validation in this change. CEF's own
|
||||
# SwiftShader subdir is untouched, and updater archives are rebuilt only
|
||||
# after the validated image is final.
|
||||
- name: Strip host graphics libs and validate final AppImage
|
||||
if: startsWith(matrix.settings.platform, 'ubuntu-')
|
||||
shell: bash
|
||||
env:
|
||||
MATRIX_TARGET: ${{ matrix.settings.target }}
|
||||
PROFILE: ${{ inputs.build_profile }}
|
||||
APPIMAGE_RUNTIME_SMOKE: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' && '1' || '0' }}
|
||||
APPIMAGE_RUNTIME_APPARMOR_USERNS: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' && '1' || '0' }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.UPDATER_PRIVATE_KEY_PASSWORD }}
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,943 @@
|
||||
# AppImage sharun library-path repair implementation plan
|
||||
|
||||
> Approved design:
|
||||
> `docs/specs/2026-07-24-appimage-sharun-lib-path-design.md` at
|
||||
> `baae2b02c73e48826e156a111b7e8aff82f214b9`.
|
||||
|
||||
## Goal
|
||||
|
||||
Repair the Linux AppImage postprocessor so sharun's generated
|
||||
`shared/lib/lib.path` contains only AppDir-anchored `+` entries, reject malformed
|
||||
paths before release, and prove that the final repacked amd64 AppImage reaches
|
||||
normal application startup when invoked from outside its extracted AppDir.
|
||||
|
||||
The implementation changes only the AppImage release path:
|
||||
|
||||
- `scripts/release/strip-appimage-graphics-libs.sh`
|
||||
- `scripts/release/test-strip-appimage-rpaths.sh`
|
||||
- a new final-artifact validator at
|
||||
`scripts/release/validate-appimage-runtime.sh`
|
||||
- the Linux portion of `.github/workflows/build-desktop.yml`
|
||||
|
||||
The existing graphics-library removal, interpreter replacement, RPATH repair,
|
||||
artifact signing, updater tarball rebuilding, x86_64/aarch64 build matrix, and
|
||||
non-Linux workflow branches remain in place.
|
||||
|
||||
## Scope guardrails
|
||||
|
||||
This plan intentionally does **not** implement the broader generated triage plan
|
||||
currently embedded in issue #5037:
|
||||
|
||||
- The unbundled `openhuman-core`/AUR failure remains a downstream packaging and
|
||||
optional-`enigo` follow-up. Do not change `Cargo.toml`, feature gates, voice
|
||||
input, computer tools, `INSTALL.md`, or AUR metadata in this work.
|
||||
- Do not change bundled `libssl.so.3`/`libcrypto.so.3`, NSS exclusions, or claim
|
||||
to fix the later OpenSSL symbol-skew/segfault behavior in #3716. The smoke
|
||||
catches an immediate loader failure, not every later CEF/OpenSSL crash.
|
||||
- Do not pin the mutable `quick-sharun.sh` or uruntime downloads in the vendored
|
||||
Tauri bundler. Supply-chain pinning is a separate follow-up.
|
||||
- Do not add an Arch/Fedora/Tumbleweed matrix or make
|
||||
`.github/workflows/pr-quality.yml`'s advisory AppImage guard blocking. The
|
||||
release-builder smoke is the selected acceptance test.
|
||||
- Do not replace sharun with a custom `LD_LIBRARY_PATH` wrapper.
|
||||
|
||||
## Confirmed sharun contract
|
||||
|
||||
Pin the tests and implementation to the actual sharun grammar:
|
||||
|
||||
- `shared/lib/lib.path` is one entry per line.
|
||||
- `+` represents the directory containing `lib.path`, which is the extracted
|
||||
AppDir's canonical `shared/lib` directory.
|
||||
- `+/subdir` represents a descendant of that directory.
|
||||
- sharun trims the complete file, replaces newlines with `:`, and then replaces
|
||||
every `+` with the absolute `shared/lib` path.
|
||||
- `+` is therefore a marker inside an entry, **not** an entry delimiter.
|
||||
|
||||
The current `tr '+:' '\n\n'` parser destroys this marker and must be removed.
|
||||
Tests must use the accepted textual forms `+` and `+/subdir`, with newline
|
||||
separation. They must not introduce an invented plus- or colon-separated input
|
||||
format.
|
||||
|
||||
## Sequencing decision
|
||||
|
||||
`strip-appimage-graphics-libs.sh` currently performs four phases in one command:
|
||||
|
||||
1. extract and postprocess an AppDir;
|
||||
2. repack and replace the final `.AppImage`;
|
||||
3. re-sign the `.AppImage`;
|
||||
4. rebuild and re-sign its updater tarball.
|
||||
|
||||
The final-artifact validator must run after phase 2 but before phase 3. Add the
|
||||
validator call immediately after the rebuilt file is moved over the original
|
||||
and before the image is appended to `MODIFIED_PATHS`. This satisfies both
|
||||
requirements that the inspected bytes are the uploaded bytes and that a bad
|
||||
launcher fails before signing. It also avoids a defer/finalize mode that could
|
||||
accidentally validate one image and then repack another.
|
||||
|
||||
Static validation runs for every Linux AppImage. The executable smoke is
|
||||
enabled only for `x86_64-unknown-linux-gnu` in this issue because that is the
|
||||
reported architecture and acceptance target. The helper remains
|
||||
architecture-neutral: a later change can enable the same smoke on arm64 by
|
||||
setting the same environment flag on an executable arm64 runner.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Replace the destructive parser with sharun-native normalization
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/release/test-strip-appimage-rpaths.sh`
|
||||
- Modify: `scripts/release/strip-appimage-graphics-libs.sh`
|
||||
|
||||
### Step 1.1: Add failing normalization fixtures
|
||||
|
||||
Extend `test-strip-appimage-rpaths.sh` after the host-ELF fixture is established
|
||||
and the target script is sourced. Add a reusable released-style AppDir builder:
|
||||
|
||||
```bash
|
||||
make_sharun_appdir() {
|
||||
local appdir="$1"
|
||||
mkdir -p "$appdir/shared/bin" "$appdir/shared/lib/plugins" "$appdir/bin"
|
||||
|
||||
cp "$HOST_ELF" "$appdir/sharun"
|
||||
printf 'Interpreter not found!' >> "$appdir/sharun"
|
||||
chmod +x "$appdir/sharun"
|
||||
ln "$appdir/sharun" "$appdir/AppRun"
|
||||
ln "$appdir/sharun" "$appdir/bin/OpenHuman"
|
||||
|
||||
cp "$HOST_ELF" "$appdir/shared/bin/OpenHuman"
|
||||
chmod +x "$appdir/shared/bin/OpenHuman"
|
||||
}
|
||||
```
|
||||
|
||||
Use separate fixture directories so one failure cannot contaminate the next.
|
||||
Add these assertions:
|
||||
|
||||
1. An input with one entry
|
||||
`"$WORK/squashfs-root/shared/lib"` rewrites to exactly `+`.
|
||||
2. An input rooted in the bundler staging layout
|
||||
`"$WORK/appimage_deb/data/usr/lib"` rewrites to exactly `+`.
|
||||
3. AppDir descendants map as follows:
|
||||
- `"$WORK/squashfs-root/shared/lib/plugins"` becomes `+/plugins`;
|
||||
- `"$WORK/appimage_deb/data/usr/lib/plugins"` becomes `+/plugins`.
|
||||
4. A file containing these newline-delimited entries:
|
||||
|
||||
```text
|
||||
+
|
||||
+/plugins
|
||||
+
|
||||
/opt/unrelated/lib
|
||||
```
|
||||
|
||||
normalizes to exactly:
|
||||
|
||||
```text
|
||||
+
|
||||
+/plugins
|
||||
```
|
||||
|
||||
Stable first-seen order is preserved and the unrelated host path is
|
||||
excluded.
|
||||
|
||||
5. Running `rewrite_sharun_lib_path` a second time leaves the bytes unchanged
|
||||
and returns the no-change status expected by `strip_one_appimage`.
|
||||
6. An input containing only `/opt/unrelated/lib` fails normalization and is not
|
||||
replaced with `shared/lib`.
|
||||
|
||||
Capture expected failures in subshells because the release helpers are allowed
|
||||
to terminate fail-closed:
|
||||
|
||||
```bash
|
||||
if ( rewrite_sharun_lib_path "$fixture" ) 2>"$WORK/rewrite.err"; then
|
||||
fail "rewrite accepted a lib.path with no AppDir library root"
|
||||
fi
|
||||
grep -F "no valid AppDir library entries" "$WORK/rewrite.err" >/dev/null \
|
||||
|| fail "rewrite failure did not explain why lib.path was rejected"
|
||||
```
|
||||
|
||||
Run the focused test on Linux:
|
||||
|
||||
```bash
|
||||
bash scripts/release/test-strip-appimage-rpaths.sh
|
||||
```
|
||||
|
||||
Expected RED: at least the canonical-marker case fails because the current
|
||||
implementation splits on `+` and writes `shared/lib`; the no-valid-root case
|
||||
also fails because the current fallback silently writes `shared/lib`.
|
||||
|
||||
On macOS, first run:
|
||||
|
||||
```bash
|
||||
bash scripts/release/test-strip-appimage-rpaths.sh
|
||||
```
|
||||
|
||||
Expected local result: a clean `SKIP` when `patchelf` or a host ELF is
|
||||
unavailable. Do not treat that skip as execution evidence; the RED/GREEN result
|
||||
for this task must come from the existing Ubuntu PR job or another Linux host.
|
||||
|
||||
### Step 1.2: Implement newline-based normalization
|
||||
|
||||
Refactor `rewrite_sharun_lib_path` without changing its public signature:
|
||||
|
||||
```bash
|
||||
rewrite_sharun_lib_path() {
|
||||
local appdir="$1"
|
||||
# Return 1 for non-sharun/no-file/no-change.
|
||||
# Return 0 after writing a changed valid file.
|
||||
# Exit non-zero with an ERROR for malformed input from which no valid
|
||||
# AppDir-rooted entry can be derived.
|
||||
}
|
||||
```
|
||||
|
||||
Implement the following exact algorithm:
|
||||
|
||||
1. Keep the `uses_sharun_launcher` and non-empty-file guards.
|
||||
2. Read with `while IFS= read -r entry || [ -n "$entry" ]`; do not use `tr`,
|
||||
word splitting, `eval`, or colon splitting.
|
||||
3. Reject empty interior lines, carriage returns, embedded `:`, and surrounding
|
||||
whitespace as malformed rather than letting the loader interpret them
|
||||
differently from the validator.
|
||||
4. Normalize each entry:
|
||||
- `+` stays `+`;
|
||||
- `+/suffix` stays `+/suffix` after the descendant checks in Task 2;
|
||||
- an absolute path containing `/squashfs-root/shared/lib` maps that root to
|
||||
`+` and its suffix to `+/suffix`;
|
||||
- an absolute path containing `/appimage_deb/data/usr/lib` or `/data/usr/lib`
|
||||
maps that root to `+` and its suffix to `+/suffix`;
|
||||
- another absolute path is dropped;
|
||||
- a bare relative path is malformed and causes normalization to fail.
|
||||
5. For derived suffixes, remove one leading slash and no other content. A root
|
||||
with an empty suffix becomes `+`.
|
||||
6. Deduplicate using exact whole-entry equality while preserving first-seen
|
||||
order. Use an indexed Bash array and a small equality loop so the script
|
||||
remains syntax-compatible with macOS Bash 3.2; do not introduce associative
|
||||
arrays.
|
||||
7. If no valid AppDir-rooted entries remain, print:
|
||||
|
||||
```text
|
||||
[strip-libs] ERROR: shared/lib/lib.path contains no valid AppDir library entries
|
||||
```
|
||||
|
||||
and return non-zero without modifying the file.
|
||||
|
||||
8. Join entries with newline characters and write through a temporary file in
|
||||
the same directory followed by `mv`. Preserve the final format as one entry
|
||||
per line; a trailing newline is acceptable but must be deterministic.
|
||||
9. Compare the normalized temporary file with the original using `cmp -s`.
|
||||
Delete the temporary and return `1` on an idempotent no-op. On change, move
|
||||
it into place, log the normalized entries, and return `0`.
|
||||
|
||||
Remove the early grep that only invokes rewriting when `/home/runner` or `/__w`
|
||||
is present. Valid marker files still need the idempotency path, and malformed
|
||||
bare-relative files must not bypass normalization.
|
||||
|
||||
### Step 1.3: Run the GREEN checks
|
||||
|
||||
Linux:
|
||||
|
||||
```bash
|
||||
bash -n scripts/release/strip-appimage-graphics-libs.sh
|
||||
bash -n scripts/release/test-strip-appimage-rpaths.sh
|
||||
bash scripts/release/test-strip-appimage-rpaths.sh
|
||||
```
|
||||
|
||||
Expected GREEN: all old RPATH/required-library tests and all new normalization
|
||||
cases pass.
|
||||
|
||||
macOS:
|
||||
|
||||
```bash
|
||||
bash -n scripts/release/strip-appimage-graphics-libs.sh
|
||||
bash -n scripts/release/test-strip-appimage-rpaths.sh
|
||||
bash scripts/release/test-strip-appimage-rpaths.sh
|
||||
```
|
||||
|
||||
Expected: both syntax checks pass and the execution test skips cleanly if Linux
|
||||
ELF tooling is absent.
|
||||
|
||||
### Step 1.4: Commit the normalization
|
||||
|
||||
```bash
|
||||
atomic-commit "fix(appimage): emit sharun-native library paths" -- \
|
||||
scripts/release/test-strip-appimage-rpaths.sh \
|
||||
scripts/release/strip-appimage-graphics-libs.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Make `lib.path` validation fail closed and cover the shipped ELF launcher
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `scripts/release/test-strip-appimage-rpaths.sh`
|
||||
- Modify: `scripts/release/strip-appimage-graphics-libs.sh`
|
||||
|
||||
### Step 2.1: Add failing validator and launcher-layout cases
|
||||
|
||||
Reuse `make_sharun_appdir` and add a table-driven helper:
|
||||
|
||||
```bash
|
||||
assert_lib_path_rejected() {
|
||||
local label="$1"
|
||||
local value="$2"
|
||||
local expected_entry="$3"
|
||||
local fixture="$WORK/reject-$label"
|
||||
make_sharun_appdir "$fixture"
|
||||
printf '%s\n' "$value" >"$fixture/shared/lib/lib.path"
|
||||
|
||||
if ( validate_sharun_lib_path "$fixture" ) 2>"$fixture/error.log"; then
|
||||
fail "validate_sharun_lib_path accepted $label"
|
||||
fi
|
||||
grep -F "$expected_entry" "$fixture/error.log" >/dev/null \
|
||||
|| fail "$label error did not identify offending entry '$expected_entry'"
|
||||
}
|
||||
```
|
||||
|
||||
Add rejection cases for:
|
||||
|
||||
- `shared/lib` (bare relative);
|
||||
- `/home/runner/work/openhuman/squashfs-root/shared/lib` (CI absolute);
|
||||
- `/__w/openhuman/openhuman/appimage_deb/data/usr/lib` (CI absolute);
|
||||
- `+/../outside` (parent traversal);
|
||||
- `+/plugins/../../outside` (nested traversal);
|
||||
- `+/missing` (marker-relative directory absent from the fixture);
|
||||
- `+/plugins:shared/lib` (embedded loader separator);
|
||||
- ` +` and `+ ` (whitespace changes loader semantics).
|
||||
|
||||
Each error assertion must match the complete offending entry in the diagnostic,
|
||||
not only a generic error category.
|
||||
|
||||
Add accepted cases:
|
||||
|
||||
- `+`;
|
||||
- `+` followed by `+/plugins` on a new line;
|
||||
- a normalized file passed twice, proving validation is non-mutating.
|
||||
|
||||
Add explicit released-layout assertions:
|
||||
|
||||
```bash
|
||||
[ "$fixture/AppRun" -ef "$fixture/sharun" ] \
|
||||
|| fail "AppRun fixture is not hard-linked to sharun"
|
||||
[ "$fixture/bin/OpenHuman" -ef "$fixture/sharun" ] \
|
||||
|| fail "bin/OpenHuman fixture is not hard-linked to sharun"
|
||||
is_executable_elf "$fixture/AppRun" \
|
||||
|| fail "released-style AppRun fixture is not ELF"
|
||||
uses_sharun_launcher "$fixture" \
|
||||
|| fail "released-style hard-linked ELF launcher was not detected"
|
||||
```
|
||||
|
||||
Write `+` to its `lib.path`, validate it, change to an unrelated directory, and
|
||||
model sharun's pinned expansion:
|
||||
|
||||
```bash
|
||||
caller="$WORK/caller"
|
||||
mkdir -p "$caller"
|
||||
(
|
||||
cd "$caller"
|
||||
expanded="$(
|
||||
sed "s|+|$fixture/shared/lib|g" "$fixture/shared/lib/lib.path" |
|
||||
paste -sd ':' -
|
||||
)"
|
||||
[ "$expanded" = "$fixture/shared/lib" ] \
|
||||
|| fail "sharun marker did not expand beneath the fixture AppDir"
|
||||
)
|
||||
```
|
||||
|
||||
Also assert that `patch_apprun_sharun_cwd "$fixture"` returns its no-change
|
||||
status and does not mutate the ELF inode. This pins the compatibility helper as
|
||||
shell-wrapper-only hardening, not the fix for released ELF AppRuns.
|
||||
|
||||
Run on Linux:
|
||||
|
||||
```bash
|
||||
bash scripts/release/test-strip-appimage-rpaths.sh
|
||||
```
|
||||
|
||||
Expected RED: the current validator accepts bare relatives, traversal, missing
|
||||
directories, and an ELF AppRun without any meaningful library-path checks.
|
||||
|
||||
### Step 2.2: Implement exact fail-closed validation
|
||||
|
||||
Keep the `validate_sharun_lib_path "$appdir"` interface. Parse the file by
|
||||
newline using the same helper/loop as normalization so rewrite and validation
|
||||
cannot disagree.
|
||||
|
||||
For every non-empty entry:
|
||||
|
||||
1. Accept only `+` or a string beginning `+/`.
|
||||
2. Reject `:`, carriage returns, leading/trailing whitespace, absolute paths,
|
||||
and bare relative paths.
|
||||
3. Split the marker-relative suffix on `/` and reject empty, `.`, or `..`
|
||||
components. This rejects traversal before filesystem canonicalization.
|
||||
4. Resolve `+` to `$appdir/shared/lib`; resolve `+/suffix` to
|
||||
`$appdir/shared/lib/suffix`.
|
||||
5. Require the resolved path to be a directory.
|
||||
6. Canonicalize both the library root and resolved directory with `realpath`,
|
||||
then require the result to equal the root or start with
|
||||
`"$canonical_root/"`. This prevents an in-tree symlink from escaping the
|
||||
AppDir even when the textual path contains no `..`.
|
||||
7. On failure, print one line with the offending entry:
|
||||
|
||||
```text
|
||||
[strip-libs] ERROR: invalid sharun lib.path entry '<entry>': <reason>
|
||||
```
|
||||
|
||||
and return/exit non-zero before repacking.
|
||||
|
||||
Require at least one entry and retain the existing missing-file diagnostic.
|
||||
Do not mutate the file in validation.
|
||||
|
||||
Update the comment above `patch_apprun_sharun_cwd` and its success/warning logs:
|
||||
|
||||
- state that valid `+` paths are the primary released-layout fix;
|
||||
- state that the helper remains for historical shell `AppRun` layouts;
|
||||
- remove claims that changing CWD is required for an ELF sharun launcher;
|
||||
- preserve the current shell patch behavior and return convention.
|
||||
|
||||
### Step 2.3: Run GREEN checks
|
||||
|
||||
Linux:
|
||||
|
||||
```bash
|
||||
bash -n scripts/release/strip-appimage-graphics-libs.sh
|
||||
bash scripts/release/test-strip-appimage-rpaths.sh
|
||||
```
|
||||
|
||||
Expected GREEN: accepted marker paths pass; every malformed entry fails with
|
||||
the exact entry in stderr; the ELF hard-link fixture is detected and remains
|
||||
unchanged by the shell compatibility patch.
|
||||
|
||||
macOS:
|
||||
|
||||
```bash
|
||||
bash -n scripts/release/strip-appimage-graphics-libs.sh
|
||||
bash -n scripts/release/test-strip-appimage-rpaths.sh
|
||||
bash scripts/release/test-strip-appimage-rpaths.sh
|
||||
```
|
||||
|
||||
Expected: syntax passes and the execution test cleanly skips if required.
|
||||
|
||||
### Step 2.4: Commit fail-closed validation
|
||||
|
||||
```bash
|
||||
atomic-commit "test(appimage): reject malformed sharun library paths" -- \
|
||||
scripts/release/test-strip-appimage-rpaths.sh \
|
||||
scripts/release/strip-appimage-graphics-libs.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Inspect and smoke-test the final repacked AppImage before signing
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `scripts/release/validate-appimage-runtime.sh`
|
||||
- Modify: `scripts/release/test-strip-appimage-rpaths.sh`
|
||||
- Modify: `scripts/release/strip-appimage-graphics-libs.sh`
|
||||
|
||||
### Step 3.1: Add failing tests for extracted-layout validation
|
||||
|
||||
The new script must be sourceable for fixture tests and executable for a real
|
||||
AppImage. Before implementing it, extend
|
||||
`test-strip-appimage-rpaths.sh` to source it after sourcing the postprocessor:
|
||||
|
||||
```bash
|
||||
RUNTIME_VALIDATOR="$SCRIPT_DIR/validate-appimage-runtime.sh"
|
||||
[ -f "$RUNTIME_VALIDATOR" ] \
|
||||
|| fail "$RUNTIME_VALIDATOR not found"
|
||||
# shellcheck source=/dev/null
|
||||
source "$RUNTIME_VALIDATOR"
|
||||
```
|
||||
|
||||
Add fixture tests for the function interface:
|
||||
|
||||
```bash
|
||||
validate_extracted_appdir "$appdir"
|
||||
validate_final_appimage "$appimage"
|
||||
smoke_extracted_apprun "$appdir" "$foreign_cwd" "$log_file"
|
||||
```
|
||||
|
||||
`validate_extracted_appdir` is the unit-test seam; it must not extract or launch
|
||||
anything. Build a complete fixture with:
|
||||
|
||||
- hard-linked ELF `AppRun`, `sharun`, and `bin/OpenHuman`;
|
||||
- executable ELF `shared/bin/OpenHuman`;
|
||||
- `shared/lib/lib.path` containing `+`;
|
||||
- `shared/lib/anylinux.so`;
|
||||
- `shared/lib/libxdo.so.3`;
|
||||
- `shared/lib/libcef.so`;
|
||||
- clean RPATHs on all copied ELFs.
|
||||
|
||||
Because `/bin/true` does not naturally declare the production `NEEDED`
|
||||
libraries, make the needed-name check injectable only in fixture mode:
|
||||
|
||||
```bash
|
||||
APPIMAGE_EXPECTED_NEEDED="${APPIMAGE_EXPECTED_NEEDED:-libxdo.so.3 libcef.so}"
|
||||
```
|
||||
|
||||
Set `APPIMAGE_EXPECTED_NEEDED=""` only around unit fixtures. The executable CLI
|
||||
and release workflow must leave the production default intact.
|
||||
|
||||
Assert the complete fixture passes. Then clone it into isolated cases and
|
||||
assert rejection when:
|
||||
|
||||
- `anylinux.so` is absent;
|
||||
- `libxdo.so.3` is absent;
|
||||
- `libcef.so` is absent;
|
||||
- `shared/bin/OpenHuman` is absent or is not ELF;
|
||||
- `AppRun` is neither the same inode nor byte-equivalent to `sharun`;
|
||||
- `bin/OpenHuman` is neither the same inode nor byte-equivalent to `sharun`;
|
||||
- an ELF RPATH contains
|
||||
`/home/runner/work/openhuman/openhuman/shared/lib`;
|
||||
- an ELF RPATH contains `/__w/openhuman/openhuman/shared/lib`;
|
||||
- the real app's `NEEDED` list lacks `libxdo.so.3`;
|
||||
- the real app's `NEEDED` list lacks `libcef.so`.
|
||||
|
||||
For equivalent launcher support, copy rather than hard-link `sharun` to
|
||||
`AppRun` and `bin/OpenHuman`; byte-identical executable copies must pass. This
|
||||
covers a repacker that preserves launcher contents but not hard-link metadata.
|
||||
|
||||
Add a source-order regression that invokes a stub validator from
|
||||
`strip_one_appimage` and proves it runs after the final `mv` but before the path
|
||||
is appended to `MODIFIED_PATHS`. Do this through an overridable command:
|
||||
|
||||
```bash
|
||||
APPIMAGE_RUNTIME_VALIDATOR="${APPIMAGE_RUNTIME_VALIDATOR:-$SCRIPT_DIR/validate-appimage-runtime.sh}"
|
||||
```
|
||||
|
||||
The test stub must record that the supplied final path exists and that
|
||||
`MODIFIED_PATHS` is still empty. Do not execute or repack a real AppImage in
|
||||
this unit test; factor the post-repack handoff into:
|
||||
|
||||
```bash
|
||||
validate_rebuilt_appimage() {
|
||||
local rebuilt_path="$1"
|
||||
"$APPIMAGE_RUNTIME_VALIDATOR" "$rebuilt_path"
|
||||
}
|
||||
```
|
||||
|
||||
Call this seam directly with a fixture file to pin its command/argument
|
||||
contract, and use a source-text order assertion for the three adjacent
|
||||
statements:
|
||||
|
||||
```text
|
||||
mv "$rebuilt" "$original"
|
||||
validate_rebuilt_appimage "$original"
|
||||
MODIFIED_PATHS+=("$original")
|
||||
```
|
||||
|
||||
Run on Linux:
|
||||
|
||||
```bash
|
||||
bash scripts/release/test-strip-appimage-rpaths.sh
|
||||
```
|
||||
|
||||
Expected RED: the validator file/functions and the post-repack handoff do not
|
||||
exist.
|
||||
|
||||
### Step 3.2: Implement the sourceable final-artifact validator
|
||||
|
||||
Create `scripts/release/validate-appimage-runtime.sh` with `set -euo pipefail`,
|
||||
three shell functions, and a direct-execution guard. The exact interfaces are:
|
||||
|
||||
- `validate_extracted_appdir "$appdir"` performs the eight ordered static checks
|
||||
immediately below.
|
||||
- `smoke_extracted_apprun "$appdir" "$foreign_cwd" "$log_file"` performs the
|
||||
eight ordered launch checks immediately below.
|
||||
- `validate_final_appimage "$image"` performs the five ordered extraction
|
||||
checks immediately below.
|
||||
|
||||
Use this direct-execution guard:
|
||||
|
||||
```bash
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||
[ "$#" -eq 1 ] || {
|
||||
echo "Usage: $0 <final.AppImage>" >&2
|
||||
exit 2
|
||||
}
|
||||
validate_final_appimage "$1"
|
||||
fi
|
||||
```
|
||||
|
||||
Do not duplicate `lib.path` grammar. Resolve the script directory and source
|
||||
`strip-appimage-graphics-libs.sh`, whose direct-execution guard prevents
|
||||
`main()` from running. Reuse:
|
||||
|
||||
- `is_elf`;
|
||||
- `is_executable_elf`;
|
||||
- `uses_sharun_launcher`;
|
||||
- `validate_sharun_lib_path`;
|
||||
- `validate_appimage_required_libs`.
|
||||
|
||||
Implement `validate_extracted_appdir "$appdir"` as follows:
|
||||
|
||||
1. Print the extracted path and the normalized `lib.path` contents with each
|
||||
line prefixed for readable CI logs.
|
||||
2. Require `uses_sharun_launcher "$appdir"`.
|
||||
3. Require ELF executables at `AppRun`, `sharun`, `bin/OpenHuman`, and
|
||||
`shared/bin/OpenHuman`.
|
||||
4. For both launcher aliases, accept either `[ alias -ef sharun ]` or
|
||||
`cmp -s alias sharun`; reject anything else.
|
||||
5. Call `validate_sharun_lib_path` and
|
||||
`validate_appimage_required_libs`.
|
||||
6. Add `anylinux.so` to the required-library contract in
|
||||
`validate_appimage_required_libs`; its missing diagnostic must identify the
|
||||
preload library separately from the existing libxdo/CEF diagnostics.
|
||||
7. Run `patchelf --print-needed "$appdir/shared/bin/OpenHuman"` and require each
|
||||
whitespace-delimited name in
|
||||
`${APPIMAGE_EXPECTED_NEEDED:-libxdo.so.3 libcef.so}` as an exact full line.
|
||||
Print the NEEDED list before checking it.
|
||||
8. Walk ELF files under `shared/bin`, `shared/lib`, `bin`, `lib`, `usr/bin`, and
|
||||
`usr/lib`. For each, read RPATH with `patchelf --print-rpath`; reject
|
||||
`/home/runner/` and `/__w/`. This is validation only: never patch the final
|
||||
artifact.
|
||||
|
||||
`validate_final_appimage "$image"` must:
|
||||
|
||||
1. Resolve the input with `realpath`, require an executable regular file, and
|
||||
require `patchelf`.
|
||||
2. Create one temporary root and a distinct `foreign-cwd` below it; install a
|
||||
trap that removes only that exact temporary root.
|
||||
3. Change into the foreign CWD before invoking
|
||||
`"$image" --appimage-extract`, redirecting extraction output to a log. This
|
||||
ensures extraction itself does not rely on the artifact directory.
|
||||
4. Require `$foreign_cwd/squashfs-root`, then call
|
||||
`validate_extracted_appdir` on it.
|
||||
5. If `APPIMAGE_RUNTIME_SMOKE=1`, call `smoke_extracted_apprun`; otherwise print
|
||||
an explicit architecture/static-only message.
|
||||
|
||||
`smoke_extracted_apprun "$appdir" "$foreign_cwd" "$log_file"` must:
|
||||
|
||||
1. Require `timeout` and `xvfb-run`.
|
||||
2. Print the AppDir and caller CWD.
|
||||
3. Create isolated `home`, `config`, `data`, and `cache` directories beneath
|
||||
the temporary root so the release smoke cannot read or mutate the runner's
|
||||
real OpenHuman state.
|
||||
4. Launch from `$foreign_cwd` with:
|
||||
|
||||
```bash
|
||||
timeout --signal=TERM --kill-after=5s 15s \
|
||||
xvfb-run -a --server-args="-screen 0 1280x960x24" \
|
||||
env \
|
||||
HOME="$smoke_home" \
|
||||
XDG_CONFIG_HOME="$smoke_config" \
|
||||
XDG_DATA_HOME="$smoke_data" \
|
||||
XDG_CACHE_HOME="$smoke_cache" \
|
||||
OPENHUMAN_CEF_PREWARM=0 \
|
||||
OPENHUMAN_DISABLE_GPU=1 \
|
||||
"$appdir/AppRun"
|
||||
```
|
||||
|
||||
Before `env`, unset `GITHUB_TOKEN`, `GH_TOKEN`,
|
||||
`TAURI_SIGNING_PRIVATE_KEY`, `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`,
|
||||
`SENTRY_AUTH_TOKEN`, and all `OPENAI_*` variables present in the environment.
|
||||
Build the `env -u NAME` arguments from variable **names only**; never print
|
||||
values.
|
||||
|
||||
5. Capture stdout/stderr in `$log_file` and preserve the timeout status despite
|
||||
`set -e`.
|
||||
6. Treat status `124` as success: the GUI remained alive through the bounded
|
||||
startup window. Treat any other status as failure, including an immediate
|
||||
clean exit, because it did not prove a live application boundary.
|
||||
7. Regardless of status, search the log case-insensitively for:
|
||||
- `anylinux.so` combined with `cannot be preloaded`;
|
||||
- `libxdo.so.3` combined with `cannot open shared object file`;
|
||||
- `libcef.so` combined with `cannot open shared object file`;
|
||||
- `error while loading shared libraries`.
|
||||
8. On a forbidden diagnostic or unexpected exit, print the complete smoke log
|
||||
and fail. On success, print only lines matching loader/CEF/startup keywords
|
||||
plus a message that the application remained alive for the startup window.
|
||||
This keeps logs useful without dumping inherited environment data.
|
||||
|
||||
### Step 3.3: Call the validator after repack and before signing
|
||||
|
||||
In `strip-appimage-graphics-libs.sh`:
|
||||
|
||||
1. Define `SCRIPT_DIR` once from `BASH_SOURCE[0]`.
|
||||
2. Define
|
||||
`APPIMAGE_RUNTIME_VALIDATOR="${APPIMAGE_RUNTIME_VALIDATOR:-$SCRIPT_DIR/validate-appimage-runtime.sh}"`.
|
||||
3. Add `validate_rebuilt_appimage` with the exact one-argument forwarding
|
||||
contract above.
|
||||
4. In `strip_one_appimage`, change the final order to:
|
||||
|
||||
```bash
|
||||
mv "$rebuilt" "$original"
|
||||
validate_rebuilt_appimage "$original"
|
||||
rm -rf "$workdir"
|
||||
MODIFIED_PATHS+=("$original")
|
||||
```
|
||||
|
||||
Because `set -e` is active, any static or launch failure stops before
|
||||
`MODIFIED_PATHS` is populated and before `main` reaches `resign_artifact` or
|
||||
updater tarball rebuilding.
|
||||
|
||||
Keep the intermediate AppDir validation at its current pre-repack location.
|
||||
It provides a faster diagnostic; the new call proves the repacked bytes.
|
||||
|
||||
### Step 3.4: Run GREEN checks
|
||||
|
||||
Linux unit/fixture lane:
|
||||
|
||||
```bash
|
||||
bash -n scripts/release/strip-appimage-graphics-libs.sh
|
||||
bash -n scripts/release/validate-appimage-runtime.sh
|
||||
bash -n scripts/release/test-strip-appimage-rpaths.sh
|
||||
bash scripts/release/test-strip-appimage-rpaths.sh
|
||||
```
|
||||
|
||||
Expected GREEN: extracted-layout fixtures cover required libraries, NEEDED
|
||||
names, RPATHs, equivalent/hard-linked launchers, and post-repack ordering.
|
||||
|
||||
Linux real-artifact check, after building an amd64 AppImage:
|
||||
|
||||
```bash
|
||||
APPIMAGE_RUNTIME_SMOKE=1 \
|
||||
bash scripts/release/validate-appimage-runtime.sh \
|
||||
app/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||
```
|
||||
|
||||
If the artifact exists under the root target tree instead, use the one exact
|
||||
matching file under:
|
||||
|
||||
```text
|
||||
target/x86_64-unknown-linux-gnu/release/bundle/appimage/
|
||||
```
|
||||
|
||||
Do not pass multiple glob matches to the one-image CLI; fail the build if the
|
||||
bundle root unexpectedly contains zero or multiple amd64 AppImages.
|
||||
|
||||
macOS:
|
||||
|
||||
```bash
|
||||
bash -n scripts/release/strip-appimage-graphics-libs.sh
|
||||
bash -n scripts/release/validate-appimage-runtime.sh
|
||||
bash -n scripts/release/test-strip-appimage-rpaths.sh
|
||||
bash scripts/release/test-strip-appimage-rpaths.sh
|
||||
```
|
||||
|
||||
Expected: syntax checks pass and the fixture suite skips cleanly without Linux
|
||||
ELF tooling. Do not attempt to execute the AppImage or claim runtime validation
|
||||
from macOS.
|
||||
|
||||
### Step 3.5: Commit final-artifact validation
|
||||
|
||||
```bash
|
||||
atomic-commit "fix(appimage): validate the repacked runtime before signing" -- \
|
||||
scripts/release/validate-appimage-runtime.sh \
|
||||
scripts/release/test-strip-appimage-rpaths.sh \
|
||||
scripts/release/strip-appimage-graphics-libs.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Enable the amd64 foreign-CWD smoke in the Linux desktop build
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `.github/workflows/build-desktop.yml`
|
||||
|
||||
### Step 4.1: Establish the wiring gap
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n 'APPIMAGE_RUNTIME_SMOKE|xvfb' .github/workflows/build-desktop.yml
|
||||
```
|
||||
|
||||
Expected RED: the workflow neither installs Xvfb nor enables the final
|
||||
AppImage smoke.
|
||||
|
||||
The behavior itself is already fixture-tested in Task 3. This task is declarative
|
||||
workflow wiring; validate it with YAML/action checks rather than duplicating the
|
||||
shell behavior in a YAML text test.
|
||||
|
||||
### Step 4.2: Add Linux runtime prerequisites
|
||||
|
||||
In `Install Tauri dependencies (ubuntu only)`, add `xvfb` to the existing
|
||||
`apt-get install -y` list. `timeout` is supplied by Ubuntu's existing
|
||||
`coreutils`; do not add another timeout package.
|
||||
|
||||
Do not add these dependencies to macOS or Windows.
|
||||
|
||||
### Step 4.3: Enable smoke only on the reported architecture
|
||||
|
||||
Rename the existing step to:
|
||||
|
||||
```text
|
||||
Strip host graphics libs and validate final AppImage
|
||||
```
|
||||
|
||||
Keep its Linux-only `if:` expression, bundle-root arguments, target/profile
|
||||
variables, and signing secrets unchanged. Add:
|
||||
|
||||
```yaml
|
||||
APPIMAGE_RUNTIME_SMOKE: ${{ matrix.settings.target == 'x86_64-unknown-linux-gnu' && '1' || '0' }}
|
||||
```
|
||||
|
||||
The postprocessor now performs, in order:
|
||||
|
||||
1. graphics stripping/interpreter/RPATH/lib.path repair;
|
||||
2. repack;
|
||||
3. final artifact extraction and static validation on both Linux architectures;
|
||||
4. foreign-CWD Xvfb smoke on amd64;
|
||||
5. signing and updater archive rebuilding.
|
||||
|
||||
Do not use `continue-on-error`. Do not change the advisory status of
|
||||
`pr-quality.yml`; release/staging desktop callers already consume this reusable
|
||||
workflow and will fail before upload if the validator fails.
|
||||
|
||||
Update the step comment to state that:
|
||||
|
||||
- final-artifact validation occurs before re-signing;
|
||||
- amd64 launch is bounded and runs from a foreign CWD;
|
||||
- arm64 receives static validation in this change;
|
||||
- updater archives are still rebuilt only after the validated image is final.
|
||||
|
||||
### Step 4.4: Validate workflow syntax and formatting
|
||||
|
||||
Run whichever actionlint path the repository provides:
|
||||
|
||||
```bash
|
||||
if command -v actionlint >/dev/null 2>&1; then
|
||||
actionlint .github/workflows/build-desktop.yml
|
||||
else
|
||||
echo "actionlint unavailable locally; rely on GitHub workflow validation"
|
||||
fi
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
pnpm exec prettier --check .github/workflows/build-desktop.yml
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected GREEN: actionlint (when installed), Prettier, and whitespace checks
|
||||
pass.
|
||||
|
||||
### Step 4.5: Commit workflow wiring
|
||||
|
||||
```bash
|
||||
atomic-commit "ci(appimage): smoke-test the final amd64 launcher" -- \
|
||||
.github/workflows/build-desktop.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Run local checks and obtain Linux evidence
|
||||
|
||||
**Files:**
|
||||
|
||||
- No new files; verification only.
|
||||
|
||||
### Step 5.1: Run the complete macOS-safe local gate
|
||||
|
||||
From the worktree root:
|
||||
|
||||
```bash
|
||||
bash -n scripts/release/strip-appimage-graphics-libs.sh
|
||||
bash -n scripts/release/validate-appimage-runtime.sh
|
||||
bash -n scripts/release/test-strip-appimage-rpaths.sh
|
||||
bash scripts/release/test-strip-appimage-rpaths.sh
|
||||
pnpm exec prettier --check \
|
||||
docs/plans/2026-07-24-appimage-sharun-lib-path.md \
|
||||
.github/workflows/build-desktop.yml
|
||||
git diff --check
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected on macOS:
|
||||
|
||||
- all three shell syntax checks pass;
|
||||
- the Linux fixture script reports `SKIP`, not `FAIL`, if `patchelf`/ELF is
|
||||
unavailable;
|
||||
- Prettier and `git diff --check` pass;
|
||||
- status contains no unrelated files.
|
||||
|
||||
Do not run `pnpm format`, because it writes across the repository and can mix
|
||||
unrelated formatting into these atomic commits.
|
||||
|
||||
### Step 5.2: Require Linux unit evidence
|
||||
|
||||
The existing advisory PR-quality job must run:
|
||||
|
||||
```bash
|
||||
bash -n scripts/release/strip-appimage-graphics-libs.sh
|
||||
bash scripts/release/test-strip-appimage-rpaths.sh
|
||||
```
|
||||
|
||||
Confirm its log includes passing cases for:
|
||||
|
||||
- CI/build path to `+`;
|
||||
- `+/subdir`, order, deduplication, and idempotency;
|
||||
- bare relative, absolute CI, traversal, missing-target, and symlink-escape
|
||||
rejection;
|
||||
- hard-linked/equivalent ELF launcher layout;
|
||||
- anylinux/libxdo/libcef presence;
|
||||
- NEEDED and RPATH validation.
|
||||
|
||||
The job remains advisory by design, but a failure is still a blocker for this
|
||||
change.
|
||||
|
||||
### Step 5.3: Require a real amd64 artifact run
|
||||
|
||||
Run the reusable desktop build on an amd64 Ubuntu runner. The log from
|
||||
`Strip host graphics libs and validate final AppImage` must show:
|
||||
|
||||
- final extracted AppDir path;
|
||||
- caller working directory outside that AppDir;
|
||||
- normalized `lib.path` containing `+` entries only;
|
||||
- `shared/bin/OpenHuman` NEEDED entries;
|
||||
- no CI/build-machine RPATHs;
|
||||
- presence of `anylinux.so`, `libxdo.so.3`, and `libcef.so`;
|
||||
- timeout status `124`, documented as the expected live-GUI boundary;
|
||||
- no preload or loader error for those three libraries;
|
||||
- signing/updater archive messages only after validation succeeds.
|
||||
|
||||
Inspect the later upload step and confirm it consumes the same AppImage path
|
||||
that was validated. Do not substitute an intermediate `squashfs-root` run for
|
||||
this evidence.
|
||||
|
||||
### Step 5.4: Final history and scope review
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git diff --check HEAD~4..HEAD
|
||||
git log --oneline -4
|
||||
git diff --stat baae2b02c..HEAD
|
||||
git diff --name-only baae2b02c..HEAD
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- four atomic implementation commits after the approved spec;
|
||||
- only the four implementation paths named at the top of this plan changed;
|
||||
- no Cargo, AUR, OpenSSL, vendor pin, or PR-quality blocking changes;
|
||||
- no submodule pointer changes.
|
||||
|
||||
If implementation discovers that sharun's bundled version uses a grammar
|
||||
different from the confirmed newline-plus contract, stop and amend the design
|
||||
with artifact/source evidence before changing the parser. Do not guess at a
|
||||
second delimiter grammar in release code.
|
||||
|
||||
## Acceptance checklist
|
||||
|
||||
- [ ] CI/build paths normalize to `+` or `+/descendant`.
|
||||
- [ ] `+` is never treated as a delimiter.
|
||||
- [ ] Output ordering is stable, deduplicated, deterministic, and idempotent.
|
||||
- [ ] Bare relatives, absolute CI paths, traversal, missing directories,
|
||||
separator injection, whitespace ambiguity, and symlink escape fail closed.
|
||||
- [ ] The released ELF hard-link launcher layout is covered; the shell CWD patch
|
||||
remains compatibility-only.
|
||||
- [ ] Intermediate and final repacked AppDirs both pass library-path,
|
||||
required-library, NEEDED, and RPATH validation.
|
||||
- [ ] The final amd64 AppImage is launched from a foreign CWD under Xvfb and
|
||||
remains alive through the bounded startup window.
|
||||
- [ ] Loader logs contain no `anylinux.so`, `libxdo.so.3`, or `libcef.so`
|
||||
resolution error.
|
||||
- [ ] Validation completes before signing/updater archive rebuilding/upload.
|
||||
- [ ] Arm64 static validation and all non-Linux workflow behavior remain intact.
|
||||
- [ ] #3716/OpenSSL, unbundled AUR/core, optional `enigo`, rolling-distro
|
||||
matrices, and quick-sharun pinning remain explicit follow-ups.
|
||||
@@ -0,0 +1,215 @@
|
||||
# AppImage sharun library-path repair
|
||||
|
||||
## Context
|
||||
|
||||
Issue #5037 reports that Linux AppImages fail when launched outside their
|
||||
extracted AppDir. The launcher logs that `anylinux.so` cannot be preloaded and
|
||||
then fails to resolve `libxdo.so.3`, even though both libraries are bundled.
|
||||
Running the extracted application with an explicit AppDir-rooted
|
||||
`LD_LIBRARY_PATH` succeeds.
|
||||
|
||||
Read-only analysis verified the final v0.61.2 and v0.63.1 amd64 AppImages:
|
||||
|
||||
- `AppRun`, `sharun`, and `bin/OpenHuman` are hard links to the sharun ELF
|
||||
launcher.
|
||||
- The real application is `shared/bin/OpenHuman`.
|
||||
- `lib/` contains `anylinux.so`, `libxdo.so.3`, `libcef.so`, `libssl.so.3`, and
|
||||
`libcrypto.so.3`; `shared/lib` is a symlink to `../lib`.
|
||||
- `shared/lib/lib.path` contains the bare relative entry `shared/lib`.
|
||||
- sharun 2.2.4 and 2.2.5 expand the canonical `+` marker to an absolute path
|
||||
rooted at the AppDir's `lib/` directory. They do not anchor a bare
|
||||
`shared/lib` entry.
|
||||
|
||||
The release postprocessor causes the malformed entry.
|
||||
`rewrite_sharun_lib_path` converts absolute CI paths beneath
|
||||
`squashfs-root/shared/lib` into `shared/lib`. The intended compensation,
|
||||
`patch_apprun_sharun_cwd`, only edits a shell `AppRun` and intentionally skips
|
||||
the shipped ELF `AppRun`.
|
||||
|
||||
Consequently, `shared/lib` resolves from the caller's current directory. It
|
||||
works when the caller happens to be the AppDir and fails from locations such as
|
||||
`~/Downloads` or `/tmp`.
|
||||
|
||||
## Evidence boundary
|
||||
|
||||
Artifact hashes, SquashFS layouts, ELF metadata, release-script behavior, sharun
|
||||
path expansion, and caller-directory resolution were verified directly.
|
||||
|
||||
This development host is macOS, and its Docker daemon is unavailable, so it
|
||||
cannot execute the Linux ELF launcher. An identical v0.63.1 process failure is a
|
||||
strong inference from the verified path semantics and must be confirmed by a
|
||||
Linux CI smoke test.
|
||||
|
||||
## Goals
|
||||
|
||||
- Write sharun-native, AppDir-anchored entries to `shared/lib/lib.path`.
|
||||
- Reject malformed bare-relative or CI-runner paths before an AppImage ships.
|
||||
- Cover the actual ELF `AppRun` layout used by released artifacts.
|
||||
- Validate the final repacked AppImage, rather than only the intermediate
|
||||
AppDir.
|
||||
- Exercise the final launcher from a working directory outside the AppDir on
|
||||
Linux.
|
||||
- Preserve the existing graphics-library stripping, interpreter repair,
|
||||
signing, updater archive, and multi-architecture behavior.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Making the unbundled AUR/core binary independent of system `libxdo`.
|
||||
- Changing the unconditional `enigo` dependency or voice input behavior.
|
||||
- Replacing bundled OpenSSL or claiming to resolve issue #3716's later-stage
|
||||
symbol-skew/segfault report.
|
||||
- Bypassing sharun with a custom `LD_LIBRARY_PATH` launcher.
|
||||
- Pinning mutable quick-sharun downloads; that is a separate supply-chain
|
||||
follow-up.
|
||||
- Converting the existing PR-quality AppImage job from advisory to blocking.
|
||||
|
||||
## Considered approaches
|
||||
|
||||
### 1. Emit sharun-native `+` paths and validate them
|
||||
|
||||
Rewrite AppDir library roots to sharun's canonical `+` representation. A root
|
||||
library directory becomes `+`; any retained directory beneath it becomes a
|
||||
`+/subdir` entry. Preserve the format's entry separator without treating the
|
||||
marker itself as a separator.
|
||||
|
||||
Validation rejects:
|
||||
|
||||
- absolute CI runner paths;
|
||||
- bare relative entries such as `shared/lib`;
|
||||
- traversal components;
|
||||
- AppDir-relative targets that do not exist.
|
||||
|
||||
This directly restores sharun's intended absolute path expansion and is the
|
||||
selected approach.
|
||||
|
||||
### 2. Change the working directory in `AppRun.sh`
|
||||
|
||||
Patching the shipped shell wrapper or setting a sharun working-directory
|
||||
variable could make `shared/lib` resolve by accident. It leaves malformed
|
||||
library-path semantics in place, depends on launcher shape, and does not explain
|
||||
the existing ELF `AppRun` skip clearly enough to be the primary fix.
|
||||
|
||||
### 3. Bypass sharun
|
||||
|
||||
A wrapper could execute `shared/bin/OpenHuman` with an explicit absolute
|
||||
`LD_LIBRARY_PATH`. This duplicates sharun's loader and portability behavior and
|
||||
risks losing its environment and compatibility hooks.
|
||||
|
||||
## Design
|
||||
|
||||
### Library-path normalization
|
||||
|
||||
Refactor `rewrite_sharun_lib_path` in
|
||||
`scripts/release/strip-appimage-graphics-libs.sh` so CI-origin entries are
|
||||
normalized into sharun-native AppDir library entries.
|
||||
|
||||
The normalizer must:
|
||||
|
||||
1. Parse entries without destroying the `+` marker.
|
||||
2. Recognize CI paths rooted under an extracted `squashfs-root` or the existing
|
||||
build `data` layout.
|
||||
3. Map the AppDir's canonical library root to `+`, and valid descendants to
|
||||
`+/relative/subdir`.
|
||||
4. Deduplicate entries while preserving stable order.
|
||||
5. Exclude unrelated absolute host paths.
|
||||
6. Fail instead of falling back to a bare `shared/lib` entry when no valid
|
||||
AppDir library root can be derived.
|
||||
7. Be idempotent when the file already contains valid sharun-native entries.
|
||||
|
||||
The implementation must follow sharun's actual `lib.path` delimiter grammar as
|
||||
confirmed from the bundled sharun version. Tests must pin the accepted textual
|
||||
forms rather than rely on an informal reconstruction.
|
||||
|
||||
### Fail-closed validation
|
||||
|
||||
Extend `validate_sharun_lib_path` to reject any non-empty entry that is:
|
||||
|
||||
- a CI/build-machine absolute path;
|
||||
- a bare relative path not beginning with the sharun marker;
|
||||
- a traversal outside the bundled library root;
|
||||
- a marker-relative directory absent from the AppDir.
|
||||
|
||||
The validation error must identify the offending entry and refuse to repack or
|
||||
publish the artifact.
|
||||
|
||||
`patch_apprun_sharun_cwd` may remain as compatibility hardening for historical
|
||||
shell AppRun layouts, but the new tests must establish that an ELF AppRun does
|
||||
not need this patch once `lib.path` is valid. Comments that describe changing
|
||||
the working directory as the primary fix must be updated to reflect that role.
|
||||
|
||||
### Regression tests
|
||||
|
||||
Extend `scripts/release/test-strip-appimage-rpaths.sh` with isolated cases for:
|
||||
|
||||
- a CI absolute `.../squashfs-root/shared/lib` entry rewriting to the canonical
|
||||
marker;
|
||||
- valid marker/subdirectory entries and stable ordering;
|
||||
- deduplication and idempotency;
|
||||
- rejection of bare `shared/lib`, CI paths, traversal, and missing targets;
|
||||
- the released launcher layout where ELF `AppRun`, `sharun`, and
|
||||
`bin/OpenHuman` are hard-linked or equivalent launcher entries;
|
||||
- expansion of the rewritten value according to sharun's algorithm, proving it
|
||||
resolves beneath the fixture AppDir regardless of the caller's directory.
|
||||
|
||||
The test remains Linux-only because it needs ELF fixtures and `patchelf`; it
|
||||
continues to skip cleanly on unsupported development hosts.
|
||||
|
||||
### Final-artifact validation and smoke
|
||||
|
||||
After postprocessing and repacking, Linux CI must inspect the final `.AppImage`
|
||||
that will be uploaded:
|
||||
|
||||
1. Extract it into a temporary directory.
|
||||
2. Re-run the sharun `lib.path`, required-library, ELF `NEEDED`, and RPATH
|
||||
validations against the extracted final layout.
|
||||
3. Change to an unrelated temporary working directory.
|
||||
4. Launch the extracted `AppRun` under a bounded timeout and the existing
|
||||
headless/Xvfb facilities.
|
||||
5. Fail on preload or loader diagnostics for `anylinux.so`, `libxdo.so.3`, or
|
||||
`libcef.so`.
|
||||
6. Treat reaching the normal application initialization boundary as success;
|
||||
the smoke does not require an interactive desktop session.
|
||||
|
||||
The smoke belongs in the Linux desktop build path after
|
||||
`Strip host graphics libs from AppImage`, so it exercises the same final
|
||||
artifact that later signing/upload steps consume. It must cover amd64 first,
|
||||
where #5037 was reproduced, while keeping the validator architecture-neutral so
|
||||
arm64 can use it when an executable runner is available.
|
||||
|
||||
## Failure behavior
|
||||
|
||||
An invalid `lib.path`, missing bundled runtime library, failed final extraction,
|
||||
or foreign-CWD loader error fails the Linux artifact build before signing or
|
||||
upload. The release must not continue with a warning-only malformed launcher.
|
||||
|
||||
The smoke must print the extracted path, caller working directory, normalized
|
||||
`lib.path`, and relevant loader diagnostics without leaking credentials.
|
||||
|
||||
## Validation
|
||||
|
||||
Local/macOS:
|
||||
|
||||
- `bash -n scripts/release/strip-appimage-graphics-libs.sh`
|
||||
- the regression script skips cleanly when Linux ELF tooling is unavailable;
|
||||
- `git diff --check` and repository formatting checks.
|
||||
|
||||
Linux:
|
||||
|
||||
- run `scripts/release/test-strip-appimage-rpaths.sh` with `patchelf`;
|
||||
- run the final-artifact validator against a freshly built amd64 AppImage;
|
||||
- launch from a foreign working directory under timeout/Xvfb;
|
||||
- confirm no `anylinux.so`, `libxdo.so.3`, or `libcef.so` loader errors;
|
||||
- confirm the updater archive/signing flow still consumes the validated
|
||||
postprocessed artifact.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Final `lib.path` uses only valid sharun-native AppDir entries.
|
||||
- Bare `shared/lib` and CI absolute entries fail validation.
|
||||
- Rewriting is deterministic and idempotent.
|
||||
- Released-style ELF AppRun fixtures are covered.
|
||||
- The final repacked amd64 AppImage launches from outside its AppDir without the
|
||||
#5037 preload/library-resolution errors.
|
||||
- Required bundled libraries, ELF dependencies, and RPATHs remain valid.
|
||||
- Existing graphics stripping, signing, updater archive, and non-Linux build
|
||||
behavior remain unchanged.
|
||||
@@ -23,9 +23,16 @@
|
||||
# APPIMAGETOOL_SHA256 — expected SHA256 of the download
|
||||
# (verified before use when set; rotate
|
||||
# alongside APPIMAGETOOL_URL)
|
||||
# APPIMAGE_RUNTIME_SMOKE — set to 1 to run the final AppRun
|
||||
# startup smoke after static validation
|
||||
# APPIMAGE_RUNTIME_VALIDATOR — override the final-artifact validator
|
||||
# command (intended for regression tests)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APPIMAGE_RUNTIME_VALIDATOR="${APPIMAGE_RUNTIME_VALIDATOR:-$SCRIPT_DIR/validate-appimage-runtime.sh}"
|
||||
|
||||
EXCLUDE_PATTERNS=(
|
||||
'libGL.so.*'
|
||||
'libGLX.so.*'
|
||||
@@ -293,58 +300,111 @@ rewrite_sharun_lib_path() {
|
||||
local lib_path="$appdir/shared/lib/lib.path"
|
||||
[ -s "$lib_path" ] || return 1
|
||||
|
||||
if ! grep -E '(^|[+:])/home/runner/|(^|[+:])/__w/' "$lib_path" >/dev/null; then
|
||||
local -a normalized_entries=()
|
||||
local entry normalized suffix existing duplicate index
|
||||
local normalized_count=0
|
||||
while IFS= read -r entry || [ -n "$entry" ]; do
|
||||
if [ -z "$entry" ]; then
|
||||
echo "[strip-libs] ERROR: shared/lib/lib.path contains an empty entry" >&2
|
||||
return 1
|
||||
fi
|
||||
case "$entry" in
|
||||
*$'\r'*|*:*)
|
||||
echo "[strip-libs] ERROR: shared/lib/lib.path contains a malformed entry: '$entry'" >&2
|
||||
return 1
|
||||
;;
|
||||
[[:space:]]*|*[[:space:]])
|
||||
echo "[strip-libs] ERROR: shared/lib/lib.path contains a malformed entry: '$entry'" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$entry" in
|
||||
+)
|
||||
normalized="+"
|
||||
;;
|
||||
+/*)
|
||||
normalized="$entry"
|
||||
;;
|
||||
/*)
|
||||
case "$entry" in
|
||||
*/squashfs-root/shared/lib)
|
||||
normalized="+"
|
||||
;;
|
||||
*/squashfs-root/shared/lib/*)
|
||||
suffix="${entry#*/squashfs-root/shared/lib/}"
|
||||
normalized="+/$suffix"
|
||||
;;
|
||||
*/appimage_deb/data/usr/lib)
|
||||
normalized="+"
|
||||
;;
|
||||
*/appimage_deb/data/usr/lib/*)
|
||||
suffix="${entry#*/appimage_deb/data/usr/lib/}"
|
||||
normalized="+/$suffix"
|
||||
;;
|
||||
# lib4bin/quick-sharun staging roots vary by runner and tool
|
||||
# version; older artifacts use an arbitrary absolute prefix ending
|
||||
# in data/usr/lib rather than the appimage_deb directory above.
|
||||
# Match that stable staged-layout tail here. The derived marker is
|
||||
# validated below against the extracted AppDir's shared/lib tree.
|
||||
*/data/usr/lib)
|
||||
normalized="+"
|
||||
;;
|
||||
*/data/usr/lib/*)
|
||||
suffix="${entry#*/data/usr/lib/}"
|
||||
normalized="+/$suffix"
|
||||
;;
|
||||
*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo "[strip-libs] ERROR: shared/lib/lib.path contains a malformed entry: '$entry'" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${normalized#+}" in
|
||||
*+*)
|
||||
echo "[strip-libs] ERROR: shared/lib/lib.path contains a malformed entry: '$entry'" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
duplicate=0
|
||||
index=0
|
||||
while [ "$index" -lt "$normalized_count" ]; do
|
||||
existing="${normalized_entries[$index]}"
|
||||
if [ "$existing" = "$normalized" ]; then
|
||||
duplicate=1
|
||||
break
|
||||
fi
|
||||
index=$((index + 1))
|
||||
done
|
||||
if [ "$duplicate" -eq 0 ]; then
|
||||
normalized_entries[$normalized_count]="$normalized"
|
||||
normalized_count=$((normalized_count + 1))
|
||||
fi
|
||||
done <"$lib_path"
|
||||
|
||||
if [ "$normalized_count" -eq 0 ]; then
|
||||
echo "[strip-libs] ERROR: shared/lib/lib.path contains no valid AppDir library entries" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "[strip-libs] rewriting CI runner paths in shared/lib/lib.path"
|
||||
|
||||
local raw
|
||||
raw="$(cat "$lib_path")"
|
||||
|
||||
local -a entries=()
|
||||
local entry
|
||||
while IFS= read -r entry; do
|
||||
[ -n "$entry" ] || continue
|
||||
entries+=("$entry")
|
||||
done < <(printf '%s' "$raw" | tr '+:' '\n\n')
|
||||
|
||||
local -a cleaned=()
|
||||
local rel seen_set=""
|
||||
for entry in "${entries[@]}"; do
|
||||
case "$entry" in
|
||||
/home/runner/*|/__w/*)
|
||||
rel="${entry##*/squashfs-root/}"
|
||||
if [ "$rel" = "$entry" ]; then
|
||||
rel="${entry##*/data/}"
|
||||
[ "$rel" != "$entry" ] || continue
|
||||
fi
|
||||
;;
|
||||
/*)
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
rel="$entry"
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -d "$appdir/$rel" ] || continue
|
||||
|
||||
case "+${seen_set}+" in
|
||||
*"+${rel}+"*) continue ;;
|
||||
esac
|
||||
seen_set="${seen_set}+${rel}"
|
||||
cleaned+=("$rel")
|
||||
done
|
||||
|
||||
if [ "${#cleaned[@]}" -eq 0 ]; then
|
||||
cleaned=("shared/lib")
|
||||
local normalized_file
|
||||
normalized_file="$(mktemp "$lib_path.tmp.XXXXXX")"
|
||||
printf '%s\n' "${normalized_entries[@]}" >"$normalized_file"
|
||||
if cmp -s "$normalized_file" "$lib_path"; then
|
||||
rm -f "$normalized_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local joined
|
||||
joined="$(IFS='+'; echo "${cleaned[*]}")"
|
||||
printf '%s' "$joined" > "$lib_path"
|
||||
echo "[strip-libs] lib.path rewritten to: $joined"
|
||||
mv "$normalized_file" "$lib_path"
|
||||
echo "[strip-libs] normalized shared/lib/lib.path entries:"
|
||||
printf '[strip-libs] %s\n' "${normalized_entries[@]}"
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_sharun_lib_path() {
|
||||
@@ -356,13 +416,89 @@ validate_sharun_lib_path() {
|
||||
local lib_path="$appdir/shared/lib/lib.path"
|
||||
if [ ! -s "$lib_path" ]; then
|
||||
echo "[strip-libs] ERROR: sharun AppImage is missing shared/lib/lib.path; refusing to ship an AppImage that exits with 'Interpreter not found!'" >&2
|
||||
exit 1
|
||||
return 1
|
||||
fi
|
||||
|
||||
if grep -E '(^|[+:])/home/runner/|(^|[+:])/__w/' "$lib_path" >/dev/null; then
|
||||
echo "[strip-libs] ERROR: shared/lib/lib.path contains CI runner paths; regenerate it with bundle-relative entries before release." >&2
|
||||
exit 1
|
||||
local library_root="$appdir/shared/lib"
|
||||
local canonical_root
|
||||
if [ ! -d "$library_root" ] || ! canonical_root="$(realpath "$library_root")"; then
|
||||
echo "[strip-libs] ERROR: sharun AppImage is missing shared/lib; refusing to validate library paths" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local entry suffix resolved canonical_resolved
|
||||
local entry_count=0
|
||||
while IFS= read -r entry || [ -n "$entry" ]; do
|
||||
entry_count=$((entry_count + 1))
|
||||
|
||||
if [ -z "$entry" ]; then
|
||||
echo "[strip-libs] ERROR: invalid sharun lib.path entry '': empty entries are not allowed" >&2
|
||||
return 1
|
||||
fi
|
||||
case "$entry" in
|
||||
*$'\r'*)
|
||||
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': carriage returns are not allowed" >&2
|
||||
return 1
|
||||
;;
|
||||
*:*)
|
||||
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': loader separators are not allowed" >&2
|
||||
return 1
|
||||
;;
|
||||
[[:space:]]*|*[[:space:]])
|
||||
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': surrounding whitespace is not allowed" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$entry" in
|
||||
+)
|
||||
suffix=""
|
||||
resolved="$library_root"
|
||||
;;
|
||||
+/*)
|
||||
suffix="${entry#+/}"
|
||||
case "$suffix" in
|
||||
*+*)
|
||||
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': extra '+' markers are not allowed" >&2
|
||||
return 1
|
||||
;;
|
||||
""|/*|*/|*//*|.|..|./*|../*|*/./*|*/../*|*/.|*/..)
|
||||
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': path components must be non-empty descendants" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
resolved="$library_root/$suffix"
|
||||
;;
|
||||
*)
|
||||
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': expected '+' or '+/suffix'" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -d "$resolved" ]; then
|
||||
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': resolved directory does not exist" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! canonical_resolved="$(realpath "$resolved")"; then
|
||||
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': could not canonicalize resolved directory" >&2
|
||||
return 1
|
||||
fi
|
||||
case "$canonical_resolved" in
|
||||
"$canonical_root"|"$canonical_root"/*)
|
||||
;;
|
||||
*)
|
||||
echo "[strip-libs] ERROR: invalid sharun lib.path entry '$entry': resolved directory escapes shared/lib" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done <"$lib_path"
|
||||
|
||||
if [ "$entry_count" -eq 0 ]; then
|
||||
echo "[strip-libs] ERROR: invalid sharun lib.path entry '': at least one entry is required" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# is_elf — true if the file begins with the ELF magic, regardless of the +x
|
||||
@@ -476,9 +612,9 @@ sanitize_elf_rpaths() {
|
||||
return $rewrote
|
||||
}
|
||||
|
||||
# validate_appimage_required_libs — fail the build loudly if a library the app
|
||||
# binary hard-links (NEEDED) but which MUST travel inside the AppImage is absent
|
||||
# from the bundle.
|
||||
# validate_appimage_required_libs — fail the build loudly if the sharun preload
|
||||
# library or a runtime library the app hard-links (NEEDED) is absent from the
|
||||
# bundle.
|
||||
#
|
||||
# Problem (issues #3224 and #4020): the app links libxdo.so.3 via enigo
|
||||
# (`#[link(name = "xdo")]`, used by src/openhuman/tools/impl/computer for Linux
|
||||
@@ -490,6 +626,8 @@ sanitize_elf_rpaths() {
|
||||
# linux_cef_deb_runtime_e2e; the AppImage path had no equivalent. This turns a
|
||||
# silent runtime segfault into a loud build failure. CEF is staged separately
|
||||
# from the ldd walk, so verify its runtime library survived bundling as well.
|
||||
# anylinux.so establishes sharun's portable runtime before either dependency
|
||||
# can load, so it is part of the same release contract.
|
||||
validate_appimage_required_libs() {
|
||||
local appdir="$1"
|
||||
if ! uses_sharun_launcher "$appdir"; then
|
||||
@@ -497,7 +635,7 @@ validate_appimage_required_libs() {
|
||||
fi
|
||||
|
||||
local root pattern found
|
||||
for pattern in 'libxdo.so*' 'libcef.so*'; do
|
||||
for pattern in 'anylinux.so' 'libxdo.so*' 'libcef.so*'; do
|
||||
found=0
|
||||
for root in "$appdir/shared/lib" "$appdir/usr/lib" "$appdir/lib"; do
|
||||
[ -d "$root" ] || continue
|
||||
@@ -509,6 +647,9 @@ validate_appimage_required_libs() {
|
||||
[ "$found" -eq 1 ] && continue
|
||||
|
||||
case "$pattern" in
|
||||
anylinux.so)
|
||||
echo "[strip-libs] ERROR: AppImage is missing anylinux.so — the sharun preload library was not copied into the final bundle. The AppImage launcher cannot establish its portable runtime without it." >&2
|
||||
;;
|
||||
libxdo.so\*)
|
||||
echo "[strip-libs] ERROR: AppImage is missing libxdo.so.* — the enigo NEEDED dependency was not bundled (issue #3224). The app would segfault on launch on hosts without the legacy libxdo soname (e.g. Arch). Ensure libxdo-dev is installed on the build runner so lib4bin's ldd-walk bundles it." >&2
|
||||
;;
|
||||
@@ -520,21 +661,15 @@ validate_appimage_required_libs() {
|
||||
done
|
||||
}
|
||||
|
||||
# patch_apprun_sharun_cwd — inject `cd "$APPDIR"` into AppRun before the final
|
||||
# exec call so sharun resolves preload/library paths relative to the AppDir
|
||||
# rather than the caller's CWD.
|
||||
# patch_apprun_sharun_cwd — retain historical shell-AppRun CWD hardening.
|
||||
#
|
||||
# Problem (issue #2822): sharun reads its `.preload` entry and library search
|
||||
# paths relative to the process CWD. When a user launches the AppImage from
|
||||
# any directory other than the AppDir itself (e.g. double-click from ~/Downloads)
|
||||
# CWD != AppDir, so ld.so can't find `anylinux.so` (preload) or `libcef.so`
|
||||
# (LD_LIBRARY_PATH entry). SHARUN_DIR is set correctly — the AppDir IS known —
|
||||
# but sharun doesn't use it to anchor the preload/library arguments it hands to
|
||||
# ld.so.
|
||||
# Canonical `+` entries in shared/lib/lib.path are the primary fix for released
|
||||
# layouts, where AppRun is the sharun ELF launcher itself. sharun expands `+`
|
||||
# relative to the AppDir regardless of the caller's CWD.
|
||||
#
|
||||
# Fix: prepend `cd "$APPDIR"` to the exec line in AppRun so the working
|
||||
# directory is always the AppDir by the time sharun/the binary runs. This
|
||||
# mirrors the verified manual workaround from the bug report.
|
||||
# Older layouts may instead have a shell AppRun that ultimately execs sharun.
|
||||
# Keep prepending `cd "$APPDIR"` there as compatibility hardening, without
|
||||
# treating it as the released ELF launcher's library-path repair.
|
||||
#
|
||||
# Returns 0 (true) if the AppRun was modified, 1 if no change was needed.
|
||||
patch_apprun_sharun_cwd() {
|
||||
@@ -585,15 +720,20 @@ patch_apprun_sharun_cwd() {
|
||||
&& grep -Eq "$patched_line_re" "$tmp_apprun"; then
|
||||
chmod --reference="$apprun" "$tmp_apprun"
|
||||
mv "$tmp_apprun" "$apprun"
|
||||
echo "[strip-libs] patched AppRun: added 'cd \"\$APPDIR\"' before exec to fix sharun CWD preload resolution (issue #2822)"
|
||||
echo "[strip-libs] patched historical shell AppRun: added 'cd \"\$APPDIR\"' before exec"
|
||||
return 0
|
||||
else
|
||||
rm -f "$tmp_apprun"
|
||||
echo "[strip-libs] WARNING: could not locate 'exec \"\$@\"' in AppRun — sharun CWD fix not applied; AppImage may fail to launch from non-AppDir CWDs" >&2
|
||||
echo "[strip-libs] WARNING: could not locate 'exec \"\$@\"' in historical shell AppRun; canonical '+' lib.path entries remain the released-layout fix" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_rebuilt_appimage() {
|
||||
local rebuilt_path="$1"
|
||||
"$APPIMAGE_RUNTIME_VALIDATOR" "$rebuilt_path" || return 1
|
||||
}
|
||||
|
||||
strip_one_appimage() {
|
||||
local img="$1"
|
||||
local original
|
||||
@@ -659,7 +799,10 @@ strip_one_appimage() {
|
||||
if sanitize_elf_rpaths "$appdir"; then
|
||||
rewrote_rpaths=1
|
||||
fi
|
||||
validate_sharun_lib_path "$appdir"
|
||||
if ! validate_sharun_lib_path "$appdir"; then
|
||||
rm -rf "$workdir"
|
||||
return 1
|
||||
fi
|
||||
if ! validate_appimage_required_libs "$appdir"; then
|
||||
rm -rf "$workdir"
|
||||
return 1
|
||||
@@ -667,6 +810,10 @@ strip_one_appimage() {
|
||||
|
||||
if [ "$removed" -eq 0 ] && [ "$added_loader" -eq 0 ] && [ "$rewrote_libpath" -eq 0 ] && [ "$patched_apprun" -eq 0 ] && [ "$rewrote_rpaths" -eq 0 ]; then
|
||||
echo "[strip-libs] No graphics libs, missing sharun interpreter, or build-machine RPATHs found in $original; leaving unchanged."
|
||||
if ! validate_rebuilt_appimage "$original"; then
|
||||
rm -rf "$workdir"
|
||||
return 1
|
||||
fi
|
||||
rm -rf "$workdir"
|
||||
return
|
||||
fi
|
||||
@@ -681,6 +828,10 @@ strip_one_appimage() {
|
||||
--no-appstream squashfs-root "$rebuilt" >/dev/null
|
||||
)
|
||||
mv "$rebuilt" "$original"
|
||||
if ! validate_rebuilt_appimage "$original"; then
|
||||
rm -rf "$workdir"
|
||||
return 1
|
||||
fi
|
||||
rm -rf "$workdir"
|
||||
MODIFIED_PATHS+=("$original")
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
# 1. sanitize_elf_rpaths — strips /home/runner|/__w build-machine
|
||||
# RPATHs from bundled ELFs, rewriting to
|
||||
# $ORIGIN-relative.
|
||||
# 2. validate_appimage_required_libs — hard-fails when libxdo.so.* or
|
||||
# libcef.so is missing from a sharun AppDir.
|
||||
# 2. validate_appimage_required_libs — hard-fails when anylinux.so,
|
||||
# libxdo.so.*, or libcef.so is missing from
|
||||
# a sharun AppDir.
|
||||
# 3. validate-appimage-runtime.sh — checks the final extracted sharun layout
|
||||
# and the pre-signing validation seam.
|
||||
#
|
||||
# Linux-only: needs `patchelf` and a host ELF to mutate. Skips cleanly (exit 0)
|
||||
# on macOS / any host without patchelf so it is a no-op on dev boxes and a real
|
||||
@@ -42,11 +45,785 @@ done
|
||||
# shellcheck source=/dev/null
|
||||
source "$TARGET"
|
||||
|
||||
RUNTIME_VALIDATOR="$SCRIPT_DIR/validate-appimage-runtime.sh"
|
||||
[ -f "$RUNTIME_VALIDATOR" ] \
|
||||
|| { echo "[test-rpaths] FAIL: $RUNTIME_VALIDATOR not found" >&2; exit 1; }
|
||||
# shellcheck source=/dev/null
|
||||
source "$RUNTIME_VALIDATOR"
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
fail() { echo "[test-rpaths] FAIL: $1" >&2; exit 1; }
|
||||
|
||||
make_sharun_appdir() {
|
||||
local appdir="$1"
|
||||
mkdir -p "$appdir/shared/bin" "$appdir/shared/lib/plugins" "$appdir/bin"
|
||||
|
||||
cp "$HOST_ELF" "$appdir/sharun"
|
||||
printf 'Interpreter not found!' >> "$appdir/sharun"
|
||||
chmod +x "$appdir/sharun"
|
||||
ln "$appdir/sharun" "$appdir/AppRun"
|
||||
ln "$appdir/sharun" "$appdir/bin/OpenHuman"
|
||||
|
||||
cp "$HOST_ELF" "$appdir/shared/bin/OpenHuman"
|
||||
chmod +x "$appdir/shared/bin/OpenHuman"
|
||||
}
|
||||
|
||||
assert_lib_path_contents() {
|
||||
local fixture="$1"
|
||||
local expected="$2"
|
||||
local expected_file="$fixture/expected-lib.path"
|
||||
printf '%s\n' "$expected" >"$expected_file"
|
||||
cmp -s "$expected_file" "$fixture/shared/lib/lib.path" \
|
||||
|| fail "unexpected normalized lib.path for ${fixture##*/}"
|
||||
}
|
||||
|
||||
# --- Case 0: rewrite_sharun_lib_path emits sharun-native marker entries -----
|
||||
REWRITE_SQUASHFS="$WORK/rewrite-squashfs"
|
||||
make_sharun_appdir "$REWRITE_SQUASHFS"
|
||||
printf '%s\n' "$WORK/squashfs-root/shared/lib" >"$REWRITE_SQUASHFS/shared/lib/lib.path"
|
||||
rewrite_sharun_lib_path "$REWRITE_SQUASHFS" \
|
||||
|| fail "squashfs-root shared/lib was not normalized"
|
||||
assert_lib_path_contents "$REWRITE_SQUASHFS" '+'
|
||||
|
||||
REWRITE_STAGING="$WORK/rewrite-staging"
|
||||
make_sharun_appdir "$REWRITE_STAGING"
|
||||
printf '%s\n' "$WORK/appimage_deb/data/usr/lib" >"$REWRITE_STAGING/shared/lib/lib.path"
|
||||
rewrite_sharun_lib_path "$REWRITE_STAGING" \
|
||||
|| fail "appimage_deb data/usr/lib was not normalized"
|
||||
assert_lib_path_contents "$REWRITE_STAGING" '+'
|
||||
|
||||
REWRITE_DESCENDANTS="$WORK/rewrite-descendants"
|
||||
make_sharun_appdir "$REWRITE_DESCENDANTS"
|
||||
printf '%s\n%s\n' \
|
||||
"$WORK/squashfs-root/shared/lib/plugins" \
|
||||
"$WORK/appimage_deb/data/usr/lib/plugins" \
|
||||
>"$REWRITE_DESCENDANTS/shared/lib/lib.path"
|
||||
rewrite_sharun_lib_path "$REWRITE_DESCENDANTS" \
|
||||
|| fail "AppDir library descendants were not normalized"
|
||||
assert_lib_path_contents "$REWRITE_DESCENDANTS" '+/plugins'
|
||||
|
||||
REWRITE_MIXED="$WORK/rewrite-mixed"
|
||||
make_sharun_appdir "$REWRITE_MIXED"
|
||||
printf '%s\n' '+' '+/plugins' '+' '/opt/unrelated/lib' \
|
||||
>"$REWRITE_MIXED/shared/lib/lib.path"
|
||||
rewrite_sharun_lib_path "$REWRITE_MIXED" \
|
||||
|| fail "mixed marker entries were not normalized"
|
||||
assert_lib_path_contents "$REWRITE_MIXED" "$(printf '+\n+/plugins')"
|
||||
|
||||
cp "$REWRITE_MIXED/shared/lib/lib.path" "$REWRITE_MIXED/lib.path.before"
|
||||
if rewrite_sharun_lib_path "$REWRITE_MIXED"; then
|
||||
fail "rewrite_sharun_lib_path was not idempotent"
|
||||
fi
|
||||
cmp -s "$REWRITE_MIXED/lib.path.before" "$REWRITE_MIXED/shared/lib/lib.path" \
|
||||
|| fail "idempotent rewrite changed lib.path bytes"
|
||||
|
||||
REWRITE_INVALID="$WORK/rewrite-invalid"
|
||||
make_sharun_appdir "$REWRITE_INVALID"
|
||||
printf '%s\n' '/opt/unrelated/lib' >"$REWRITE_INVALID/shared/lib/lib.path"
|
||||
if ( rewrite_sharun_lib_path "$REWRITE_INVALID" ) 2>"$WORK/rewrite.err"; then
|
||||
fail "rewrite accepted a lib.path with no AppDir library root"
|
||||
fi
|
||||
grep -F "no valid AppDir library entries" "$WORK/rewrite.err" >/dev/null \
|
||||
|| fail "rewrite failure did not explain why lib.path was rejected"
|
||||
assert_lib_path_contents "$REWRITE_INVALID" '/opt/unrelated/lib'
|
||||
|
||||
REWRITE_BARE_RELATIVE="$WORK/rewrite-bare-relative"
|
||||
make_sharun_appdir "$REWRITE_BARE_RELATIVE"
|
||||
printf '%s\n' '+' 'shared/lib' \
|
||||
>"$REWRITE_BARE_RELATIVE/shared/lib/lib.path"
|
||||
if ( rewrite_sharun_lib_path "$REWRITE_BARE_RELATIVE" ) \
|
||||
2>"$REWRITE_BARE_RELATIVE/error.log"; then
|
||||
fail "rewrite accepted a mixed lib.path containing a bare-relative entry"
|
||||
fi
|
||||
grep -F "malformed entry: 'shared/lib'" \
|
||||
"$REWRITE_BARE_RELATIVE/error.log" >/dev/null \
|
||||
|| fail "rewrite bare-relative error did not identify 'shared/lib'"
|
||||
assert_lib_path_contents "$REWRITE_BARE_RELATIVE" "$(printf '+\nshared/lib')"
|
||||
|
||||
REWRITE_MARKER_INJECTION="$WORK/rewrite-marker-injection"
|
||||
make_sharun_appdir "$REWRITE_MARKER_INJECTION"
|
||||
mkdir -p "$REWRITE_MARKER_INJECTION/shared/lib/plugins+extra"
|
||||
printf '%s\n' '+/plugins+extra' \
|
||||
>"$REWRITE_MARKER_INJECTION/shared/lib/lib.path"
|
||||
if ( rewrite_sharun_lib_path "$REWRITE_MARKER_INJECTION" ) \
|
||||
2>"$REWRITE_MARKER_INJECTION/error.log"; then
|
||||
fail "rewrite accepted an extra marker in a canonical suffix"
|
||||
fi
|
||||
grep -F "malformed entry: '+/plugins+extra'" \
|
||||
"$REWRITE_MARKER_INJECTION/error.log" >/dev/null \
|
||||
|| fail "rewrite marker-injection error did not identify '+/plugins+extra'"
|
||||
|
||||
REWRITE_DERIVED_MARKER="$WORK/rewrite-derived-marker"
|
||||
make_sharun_appdir "$REWRITE_DERIVED_MARKER"
|
||||
derived_marker_path="$WORK/squashfs-root/shared/lib/plugins+extra"
|
||||
printf '%s\n' "$derived_marker_path" \
|
||||
>"$REWRITE_DERIVED_MARKER/shared/lib/lib.path"
|
||||
if ( rewrite_sharun_lib_path "$REWRITE_DERIVED_MARKER" ) \
|
||||
2>"$REWRITE_DERIVED_MARKER/error.log"; then
|
||||
fail "rewrite accepted an extra marker in a derived suffix"
|
||||
fi
|
||||
grep -F "malformed entry: '$derived_marker_path'" \
|
||||
"$REWRITE_DERIVED_MARKER/error.log" >/dev/null \
|
||||
|| fail "derived marker-injection error did not identify '$derived_marker_path'"
|
||||
echo "[test-rpaths] ok: sharun lib.path normalization is canonical and idempotent"
|
||||
|
||||
# --- Case 0b: validate_sharun_lib_path rejects unsafe loader entries --------
|
||||
assert_lib_path_rejected() {
|
||||
local label="$1"
|
||||
local value="$2"
|
||||
local expected_entry="$3"
|
||||
local fixture="$WORK/reject-$label"
|
||||
make_sharun_appdir "$fixture"
|
||||
printf '%s\n' "$value" >"$fixture/shared/lib/lib.path"
|
||||
|
||||
if ( validate_sharun_lib_path "$fixture" ) 2>"$fixture/error.log"; then
|
||||
fail "validate_sharun_lib_path accepted $label"
|
||||
fi
|
||||
grep -F "invalid sharun lib.path entry '$expected_entry':" \
|
||||
"$fixture/error.log" >/dev/null \
|
||||
|| fail "$label error did not identify offending entry '$expected_entry'"
|
||||
}
|
||||
|
||||
assert_lib_path_rejected bare-relative 'shared/lib' 'shared/lib'
|
||||
assert_lib_path_rejected runner-absolute \
|
||||
'/home/runner/work/openhuman/squashfs-root/shared/lib' \
|
||||
'/home/runner/work/openhuman/squashfs-root/shared/lib'
|
||||
assert_lib_path_rejected actions-absolute \
|
||||
'/__w/openhuman/openhuman/appimage_deb/data/usr/lib' \
|
||||
'/__w/openhuman/openhuman/appimage_deb/data/usr/lib'
|
||||
assert_lib_path_rejected parent-traversal '+/../outside' '+/../outside'
|
||||
assert_lib_path_rejected nested-traversal \
|
||||
'+/plugins/../../outside' '+/plugins/../../outside'
|
||||
assert_lib_path_rejected missing-directory '+/missing' '+/missing'
|
||||
assert_lib_path_rejected loader-separator \
|
||||
'+/plugins:shared/lib' '+/plugins:shared/lib'
|
||||
assert_lib_path_rejected leading-whitespace ' +' ' +'
|
||||
assert_lib_path_rejected trailing-whitespace '+ ' '+ '
|
||||
|
||||
VALID_ROOT="$WORK/valid-root"
|
||||
make_sharun_appdir "$VALID_ROOT"
|
||||
printf '%s\n' '+' >"$VALID_ROOT/shared/lib/lib.path"
|
||||
validate_sharun_lib_path "$VALID_ROOT" \
|
||||
|| fail "validate_sharun_lib_path rejected canonical root marker"
|
||||
|
||||
VALID_DESCENDANT="$WORK/valid-descendant"
|
||||
make_sharun_appdir "$VALID_DESCENDANT"
|
||||
printf '%s\n' '+' '+/plugins' >"$VALID_DESCENDANT/shared/lib/lib.path"
|
||||
cp "$VALID_DESCENDANT/shared/lib/lib.path" "$VALID_DESCENDANT/lib.path.before"
|
||||
validate_sharun_lib_path "$VALID_DESCENDANT" \
|
||||
|| fail "validate_sharun_lib_path rejected canonical descendant marker"
|
||||
validate_sharun_lib_path "$VALID_DESCENDANT" \
|
||||
|| fail "validate_sharun_lib_path rejected a second validation pass"
|
||||
cmp -s \
|
||||
"$VALID_DESCENDANT/lib.path.before" \
|
||||
"$VALID_DESCENDANT/shared/lib/lib.path" \
|
||||
|| fail "validate_sharun_lib_path mutated a normalized file"
|
||||
|
||||
MARKER_INJECTION="$WORK/marker-injection"
|
||||
make_sharun_appdir "$MARKER_INJECTION"
|
||||
mkdir -p "$MARKER_INJECTION/shared/lib/plugins+extra"
|
||||
printf '%s\n' '+/plugins+extra' >"$MARKER_INJECTION/shared/lib/lib.path"
|
||||
if ( validate_sharun_lib_path "$MARKER_INJECTION" ) \
|
||||
2>"$MARKER_INJECTION/error.log"; then
|
||||
fail "validate_sharun_lib_path accepted an extra marker in a suffix"
|
||||
fi
|
||||
grep -F "invalid sharun lib.path entry '+/plugins+extra':" \
|
||||
"$MARKER_INJECTION/error.log" >/dev/null \
|
||||
|| fail "marker-injection error did not identify '+/plugins+extra'"
|
||||
|
||||
ESCAPE_DIR="$WORK/escape-symlink"
|
||||
make_sharun_appdir "$ESCAPE_DIR"
|
||||
mkdir -p "$WORK/outside"
|
||||
ln -s "$WORK/outside" "$ESCAPE_DIR/shared/lib/escape"
|
||||
printf '%s\n' '+/escape' >"$ESCAPE_DIR/shared/lib/lib.path"
|
||||
if ( validate_sharun_lib_path "$ESCAPE_DIR" ) 2>"$ESCAPE_DIR/error.log"; then
|
||||
fail "validate_sharun_lib_path accepted a symlink escaping shared/lib"
|
||||
fi
|
||||
grep -F "invalid sharun lib.path entry '+/escape':" \
|
||||
"$ESCAPE_DIR/error.log" >/dev/null \
|
||||
|| fail "symlink escape error did not identify offending entry '+/escape'"
|
||||
|
||||
RELEASED_LAYOUT="$WORK/released-layout"
|
||||
make_sharun_appdir "$RELEASED_LAYOUT"
|
||||
printf '%s\n' '+' >"$RELEASED_LAYOUT/shared/lib/lib.path"
|
||||
[ "$RELEASED_LAYOUT/AppRun" -ef "$RELEASED_LAYOUT/sharun" ] \
|
||||
|| fail "AppRun fixture is not hard-linked to sharun"
|
||||
[ "$RELEASED_LAYOUT/bin/OpenHuman" -ef "$RELEASED_LAYOUT/sharun" ] \
|
||||
|| fail "bin/OpenHuman fixture is not hard-linked to sharun"
|
||||
is_executable_elf "$RELEASED_LAYOUT/AppRun" \
|
||||
|| fail "released-style AppRun fixture is not ELF"
|
||||
uses_sharun_launcher "$RELEASED_LAYOUT" \
|
||||
|| fail "released-style hard-linked ELF launcher was not detected"
|
||||
validate_sharun_lib_path "$RELEASED_LAYOUT" \
|
||||
|| fail "released-style AppDir has an invalid canonical lib.path"
|
||||
|
||||
CALLER="$WORK/caller"
|
||||
mkdir -p "$CALLER"
|
||||
(
|
||||
cd "$CALLER"
|
||||
expanded="$(
|
||||
sed "s|+|$RELEASED_LAYOUT/shared/lib|g" \
|
||||
"$RELEASED_LAYOUT/shared/lib/lib.path" |
|
||||
paste -sd ':' -
|
||||
)"
|
||||
[ "$expanded" = "$RELEASED_LAYOUT/shared/lib" ] \
|
||||
|| fail "sharun marker did not expand beneath the fixture AppDir"
|
||||
)
|
||||
|
||||
released_apprun_inode="$(ls -di "$RELEASED_LAYOUT/AppRun")"
|
||||
if patch_apprun_sharun_cwd "$RELEASED_LAYOUT"; then
|
||||
fail "patch_apprun_sharun_cwd modified a released-style ELF AppRun"
|
||||
fi
|
||||
[ "$released_apprun_inode" = "$(ls -di "$RELEASED_LAYOUT/AppRun")" ] \
|
||||
|| fail "patch_apprun_sharun_cwd changed the released-style ELF AppRun inode"
|
||||
echo "[test-rpaths] ok: sharun lib.path validation is fail-closed for the released ELF launcher"
|
||||
|
||||
# --- Case 0c: final extracted runtime layout is validated before signing ----
|
||||
make_runtime_appdir() {
|
||||
local appdir="$1"
|
||||
make_sharun_appdir "$appdir"
|
||||
printf '%s\n' '+' >"$appdir/shared/lib/lib.path"
|
||||
|
||||
cp "$HOST_ELF" "$appdir/shared/lib/anylinux.so"
|
||||
cp "$HOST_ELF" "$appdir/shared/lib/libxdo.so.3"
|
||||
cp "$HOST_ELF" "$appdir/shared/lib/libcef.so"
|
||||
|
||||
local elf
|
||||
for elf in \
|
||||
"$appdir/AppRun" \
|
||||
"$appdir/sharun" \
|
||||
"$appdir/bin/OpenHuman" \
|
||||
"$appdir/shared/bin/OpenHuman" \
|
||||
"$appdir/shared/lib/anylinux.so" \
|
||||
"$appdir/shared/lib/libxdo.so.3" \
|
||||
"$appdir/shared/lib/libcef.so"; do
|
||||
patchelf --remove-rpath "$elf"
|
||||
done
|
||||
}
|
||||
|
||||
assert_runtime_layout_rejected() {
|
||||
local label="$1"
|
||||
local expected="$2"
|
||||
shift 2
|
||||
local fixture="$WORK/runtime-$label"
|
||||
make_runtime_appdir "$fixture"
|
||||
"$@" "$fixture"
|
||||
if ( APPIMAGE_EXPECTED_NEEDED="" validate_extracted_appdir "$fixture" ) \
|
||||
>"$fixture/output.log" 2>&1; then
|
||||
fail "validate_extracted_appdir accepted $label"
|
||||
fi
|
||||
grep -F "$expected" "$fixture/output.log" >/dev/null \
|
||||
|| fail "$label failure did not report '$expected'"
|
||||
}
|
||||
|
||||
remove_anylinux() { rm -f "$1/shared/lib/anylinux.so"; }
|
||||
remove_libxdo() { rm -f "$1/shared/lib/libxdo.so.3"; }
|
||||
remove_libcef() { rm -f "$1/shared/lib/libcef.so"; }
|
||||
remove_real_app() { rm -f "$1/shared/bin/OpenHuman"; }
|
||||
replace_real_app_with_text() {
|
||||
rm -f "$1/shared/bin/OpenHuman"
|
||||
printf '%s\n' 'not an ELF' >"$1/shared/bin/OpenHuman"
|
||||
chmod +x "$1/shared/bin/OpenHuman"
|
||||
}
|
||||
replace_apprun() {
|
||||
rm -f "$1/AppRun"
|
||||
cp "$HOST_ELF" "$1/AppRun"
|
||||
chmod +x "$1/AppRun"
|
||||
}
|
||||
replace_bin_launcher() {
|
||||
rm -f "$1/bin/OpenHuman"
|
||||
cp "$HOST_ELF" "$1/bin/OpenHuman"
|
||||
chmod +x "$1/bin/OpenHuman"
|
||||
}
|
||||
inject_runner_rpath() {
|
||||
patchelf --set-rpath \
|
||||
'/home/runner/work/openhuman/openhuman/shared/lib' \
|
||||
"$1/shared/lib/libcef.so"
|
||||
}
|
||||
inject_actions_rpath() {
|
||||
patchelf --set-rpath \
|
||||
'/__w/openhuman/openhuman/shared/lib' \
|
||||
"$1/shared/lib/libcef.so"
|
||||
}
|
||||
|
||||
RUNTIME_COMPLETE="$WORK/runtime-complete"
|
||||
make_runtime_appdir "$RUNTIME_COMPLETE"
|
||||
APPIMAGE_EXPECTED_NEEDED="" validate_extracted_appdir "$RUNTIME_COMPLETE" \
|
||||
|| fail "validate_extracted_appdir rejected a complete runtime fixture"
|
||||
|
||||
RUNTIME_EQUIVALENT="$WORK/runtime-equivalent-launchers"
|
||||
make_runtime_appdir "$RUNTIME_EQUIVALENT"
|
||||
rm "$RUNTIME_EQUIVALENT/AppRun" "$RUNTIME_EQUIVALENT/bin/OpenHuman"
|
||||
cp "$RUNTIME_EQUIVALENT/sharun" "$RUNTIME_EQUIVALENT/AppRun"
|
||||
cp "$RUNTIME_EQUIVALENT/sharun" "$RUNTIME_EQUIVALENT/bin/OpenHuman"
|
||||
chmod +x "$RUNTIME_EQUIVALENT/AppRun" "$RUNTIME_EQUIVALENT/bin/OpenHuman"
|
||||
APPIMAGE_EXPECTED_NEEDED="" validate_extracted_appdir "$RUNTIME_EQUIVALENT" \
|
||||
|| fail "validate_extracted_appdir rejected byte-equivalent launchers"
|
||||
|
||||
FAKE_APPIMAGE="$WORK/final-fixture.AppImage"
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'[ "$1" = "--appimage-extract" ] || exit 9' \
|
||||
'cp -R "$EXTRACT_SOURCE" squashfs-root' \
|
||||
>"$FAKE_APPIMAGE"
|
||||
chmod +x "$FAKE_APPIMAGE"
|
||||
export EXTRACT_SOURCE="$RUNTIME_COMPLETE"
|
||||
APPIMAGE_EXPECTED_NEEDED="" validate_final_appimage "$FAKE_APPIMAGE" \
|
||||
|| fail "validate_final_appimage rejected a complete extracted fixture"
|
||||
|
||||
if APPIMAGE_EXPECTED_NEEDED="" \
|
||||
"$RUNTIME_VALIDATOR" "$FAKE_APPIMAGE" \
|
||||
>"$WORK/direct-validator.log" 2>&1; then
|
||||
fail "direct validator allowed a fixture override to disable production NEEDED checks"
|
||||
fi
|
||||
grep -F "missing NEEDED entry 'libxdo.so.3'" \
|
||||
"$WORK/direct-validator.log" >/dev/null \
|
||||
|| fail "direct validator did not enforce the production NEEDED contract"
|
||||
|
||||
RUNTIME_BAD_LIBPATH="$WORK/runtime-bad-libpath"
|
||||
cp -R "$RUNTIME_COMPLETE" "$RUNTIME_BAD_LIBPATH"
|
||||
printf '%s\n' 'shared/lib' >"$RUNTIME_BAD_LIBPATH/shared/lib/lib.path"
|
||||
if ( APPIMAGE_EXPECTED_NEEDED="" \
|
||||
validate_extracted_appdir "$RUNTIME_BAD_LIBPATH" ) \
|
||||
>"$RUNTIME_BAD_LIBPATH/conditional.log" 2>&1; then
|
||||
fail "conditional validate_extracted_appdir suppressed lib.path validation failure"
|
||||
fi
|
||||
|
||||
RUNTIME_FINAL_INVALID="$WORK/runtime-final-invalid"
|
||||
cp -R "$RUNTIME_COMPLETE" "$RUNTIME_FINAL_INVALID"
|
||||
rm -f "$RUNTIME_FINAL_INVALID/shared/lib/anylinux.so"
|
||||
if ( EXTRACT_SOURCE="$RUNTIME_FINAL_INVALID" APPIMAGE_EXPECTED_NEEDED="" \
|
||||
validate_final_appimage "$FAKE_APPIMAGE" ) \
|
||||
>"$WORK/conditional-final.log" 2>&1; then
|
||||
fail "conditional validate_final_appimage suppressed extracted-layout failure"
|
||||
fi
|
||||
|
||||
REAL_PATCHELF="$(command -v patchelf)"
|
||||
PATCHELF_FAIL_BIN="$WORK/patchelf-fail-bin"
|
||||
mkdir -p "$PATCHELF_FAIL_BIN"
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'if [ "$1" = "--print-needed" ]; then exit 42; fi' \
|
||||
'exec "$REAL_PATCHELF" "$@"' \
|
||||
>"$PATCHELF_FAIL_BIN/patchelf"
|
||||
chmod +x "$PATCHELF_FAIL_BIN/patchelf"
|
||||
export REAL_PATCHELF
|
||||
if ( PATH="$PATCHELF_FAIL_BIN:$PATH" APPIMAGE_EXPECTED_NEEDED="" \
|
||||
validate_extracted_appdir "$RUNTIME_COMPLETE" ) \
|
||||
>"$WORK/patchelf-failure.log" 2>&1; then
|
||||
fail "conditional validate_extracted_appdir suppressed patchelf failure"
|
||||
fi
|
||||
|
||||
SMOKE_BIN="$WORK/smoke-bin"
|
||||
mkdir -p "$SMOKE_BIN"
|
||||
SMOKE_RECORD="$WORK/smoke-command-record"
|
||||
SMOKE_PROBE="$WORK/smoke-probe-record"
|
||||
REAL_ENV="$(command -v env)"
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'printf "timeout-cwd=%s\n" "$PWD" >>"$SMOKE_RECORD"' \
|
||||
'while [ "$#" -gt 0 ]; do' \
|
||||
' case "$1" in' \
|
||||
' --signal=*|--kill-after=*|15s) shift ;;' \
|
||||
' *) break ;;' \
|
||||
' esac' \
|
||||
'done' \
|
||||
'"$@"' \
|
||||
'[ -n "${SMOKE_TIMEOUT_MESSAGE:-}" ] && printf "%s\n" "$SMOKE_TIMEOUT_MESSAGE" >&2' \
|
||||
'exit "${SMOKE_TIMEOUT_STATUS:-124}"' \
|
||||
>"$SMOKE_BIN/timeout"
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'printf "xvfb-invoked\n" >>"$SMOKE_RECORD"' \
|
||||
'while [ "$#" -gt 0 ]; do' \
|
||||
' case "$1" in' \
|
||||
' -a|--server-args=*) shift ;;' \
|
||||
' *) break ;;' \
|
||||
' esac' \
|
||||
'done' \
|
||||
'exec "$@"' \
|
||||
>"$SMOKE_BIN/xvfb-run"
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'args=("$@")' \
|
||||
'index=0' \
|
||||
'while [ "$index" -lt "${#args[@]}" ]; do' \
|
||||
' arg="${args[$index]}"' \
|
||||
' case "$arg" in' \
|
||||
' -u)' \
|
||||
' next=$((index + 1))' \
|
||||
' printf "env-unset=%s\n" "${args[$next]}" >>"$SMOKE_RECORD"' \
|
||||
' index=$((index + 2))' \
|
||||
' ;;' \
|
||||
' HOME=*|XDG_CONFIG_HOME=*|XDG_DATA_HOME=*|XDG_CACHE_HOME=*)' \
|
||||
' printf "env-assignment=%s\n" "$arg" >>"$SMOKE_RECORD"' \
|
||||
' index=$((index + 1))' \
|
||||
' ;;' \
|
||||
' OPENHUMAN_CEF_PREWARM=*|OPENHUMAN_DISABLE_GPU=*) index=$((index + 1)) ;;' \
|
||||
' *) break ;;' \
|
||||
' esac' \
|
||||
'done' \
|
||||
'exec "$REAL_ENV" "${args[@]}"' \
|
||||
>"$SMOKE_BIN/env"
|
||||
chmod +x "$SMOKE_BIN/timeout" "$SMOKE_BIN/xvfb-run" "$SMOKE_BIN/env"
|
||||
SMOKE_APPDIR="$WORK/smoke-appdir"
|
||||
mkdir -p "$SMOKE_APPDIR"
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'printf "apprun-cwd=%s\n" "$PWD" >>"$SMOKE_PROBE"' \
|
||||
'printf "home=%s\n" "$HOME" >>"$SMOKE_PROBE"' \
|
||||
'printf "config=%s\n" "$XDG_CONFIG_HOME" >>"$SMOKE_PROBE"' \
|
||||
'printf "data=%s\n" "$XDG_DATA_HOME" >>"$SMOKE_PROBE"' \
|
||||
'printf "cache=%s\n" "$XDG_CACHE_HOME" >>"$SMOKE_PROBE"' \
|
||||
'[ -z "${GITHUB_TOKEN+x}" ] || exit 71' \
|
||||
'[ -z "${GH_TOKEN+x}" ] || exit 72' \
|
||||
'[ -z "${TAURI_SIGNING_PRIVATE_KEY+x}" ] || exit 73' \
|
||||
'[ -z "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD+x}" ] || exit 74' \
|
||||
'[ -z "${SENTRY_AUTH_TOKEN+x}" ] || exit 75' \
|
||||
'[ -z "${OPENAI_API_KEY+x}" ] || exit 76' \
|
||||
'printf "probe-executed\n" >>"$SMOKE_PROBE"' \
|
||||
>"$SMOKE_APPDIR/AppRun"
|
||||
chmod +x "$SMOKE_APPDIR/AppRun"
|
||||
export SMOKE_RECORD SMOKE_PROBE REAL_ENV
|
||||
SMOKE_ROOT="$WORK/smoke-runtime"
|
||||
mkdir -p "$SMOKE_ROOT/foreign-cwd"
|
||||
GITHUB_TOKEN='secret-github-value' \
|
||||
GH_TOKEN='secret-gh-value' \
|
||||
TAURI_SIGNING_PRIVATE_KEY='secret-signing-value' \
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD='secret-password-value' \
|
||||
SENTRY_AUTH_TOKEN='secret-sentry-value' \
|
||||
OPENAI_API_KEY='secret-openai-value' \
|
||||
PATH="$SMOKE_BIN:$PATH" smoke_extracted_apprun \
|
||||
"$SMOKE_APPDIR" \
|
||||
"$SMOKE_ROOT/foreign-cwd" \
|
||||
"$SMOKE_ROOT/success.log" \
|
||||
|| fail "smoke_extracted_apprun did not accept timeout status 124"
|
||||
grep -Fx "timeout-cwd=$SMOKE_ROOT/foreign-cwd" "$SMOKE_RECORD" >/dev/null \
|
||||
|| fail "smoke did not invoke timeout from the foreign CWD"
|
||||
grep -Fx "xvfb-invoked" "$SMOKE_RECORD" >/dev/null \
|
||||
|| fail "smoke did not invoke xvfb-run"
|
||||
for secret_name in \
|
||||
GITHUB_TOKEN \
|
||||
GH_TOKEN \
|
||||
OPENHUMAN_CEF_NO_SANDBOX \
|
||||
TAURI_SIGNING_PRIVATE_KEY \
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD \
|
||||
SENTRY_AUTH_TOKEN \
|
||||
OPENAI_API_KEY; do
|
||||
grep -Fx "env-unset=$secret_name" "$SMOKE_RECORD" >/dev/null \
|
||||
|| fail "smoke env did not unset $secret_name"
|
||||
done
|
||||
grep -Fx "env-assignment=HOME=$SMOKE_ROOT/home" "$SMOKE_RECORD" >/dev/null \
|
||||
|| fail "smoke HOME was not isolated"
|
||||
grep -Fx "env-assignment=XDG_CONFIG_HOME=$SMOKE_ROOT/config" "$SMOKE_RECORD" >/dev/null \
|
||||
|| fail "smoke XDG_CONFIG_HOME was not isolated"
|
||||
grep -Fx "env-assignment=XDG_DATA_HOME=$SMOKE_ROOT/data" "$SMOKE_RECORD" >/dev/null \
|
||||
|| fail "smoke XDG_DATA_HOME was not isolated"
|
||||
grep -Fx "env-assignment=XDG_CACHE_HOME=$SMOKE_ROOT/cache" "$SMOKE_RECORD" >/dev/null \
|
||||
|| fail "smoke XDG_CACHE_HOME was not isolated"
|
||||
grep -Fx "apprun-cwd=$SMOKE_ROOT/foreign-cwd" "$SMOKE_PROBE" >/dev/null \
|
||||
|| fail "smoke AppRun did not execute from the foreign CWD"
|
||||
grep -Fx "probe-executed" "$SMOKE_PROBE" >/dev/null \
|
||||
|| fail "smoke AppRun probe did not execute"
|
||||
if grep -F 'secret-' "$SMOKE_RECORD" "$SMOKE_PROBE" >/dev/null; then
|
||||
fail "smoke command records exposed a secret value"
|
||||
fi
|
||||
if ( PATH="$SMOKE_BIN:$PATH" SMOKE_TIMEOUT_STATUS=0 \
|
||||
smoke_extracted_apprun \
|
||||
"$SMOKE_APPDIR" \
|
||||
"$SMOKE_ROOT/foreign-cwd" \
|
||||
"$SMOKE_ROOT/early-exit.log" ) >/dev/null 2>&1; then
|
||||
fail "smoke_extracted_apprun accepted an immediate clean exit"
|
||||
fi
|
||||
if ( PATH="$SMOKE_BIN:$PATH" \
|
||||
SMOKE_TIMEOUT_MESSAGE='libcef.so: cannot open shared object file' \
|
||||
smoke_extracted_apprun \
|
||||
"$SMOKE_APPDIR" \
|
||||
"$SMOKE_ROOT/foreign-cwd" \
|
||||
"$SMOKE_ROOT/loader-error.log" ) >/dev/null 2>&1; then
|
||||
fail "smoke_extracted_apprun accepted a forbidden loader diagnostic"
|
||||
fi
|
||||
(
|
||||
set +e
|
||||
PATH="$SMOKE_BIN:$PATH" smoke_extracted_apprun \
|
||||
"$SMOKE_APPDIR" \
|
||||
"$SMOKE_ROOT/foreign-cwd" \
|
||||
"$SMOKE_ROOT/errexit-disabled.log" \
|
||||
|| exit 81
|
||||
case "$-" in
|
||||
*e*) exit 82 ;;
|
||||
esac
|
||||
) || fail "smoke_extracted_apprun changed a sourced caller's disabled errexit state"
|
||||
|
||||
APPARMOR_BIN="$WORK/apparmor-bin"
|
||||
APPARMOR_RECORD="$WORK/apparmor-command-record"
|
||||
APPARMOR_PROFILE="$WORK/openhuman-appimage-smoke.profile"
|
||||
APPARMOR_PROFILE_SNAPSHOT="$WORK/openhuman-appimage-smoke.profile.loaded"
|
||||
mkdir -p "$APPARMOR_BIN"
|
||||
printf '%s\n' '#!/usr/bin/env bash' 'exit 0' \
|
||||
>"$APPARMOR_BIN/apparmor_parser"
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'printf "%s\n" "$*" >>"$APPARMOR_RECORD"' \
|
||||
'if [ "$2" = "apparmor_parser" ] && [ "$3" = "--replace" ]; then' \
|
||||
' cp "$4" "$APPARMOR_PROFILE_SNAPSHOT"' \
|
||||
'fi' \
|
||||
>"$APPARMOR_BIN/sudo"
|
||||
chmod +x "$APPARMOR_BIN/apparmor_parser" "$APPARMOR_BIN/sudo"
|
||||
export APPARMOR_RECORD APPARMOR_PROFILE_SNAPSHOT
|
||||
PATH="$APPARMOR_BIN:$PATH" install_smoke_userns_profile \
|
||||
"$RUNTIME_COMPLETE" "$APPARMOR_PROFILE" \
|
||||
|| fail "install_smoke_userns_profile rejected the fixture AppDir"
|
||||
grep -F "\"$RUNTIME_COMPLETE/shared/bin/OpenHuman\"" \
|
||||
"$APPARMOR_PROFILE_SNAPSHOT" >/dev/null \
|
||||
|| fail "AppArmor profile did not attach to the extracted real executable"
|
||||
grep -Fx " userns," "$APPARMOR_PROFILE_SNAPSHOT" >/dev/null \
|
||||
|| fail "AppArmor profile did not preserve Chromium's userns sandbox"
|
||||
grep -Fx -- \
|
||||
"--non-interactive apparmor_parser --replace $APPARMOR_PROFILE" \
|
||||
"$APPARMOR_RECORD" >/dev/null \
|
||||
|| fail "AppArmor profile was not loaded through non-interactive sudo"
|
||||
PATH="$APPARMOR_BIN:$PATH" remove_smoke_userns_profile "$APPARMOR_PROFILE" \
|
||||
|| fail "remove_smoke_userns_profile rejected the loaded profile"
|
||||
grep -Fx -- \
|
||||
"--non-interactive apparmor_parser --remove $APPARMOR_PROFILE" \
|
||||
"$APPARMOR_RECORD" >/dev/null \
|
||||
|| fail "AppArmor profile was not removed after the smoke"
|
||||
|
||||
: >"$APPARMOR_RECORD"
|
||||
set +e
|
||||
(
|
||||
smoke_extracted_apprun() {
|
||||
printf '%s\n' "smoke" >>"$APPARMOR_RECORD"
|
||||
return 37
|
||||
}
|
||||
PATH="$APPARMOR_BIN:$PATH" smoke_extracted_apprun_with_userns \
|
||||
"$RUNTIME_COMPLETE" \
|
||||
"$SMOKE_ROOT/foreign-cwd" \
|
||||
"$SMOKE_ROOT/apparmor-failure.log" \
|
||||
"$APPARMOR_PROFILE"
|
||||
)
|
||||
apparmor_smoke_status=$?
|
||||
set -e
|
||||
[ "$apparmor_smoke_status" -eq 37 ] \
|
||||
|| fail "AppArmor wrapper did not preserve smoke failure status 37"
|
||||
printf '%s\n' \
|
||||
"--non-interactive apparmor_parser --replace $APPARMOR_PROFILE" \
|
||||
"smoke" \
|
||||
"--non-interactive apparmor_parser --remove $APPARMOR_PROFILE" \
|
||||
>"$WORK/apparmor-expected-order"
|
||||
cmp -s "$WORK/apparmor-expected-order" "$APPARMOR_RECORD" \
|
||||
|| fail "AppArmor profile was not loaded before and removed after a failing smoke"
|
||||
|
||||
APPARMOR_TERM_RECORD="$WORK/apparmor-term-command-record"
|
||||
APPARMOR_TERM_MARKER="$WORK/apparmor-term-smoke-started"
|
||||
APPARMOR_TERM_RUNNER="$WORK/apparmor-term-runner.sh"
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'set -euo pipefail' \
|
||||
'# shellcheck source=/dev/null' \
|
||||
'source "$RUNTIME_VALIDATOR"' \
|
||||
'smoke_extracted_apprun() {' \
|
||||
' printf "%s\n" "smoke-start" >>"$APPARMOR_RECORD"' \
|
||||
' : >"$APPARMOR_TERM_MARKER"' \
|
||||
' while :; do sleep 1; done' \
|
||||
'}' \
|
||||
'smoke_extracted_apprun_with_userns \' \
|
||||
' "$RUNTIME_COMPLETE" \' \
|
||||
' "$SMOKE_ROOT/foreign-cwd" \' \
|
||||
' "$SMOKE_ROOT/apparmor-term.log" \' \
|
||||
' "$APPARMOR_PROFILE"' \
|
||||
>"$APPARMOR_TERM_RUNNER"
|
||||
chmod +x "$APPARMOR_TERM_RUNNER"
|
||||
export \
|
||||
APPARMOR_TERM_MARKER \
|
||||
RUNTIME_COMPLETE \
|
||||
RUNTIME_VALIDATOR \
|
||||
SMOKE_ROOT \
|
||||
APPARMOR_PROFILE
|
||||
APPARMOR_RECORD="$APPARMOR_TERM_RECORD" \
|
||||
PATH="$APPARMOR_BIN:$PATH" \
|
||||
bash "$APPARMOR_TERM_RUNNER" &
|
||||
apparmor_term_pid=$!
|
||||
for _ in $(seq 1 100); do
|
||||
if [ -s "$APPARMOR_TERM_RECORD" ] && [ -f "$APPARMOR_TERM_MARKER" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.05
|
||||
done
|
||||
[ -s "$APPARMOR_TERM_RECORD" ] && [ -f "$APPARMOR_TERM_MARKER" ] || {
|
||||
kill -TERM "$apparmor_term_pid" 2>/dev/null || true
|
||||
fail "AppArmor TERM fixture did not reach the smoke"
|
||||
}
|
||||
kill -TERM "$apparmor_term_pid"
|
||||
set +e
|
||||
wait "$apparmor_term_pid"
|
||||
apparmor_term_status=$?
|
||||
set -e
|
||||
[ "$apparmor_term_status" -eq 143 ] \
|
||||
|| fail "AppArmor TERM fixture exited $apparmor_term_status instead of 143"
|
||||
printf '%s\n' \
|
||||
"--non-interactive apparmor_parser --replace $APPARMOR_PROFILE" \
|
||||
"smoke-start" \
|
||||
"--non-interactive apparmor_parser --remove $APPARMOR_PROFILE" \
|
||||
>"$WORK/apparmor-term-expected-order"
|
||||
cmp -s "$WORK/apparmor-term-expected-order" "$APPARMOR_TERM_RECORD" \
|
||||
|| fail "AppArmor profile was not removed exactly once after TERM"
|
||||
echo "[test-rpaths] ok: CI smoke grants userns only to the extracted executable"
|
||||
|
||||
assert_runtime_layout_rejected missing-anylinux \
|
||||
"missing anylinux.so" remove_anylinux
|
||||
assert_runtime_layout_rejected missing-libxdo \
|
||||
"missing libxdo.so.*" remove_libxdo
|
||||
assert_runtime_layout_rejected missing-libcef \
|
||||
"missing libcef.so" remove_libcef
|
||||
assert_runtime_layout_rejected missing-real-app \
|
||||
"shared/bin/OpenHuman is not an executable ELF" remove_real_app
|
||||
assert_runtime_layout_rejected text-real-app \
|
||||
"shared/bin/OpenHuman is not an executable ELF" replace_real_app_with_text
|
||||
assert_runtime_layout_rejected mismatched-apprun \
|
||||
"AppRun does not match sharun" replace_apprun
|
||||
assert_runtime_layout_rejected mismatched-bin-launcher \
|
||||
"bin/OpenHuman does not match sharun" replace_bin_launcher
|
||||
assert_runtime_layout_rejected runner-rpath \
|
||||
"/home/runner/work/openhuman/openhuman/shared/lib" inject_runner_rpath
|
||||
assert_runtime_layout_rejected actions-rpath \
|
||||
"/__w/openhuman/openhuman/shared/lib" inject_actions_rpath
|
||||
|
||||
RUNTIME_NEEDED="$WORK/runtime-needed"
|
||||
make_runtime_appdir "$RUNTIME_NEEDED"
|
||||
if ( APPIMAGE_EXPECTED_NEEDED="libxdo.so.3" \
|
||||
validate_extracted_appdir "$RUNTIME_NEEDED" ) \
|
||||
>"$RUNTIME_NEEDED/missing-xdo.log" 2>&1; then
|
||||
fail "validate_extracted_appdir accepted a missing libxdo NEEDED entry"
|
||||
fi
|
||||
grep -F "missing NEEDED entry 'libxdo.so.3'" \
|
||||
"$RUNTIME_NEEDED/missing-xdo.log" >/dev/null \
|
||||
|| fail "missing libxdo NEEDED diagnostic was not specific"
|
||||
|
||||
patchelf --add-needed libxdo.so.3 "$RUNTIME_NEEDED/shared/bin/OpenHuman"
|
||||
if ( APPIMAGE_EXPECTED_NEEDED="libcef.so" \
|
||||
validate_extracted_appdir "$RUNTIME_NEEDED" ) \
|
||||
>"$RUNTIME_NEEDED/missing-cef.log" 2>&1; then
|
||||
fail "validate_extracted_appdir accepted a missing libcef NEEDED entry"
|
||||
fi
|
||||
grep -F "missing NEEDED entry 'libcef.so'" \
|
||||
"$RUNTIME_NEEDED/missing-cef.log" >/dev/null \
|
||||
|| fail "missing libcef NEEDED diagnostic was not specific"
|
||||
|
||||
patchelf --add-needed libcef.so "$RUNTIME_NEEDED/shared/bin/OpenHuman"
|
||||
APPIMAGE_EXPECTED_NEEDED="libxdo.so.3 libcef.so" \
|
||||
validate_extracted_appdir "$RUNTIME_NEEDED" \
|
||||
|| fail "validate_extracted_appdir rejected complete NEEDED entries"
|
||||
|
||||
VALIDATOR_RECORD="$WORK/runtime-validator-record"
|
||||
runtime_validator_stub() {
|
||||
[ -f "$1" ] || return 10
|
||||
[ "${#MODIFIED_PATHS[@]}" -eq 0 ] || return 11
|
||||
printf '%s\n' "$1" >"$VALIDATOR_RECORD"
|
||||
}
|
||||
REBUILT_FIXTURE="$WORK/rebuilt.AppImage"
|
||||
: >"$REBUILT_FIXTURE"
|
||||
MODIFIED_PATHS=()
|
||||
APPIMAGE_RUNTIME_VALIDATOR=runtime_validator_stub \
|
||||
validate_rebuilt_appimage "$REBUILT_FIXTURE" \
|
||||
|| fail "validate_rebuilt_appimage did not forward to its configured command"
|
||||
[ "$(cat "$VALIDATOR_RECORD")" = "$REBUILT_FIXTURE" ] \
|
||||
|| fail "validate_rebuilt_appimage forwarded the wrong artifact path"
|
||||
runtime_validator_failure_stub() { return 37; }
|
||||
if APPIMAGE_RUNTIME_VALIDATOR=runtime_validator_failure_stub \
|
||||
validate_rebuilt_appimage "$REBUILT_FIXTURE"; then
|
||||
fail "conditional validate_rebuilt_appimage suppressed validator failure"
|
||||
fi
|
||||
|
||||
CLEANUP_IMAGE="$WORK/cleanup-invalid.AppImage"
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'[ "$1" = "--appimage-extract" ] || exit 9' \
|
||||
'mkdir -p squashfs-root/shared/lib' \
|
||||
>"$CLEANUP_IMAGE"
|
||||
chmod +x "$CLEANUP_IMAGE"
|
||||
CLEANUP_WORKDIR="$WORK/cleanup-invalid-workdir"
|
||||
CLEANUP_LOG="$WORK/cleanup-invalid.log"
|
||||
set +e
|
||||
TARGET="$TARGET" \
|
||||
CLEANUP_IMAGE="$CLEANUP_IMAGE" \
|
||||
CLEANUP_WORKDIR="$CLEANUP_WORKDIR" \
|
||||
bash -c '
|
||||
set -euo pipefail
|
||||
# shellcheck source=/dev/null
|
||||
source "$TARGET"
|
||||
mktemp() {
|
||||
if [ "${1:-}" = "-d" ]; then
|
||||
mkdir -p "$CLEANUP_WORKDIR"
|
||||
printf "%s\n" "$CLEANUP_WORKDIR"
|
||||
else
|
||||
command mktemp "$@"
|
||||
fi
|
||||
}
|
||||
ensure_sharun_interpreter() { return 1; }
|
||||
rewrite_sharun_lib_path() { return 1; }
|
||||
patch_apprun_sharun_cwd() { return 1; }
|
||||
sanitize_elf_rpaths() { return 1; }
|
||||
validate_sharun_lib_path() {
|
||||
echo "intentional lib.path validation failure" >&2
|
||||
return 47
|
||||
}
|
||||
validate_appimage_required_libs() { return 0; }
|
||||
validate_rebuilt_appimage() { return 0; }
|
||||
strip_one_appimage "$CLEANUP_IMAGE"
|
||||
' >"$CLEANUP_LOG" 2>&1
|
||||
cleanup_status=$?
|
||||
set -e
|
||||
[ "$cleanup_status" -ne 0 ] \
|
||||
|| fail "strip_one_appimage accepted a failing lib.path validation"
|
||||
grep -F "intentional lib.path validation failure" "$CLEANUP_LOG" >/dev/null \
|
||||
|| fail "strip_one_appimage hid the original lib.path validation error"
|
||||
[ ! -e "$CLEANUP_WORKDIR" ] \
|
||||
|| fail "strip_one_appimage leaked its workdir after lib.path validation failed"
|
||||
|
||||
NOOP_IMAGE="$WORK/noop-final.AppImage"
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'[ "$1" = "--appimage-extract" ] || exit 9' \
|
||||
'mkdir -p squashfs-root' \
|
||||
>"$NOOP_IMAGE"
|
||||
chmod +x "$NOOP_IMAGE"
|
||||
NOOP_IMAGE_BEFORE="$(sha256sum "$NOOP_IMAGE" | awk '{print $1}')"
|
||||
NOOP_VALIDATOR_RECORD="$WORK/noop-validator-record"
|
||||
noop_runtime_validator_stub() {
|
||||
[ -f "$1" ] || return 20
|
||||
[ "${#MODIFIED_PATHS[@]}" -eq 0 ] || return 21
|
||||
printf '%s\n' "$1" >>"$NOOP_VALIDATOR_RECORD"
|
||||
}
|
||||
MODIFIED_PATHS=()
|
||||
APPIMAGE_RUNTIME_VALIDATOR=noop_runtime_validator_stub \
|
||||
strip_one_appimage "$NOOP_IMAGE" \
|
||||
|| fail "strip_one_appimage rejected a valid no-change final artifact"
|
||||
[ "$(wc -l <"$NOOP_VALIDATOR_RECORD" | tr -d ' ')" -eq 1 ] \
|
||||
|| fail "no-change AppImage was not validated exactly once"
|
||||
[ "$(cat "$NOOP_VALIDATOR_RECORD")" = "$(realpath "$NOOP_IMAGE")" ] \
|
||||
|| fail "no-change validation did not inspect the original final AppImage"
|
||||
[ "${#MODIFIED_PATHS[@]}" -eq 0 ] \
|
||||
|| fail "no-change AppImage was incorrectly registered for re-signing"
|
||||
[ "$NOOP_IMAGE_BEFORE" = "$(sha256sum "$NOOP_IMAGE" | awk '{print $1}')" ] \
|
||||
|| fail "no-change AppImage bytes were unexpectedly replaced"
|
||||
|
||||
mv_line="$(grep -nF 'mv "$rebuilt" "$original"' "$TARGET" | cut -d: -f1)"
|
||||
validate_line="$(
|
||||
grep -nF 'validate_rebuilt_appimage "$original"' "$TARGET" \
|
||||
| tail -n 1 \
|
||||
| cut -d: -f1
|
||||
)"
|
||||
modified_line="$(grep -nF 'MODIFIED_PATHS+=("$original")' "$TARGET" | cut -d: -f1)"
|
||||
[ -n "$mv_line" ] && [ -n "$validate_line" ] && [ -n "$modified_line" ] \
|
||||
|| fail "post-repack validation ordering statements are missing"
|
||||
[ "$mv_line" -lt "$validate_line" ] && [ "$validate_line" -lt "$modified_line" ] \
|
||||
|| fail "final artifact validation does not run after mv and before signing registration"
|
||||
echo "[test-rpaths] ok: final runtime layout and pre-signing handoff are fail-closed"
|
||||
|
||||
# --- Case 1: sanitize_elf_rpaths strips a CI build-machine RPATH ------------
|
||||
APPDIR="$WORK/squashfs-root"
|
||||
mkdir -p "$APPDIR/usr/lib" "$APPDIR/shared/lib"
|
||||
@@ -130,20 +907,28 @@ chmod +x "$GUARD_DIR/sharun"
|
||||
# guard's early return would make this case vacuously pass.
|
||||
uses_sharun_launcher "$GUARD_DIR" || fail "fixture did not register as sharun launcher"
|
||||
|
||||
# 2a — libxdo absent → must exit non-zero.
|
||||
# 2a — the sharun preload library absent → must exit non-zero.
|
||||
if ( validate_appimage_required_libs "$GUARD_DIR" ) 2>/dev/null; then
|
||||
fail "validate_appimage_required_libs passed despite missing anylinux.so"
|
||||
fi
|
||||
echo "[test-rpaths] ok: guard fails when anylinux.so is absent"
|
||||
|
||||
: > "$GUARD_DIR/shared/lib/anylinux.so"
|
||||
|
||||
# 2b — libxdo absent → must exit non-zero.
|
||||
if ( validate_appimage_required_libs "$GUARD_DIR" ) 2>/dev/null; then
|
||||
fail "validate_appimage_required_libs passed despite missing libxdo.so.*"
|
||||
fi
|
||||
echo "[test-rpaths] ok: guard fails when libxdo.so.* is absent"
|
||||
|
||||
# 2b — libxdo present but libcef absent → must still fail.
|
||||
# 2c — libxdo present but libcef absent → must still fail.
|
||||
: > "$GUARD_DIR/shared/lib/libxdo.so.3"
|
||||
if ( validate_appimage_required_libs "$GUARD_DIR" ) 2>/dev/null; then
|
||||
fail "validate_appimage_required_libs passed despite missing libcef.so"
|
||||
fi
|
||||
echo "[test-rpaths] ok: guard fails when libcef.so is absent"
|
||||
|
||||
# 2c — both required runtime libraries present → must pass.
|
||||
# 2d — all required runtime libraries present → must pass.
|
||||
mkdir -p "$GUARD_DIR/usr/lib"
|
||||
: > "$GUARD_DIR/shared/lib/libxdo.so.3"
|
||||
: > "$GUARD_DIR/usr/lib/libcef.so"
|
||||
|
||||
Executable
+368
@@ -0,0 +1,368 @@
|
||||
#!/usr/bin/env bash
|
||||
# Validate the final, repacked AppImage bytes before release signing.
|
||||
#
|
||||
# This file is sourceable for fixture tests. When executed directly it extracts
|
||||
# exactly one AppImage from a foreign working directory, validates the released
|
||||
# sharun layout, and optionally performs a bounded Xvfb startup smoke.
|
||||
#
|
||||
# Usage: validate-appimage-runtime.sh <final.AppImage>
|
||||
#
|
||||
# Env:
|
||||
# APPIMAGE_RUNTIME_SMOKE — set to 1 to require the bounded Xvfb startup smoke;
|
||||
# otherwise only static final-artifact checks run
|
||||
# APPIMAGE_RUNTIME_APPARMOR_USERNS — set to 1 on Ubuntu 24.04 CI to load a
|
||||
# temporary, per-executable AppArmor profile granting
|
||||
# Chromium's sandbox access to user namespaces
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RUNTIME_VALIDATOR_SCRIPT_DIR="$(
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")" && pwd
|
||||
)"
|
||||
# shellcheck source=strip-appimage-graphics-libs.sh
|
||||
source "$RUNTIME_VALIDATOR_SCRIPT_DIR/strip-appimage-graphics-libs.sh"
|
||||
|
||||
runtime_validation_error() {
|
||||
echo "[appimage-runtime] ERROR: $*" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
validate_extracted_appdir() {
|
||||
local appdir="$1"
|
||||
local lib_path="$appdir/shared/lib/lib.path"
|
||||
|
||||
echo "[appimage-runtime] Validating extracted AppDir: $appdir"
|
||||
if [ -f "$lib_path" ]; then
|
||||
echo "[appimage-runtime] shared/lib/lib.path:"
|
||||
sed 's/^/[appimage-runtime] /' "$lib_path"
|
||||
else
|
||||
echo "[appimage-runtime] shared/lib/lib.path: <missing>"
|
||||
fi
|
||||
|
||||
if ! uses_sharun_launcher "$appdir"; then
|
||||
runtime_validation_error "released AppDir does not use a detectable sharun launcher"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local executable
|
||||
for executable in \
|
||||
"$appdir/AppRun" \
|
||||
"$appdir/sharun" \
|
||||
"$appdir/bin/OpenHuman" \
|
||||
"$appdir/shared/bin/OpenHuman"; do
|
||||
if ! is_executable_elf "$executable"; then
|
||||
runtime_validation_error "${executable#"$appdir"/} is not an executable ELF"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
local alias
|
||||
for alias in "$appdir/AppRun" "$appdir/bin/OpenHuman"; do
|
||||
if [ "$alias" -ef "$appdir/sharun" ] || cmp -s "$alias" "$appdir/sharun"; then
|
||||
continue
|
||||
fi
|
||||
runtime_validation_error "${alias#"$appdir"/} does not match sharun"
|
||||
return 1
|
||||
done
|
||||
|
||||
validate_sharun_lib_path "$appdir" || return 1
|
||||
validate_appimage_required_libs "$appdir" || return 1
|
||||
|
||||
local real_app="$appdir/shared/bin/OpenHuman"
|
||||
local needed
|
||||
if ! needed="$(patchelf --print-needed "$real_app")"; then
|
||||
runtime_validation_error \
|
||||
"could not read NEEDED entries from shared/bin/OpenHuman"
|
||||
return 1
|
||||
fi
|
||||
echo "[appimage-runtime] shared/bin/OpenHuman NEEDED entries:"
|
||||
if [ -n "$needed" ]; then
|
||||
printf '%s\n' "$needed" | sed 's/^/[appimage-runtime] /'
|
||||
else
|
||||
echo "[appimage-runtime] <none>"
|
||||
fi
|
||||
|
||||
local expected_needed="${APPIMAGE_EXPECTED_NEEDED-libxdo.so.3 libcef.so}"
|
||||
local needed_name
|
||||
for needed_name in $expected_needed; do
|
||||
if ! printf '%s\n' "$needed" | grep -Fx "$needed_name" >/dev/null; then
|
||||
runtime_validation_error "shared/bin/OpenHuman is missing NEEDED entry '$needed_name'"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
local root elf rpath
|
||||
for root in \
|
||||
"$appdir/shared/bin" \
|
||||
"$appdir/shared/lib" \
|
||||
"$appdir/bin" \
|
||||
"$appdir/lib" \
|
||||
"$appdir/usr/bin" \
|
||||
"$appdir/usr/lib"; do
|
||||
[ -d "$root" ] || continue
|
||||
while IFS= read -r -d '' elf; do
|
||||
is_elf "$elf" || continue
|
||||
rpath="$(patchelf --print-rpath "$elf" 2>/dev/null || true)"
|
||||
case "$rpath" in
|
||||
*"/home/runner/"*|*"/__w/"*)
|
||||
runtime_validation_error \
|
||||
"${elf#"$appdir"/} retains forbidden build-runner RPATH '$rpath'"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done < <(find "$root" -type f -print0)
|
||||
done
|
||||
}
|
||||
|
||||
smoke_extracted_apprun() {
|
||||
local appdir="$1"
|
||||
local foreign_cwd="$2"
|
||||
local log_file="$3"
|
||||
|
||||
command -v timeout >/dev/null 2>&1 \
|
||||
|| { runtime_validation_error "timeout is required for the AppImage smoke"; return 1; }
|
||||
command -v xvfb-run >/dev/null 2>&1 \
|
||||
|| { runtime_validation_error "xvfb-run is required for the AppImage smoke"; return 1; }
|
||||
|
||||
echo "[appimage-runtime] Smoking AppRun from AppDir: $appdir"
|
||||
echo "[appimage-runtime] Smoke caller CWD: $foreign_cwd"
|
||||
|
||||
local temp_root
|
||||
temp_root="$(dirname "$foreign_cwd")"
|
||||
local smoke_home="$temp_root/home"
|
||||
local smoke_config="$temp_root/config"
|
||||
local smoke_data="$temp_root/data"
|
||||
local smoke_cache="$temp_root/cache"
|
||||
mkdir -p \
|
||||
"$foreign_cwd" \
|
||||
"$smoke_home" \
|
||||
"$smoke_config" \
|
||||
"$smoke_data" \
|
||||
"$smoke_cache"
|
||||
|
||||
local -a unset_args=()
|
||||
local secret_name
|
||||
for secret_name in \
|
||||
GITHUB_TOKEN \
|
||||
GH_TOKEN \
|
||||
OPENHUMAN_CEF_NO_SANDBOX \
|
||||
TAURI_SIGNING_PRIVATE_KEY \
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD \
|
||||
SENTRY_AUTH_TOKEN; do
|
||||
unset_args+=(-u "$secret_name")
|
||||
done
|
||||
while IFS= read -r secret_name; do
|
||||
[ -n "$secret_name" ] && unset_args+=(-u "$secret_name")
|
||||
done < <(compgen -v OPENAI_ || true)
|
||||
|
||||
local status
|
||||
if (
|
||||
cd "$foreign_cwd"
|
||||
timeout --signal=TERM --kill-after=5s 15s \
|
||||
xvfb-run -a --server-args="-screen 0 1280x960x24" \
|
||||
env "${unset_args[@]}" \
|
||||
HOME="$smoke_home" \
|
||||
XDG_CONFIG_HOME="$smoke_config" \
|
||||
XDG_DATA_HOME="$smoke_data" \
|
||||
XDG_CACHE_HOME="$smoke_cache" \
|
||||
OPENHUMAN_CEF_PREWARM=0 \
|
||||
OPENHUMAN_DISABLE_GPU=1 \
|
||||
"$appdir/AppRun"
|
||||
) >"$log_file" 2>&1; then
|
||||
status=0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
|
||||
local forbidden=0
|
||||
if grep -Eiq \
|
||||
'anylinux\.so.*cannot be preloaded|cannot be preloaded.*anylinux\.so' \
|
||||
"$log_file"; then
|
||||
forbidden=1
|
||||
fi
|
||||
if grep -Eiq \
|
||||
'libxdo\.so\.3.*cannot open shared object file|cannot open shared object file.*libxdo\.so\.3' \
|
||||
"$log_file"; then
|
||||
forbidden=1
|
||||
fi
|
||||
if grep -Eiq \
|
||||
'libcef\.so.*cannot open shared object file|cannot open shared object file.*libcef\.so' \
|
||||
"$log_file"; then
|
||||
forbidden=1
|
||||
fi
|
||||
if grep -Eiq 'error while loading shared libraries' "$log_file"; then
|
||||
forbidden=1
|
||||
fi
|
||||
|
||||
# The desktop process is expected to remain alive until timeout ends the
|
||||
# startup window. An earlier clean exit is a failure as well as loader output.
|
||||
if [ "$forbidden" -ne 0 ] || [ "$status" -ne 124 ]; then
|
||||
echo "[appimage-runtime] AppImage startup smoke failed (status $status):" >&2
|
||||
sed 's/^/[appimage-runtime] /' "$log_file" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
grep -Ei 'loader|loading|cef|startup|started|ready' "$log_file" \
|
||||
| sed 's/^/[appimage-runtime] /' \
|
||||
|| true
|
||||
echo "[appimage-runtime] Application remained alive for the 15-second startup window"
|
||||
}
|
||||
|
||||
install_smoke_userns_profile() {
|
||||
local appdir="$1"
|
||||
local profile_file="$2"
|
||||
local executable="$appdir/shared/bin/OpenHuman"
|
||||
|
||||
[ -x "$executable" ] \
|
||||
|| { runtime_validation_error "AppArmor target is not executable: $executable"; return 1; }
|
||||
command -v sudo >/dev/null 2>&1 \
|
||||
|| { runtime_validation_error "sudo is required for the AppImage smoke AppArmor profile"; return 1; }
|
||||
command -v apparmor_parser >/dev/null 2>&1 \
|
||||
|| { runtime_validation_error "apparmor_parser is required for the AppImage smoke AppArmor profile"; return 1; }
|
||||
|
||||
local profile_name="openhuman_appimage_runtime_smoke_$$"
|
||||
printf '%s\n' \
|
||||
'abi <abi/4.0>,' \
|
||||
'include <tunables/global>' \
|
||||
'' \
|
||||
"profile $profile_name \"$executable\" flags=(unconfined) {" \
|
||||
' userns,' \
|
||||
'}' \
|
||||
>"$profile_file"
|
||||
|
||||
sudo --non-interactive apparmor_parser --replace "$profile_file" >/dev/null \
|
||||
|| { runtime_validation_error "could not load the AppImage smoke AppArmor profile"; return 1; }
|
||||
echo "[appimage-runtime] Loaded temporary AppArmor userns profile for: $executable"
|
||||
}
|
||||
|
||||
remove_smoke_userns_profile() {
|
||||
local profile_file="$1"
|
||||
sudo --non-interactive apparmor_parser --remove "$profile_file" >/dev/null \
|
||||
|| { runtime_validation_error "could not remove the AppImage smoke AppArmor profile"; return 1; }
|
||||
echo "[appimage-runtime] Removed temporary AppArmor userns profile: $profile_file"
|
||||
}
|
||||
|
||||
smoke_extracted_apprun_with_userns() {
|
||||
local appdir="$1"
|
||||
local foreign_cwd="$2"
|
||||
local log_file="$3"
|
||||
local profile_file="$4"
|
||||
local profile_loaded=0
|
||||
local previous_hup_trap previous_int_trap previous_term_trap
|
||||
previous_hup_trap="$(trap -p HUP)"
|
||||
previous_int_trap="$(trap -p INT)"
|
||||
previous_term_trap="$(trap -p TERM)"
|
||||
|
||||
restore_smoke_signal_traps() {
|
||||
trap - HUP INT TERM
|
||||
[ -z "$previous_hup_trap" ] || eval "$previous_hup_trap"
|
||||
[ -z "$previous_int_trap" ] || eval "$previous_int_trap"
|
||||
[ -z "$previous_term_trap" ] || eval "$previous_term_trap"
|
||||
}
|
||||
|
||||
cleanup_smoke_userns_profile() {
|
||||
if [ "$profile_loaded" -eq 1 ]; then
|
||||
profile_loaded=0
|
||||
remove_smoke_userns_profile "$profile_file" \
|
||||
|| echo "[appimage-runtime] ERROR: AppArmor cleanup failed after smoke interruption" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
interrupt_smoke_with_userns() {
|
||||
local exit_status="$1"
|
||||
trap - HUP INT TERM
|
||||
cleanup_smoke_userns_profile
|
||||
exit "$exit_status"
|
||||
}
|
||||
trap 'interrupt_smoke_with_userns 129' HUP
|
||||
trap 'interrupt_smoke_with_userns 130' INT
|
||||
trap 'interrupt_smoke_with_userns 143' TERM
|
||||
|
||||
if ! install_smoke_userns_profile "$appdir" "$profile_file"; then
|
||||
restore_smoke_signal_traps
|
||||
return 1
|
||||
fi
|
||||
profile_loaded=1
|
||||
|
||||
local smoke_status
|
||||
if smoke_extracted_apprun "$appdir" "$foreign_cwd" "$log_file"; then
|
||||
smoke_status=0
|
||||
else
|
||||
smoke_status=$?
|
||||
fi
|
||||
|
||||
profile_loaded=0
|
||||
local remove_status
|
||||
if remove_smoke_userns_profile "$profile_file"; then
|
||||
remove_status=0
|
||||
else
|
||||
remove_status=$?
|
||||
fi
|
||||
|
||||
restore_smoke_signal_traps
|
||||
# Preserve the original smoke failure even if profile cleanup also fails.
|
||||
[ "$smoke_status" -eq 0 ] || return "$smoke_status"
|
||||
return "$remove_status"
|
||||
}
|
||||
|
||||
validate_final_appimage() (
|
||||
local image="$1"
|
||||
if ! image="$(realpath "$image")"; then
|
||||
runtime_validation_error "could not resolve AppImage path: $1"
|
||||
return 1
|
||||
fi
|
||||
if [ ! -f "$image" ] || [ ! -x "$image" ]; then
|
||||
runtime_validation_error "AppImage must be an executable regular file: $image"
|
||||
return 1
|
||||
fi
|
||||
command -v patchelf >/dev/null 2>&1 \
|
||||
|| { runtime_validation_error "patchelf is required for final AppImage validation"; return 1; }
|
||||
|
||||
local temp_root
|
||||
temp_root="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$temp_root"' EXIT
|
||||
local foreign_cwd="$temp_root/foreign-cwd"
|
||||
local extraction_log="$temp_root/extraction.log"
|
||||
local smoke_log="$temp_root/smoke.log"
|
||||
mkdir -p "$foreign_cwd"
|
||||
|
||||
if ! (
|
||||
cd "$foreign_cwd"
|
||||
"$image" --appimage-extract
|
||||
) >"$extraction_log" 2>&1; then
|
||||
echo "[appimage-runtime] AppImage extraction failed:" >&2
|
||||
sed 's/^/[appimage-runtime] /' "$extraction_log" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local appdir="$foreign_cwd/squashfs-root"
|
||||
if [ ! -d "$appdir" ]; then
|
||||
runtime_validation_error "extraction did not create $appdir"
|
||||
return 1
|
||||
fi
|
||||
validate_extracted_appdir "$appdir" || return 1
|
||||
|
||||
if [ "${APPIMAGE_RUNTIME_SMOKE:-0}" = "1" ]; then
|
||||
if [ "${APPIMAGE_RUNTIME_APPARMOR_USERNS:-0}" = "1" ]; then
|
||||
smoke_extracted_apprun_with_userns \
|
||||
"$appdir" \
|
||||
"$foreign_cwd" \
|
||||
"$smoke_log" \
|
||||
"$temp_root/openhuman-appimage-smoke.profile" \
|
||||
|| return 1
|
||||
else
|
||||
smoke_extracted_apprun "$appdir" "$foreign_cwd" "$smoke_log" || return 1
|
||||
fi
|
||||
else
|
||||
echo "[appimage-runtime] Static validation complete; executable smoke disabled for this architecture"
|
||||
fi
|
||||
)
|
||||
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||
[ "$#" -eq 1 ] || {
|
||||
echo "Usage: $0 <final.AppImage>" >&2
|
||||
exit 2
|
||||
}
|
||||
APPIMAGE_EXPECTED_NEEDED="libxdo.so.3 libcef.so" \
|
||||
validate_final_appimage "$1"
|
||||
fi
|
||||
Reference in New Issue
Block a user