mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-19 02:02:00 +00:00
fix(packaging): harden AppImage runtime lifecycle
This commit is contained in:
parent
42170da034
commit
9e76636571
16 changed files with 260 additions and 48 deletions
89
.github/workflows/ci.yml
vendored
89
.github/workflows/ci.yml
vendored
|
|
@ -705,8 +705,14 @@ jobs:
|
|||
APPIMAGE="$PWD/dist/Ouroboros-${VERSION}-linux-x86_64.AppImage"
|
||||
EXTRACT_ROOT="$RUNNER_TEMP/ouroboros-appimage-${GITHUB_RUN_ATTEMPT}"
|
||||
HOME_DIR="$RUNNER_TEMP/ouroboros-appimage-home"
|
||||
APP_ROOT="$HOME_DIR/Ouroboros"
|
||||
DATA_DIR="$APP_ROOT/data"
|
||||
PID_FILE="$APP_ROOT/ouroboros.pid"
|
||||
PORT_FILE="$DATA_DIR/state/server_port"
|
||||
CLI_LOG="$RUNNER_TEMP/ouroboros-appimage-cli.log"
|
||||
LDD_LOG="$RUNNER_TEMP/ouroboros-appimage-ldd.log"
|
||||
test -x "$APPIMAGE"
|
||||
mkdir -p "$EXTRACT_ROOT" "$HOME_DIR"
|
||||
mkdir -p "$EXTRACT_ROOT" "$HOME_DIR" "$DATA_DIR"
|
||||
(cd "$EXTRACT_ROOT" && "$APPIMAGE" --appimage-extract >/dev/null)
|
||||
APPDIR="$EXTRACT_ROOT/squashfs-root"
|
||||
test -x "$APPDIR/AppRun"
|
||||
|
|
@ -716,8 +722,81 @@ jobs:
|
|||
test -f "$APPDIR/usr/lib/ouroboros/_internal/repo_bundle_manifest.json"
|
||||
python scripts/fetch_claudexor_runtime.py --verify-only \
|
||||
--output-dir "$APPDIR/usr/lib/ouroboros/_internal/claudexor-runtime"
|
||||
HOME="$HOME_DIR" XDG_CACHE_HOME="$HOME_DIR/.cache" APPDIR="$APPDIR" \
|
||||
"$APPDIR/AppRun" --cli --help >/dev/null
|
||||
|
||||
VERSION_OUTPUT="$(APPIMAGE_EXTRACT_AND_RUN=1 "$APPIMAGE" --version)"
|
||||
test "$VERSION_OUTPUT" = "Ouroboros $VERSION"
|
||||
HOME="$HOME_DIR" XDG_CACHE_HOME="$HOME_DIR/.cache" \
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 "$APPIMAGE" --cli --help >/dev/null
|
||||
|
||||
cleanup_appimage_smoke() {
|
||||
if [ -s "$PID_FILE" ]; then
|
||||
pid="$(tr -d '[:space:]' < "$PID_FILE")"
|
||||
kill -TERM "$pid" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup_appimage_smoke EXIT
|
||||
|
||||
# `run --start` must launch a fresh outer AppImage runtime, not the
|
||||
# raw PyInstaller binary inside the first temporary extraction. The
|
||||
# deliberately empty prompt exits the CLI after readiness without
|
||||
# enqueueing work; the desktop runtime must remain alive afterwards.
|
||||
set +e
|
||||
env -u DISPLAY -u WAYLAND_DISPLAY \
|
||||
HOME="$HOME_DIR" XDG_CACHE_HOME="$HOME_DIR/.cache" BROWSER=/bin/true \
|
||||
OPENROUTER_API_KEY=ci-placeholder OUROBOROS_MAX_WORKERS=1 \
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 \
|
||||
"$APPIMAGE" --cli run --start --detach >"$CLI_LOG" 2>&1
|
||||
CLI_RC=$?
|
||||
set -e
|
||||
test "$CLI_RC" -eq 2
|
||||
|
||||
for _ in $(seq 1 180); do
|
||||
if [ -s "$PID_FILE" ] && [ -s "$PORT_FILE" ]; then
|
||||
PORT="$(tr -d '[:space:]' < "$PORT_FILE")"
|
||||
if curl --fail --silent "http://127.0.0.1:$PORT/api/health" >/dev/null; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
test -s "$PID_FILE"
|
||||
test -s "$PORT_FILE"
|
||||
PORT="$(tr -d '[:space:]' < "$PORT_FILE")"
|
||||
curl --fail --silent "http://127.0.0.1:$PORT/api/health" \
|
||||
| jq -e --arg version "$VERSION" '.version == $version' >/dev/null
|
||||
curl --fail --silent "http://127.0.0.1:$PORT/api/state" \
|
||||
| jq -e '.supervisor_ready == true' >/dev/null
|
||||
|
||||
LAUNCHER_PID="$(tr -d '[:space:]' < "$PID_FILE")"
|
||||
kill -0 "$LAUNCHER_PID"
|
||||
LAUNCHER_EXE="$(readlink "/proc/$LAUNCHER_PID/exe")"
|
||||
test -e "$LAUNCHER_EXE"
|
||||
ldd "$LAUNCHER_EXE" >"$LDD_LOG"
|
||||
if grep -F "not found" "$LDD_LOG"; then
|
||||
echo "AppImage launcher has unresolved shared libraries" >&2
|
||||
exit 1
|
||||
fi
|
||||
APPIMAGE_RUNTIME_ROOT="${LAUNCHER_EXE%/usr/lib/ouroboros/Ouroboros}"
|
||||
|
||||
kill -TERM "$LAUNCHER_PID"
|
||||
for _ in $(seq 1 120); do
|
||||
if ! kill -0 "$LAUNCHER_PID" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
if kill -0 "$LAUNCHER_PID" 2>/dev/null; then
|
||||
echo "AppImage launcher did not shut down after SIGTERM" >&2
|
||||
exit 1
|
||||
fi
|
||||
for _ in $(seq 1 60); do
|
||||
[ ! -e "$APPIMAGE_RUNTIME_ROOT" ] && break
|
||||
sleep 0.5
|
||||
done
|
||||
test ! -e "$APPIMAGE_RUNTIME_ROOT"
|
||||
test ! -e "$PID_FILE"
|
||||
trap - EXIT
|
||||
|
||||
HOME="$HOME_DIR" XDG_CACHE_HOME="$HOME_DIR/.cache" \
|
||||
OUROBOROS_DATA_DIR="$RUNNER_TEMP/ouroboros-appimage-claudexor-smoke" \
|
||||
OUROBOROS_BUNDLE_DIR="$APPDIR/usr/lib/ouroboros/_internal" \
|
||||
|
|
@ -786,7 +865,9 @@ jobs:
|
|||
--commit "$GITHUB_SHA" --tag "$GITHUB_REF_NAME" \
|
||||
--check embedded_repo_bundle --check embedded_claudexor_runtime \
|
||||
--check packaged_cli_help --check appimage_extract_and_run \
|
||||
--check appimage_metadata
|
||||
--check appimage_metadata --check product_version \
|
||||
--check browser_fallback_start --check gateway_readiness \
|
||||
--check clean_shutdown --check shared_libraries
|
||||
|
||||
- name: Install digest-pinned Syft
|
||||
id: syft
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -47,15 +47,16 @@ To run tasks, configure at least one supported remote provider API key or a loca
|
|||
- **Debian / Ubuntu / Astra Linux x86_64:** when the selected release lists `ouroboros_<version>_amd64.deb`, download it and run `sudo apt install ./ouroboros_<version>_amd64.deb`. It installs Git as a package dependency, installs Ouroboros to `/opt/ouroboros`, puts `ouroboros` on `PATH`, and adds a desktop entry.
|
||||
- **Fedora / RHEL x86_64:** when listed, download `ouroboros-<version>-1.x86_64.rpm` and run `sudo dnf install ./ouroboros-<version>-1.x86_64.rpm`. Same layout and Git dependency as the `.deb`.
|
||||
- **RED OS 8 x86_64:** when listed, download `ouroboros-<version>-1.red80.x86_64.rpm` and run `sudo dnf install ./ouroboros-<version>-1.red80.x86_64.rpm`. It carries the `red80` release tag. CI attempts non-blocking install-and-run smokes on Astra Linux 1.8 and RED OS 8; inspect the tagged workflow run for their outcome.
|
||||
- **Other Linux x86_64:** from the [latest stable release](https://github.com/razzant/ouroboros/releases/latest), download `Ouroboros-<version>-linux-x86_64.AppImage`, make it executable, and run it. Use `./Ouroboros-<version>-linux-x86_64.AppImage --cli <args>` for the bundled CLI. The tarball remains available for extraction-based installs.
|
||||
- **Other Linux x86_64:** from the [selected release](https://github.com/razzant/ouroboros/releases), use `Ouroboros-<version>-linux-x86_64.AppImage` when listed: make it executable and run it, or pass `--cli <args>` for the bundled CLI. Git must already be installed. If that release does not list an AppImage, use the extraction-friendly tarball.
|
||||
- **Windows x64:** from the [latest stable release](https://github.com/razzant/ouroboros/releases/latest), download `Ouroboros-<version>-windows-x64.zip`, extract it, and run `Ouroboros\Ouroboros.exe`. The optional CLI installer is `Ouroboros\bin\install-ouroboros-cli.cmd`.
|
||||
|
||||
Prerelease artifacts stay on their tag pages; `/releases/latest` points to the latest stable release. If bundled browser tools on Linux need host libraries, run `./Ouroboros/python-standalone/bin/python3 -m playwright install-deps chromium webkit`. See the [full install and verification guide](https://ouroboros-agent.ai/install/) for source setup and release proof files.
|
||||
Prerelease artifacts stay on their tag pages; `/releases/latest` points to the latest stable release. If bundled browser tools on Linux need host libraries, run `./Ouroboros/_internal/python-standalone/bin/python3 -m playwright install-deps chromium webkit`. See the [full install and verification guide](https://ouroboros-agent.ai/install/) for source setup and release proof files.
|
||||
|
||||
#### Install the Linux AppImage
|
||||
|
||||
An AppImage is a portable executable, so user-level installation means copying
|
||||
it to a stable path and making it executable; it does not need root access:
|
||||
When a release lists the AppImage, user-level installation means copying the
|
||||
portable executable to a stable path and making it executable; it does not need
|
||||
root access. Ouroboros bootstrap still requires Git on the host:
|
||||
|
||||
```bash
|
||||
VERSION=x.y.z
|
||||
|
|
@ -86,7 +87,7 @@ your distribution:
|
|||
|
||||
```bash
|
||||
"$HOME/Applications/Ouroboros.AppImage" --appimage-extract
|
||||
./squashfs-root/usr/lib/ouroboros/python-standalone/bin/python3 \
|
||||
./squashfs-root/usr/lib/ouroboros/_internal/python-standalone/bin/python3 \
|
||||
-m playwright install-deps chromium webkit
|
||||
```
|
||||
|
||||
|
|
@ -297,7 +298,7 @@ bash scripts/download_python_standalone.sh
|
|||
bash build_linux.sh
|
||||
```
|
||||
|
||||
Outputs: `dist/Ouroboros-<VERSION>-linux-<arch>.AppImage` and the extraction-friendly `dist/Ouroboros-<VERSION>-linux-<arch>.tar.gz`. Run the AppImage after `chmod +x`, or pass `--cli` to reach its bundled CLI. If FUSE is unavailable, set `APPIMAGE_EXTRACT_AND_RUN=1` when launching it. The tarball contains `Ouroboros/bin/install-ouroboros-cli`. If bundled browser tools need host libraries, run `./Ouroboros/python-standalone/bin/python3 -m playwright install-deps chromium webkit` from the extracted tarball.
|
||||
Outputs: `dist/Ouroboros-<VERSION>-linux-<arch>.AppImage` and the extraction-friendly `dist/Ouroboros-<VERSION>-linux-<arch>.tar.gz`. The AppImage needs host Git; run it after `chmod +x`, or pass `--cli` to reach its bundled CLI. If FUSE is unavailable, set `APPIMAGE_EXTRACT_AND_RUN=1` when launching it. The tarball contains `./Ouroboros/bin/install-ouroboros-cli`. If bundled browser tools need host libraries, run `./Ouroboros/_internal/python-standalone/bin/python3 -m playwright install-deps chromium webkit` from the extracted tarball.
|
||||
|
||||
On a build host where system packages are managed separately, set `OUROBOROS_SKIP_PLAYWRIGHT_INSTALL_DEPS=1`; Chromium and WebKit are still downloaded and bundled, but the build does not invoke `sudo` to install host libraries.
|
||||
|
||||
|
|
|
|||
|
|
@ -273,6 +273,8 @@ scripts/claudexor_platform_smoke.py ← the platform gate's smoke runner (daemon
|
|||
scripts/fetch_claudexor_runtime.py ← release-build fetcher for the pinned managed Claudexor runtime archive (claudexor_runtime_pin.json is the SSOT)
|
||||
build.sh ← macOS build (PyInstaller → .dmg)
|
||||
build_linux.sh ← Linux build (portable-Python PyInstaller → .AppImage + .tar.gz)
|
||||
scripts/build_appimage.sh ← AppDir assembly plus digest-pinned appimagetool and embedded type-2 runtime
|
||||
packaging/appimage/ ← AppRun desktop/CLI dispatch and Linux desktop metadata
|
||||
scripts/build_linux_packages.sh ← Wraps the x86_64 Linux payload into dependency-declaring `.deb`, generic `.rpm`, and RED OS 8 `.red80.rpm` assets
|
||||
scripts/smoke_linux_packages.sh ← Release-gating `apt`/`dnf` CLI + desktop-launcher smoke on Ubuntu 22.04/Fedora plus a non-blocking vendor-registry lane for Astra Linux/RED OS
|
||||
build_windows.ps1 ← Windows build (PyInstaller → .zip)
|
||||
|
|
@ -1947,7 +1949,7 @@ Claude Runtime Status appears when an Anthropic key exists or when backend/runti
|
|||
|
||||
The local `ouroboros-stable` ref also remains a recovery fallback maintained by explicit promotion; that local role does not select the official QA feed. Colab seeds it from the already validated shared Stable release when possible and otherwise from the selected validated channel, then leaves it pinned until promotion. Launcher metadata describes bootstrap provenance only. Runtime status, preflight, Colab bootstrap, and apply resolve the selected channel and exact fetched SHA themselves, so an older frozen launcher cannot silently redirect updates. Stable additionally requires the shared plain release tag; QA and Development do not use version comparison as an admission gate.
|
||||
|
||||
The main CI workflow has five roles: fork-safe quick checks with no provider secrets; the full cross-platform matrix; provider integration when secrets exist; official-skill install, preflight, review, dependency, and keyless execution smoke; and tag-triggered build/release. Secret-bearing skill review runs before any step that imports downloaded plugin code, and a missing required key is red rather than skipped. Release jobs build three platform archives plus three native Linux package assets, verify each final asset, produce checksums, SBOMs and source-bound attestations, and recheck the remote annotated tag against the event SHA before draft creation and publication.
|
||||
The main CI workflow has five roles: fork-safe quick checks with no provider secrets; the full cross-platform matrix; provider integration when secrets exist; official-skill install, preflight, review, dependency, and keyless execution smoke; and tag-triggered build/release. Secret-bearing skill review runs before any step that imports downloaded plugin code, and a missing required key is red rather than skipped. Release jobs build three platform archives, an AppImage, and three native Linux package assets, verify each final asset, produce checksums, SBOMs and source-bound attestations, and recheck the remote annotated tag against the event SHA before draft creation and publication.
|
||||
|
||||
Quick pull-request jobs are read-only and never use `pull_request_target`. The live catalog skill lane is intentionally a release dependency: it proves that the published payloads still install on this runtime, while keeping provider credentials out of every process that imports payload code. This external-service dependency is an explicit owner trade-off rather than an accidental source of flaky authority.
|
||||
|
||||
|
|
@ -1955,11 +1957,11 @@ The separate Scorecard workflow runs on `main` pushes and weekly. It pins every
|
|||
|
||||
### Build scripts
|
||||
|
||||
`build.sh`, `build_linux.sh`, `scripts/build_linux_packages.sh`, `scripts/smoke_linux_packages.sh`, `build_windows.ps1`, and `scripts/build_repo_bundle.py` are release-invariant owners. Linux PyInstaller runs under the same pinned portable Python shipped in the payload, so its bundled libpython keeps the payload's glibc floor instead of inheriting the release runner's newer ABI. The native Linux builder wraps that already-built x86_64 payload without replacing its runtime: package metadata declares the external Git required by bootstrap, while the bundled Python, Node, and browser remain under `/opt/ouroboros`. It prefers hardlinks in an output-local stage and falls back to one payload copy when hardlinking is unavailable. The release-gating smoke installs through `apt` or `dnf`, proves Git resolution, the desktop files, the real packaged CLI, and a bounded desktop-launcher start on Ubuntu 22.04/Fedora 42; Astra Linux and RED OS vendor-image runs remain explicit informational evidence because third-party registry availability cannot block publication. The macOS image keeps the explicit app, Applications symlink, and optional CLI installer layout; final-image verification checks the real symlink target. Release tag prerequisite: `scripts/build_repo_bundle.py` is the release-tag SSOT and verifies the annotated `v$(cat VERSION)` tag points at `HEAD` before packaging.
|
||||
`build.sh`, `build_linux.sh`, `scripts/build_appimage.sh`, `scripts/build_linux_packages.sh`, `scripts/smoke_linux_packages.sh`, `build_windows.ps1`, and `scripts/build_repo_bundle.py` are release-invariant owners. Linux PyInstaller runs under the same pinned portable Python shipped in the payload, so its bundled libpython keeps the payload's glibc floor instead of inheriting the release runner's newer ABI. The AppImage builder wraps that payload with digest-pinned tool and runtime bytes; the native Linux builder wraps the same x86_64 payload without replacing its runtime. Native package metadata declares the external Git required by bootstrap, while the bundled Python, Node, and browser remain under `/opt/ouroboros`. It prefers hardlinks in an output-local stage and falls back to one payload copy when hardlinking is unavailable. The release-gating smoke installs through `apt` or `dnf`, proves Git resolution, the desktop files, the real packaged CLI, and a bounded desktop-launcher start on Ubuntu 22.04/Fedora 42; Astra Linux and RED OS vendor-image runs remain explicit informational evidence because third-party registry availability cannot block publication. The macOS image keeps the explicit app, Applications symlink, and optional CLI installer layout; final-image verification checks the real symlink target. Release tag prerequisite: `scripts/build_repo_bundle.py` is the release-tag SSOT and verifies the annotated `v$(cat VERSION)` tag points at `HEAD` before packaging.
|
||||
|
||||
Platform builds precompile bundled Python with unchecked-hash bytecode. Sealing valid bytecode prevents runtime `__pycache__` writes from invalidating a macOS signature; runtime children also route caches outside the bundle. When signing is enabled, hardened runtime, notarization, xattr hygiene, and strict verification remain part of the stable-release path. Prerelease artifacts may intentionally be unsigned, and their evidence must report the actual signing state rather than imply notarization. Linux and Windows use the same precompile for startup parity without a macOS seal.
|
||||
|
||||
The release proof begins with the final DMG, AppImage, tarball, or ZIP rather than its staging directory. Each platform shard checks the embedded repository bundle, packaged CLI, and managed Claudexor seed plus Node by starting the owned daemon, completing a fixture task, and verifying an identity-bound stop. The Linux AppImage is extracted without FUSE for metadata, CLI, runtime, and SBOM checks, while its mounted payload remains read-only at runtime. The macOS smoke also verifies its Finder installation surfaces. On Linux, the proven tarball payload is also wrapped into the three native packages; each receives its own digest-bound package-manager smoke receipt, including the bounded desktop-launcher start, and provenance attestation. A digest-pinned Syft build produces CycloneDX inventories from extracted payload bytes; the tarball inventory is reused for the three native wrappers instead of rescanning identical bytes. GitHub SBOM attestations bind the relevant payload inventory to each final asset. The release job accepts only the seven expected release assets, recalculates their digests, verifies both predicate types, writes the checksum/evidence capsule, and rechecks the remote tag immediately before publication. Signing credentials remain step-scoped and are absent from SBOM and attestation steps.
|
||||
The release proof begins with the final DMG, AppImage, tarball, or ZIP rather than its staging directory. Each platform shard checks the embedded repository bundle, packaged CLI, and managed Claudexor seed plus Node by starting the owned daemon, completing a fixture task, and verifying an identity-bound stop. The Linux AppImage is extracted for metadata and SBOM inspection, then run through the FUSE-independent runtime to prove exact version output, CLI dispatch, browser-fallback readiness, payload lifetime after CLI-started relaunch, main-executable libraries, and clean shutdown; its payload remains read-only at runtime. This is browser-fallback evidence, not a claim that the packaged payload contains a native GTK/Qt backend. The macOS smoke also verifies its Finder installation surfaces. On Linux, the proven tarball payload is also wrapped into the three native packages; each receives its own digest-bound package-manager smoke receipt, including the bounded desktop-launcher start, and provenance attestation. A digest-pinned Syft build produces CycloneDX inventories from extracted payload bytes; the tarball inventory is reused for the three native wrappers instead of rescanning identical bytes. GitHub SBOM attestations bind the relevant payload inventory to each final asset. The release job accepts only the seven expected release assets, recalculates their digests, verifies both predicate types, writes the checksum/evidence capsule, and rechecks the remote tag immediately before publication. Signing credentials remain step-scoped and are absent from SBOM and attestation steps.
|
||||
|
||||
### Docker
|
||||
|
||||
|
|
|
|||
|
|
@ -1657,8 +1657,12 @@ floating Claudexor npm package. The macOS check also requires the
|
|||
payload, and an arm64 app executable.
|
||||
|
||||
Linux additionally emits an AppImage built by a version- and digest-pinned
|
||||
`appimagetool`. CI extracts it without FUSE and independently verifies its
|
||||
desktop metadata, embedded repository, CLI, and managed Claudexor runtime.
|
||||
`appimagetool` with a separately SHA-pinned embedded type-2 runtime. CI extracts
|
||||
it for metadata and SBOM inspection, then uses real extract-and-run invocations
|
||||
to verify product version, CLI dispatch, the browser-fallback launcher, gateway
|
||||
readiness, payload lifetime after `run --start`, shared libraries, and graceful
|
||||
shutdown. This smoke deliberately makes no native GTK/Qt claim: packaged native
|
||||
webview coverage remains a separate Linux distribution contract.
|
||||
`OUROBOROS_SKIP_PLAYWRIGHT_INSTALL_DEPS=1` is only a local-builder escape hatch
|
||||
for hosts whose system packages are managed separately: it skips Playwright's
|
||||
interactive host-library installation, not browser-binary bundling, and a build
|
||||
|
|
|
|||
|
|
@ -32,6 +32,13 @@
|
|||
"format": "tar.gz",
|
||||
"nameTemplate": "Ouroboros-{version}-linux-x86_64.tar.gz"
|
||||
},
|
||||
{
|
||||
"platform": "linux",
|
||||
"architecture": "x86_64",
|
||||
"format": "AppImage",
|
||||
"availability": "per-release",
|
||||
"nameTemplate": "Ouroboros-{version}-linux-x86_64.AppImage"
|
||||
},
|
||||
{
|
||||
"platform": "linux",
|
||||
"architecture": "x86_64",
|
||||
|
|
|
|||
|
|
@ -60,11 +60,11 @@
|
|||
|
||||
<h2>Linux and Windows</h2>
|
||||
<div class="download-grid download-grid-two">
|
||||
<article><h2>Linux</h2><p>x86_64 Linux.</p><a class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/latest">Open latest release</a><p>Use a native <code>.deb</code> or <code>.rpm</code> when listed. Otherwise copy the AppImage to <code>~/Applications/Ouroboros.AppImage</code>, make it executable, and run it; no root install is needed. Pass <code>--cli</code> for the bundled CLI, or set <code>APPIMAGE_EXTRACT_AND_RUN=1</code> when FUSE is unavailable. The tarball remains available for extraction-based installs.</p></article>
|
||||
<article><h2>Linux</h2><p>x86_64 Linux.</p><a class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/latest">Open latest release</a><p>When that release lists native packages, use its <code>.deb</code> or <code>.rpm</code>. If it does not list a native package, use the AppImage when listed: copy it to <code>~/Applications/Ouroboros.AppImage</code>, make it executable, and run it. Git must already be installed. Pass <code>--cli</code> for the bundled CLI, or set <code>APPIMAGE_EXTRACT_AND_RUN=1</code> when FUSE is unavailable. If that release lists neither option, download the tarball.</p></article>
|
||||
<article><h2>Windows</h2><p>64-bit Windows.</p><a class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/latest">Open latest release</a><p>Download the Windows ZIP, extract it, and run <code>Ouroboros.exe</code>. The CLI installer is in <code>Ouroboros\bin\</code>.</p></article>
|
||||
</div>
|
||||
|
||||
<p>Prerelease artifacts stay on their tag pages; the latest-release page points to the latest stable release. The AppImage bundles Chromium and WebKit binaries, but distro shared libraries remain host dependencies. Prefer the native package where available; otherwise extract the AppImage and run <code>./squashfs-root/usr/lib/ouroboros/python-standalone/bin/python3 -m playwright install-deps chromium webkit</code> to identify and install the required host packages.</p>
|
||||
<p>Prerelease artifacts stay on their tag pages; the latest-release page points to the latest stable release. The AppImage bundles Chromium and WebKit binaries, but distro shared libraries remain host dependencies. Prefer the native package where available; otherwise extract the AppImage and run <code>./squashfs-root/usr/lib/ouroboros/_internal/python-standalone/bin/python3 -m playwright install-deps chromium webkit</code> to identify and install the required host packages.</p>
|
||||
|
||||
<h2>Run from source</h2>
|
||||
<p>Source mode needs Python 3.10 or newer and Git.</p>
|
||||
|
|
|
|||
|
|
@ -401,6 +401,15 @@ def _launch_desktop_app(runtime: PackagedRuntime) -> None:
|
|||
|
||||
|
||||
def _desktop_app_path(bundle_root: pathlib.Path) -> pathlib.Path | None:
|
||||
if not IS_MACOS and not IS_WINDOWS:
|
||||
# Inside an AppImage, the inner PyInstaller launcher lives on a mount (or
|
||||
# temporary extract tree) owned by the outer runtime. Starting that raw
|
||||
# binary would let the CLI exit and release the payload underneath the
|
||||
# detached desktop process. Relaunch the stable AppImage path instead so
|
||||
# its own runtime owns the payload for the launcher's whole lifetime.
|
||||
appimage = pathlib.Path(os.environ.get("APPIMAGE", "").strip())
|
||||
if appimage.is_file():
|
||||
return appimage
|
||||
for parent in (bundle_root, *bundle_root.parents):
|
||||
if IS_MACOS and parent.suffix == ".app":
|
||||
return parent
|
||||
|
|
|
|||
|
|
@ -9,6 +9,18 @@ PAYLOAD="$APPDIR/usr/lib/ouroboros"
|
|||
# and cache roots; expose the same explicit bundle root to child processes.
|
||||
export OUROBOROS_BUNDLE_DIR="$PAYLOAD/_internal"
|
||||
|
||||
if [ "${1:-}" = "--version" ]; then
|
||||
VERSION_FILE="$OUROBOROS_BUNDLE_DIR/VERSION"
|
||||
if [ ! -s "$VERSION_FILE" ]; then
|
||||
echo "Ouroboros version metadata is missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
VERSION=
|
||||
IFS= read -r VERSION < "$VERSION_FILE" || true
|
||||
printf 'Ouroboros %s\n' "$VERSION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "--cli" ]; then
|
||||
shift
|
||||
exec "$PAYLOAD/bin/ouroboros" "$@"
|
||||
|
|
|
|||
|
|
@ -2,27 +2,11 @@
|
|||
set -euo pipefail
|
||||
|
||||
VERSION=$(tr -d '[:space:]' < VERSION)
|
||||
ARCH=$(uname -m)
|
||||
DIST_DIR=${OUROBOROS_DIST_DIR:-dist}
|
||||
PAYLOAD_DIR="$DIST_DIR/Ouroboros"
|
||||
APPDIR=${OUROBOROS_APPDIR:-$DIST_DIR/Ouroboros.AppDir}
|
||||
OUTPUT="$DIST_DIR/Ouroboros-${VERSION}-linux-${ARCH}.AppImage"
|
||||
TOOL_VERSION=1.9.1
|
||||
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
TOOL_ARCH=x86_64
|
||||
TOOL_SHA256=ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0
|
||||
;;
|
||||
aarch64)
|
||||
TOOL_ARCH=aarch64
|
||||
TOOL_SHA256=f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported AppImage architecture: $ARCH" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
RUNTIME_VERSION=20251108
|
||||
|
||||
if [ ! -x "$PAYLOAD_DIR/Ouroboros" ]; then
|
||||
echo "ERROR: PyInstaller payload not found at $PAYLOAD_DIR/Ouroboros" >&2
|
||||
|
|
@ -49,24 +33,59 @@ if [ "${1:-}" = "--appdir-only" ]; then
|
|||
exit 0
|
||||
fi
|
||||
|
||||
ARCH=$(uname -m)
|
||||
OUTPUT="$DIST_DIR/Ouroboros-${VERSION}-linux-${ARCH}.AppImage"
|
||||
case "$ARCH" in
|
||||
x86_64)
|
||||
TOOL_ARCH=x86_64
|
||||
TOOL_SHA256=ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0
|
||||
RUNTIME_SHA256=2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d
|
||||
;;
|
||||
aarch64)
|
||||
TOOL_ARCH=aarch64
|
||||
TOOL_SHA256=f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158
|
||||
RUNTIME_SHA256=00cbdfcf917cc6c0ff6d3347d59e0ca1f7f45a6df1a428a0d6d8a78664d87444
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported AppImage architecture: $ARCH" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
TOOL_CACHE=${APPIMAGE_TOOL_CACHE_DIR:-${XDG_CACHE_HOME:-${HOME:-/tmp}/.cache}/ouroboros/appimage}
|
||||
TOOL="$TOOL_CACHE/appimagetool-${TOOL_VERSION}-${TOOL_ARCH}.AppImage"
|
||||
TOOL_URL="https://github.com/AppImage/appimagetool/releases/download/${TOOL_VERSION}/appimagetool-${TOOL_ARCH}.AppImage"
|
||||
RUNTIME="$TOOL_CACHE/type2-runtime-${RUNTIME_VERSION}-${TOOL_ARCH}"
|
||||
RUNTIME_URL="https://github.com/AppImage/type2-runtime/releases/download/${RUNTIME_VERSION}/runtime-${TOOL_ARCH}"
|
||||
mkdir -p "$TOOL_CACHE"
|
||||
if [ ! -f "$TOOL" ]; then
|
||||
curl --fail --location --silent --show-error "$TOOL_URL" --output "$TOOL.tmp"
|
||||
mv "$TOOL.tmp" "$TOOL"
|
||||
fi
|
||||
|
||||
ACTUAL_SHA256=$(sha256sum "$TOOL" | awk '{print $1}')
|
||||
if [ "$ACTUAL_SHA256" != "$TOOL_SHA256" ]; then
|
||||
echo "appimagetool SHA256 mismatch: expected $TOOL_SHA256, got $ACTUAL_SHA256" >&2
|
||||
exit 1
|
||||
fi
|
||||
fetch_verified() {
|
||||
local destination="$1" url="$2" expected="$3" actual tmp
|
||||
if [ -f "$destination" ]; then
|
||||
actual=$(sha256sum "$destination" | awk '{print $1}')
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
tmp="${destination}.tmp.$$"
|
||||
rm -f "$tmp"
|
||||
curl --fail --location --silent --show-error "$url" --output "$tmp"
|
||||
actual=$(sha256sum "$tmp" | awk '{print $1}')
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
echo "SHA256 mismatch for $(basename "$destination"): expected $expected, got $actual" >&2
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
mv -f "$tmp" "$destination"
|
||||
}
|
||||
|
||||
fetch_verified "$TOOL" "$TOOL_URL" "$TOOL_SHA256"
|
||||
fetch_verified "$RUNTIME" "$RUNTIME_URL" "$RUNTIME_SHA256"
|
||||
chmod +x "$TOOL"
|
||||
|
||||
rm -f "$OUTPUT"
|
||||
ARCH="$TOOL_ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" "$APPDIR" "$OUTPUT"
|
||||
ARCH="$TOOL_ARCH" APPIMAGE_EXTRACT_AND_RUN=1 \
|
||||
"$TOOL" --runtime-file "$RUNTIME" "$APPDIR" "$OUTPUT"
|
||||
chmod +x "$OUTPUT"
|
||||
rm -rf "$APPDIR"
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,17 @@ REQUIRED_SMOKE_CHECKS = {
|
|||
),
|
||||
"linux-x86_64": COMMON_SMOKE_CHECKS,
|
||||
"linux-appimage-x86_64": COMMON_SMOKE_CHECKS
|
||||
| frozenset({"appimage_extract_and_run", "appimage_metadata"}),
|
||||
| frozenset(
|
||||
{
|
||||
"appimage_extract_and_run",
|
||||
"appimage_metadata",
|
||||
"browser_fallback_start",
|
||||
"clean_shutdown",
|
||||
"gateway_readiness",
|
||||
"product_version",
|
||||
"shared_libraries",
|
||||
}
|
||||
),
|
||||
"linux-deb-amd64": PACKAGE_SMOKE_CHECKS,
|
||||
"linux-rpm-x86_64": PACKAGE_SMOKE_CHECKS,
|
||||
"linux-rpm-red80-x86_64": PACKAGE_SMOKE_CHECKS,
|
||||
|
|
@ -269,7 +279,8 @@ def _release_notes(
|
|||
"",
|
||||
"On Linux, `sudo apt install ./ouroboros_*_amd64.deb` or `sudo dnf install ./ouroboros-*.x86_64.rpm`"
|
||||
" installs to `/opt/ouroboros` and puts `ouroboros` on `PATH`; RED OS 8 has its own"
|
||||
" `.red80.x86_64.rpm`, and the `.tar.gz` stays available for distributions without dpkg or rpm.",
|
||||
" `.red80.x86_64.rpm`. The AppImage runs from a user-writable path when listed and host Git"
|
||||
" is installed; the `.tar.gz` stays available for extraction-based installs.",
|
||||
"",
|
||||
"## Release proof",
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -60,11 +60,11 @@
|
|||
|
||||
<h2>Linux and Windows</h2>
|
||||
<div class="download-grid download-grid-two">
|
||||
<article><h2>Linux</h2><p>x86_64 Linux.</p><a class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/latest">Open latest release</a><p>Use a native <code>.deb</code> or <code>.rpm</code> when listed. Otherwise copy the AppImage to <code>~/Applications/Ouroboros.AppImage</code>, make it executable, and run it; no root install is needed. Pass <code>--cli</code> for the bundled CLI, or set <code>APPIMAGE_EXTRACT_AND_RUN=1</code> when FUSE is unavailable. The tarball remains available for extraction-based installs.</p></article>
|
||||
<article><h2>Linux</h2><p>x86_64 Linux.</p><a class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/latest">Open latest release</a><p>When that release lists native packages, use its <code>.deb</code> or <code>.rpm</code>. If it does not list a native package, use the AppImage when listed: copy it to <code>~/Applications/Ouroboros.AppImage</code>, make it executable, and run it. Git must already be installed. Pass <code>--cli</code> for the bundled CLI, or set <code>APPIMAGE_EXTRACT_AND_RUN=1</code> when FUSE is unavailable. If that release lists neither option, download the tarball.</p></article>
|
||||
<article><h2>Windows</h2><p>64-bit Windows.</p><a class="btn btn-primary" href="https://github.com/razzant/ouroboros/releases/latest">Open latest release</a><p>Download the Windows ZIP, extract it, and run <code>Ouroboros.exe</code>. The CLI installer is in <code>Ouroboros\bin\</code>.</p></article>
|
||||
</div>
|
||||
|
||||
<p>Prerelease artifacts stay on their tag pages; the latest-release page points to the latest stable release. The AppImage bundles Chromium and WebKit binaries, but distro shared libraries remain host dependencies. Prefer the native package where available; otherwise extract the AppImage and run <code>./squashfs-root/usr/lib/ouroboros/python-standalone/bin/python3 -m playwright install-deps chromium webkit</code> to identify and install the required host packages.</p>
|
||||
<p>Prerelease artifacts stay on their tag pages; the latest-release page points to the latest stable release. The AppImage bundles Chromium and WebKit binaries, but distro shared libraries remain host dependencies. Prefer the native package where available; otherwise extract the AppImage and run <code>./squashfs-root/usr/lib/ouroboros/_internal/python-standalone/bin/python3 -m playwright install-deps chromium webkit</code> to identify and install the required host packages.</p>
|
||||
|
||||
<h2>Run from source</h2>
|
||||
<p>Source mode needs Python 3.10 or newer and Git.</p>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,13 @@
|
|||
"format": "tar.gz",
|
||||
"nameTemplate": "Ouroboros-{version}-linux-x86_64.tar.gz"
|
||||
},
|
||||
{
|
||||
"platform": "linux",
|
||||
"architecture": "x86_64",
|
||||
"format": "AppImage",
|
||||
"availability": "per-release",
|
||||
"nameTemplate": "Ouroboros-{version}-linux-x86_64.AppImage"
|
||||
},
|
||||
{
|
||||
"platform": "linux",
|
||||
"architecture": "x86_64",
|
||||
|
|
|
|||
|
|
@ -4,8 +4,13 @@ import os
|
|||
import pathlib
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
REPO = pathlib.Path(__file__).resolve().parents[1]
|
||||
pytestmark = pytest.mark.skipif(
|
||||
os.name == "nt", reason="AppImage packaging is POSIX-only"
|
||||
)
|
||||
|
||||
|
||||
def test_appdir_layout_wraps_the_existing_pyinstaller_payload(tmp_path: pathlib.Path):
|
||||
|
|
@ -18,6 +23,10 @@ def test_appdir_layout_wraps_the_existing_pyinstaller_payload(tmp_path: pathlib.
|
|||
internal = payload / "_internal"
|
||||
internal.mkdir()
|
||||
(internal / "repo.bundle").write_bytes(b"bundle")
|
||||
(internal / "VERSION").write_text("6.96.2\n", encoding="utf-8")
|
||||
embedded_python = internal / "python-standalone/bin/python3"
|
||||
embedded_python.parent.mkdir(parents=True)
|
||||
embedded_python.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
||||
|
||||
appdir = tmp_path / "Ouroboros.AppDir"
|
||||
env = os.environ.copy()
|
||||
|
|
@ -37,10 +46,22 @@ def test_appdir_layout_wraps_the_existing_pyinstaller_payload(tmp_path: pathlib.
|
|||
assert (appdir / "ouroboros.png").is_file()
|
||||
assert (appdir / "usr/lib/ouroboros/Ouroboros").is_file()
|
||||
assert (appdir / "usr/lib/ouroboros/_internal/repo.bundle").is_file()
|
||||
assert (
|
||||
appdir / "usr/lib/ouroboros/_internal/python-standalone/bin/python3"
|
||||
).is_file()
|
||||
desktop = (appdir / "ouroboros.desktop").read_text(encoding="utf-8")
|
||||
assert "Exec=Ouroboros" in desktop
|
||||
assert "Icon=ouroboros" in desktop
|
||||
|
||||
version = subprocess.run(
|
||||
[str(appdir / "AppRun"), "--version"],
|
||||
env={**os.environ, "APPDIR": str(appdir)},
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert version.stdout.strip() == "Ouroboros 6.96.2"
|
||||
|
||||
|
||||
def test_apprun_exposes_cli_without_writing_to_the_mount(tmp_path: pathlib.Path):
|
||||
appdir = tmp_path / "AppDir"
|
||||
|
|
@ -63,3 +84,14 @@ def test_apprun_exposes_cli_without_writing_to_the_mount(tmp_path: pathlib.Path)
|
|||
text=True,
|
||||
)
|
||||
assert result.stdout.strip() == str(appdir / "usr/lib/ouroboros/_internal")
|
||||
|
||||
|
||||
def test_appimage_builder_pins_tool_and_embedded_runtime():
|
||||
script = (REPO / "scripts/build_appimage.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert "RUNTIME_VERSION=20251108" in script
|
||||
assert "2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d" in script
|
||||
assert "00cbdfcf917cc6c0ff6d3347d59e0ca1f7f45a6df1a428a0d6d8a78664d87444" in script
|
||||
assert "releases/download/${RUNTIME_VERSION}/runtime-${TOOL_ARCH}" in script
|
||||
assert 'fetch_verified "$RUNTIME" "$RUNTIME_URL" "$RUNTIME_SHA256"' in script
|
||||
assert '"$TOOL" --runtime-file "$RUNTIME" "$APPDIR" "$OUTPUT"' in script
|
||||
|
|
|
|||
|
|
@ -55,6 +55,20 @@ def test_packaged_cli_run_start_launches_desktop_and_strips_start(tmp_path, monk
|
|||
assert inner["args"] == ["run", "2+2?"]
|
||||
|
||||
|
||||
def test_packaged_cli_linux_relaunches_outer_appimage(tmp_path, monkeypatch):
|
||||
from ouroboros import packaged_cli
|
||||
|
||||
appimage = tmp_path / "Ouroboros.AppImage"
|
||||
appimage.write_bytes(b"appimage")
|
||||
bundle_root = tmp_path / "mount" / "usr" / "lib" / "ouroboros" / "_internal"
|
||||
|
||||
monkeypatch.setattr(packaged_cli, "IS_MACOS", False)
|
||||
monkeypatch.setattr(packaged_cli, "IS_WINDOWS", False)
|
||||
monkeypatch.setenv("APPIMAGE", str(appimage))
|
||||
|
||||
assert packaged_cli._desktop_app_path(bundle_root) == appimage
|
||||
|
||||
|
||||
def test_packaged_cli_does_not_treat_prompt_start_text_as_option(tmp_path, monkeypatch):
|
||||
from ouroboros import packaged_cli
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ def test_install_manifest_discovers_releases_without_future_asset_hashes():
|
|||
expected = {
|
||||
"Ouroboros-{version}.dmg": ("macos", "arm64"),
|
||||
"Ouroboros-{version}-linux-x86_64.tar.gz": ("linux", "x86_64"),
|
||||
"Ouroboros-{version}-linux-x86_64.AppImage": ("linux", "x86_64"),
|
||||
"ouroboros_{version}_amd64.deb": ("linux", "x86_64"),
|
||||
"ouroboros-{version}-1.x86_64.rpm": ("linux", "x86_64"),
|
||||
"ouroboros-{version}-1.red80.x86_64.rpm": ("linux", "x86_64"),
|
||||
|
|
@ -152,6 +153,9 @@ def test_install_manifest_discovers_releases_without_future_asset_hashes():
|
|||
]
|
||||
assert native_packages
|
||||
assert all(row["availability"] == "per-release" for row in native_packages)
|
||||
appimages = [row for row in manifest["artifacts"] if row["format"] == "AppImage"]
|
||||
assert len(appimages) == 1
|
||||
assert appimages[0]["availability"] == "per-release"
|
||||
verification = manifest["verification"]
|
||||
assert verification["availability"] == "per-release"
|
||||
assert verification["githubAttestations"] == {
|
||||
|
|
@ -174,6 +178,7 @@ def test_install_page_does_not_promise_native_packages_on_older_releases():
|
|||
html = (SITE / "install" / "index.html").read_text(encoding="utf-8")
|
||||
assert "When that release lists native packages" in html
|
||||
assert "does not list a native package" in html
|
||||
assert "usr/lib/ouroboros/_internal/python-standalone/bin/python3" in html
|
||||
|
||||
|
||||
def test_benchmark_assets_expose_status_and_accessible_text():
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ def test_assemble_binds_every_asset_smoke_and_sbom(tmp_path: Path):
|
|||
assert len(checksum_lines) == 3 * len(release_proof.PROOF_IDS)
|
||||
assert checksum_lines == sorted(checksum_lines, key=lambda line: line.split(" ", 1)[1])
|
||||
assert "A clear release note." in notes.read_text()
|
||||
assert "The AppImage runs from a user-writable path" in notes.read_text()
|
||||
assert "v6.87.4...v6.87.5" in notes.read_text()
|
||||
commands = evidence["verification"]["attestationCommands"]
|
||||
assert len(commands) == 2
|
||||
|
|
@ -340,6 +341,13 @@ def test_release_workflow_orders_smoke_sbom_attestation_and_draft_verification()
|
|||
assert "Ouroboros-*-linux-x86_64.AppImage" in workflow
|
||||
assert "--check appimage_extract_and_run" in workflow
|
||||
assert "--check appimage_metadata" in workflow
|
||||
assert "--check product_version" in workflow
|
||||
assert "--check browser_fallback_start" in workflow
|
||||
assert "--check gateway_readiness" in workflow
|
||||
assert "--check clean_shutdown" in workflow
|
||||
assert "--check shared_libraries" in workflow
|
||||
assert 'APP_ROOT="$HOME_DIR/Ouroboros"' in workflow
|
||||
assert 'OUROBOROS_APP_ROOT="$APP_ROOT"' not in workflow
|
||||
assert 'test -x "$MOUNT/Install CLI.command"' in workflow
|
||||
assert 'test -L "$MOUNT/Applications"' in workflow
|
||||
assert 'test "$(readlink "$MOUNT/Applications")" = "/Applications"' in workflow
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue