Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ jobs:
run: |
bun desktop/scripts/prepare-sidecar.ts --target aarch64-apple-darwin
bun desktop/scripts/prepare-sidecar.ts --target x86_64-apple-darwin
lipo -create desktop/src-tauri/binaries/ocx-aarch64-apple-darwin \
desktop/src-tauri/binaries/ocx-x86_64-apple-darwin \
-output desktop/src-tauri/binaries/ocx-universal-apple-darwin
lipo -verify_arch arm64 x86_64 desktop/src-tauri/binaries/ocx-universal-apple-darwin

- name: Prepare sidecar
if: runner.os != 'macOS'
Expand Down Expand Up @@ -344,6 +348,12 @@ jobs:
RELEASE_VERSION: ${{ inputs.version }}
run: bun desktop/scripts/windows-installer-config.ts "$RELEASE_VERSION" "$RUNNER_TEMP/opencodex-msi.json"

- name: Preserve the compiled Linux sidecar
if: runner.os == 'Linux'
run: |
chmod +x desktop/scripts/appimage-patchelf.py
echo "PATCHELF=$GITHUB_WORKSPACE/desktop/scripts/appimage-patchelf.py" >> "$GITHUB_ENV"

# Release signing is intentionally secret-gated. Developer ID, notarization,
# and updater signatures require maintainer-owned credentials; builds without
# those secrets remain useful for local validation but are not release assets.
Expand All @@ -363,6 +373,10 @@ jobs:
# diagnostics on the first attempt; Apple signing commands stay non-verbose.
run: bunx tauri ${{ runner.os == 'Linux' && '--verbose' || '' }} build --ci --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} --config "${{ runner.os == 'Windows' && format('{0}/opencodex-msi.json', runner.temp) || '{}' }}"

- name: Verify the packaged Linux sidecar
if: runner.os == 'Linux'
run: bash desktop/scripts/verify-linux-sidecar.sh

- name: Rename release assets
shell: bash
env:
Expand Down
26 changes: 26 additions & 0 deletions desktop/scripts/appimage-patchelf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""Keep the compiled Bun sidecar intact while linuxdeploy patches the host/libs."""
import os
from pathlib import Path
import sys


def main(args):
root = Path(__file__).resolve().parents[2]
triple = "x86_64-unknown-linux-gnu"
original = root / "desktop/src-tauri/binaries" / f"ocx-{triple}"
sidecar = root / "desktop/src-tauri/target" / triple / "release/bundle/appimage/OpenCodex.AppDir/usr/bin/ocx"
if len(args) == 3 and args[:2] == ["--set-rpath", "$ORIGIN/../lib"] and Path(args[2]).resolve() == sidecar.resolve():
# linuxdeploy's nested GTK pass runs ldd again after patching. Its
# patchelf rewrite breaks the compiled Bun ELF. This sidecar depends
# only on host glibc libraries; it needs no AppDir library search path.
# Never bless an already-modified binary or a different executable.
if sidecar.is_symlink() or original.read_bytes() != sidecar.read_bytes():
raise RuntimeError("AppImage sidecar differs from the prepared CLI")
print("Preserving compiled ocx bytes (no AppDir RPATH required)", file=sys.stderr)
return
os.execv("/usr/bin/patchelf", ["/usr/bin/patchelf", *args])


if __name__ == "__main__":
main(sys.argv[1:])
22 changes: 22 additions & 0 deletions desktop/scripts/verify-linux-sidecar.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Run only on a Linux packaging runner, against the completed AppImage.
set -euo pipefail
root="$(cd "$(dirname "$0")/../.." && pwd)"
bundle="$root/desktop/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage"
original="$root/desktop/src-tauri/binaries/ocx-x86_64-unknown-linux-gnu"
shopt -s nullglob
images=("$bundle"/*.AppImage)
if [ "${#images[@]}" -ne 1 ]; then
echo "Expected exactly one completed AppImage" >&2
exit 1
fi
scratch="$(mktemp -d)"
trap 'rm -rf "$scratch"' EXIT
cd "$scratch"
"${images[0]}" --appimage-extract > /dev/null
sidecar="$scratch/squashfs-root/usr/bin/ocx"
test ! -L "$sidecar"
cmp "$original" "$sidecar"
sha256sum "$original" "$sidecar"
mkdir "$scratch/home"
timeout 30s env OPENCODEX_HOME="$scratch/home" "$sidecar" --version
9 changes: 9 additions & 0 deletions structure/desktop-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,15 @@ marker, which the GUI detects to identify the shell without using IPC.

## Release packaging and updater

Linux AppImage packaging uses `desktop/scripts/appimage-patchelf.py` to preserve
the compiled Bun CLI when linuxdeploy sets the executable RPATH. Only the exact
AppDir sidecar, still byte-identical to the prepared CLI, is exempt; other ELF
operations use the system patchelf. `desktop/scripts/verify-linux-sidecar.sh`
extracts the completed AppImage, compares its CLI bytes and runs its version command
on the hosted runner before any release asset is collected.
The macOS release combines both prepared CLI architectures with `lipo` into the
universal external binary Tauri expects, and checks that both slices are present.

The release workflow packages the desktop shell as `OpenCodex-<version>-macos.dmg`,
`OpenCodex-<version>-windows-x64.msi`, `OpenCodex-<version>-linux-x86_64.AppImage`, and
`OpenCodex-<version>-linux-amd64.deb`. Each artifact is collected with a `.sha256` file;
Expand Down
16 changes: 16 additions & 0 deletions tests/ci-workflows/release-desktop-scripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,10 @@ describe("widget extension signing", () => {
const rust = steps.find(step => step.name === "Setup Rust");
expect(rust?.with?.targets).toContain("aarch64-apple-darwin,x86_64-apple-darwin");
expect(rust?.with?.targets).toContain("runner.os == 'macOS'");
const sidecars = steps.find(step => step.name === "Prepare macOS sidecars");
expect(sidecars?.run).toContain("lipo -create desktop/src-tauri/binaries/ocx-aarch64-apple-darwin");
expect(sidecars?.run).toContain("-output desktop/src-tauri/binaries/ocx-universal-apple-darwin");
expect(sidecars?.run).toContain("lipo -verify_arch arm64 x86_64");
const prepare = steps.find(step => step.name === "Prepare Windows installer version");
expect(prepare?.if).toBe("runner.os == 'Windows'");
expect(prepare?.env?.RELEASE_VERSION).toBe("${{ inputs.version }}");
Expand All @@ -444,6 +448,18 @@ describe("widget extension signing", () => {
expect(indexOfStep("Prepare Windows installer version")).toBeLessThan(indexOfStep("Build desktop bundles"));
});

test("Linux verifies the packaged CLI before collecting release assets", () => {
const preserve = steps.find(step => step.name === "Preserve the compiled Linux sidecar");
const verify = steps.find(step => step.name === "Verify the packaged Linux sidecar");
expect(preserve?.if).toBe("runner.os == 'Linux'");
expect(preserve?.run).toContain("PATCHELF=$GITHUB_WORKSPACE/desktop/scripts/appimage-patchelf.py");
expect(verify?.if).toBe("runner.os == 'Linux'");
expect(verify?.run).toBe("bash desktop/scripts/verify-linux-sidecar.sh");
expect(indexOfStep(preserve!.name!)).toBeLessThan(indexOfStep("Build desktop bundles"));
expect(indexOfStep(verify!.name!)).toBeGreaterThan(indexOfStep("Build desktop bundles"));
expect(indexOfStep(verify!.name!)).toBeLessThan(indexOfStep("Rename release assets"));
});

test("the release build hands the widget a signing identity and forbids an ad-hoc fallback", () => {
const build = steps.find(step => step.name === "Build WidgetKit extension");
expect(build).toBeDefined();
Expand Down
Loading