diff --git a/.github/workflows/appstore.yml b/.github/workflows/appstore.yml
new file mode 100644
index 0000000..aa2dd26
--- /dev/null
+++ b/.github/workflows/appstore.yml
@@ -0,0 +1,611 @@
+# Mac App Store build and submission pipeline for IRIS.
+#
+# Manual trigger only — run this when you're ready to submit a new build,
+# not on every commit.
+#
+# Required repository secrets:
+# APP_STORE_APP_CERT base64-encoded Mac App Distribution .p12
+# APP_STORE_APP_CERT_PWD password for the above
+# APP_STORE_INSTALLER_CERT base64-encoded Mac Installer Distribution .p12
+# APP_STORE_INSTALLER_CERT_PWD
+# APP_STORE_PROVISIONING_PROFILE base64-encoded .provisionprofile
+# APP_STORE_CONNECT_KEY_ID API key ID from App Store Connect
+# APP_STORE_CONNECT_ISSUER_ID issuer ID from App Store Connect
+# APP_STORE_CONNECT_PRIVATE_KEY base64-encoded .p8 private key
+#
+# One-time setup on developer.apple.com before first run:
+# 1. Create an App ID for io.github.danifunker.iris with these capabilities:
+# - App Sandbox
+# - Increased Memory Limit (for JIT — labelled "com.apple.security.cs.allow-jit")
+# - Camera
+# - Network (client + server)
+# 2. Create a Mac App Distribution provisioning profile for that App ID
+# 3. Create a Mac App Distribution certificate + Mac Installer Distribution certificate
+# 4. Create an app record in App Store Connect (name: IRIS, bundle ID: io.github.danifunker.iris)
+# 5. Generate an App Store Connect API key (Users and Access → Integrations → App Store Connect API)
+
+name: App Store
+
+on:
+ workflow_dispatch:
+ inputs:
+ build_branch:
+ description: Branch to build from (leave empty for workflow branch)
+ required: false
+ default: ''
+ validate_only:
+ description: Validate package but do not submit to App Store Connect
+ required: false
+ type: boolean
+ default: false
+ preflight_only:
+ description: Run ONLY the preflight (skip builds + package) — fast ~1 min secret/auth check
+ required: false
+ type: boolean
+ default: false
+ runner_image:
+ description: Runner image for the preflight, build and package jobs (default macos-26 — pinned for determinism; override with macos-latest / macos-15 if needed).
+ required: false
+ default: macos-26
+
+permissions:
+ contents: read
+
+env:
+ CARGO_TERM_COLOR: always
+ BUNDLE_ID: io.github.danifunker.iris
+
+jobs:
+ # ── Preflight: validate secrets/certs BEFORE the ~15 min of builds ───────────
+ # Imports both distribution certs into a throwaway keychain and proves each
+ # one is a usable *identity* (cert + private key), checks the provisioning
+ # profile parses, and checks the App Store Connect API key — failing in ~1 min
+ # instead of after the arm64/x64 builds if a secret is wrong.
+
+ preflight:
+ name: Preflight (validate App Store secrets)
+ runs-on: ${{ github.event.inputs.runner_image || 'macos-26' }}
+ outputs:
+ version: ${{ steps.version.outputs.version }}
+ bundle_version: ${{ steps.version.outputs.bundle_version }}
+ steps:
+ # Stamp the version here, in the first job, so the arm64/x64 build jobs can
+ # bake it into the binary (RELEASE_VERSION → APP_VERSION, shown in About)
+ # AND the packaging job writes the *same* value into Info.plist's
+ # CFBundleVersion. Generating it in the later package job left the builds
+ # with no RELEASE_VERSION, so About fell back to CARGO_PKG_VERSION (0.1.0).
+ - name: Generate version
+ id: version
+ run: |
+ VERSION=$(date -u +"%Y-%m-%d-%H-%M")
+ # App Store requires numeric integers separated by dots (e.g. 20250609.0200)
+ BUNDLE_VERSION=$(echo "$VERSION" | sed 's/\([0-9]*\)-\([0-9]*\)-\([0-9]*\)-\([0-9]*\)-\([0-9]*\)/\1\2\3.\4\5/')
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "bundle_version=$BUNDLE_VERSION" >> $GITHUB_OUTPUT
+ - name: Verify certificates, provisioning profile, and API key
+ env:
+ APP_STORE_APP_CERT: ${{ secrets.APP_STORE_APP_CERT }}
+ APP_STORE_APP_CERT_PWD: ${{ secrets.APP_STORE_APP_CERT_PWD }}
+ APP_STORE_INSTALLER_CERT: ${{ secrets.APP_STORE_INSTALLER_CERT }}
+ APP_STORE_INSTALLER_CERT_PWD: ${{ secrets.APP_STORE_INSTALLER_CERT_PWD }}
+ APP_STORE_PROVISIONING_PROFILE: ${{ secrets.APP_STORE_PROVISIONING_PROFILE }}
+ APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
+ APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
+ APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
+ run: |
+ fail=0
+ K="$RUNNER_TEMP/preflight.keychain-db"
+ KP=$(openssl rand -base64 24)
+ security create-keychain -p "$KP" "$K"
+ security set-keychain-settings -lut 3600 "$K"
+ security unlock-keychain -p "$KP" "$K"
+
+ # Decode + import a base64 .p12. rc: 0/1 imported, 2 empty, 3 bad base64.
+ import_p12() {
+ local b64="$1" pwd="$2"
+ [ -z "$b64" ] && return 2
+ local f="$RUNNER_TEMP/pf_$RANDOM.p12"
+ echo "$b64" | base64 --decode > "$f" 2>/dev/null || { rm -f "$f"; return 3; }
+ [ -s "$f" ] || { rm -f "$f"; return 3; }
+ security import "$f" -P "$pwd" -A -t cert -f pkcs12 -k "$K" >/dev/null 2>&1
+ local rc=$?; rm -f "$f"; return $rc
+ }
+
+ import_p12 "$APP_STORE_APP_CERT" "$APP_STORE_APP_CERT_PWD"
+ case $? in 2) echo "::error::APP_STORE_APP_CERT is empty"; fail=1;; 3) echo "::error::APP_STORE_APP_CERT is not valid base64"; fail=1;; esac
+
+ import_p12 "$APP_STORE_INSTALLER_CERT" "$APP_STORE_INSTALLER_CERT_PWD"
+ case $? in 2) echo "::error::APP_STORE_INSTALLER_CERT is empty"; fail=1;; 3) echo "::error::APP_STORE_INSTALLER_CERT is not valid base64"; fail=1;; esac
+
+ security set-key-partition-list -S apple-tool:,apple: -k "$KP" "$K" >/dev/null 2>&1 || true
+
+ echo "── Identities (certificate + private key) in keychain ──"
+ security find-identity "$K" || true
+ echo "────────────────────────────────────────────────────────"
+
+ # Distinguish "usable identity" vs "cert without key" vs "absent".
+ check_identity() { # $1 pattern $2 human name $3 secret name
+ if security find-identity "$K" | grep -q "$1"; then
+ echo "✅ $2: usable identity (certificate + private key present)."
+ elif security find-certificate -a -c "$1" "$K" >/dev/null 2>&1; then
+ echo "::error::$2: the certificate is present but has NO private key. $3 must be the *identity* exported from Keychain Access — expand the certificate's disclosure triangle, select the cert WITH the private key under it, and Export as .p12. A plain .cer has no key."
+ fail=1
+ else
+ echo "::error::$2: certificate not found in the keychain at all — wrong .p12 in $3, wrong ${3}_PWD, or bad base64."
+ fail=1
+ fi
+ }
+ check_identity "3rd Party Mac Developer Application" "Application (app signing)" "APP_STORE_APP_CERT"
+ check_identity "3rd Party Mac Developer Installer" "Installer (productbuild)" "APP_STORE_INSTALLER_CERT"
+
+ # SHA-1 of the cert we sign the .app with — must appear in the profile.
+ APP_SHA=$(security find-identity "$K" | grep "3rd Party Mac Developer Application" | head -1 | awk '{print $2}')
+ security delete-keychain "$K" >/dev/null 2>&1 || true
+
+ PP="$RUNNER_TEMP/pf.provisionprofile"
+ echo "$APP_STORE_PROVISIONING_PROFILE" | base64 --decode > "$PP" 2>/dev/null || true
+ if [ ! -s "$PP" ]; then
+ echo "::error::APP_STORE_PROVISIONING_PROFILE is empty or not valid base64."
+ fail=1
+ else
+ sz=$(wc -c < "$PP" | tr -d ' ')
+ echo "Provisioning profile: ${sz} bytes; first bytes: $(xxd -l 8 -p "$PP" 2>/dev/null)"
+ # The build only base64-decodes + embeds the profile (no parsing), so
+ # accept it if it decodes to a real provisioning profile. Prefer the
+ # signed-CMS decode; fall back to grepping the embedded plist (the
+ # profile's plist is plaintext inside the CMS envelope).
+ if PLIST=$(security cms -D -i "$PP" 2>/dev/null) && [ -n "$PLIST" ]; then
+ NAME=$(printf '%s' "$PLIST" | plutil -extract Name raw - 2>/dev/null || echo "?")
+ echo "✅ Provisioning profile parses (Name: ${NAME})."
+ # The .app's executable must be signed with a cert that is embedded
+ # in the profile (App Store rejects a mismatch). Compare SHA-1s.
+ printf '%s' "$PLIST" > "$RUNNER_TEMP/pp.plist"
+ PROF_SHAS=$(python3 -c "import plistlib,hashlib,sys; d=plistlib.load(open(sys.argv[1],'rb')); print(chr(10).join(hashlib.sha1(bytes(c)).hexdigest().upper() for c in d.get('DeveloperCertificates',[])))" "$RUNNER_TEMP/pp.plist")
+ echo "Profile certificate SHA-1(s): $(echo "$PROF_SHAS" | tr '\n' ' ')"
+ if [ -n "$APP_SHA" ] && echo "$PROF_SHAS" | grep -qi "$APP_SHA"; then
+ echo "✅ Signing certificate ($APP_SHA) is present in the provisioning profile."
+ else
+ echo "::error::The provisioning profile does NOT contain your app-signing certificate (3rd Party Mac Developer Application, ${APP_SHA}). The profile references a different cert (likely 'Apple Distribution'). Regenerate the Mac App Store profile at developer.apple.com selecting the '3rd Party Mac Developer Application' certificate, then re-encode it into APP_STORE_PROVISIONING_PROFILE."
+ fail=1
+ fi
+ rm -f "$RUNNER_TEMP/pp.plist"
+ elif grep -qa "ProvisionedDevices\|AppIDName\|TeamIdentifier\|com.apple.application-identifier" "$PP"; then
+ echo "✅ Provisioning profile decodes and contains profile markers."
+ else
+ echo "::error::APP_STORE_PROVISIONING_PROFILE decoded to ${sz} bytes but is not a provisioning profile (no profile markers). Likely the wrong file, a base64-of-base64, or a .mobileprovision saved as text. Download the Mac App Store provisioning profile (.provisionprofile) for App ID ${BUNDLE_ID} from developer.apple.com and re-encode it with: base64 -i profile.provisionprofile | pbcopy"
+ fail=1
+ fi
+ fi
+ rm -f "$PP"
+
+ [ -n "$APP_STORE_CONNECT_KEY_ID" ] && echo "✅ APP_STORE_CONNECT_KEY_ID set" || { echo "::error::APP_STORE_CONNECT_KEY_ID is empty"; fail=1; }
+ [ -n "$APP_STORE_CONNECT_ISSUER_ID" ] && echo "✅ APP_STORE_CONNECT_ISSUER_ID set" || { echo "::error::APP_STORE_CONNECT_ISSUER_ID is empty"; fail=1; }
+ if echo "$APP_STORE_CONNECT_PRIVATE_KEY" | base64 --decode 2>/dev/null | grep -q "PRIVATE KEY"; then
+ echo "✅ APP_STORE_CONNECT_PRIVATE_KEY is a .p8 private key"
+ # Smoke-test the API key end to end the way Apple intends now: mint an
+ # ES256 JWT from the .p8 and call the App Store Connect API. Xcode 26's
+ # altool (what macos-latest resolves to since the macOS 26 rollout)
+ # dropped API-key auth from `altool --list-providers` — username/
+ # password only there now — so the old check failed on every macOS 26
+ # run even with a valid key. --generate-jwt + GET /v1/apps is the same
+ # auth the upload uses and is independent of altool subcommand changes;
+ # a bad key/issuer/role fails here in ~1 min with the real HTTP status.
+ if [ -n "$APP_STORE_CONNECT_KEY_ID" ] && [ -n "$APP_STORE_CONNECT_ISSUER_ID" ]; then
+ mkdir -p "$HOME/.appstoreconnect/private_keys"
+ KEYF="$HOME/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8"
+ echo "$APP_STORE_CONNECT_PRIVATE_KEY" | base64 --decode > "$KEYF"
+ # Mint the ES256 App Store Connect JWT with openssl + python stdlib
+ # only — NOT altool. GitHub's macos-latest is mid-migration and lands
+ # unpredictably on macOS 15 (altool 8.303) or macOS 26 (altool 26.x),
+ # whose altool builds disagree on JWT / --list-providers support, so
+ # any altool-based check is a coin flip. openssl+python are identical
+ # on every runner image. python parses openssl's DER ECDSA signature
+ # into the JOSE raw r||s (64 bytes) the JWT spec wants.
+ NOW=$(date +%s)
+ b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
+ HDR=$(printf '{"alg":"ES256","kid":"%s","typ":"JWT"}' "$APP_STORE_CONNECT_KEY_ID" | b64url)
+ PL=$(printf '{"iss":"%s","iat":%d,"exp":%d,"aud":"appstoreconnect-v1"}' "$APP_STORE_CONNECT_ISSUER_ID" "$NOW" "$((NOW + 600))" | b64url)
+ SI="${HDR}.${PL}"
+ SIG=$(printf '%s' "$SI" | openssl dgst -sha256 -sign "$KEYF" \
+ | python3 -c 'import sys,base64; der=sys.stdin.buffer.read(); i=2 if der[1]<0x80 else 2+(der[1]&0x7f); rl=der[i+1]; r=der[i+2:i+2+rl]; j=i+2+rl; sl=der[j+1]; s=der[j+2:j+2+sl]; print(base64.urlsafe_b64encode(r.lstrip(b"\x00").rjust(32,b"\x00")+s.lstrip(b"\x00").rjust(32,b"\x00")).decode().rstrip("="))')
+ JWT="${SI}.${SIG}"
+ if [ -z "$SIG" ]; then
+ echo "::error::Could not sign the App Store Connect JWT — the .p8 in APP_STORE_CONNECT_PRIVATE_KEY may be malformed (openssl/python signing failed above)."
+ fail=1
+ else
+ code=$(curl -sS -o "$RUNNER_TEMP/asc.json" -w '%{http_code}' \
+ -H "Authorization: Bearer $JWT" \
+ "https://api.appstoreconnect.apple.com/v1/apps?limit=1")
+ case "$code" in
+ 200) echo "✅ App Store Connect API key authenticates." ;;
+ 401) echo "::error::App Store Connect rejected the key (401) — APP_STORE_CONNECT_KEY_ID / APP_STORE_CONNECT_ISSUER_ID / .p8 don't match, or the key was revoked."; fail=1 ;;
+ 403) echo "::error::Key authenticates but isn't authorized (403) — it needs the App Manager (or Admin) role to upload builds."; fail=1 ;;
+ *) echo "::error::App Store Connect auth check returned HTTP $code: $(cat "$RUNNER_TEMP/asc.json" 2>/dev/null)"; fail=1 ;;
+ esac
+ fi
+ rm -f "$KEYF"
+ fi
+ else echo "::error::APP_STORE_CONNECT_PRIVATE_KEY is not a base64-encoded .p8 private key."; fail=1; fi
+
+ if [ "$fail" != "0" ]; then
+ echo ""; echo "Preflight FAILED — fix the secrets above; the builds were skipped to save time."; exit 1
+ fi
+ echo ""; echo "Preflight passed ✅ — certs, provisioning profile, and API key all look good."
+
+ # ── Build (both arches on ONE arm64 runner; x86_64 cross-compiled) ───────────
+ # macOS ships a universal SDK, so an Apple-Silicon runner builds
+ # x86_64-apple-darwin natively — no separate Intel runner (which was flaky on
+ # actions/cache and is EOL in 2027 anyway). One job emits both slices; the
+ # package job lipos them into a universal binary exactly as before.
+
+ build:
+ name: Build (arm64 + x86_64 cross)
+ needs: preflight
+ if: ${{ github.event.inputs.preflight_only != 'true' }}
+ runs-on: ${{ github.event.inputs.runner_image || 'macos-26' }}
+ timeout-minutes: 45 # a hung actions/cache step can't sit for the 6h default
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ github.event.inputs.build_branch || 'main' }}
+
+ - name: Add both macOS targets to the pinned (nightly) toolchain
+ run: |
+ # rust-toolchain.toml pins nightly, so cargo uses THAT — not stable.
+ # `rustup show` (run in the checked-out repo) auto-installs the pinned
+ # toolchain; then add both macOS targets to it. The host arm64 std is
+ # already present, but x86_64 must be added or the cross build can't
+ # find core/std (adding it to stable via dtolnay wouldn't help — the
+ # build uses nightly). Mirrors the riscv64 job in release.yml.
+ rustup show
+ rustup target add aarch64-apple-darwin x86_64-apple-darwin
+
+ - name: Cache cargo
+ uses: actions/cache@v5
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: macos-universal-appstore-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ # ── Guideline 2.5.1, part 1: the winit patch must actually apply ─────────
+ # winit's macOS backend calls the private SkyLight APIs
+ # `_CGSSetWindowBackgroundBlurRadius` / `_CGSMainConnectionID` from
+ # `WindowDelegate::set_blur`. IRIS never requests blur, but the import
+ # lands in the binary anyway and Apple's static scan rejects it — that is
+ # what killed submission 2ed07ab1. `third_party/winit-0.30.13/` is a
+ # vendored copy with the call stubbed out, wired in via `[patch.crates-io]`.
+ #
+ # The dangerous failure mode is silent: a patch that matches nothing makes
+ # cargo print "patch ... was not used in the crate graph" and fall back to
+ # the UNPATCHED registry crate — the build still succeeds. So assert the
+ # patched (path-sourced, i.e. no `source =` line) winit is really in the
+ # resolved graph. Cargo.lock is gitignored, so resolve it here first
+ # instead of reading a file a fresh checkout doesn't have.
+ #
+ # A registry winit ALONGSIDE the patched one is only a warning: the patch
+ # matches the 0.30.x requirement only, so `iris`'s own winit 0.29 (used for
+ # the KeyCode type) stays unpatched. It has historically been dead-stripped
+ # — but it is exactly the copy that regressed on 2026-08-03, so it is worth
+ # naming. The nm gate after the build is the authoritative check.
+ # Detail: rules/macos/appstore-private-api.md
+ - name: Resolve the graph and verify the winit patch applied
+ run: |
+ cargo fetch
+ winits=$(awk '
+ /^\[\[package\]\]/ { name=""; ver=""; src="patched" }
+ /^name = / { gsub(/"/,""); name=$3 }
+ /^version = / { gsub(/"/,""); ver=$3 }
+ /^source = / { src="registry" }
+ /^$/ { if (name=="winit") { print ver, src; name="" } }
+ END { if (name=="winit") print ver, src }
+ ' Cargo.lock)
+ echo "winit in the resolved graph:"
+ printf '%s\n' "$winits" | sed 's/^/ /'
+ if ! printf '%s\n' "$winits" | grep -q ' patched$'; then
+ echo "::error::No path-patched winit in the resolved graph — [patch.crates-io] winit = { path = \"third_party/winit-0.30.13\" } did not apply, so this build links the UNPATCHED winit and imports the private SkyLight API. App Store review rejects that under guideline 2.5.1. See rules/macos/appstore-private-api.md."
+ exit 1
+ fi
+ unpatched=$(printf '%s\n' "$winits" | grep -c ' registry$' || true)
+ if [ "$unpatched" != "0" ]; then
+ echo "::warning::${unpatched} winit copy/copies resolve from the registry and are NOT covered by [patch.crates-io] (it matches the 0.30.x requirement only). Their set_blur still calls the private SkyLight API; it is dead-stripped today, but this is the condition that regressed on 2026-08-03. Unifying the graph on winit 0.30.13 removes the risk — see rules/macos/appstore-private-api.md."
+ fi
+
+ - name: Build iris-gui for both arches (lightning + CHD, App Store)
+ env:
+ # Baked into APP_VERSION by iris-gui/build.rs and shown in About.
+ # Must equal the CFBundleVersion the packaging job writes.
+ RELEASE_VERSION: ${{ needs.preflight.outputs.bundle_version }}
+ run: |
+ cargo build --release --target aarch64-apple-darwin -p iris-gui \
+ --features iris/lightning,appstore
+ cargo build --release --target x86_64-apple-darwin -p iris-gui \
+ --features iris/lightning,appstore
+
+ # ── App Store gate: no private SkyLight API in the linked binaries ───────
+ # eframe → egui-winit → winit calls the private `_CGSSetWindowBackgroundBlurRadius`
+ # (via `_CGSMainConnectionID`) in `WindowDelegate::set_blur`. IRIS never
+ # requests blur, but the import lands in the binary regardless and Apple's
+ # static scan rejects it under guideline 2.5.1 — submission 2ed07ab1 died
+ # exactly this way. third_party/winit-0.30.13 stubs the call out; this gate
+ # proves the stub actually took effect, on the linked binary, before we
+ # spend a job signing and uploading something Apple will bounce.
+ #
+ # `_CGShieldingWindowLevel` is PUBLIC CoreGraphics and legitimately appears
+ # in `nm -u` — never match on it. It is used here as a positive control: a
+ # silently-broken `nm` returning nothing would otherwise grep "clean".
+ # Do NOT try to attribute the symbol by running `nm` over rlibs in
+ # target/*/release/deps — the release profile is `lto = "fat"`, so those
+ # hold LLVM bitcode and `nm` reports "no symbols", which proves nothing.
+ - name: Check for private SkyLight API symbols
+ run: |
+ fail=0
+ check() {
+ bin="$1"
+ if [ ! -f "$bin" ]; then
+ echo "::error::$bin does not exist — the private-API check could not run."
+ fail=1; return
+ fi
+ if ! undef=$(nm -u "$bin"); then
+ echo "::error::nm -u failed on $bin — failing rather than passing a check that did not run."
+ fail=1; return
+ fi
+ if ! printf '%s\n' "$undef" | grep -q '_CGShieldingWindowLevel'; then
+ echo "::error::$bin: nm -u returned $(printf '%s\n' "$undef" | wc -l | tr -d ' ') lines but not the expected PUBLIC _CGShieldingWindowLevel import. The control failed, so a clean private-API result can't be trusted — investigate before shipping."
+ fail=1; return
+ fi
+ if printf '%s\n' "$undef" | grep -qE '_CGSSetWindowBackgroundBlurRadius|_CGSMainConnectionID'; then
+ echo "::error::Private SkyLight API symbol found in $bin — App Store guideline 2.5.1 will reject this build. See rules/macos/appstore-private-api.md."
+ printf '%s\n' "$undef" | grep -E '_CGS' || true
+ fail=1; return
+ fi
+ echo "OK: no private SkyLight symbols in $bin"
+ }
+ check target/aarch64-apple-darwin/release/iris-gui
+ check target/x86_64-apple-darwin/release/iris-gui
+ exit $fail
+
+ - name: Upload arm64 binary
+ uses: actions/upload-artifact@v7
+ with:
+ name: iris-gui-arm64
+ path: target/aarch64-apple-darwin/release/iris-gui
+
+ - name: Upload x86_64 binary
+ uses: actions/upload-artifact@v7
+ with:
+ name: iris-gui-x64
+ path: target/x86_64-apple-darwin/release/iris-gui
+
+ # ── Package and submit ───────────────────────────────────────────────────────
+
+ package-and-submit:
+ name: Package and Submit
+ needs: [preflight, build]
+ if: ${{ github.event.inputs.preflight_only != 'true' }}
+ runs-on: ${{ github.event.inputs.runner_image || 'macos-26' }}
+ env:
+ HAS_CERTS: ${{ secrets.APP_STORE_APP_CERT != '' }}
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ github.event.inputs.build_branch || 'main' }}
+
+ # Reuse the version stamped by the preflight job so the binary's baked-in
+ # APP_VERSION and the Info.plist CFBundleVersion are identical (re-running
+ # `date` here would drift by the build duration).
+ - name: Generate version
+ id: version
+ run: |
+ echo "version=${{ needs.preflight.outputs.version }}" >> $GITHUB_OUTPUT
+ echo "bundle_version=${{ needs.preflight.outputs.bundle_version }}" >> $GITHUB_OUTPUT
+
+ - name: Download binaries
+ uses: actions/download-artifact@v8
+ with:
+ path: bins
+
+ - name: Create universal binary with lipo
+ run: |
+ chmod +x bins/iris-gui-arm64/iris-gui bins/iris-gui-x64/iris-gui
+ lipo -create -output iris-gui-universal \
+ bins/iris-gui-arm64/iris-gui \
+ bins/iris-gui-x64/iris-gui
+ lipo -info iris-gui-universal
+
+ - name: Import certificates
+ env:
+ APP_STORE_APP_CERT: ${{ secrets.APP_STORE_APP_CERT }}
+ APP_STORE_APP_CERT_PWD: ${{ secrets.APP_STORE_APP_CERT_PWD }}
+ APP_STORE_INSTALLER_CERT: ${{ secrets.APP_STORE_INSTALLER_CERT }}
+ APP_STORE_INSTALLER_CERT_PWD: ${{ secrets.APP_STORE_INSTALLER_CERT_PWD }}
+ run: |
+ KEYCHAIN_PATH=$RUNNER_TEMP/appstore.keychain-db
+ KEYCHAIN_PWD=$(openssl rand -base64 32)
+ security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"
+ security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
+ security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"
+
+ import_cert() {
+ local b64="$1" pwd="$2"
+ local path=$RUNNER_TEMP/cert_$RANDOM.p12
+ echo "$b64" | base64 --decode > "$path"
+ security import "$path" -P "$pwd" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
+ rm "$path"
+ }
+
+ import_cert "$APP_STORE_APP_CERT" "$APP_STORE_APP_CERT_PWD"
+ import_cert "$APP_STORE_INSTALLER_CERT" "$APP_STORE_INSTALLER_CERT_PWD"
+
+ security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"
+ security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db
+
+ - name: Resolve signing identities
+ id: identities
+ run: |
+ KEYCHAIN=$RUNNER_TEMP/appstore.keychain-db
+ # List every identity (cert+key pair) that imported. Both the App and
+ # Installer distribution identities must appear here.
+ echo "Identities in keychain:"
+ security find-identity "$KEYCHAIN" || true
+
+ APP_ID=$(security find-identity -v -p codesigning "$KEYCHAIN" \
+ | grep "3rd Party Mac Developer Application" | head -1 \
+ | sed 's/.*"\(.*\)".*/\1/')
+ # The installer (Mac Installer Distribution) cert isn't valid for the
+ # codesigning policy, so use the unfiltered list. It only shows up if
+ # the .p12 contained the certificate AND its private key.
+ INSTALLER_ID=$(security find-identity "$KEYCHAIN" \
+ | grep "3rd Party Mac Developer Installer" | head -1 \
+ | sed 's/.*"\(.*\)".*/\1/')
+
+ if [ -z "$APP_ID" ]; then
+ echo "::error::No '3rd Party Mac Developer Application' identity in the keychain. APP_STORE_APP_CERT must be a base64 .p12 containing that certificate and its private key."
+ exit 1
+ fi
+ if [ -z "$INSTALLER_ID" ]; then
+ echo "::error::No '3rd Party Mac Developer Installer' identity in the keychain. APP_STORE_INSTALLER_CERT must be a base64 .p12 that contains the Mac Installer Distribution certificate AND its private key — export the *identity* from Keychain Access (the row with a disclosure triangle hiding a private key), not just the certificate. Also confirm APP_STORE_INSTALLER_CERT_PWD matches the export password."
+ exit 1
+ fi
+
+ echo "app_identity=$APP_ID" >> $GITHUB_OUTPUT
+ echo "installer_identity=$INSTALLER_ID" >> $GITHUB_OUTPUT
+ echo "App identity: $APP_ID"
+ echo "Installer identity: $INSTALLER_ID"
+
+ - name: Embed provisioning profile and build .app bundle
+ env:
+ APP_STORE_PROVISIONING_PROFILE: ${{ secrets.APP_STORE_PROVISIONING_PROFILE }}
+ VER: ${{ steps.version.outputs.version }}
+ BUNDLE_VER: ${{ steps.version.outputs.bundle_version }}
+ run: |
+ BUNDLE="IRIS.app"
+ mkdir -p "${BUNDLE}/Contents/MacOS" "${BUNDLE}/Contents/Resources"
+
+ cp iris-gui-universal "${BUNDLE}/Contents/MacOS/iris-gui"
+ chmod +x "${BUNDLE}/Contents/MacOS/iris-gui"
+ cp "iris-gui/assets/icons/icon.icns" "${BUNDLE}/Contents/Resources/AppIcon.icns"
+
+ # Embed provisioning profile (required for App Store)
+ echo "$APP_STORE_PROVISIONING_PROFILE" | base64 --decode \
+ > "${BUNDLE}/Contents/embedded.provisionprofile"
+
+ cat > "${BUNDLE}/Contents/Info.plist" << PLIST
+
+
+
+
+ CFBundleNameIRIS
+ CFBundleDisplayNameIRIS
+ CFBundleIdentifier${{ env.BUNDLE_ID }}
+ CFBundleVersion${BUNDLE_VER}
+ CFBundleShortVersionString${BUNDLE_VER}
+ CFBundleExecutableiris-gui
+ CFBundleIconFileAppIcon.icns
+ CFBundlePackageTypeAPPL
+ NSHighResolutionCapable
+ LSMinimumSystemVersion10.13
+ LSApplicationCategoryTypepublic.app-category.developer-tools
+ NSCameraUsageDescriptionProvides the IndyCam video input for SGI Indy emulation (VINO device).
+
+ ITSAppUsesNonExemptEncryption
+
+
+ PLIST
+
+ - name: Sign .app with entitlements
+ env:
+ IDENTITY: ${{ steps.identities.outputs.app_identity }}
+ run: |
+ codesign --force --options runtime \
+ --entitlements installer/iris-gui.entitlements \
+ --sign "$IDENTITY" \
+ IRIS.app
+ codesign --verify --deep --strict IRIS.app
+ echo "Signature verified."
+
+ # Second half of the guideline-2.5.1 gate (the first runs on the thin
+ # binaries in the build job). Cheap, and it covers the universal binary as
+ # actually shipped — a wrong artifact download or a stale lipo input can't
+ # sneak the private symbol past the earlier check. `nm -u` on a fat binary
+ # lists both slices' undefined symbols.
+ - name: Re-check the signed bundle for private SkyLight symbols
+ run: |
+ BIN="IRIS.app/Contents/MacOS/iris-gui"
+ undef=$(nm -u "$BIN") || { echo "::error::nm -u failed on $BIN"; exit 1; }
+ if ! printf '%s\n' "$undef" | grep -q '_CGShieldingWindowLevel'; then
+ echo "::error::$BIN: the public _CGShieldingWindowLevel control import is missing, so this check can't be trusted."
+ exit 1
+ fi
+ if printf '%s\n' "$undef" | grep -qE '_CGSSetWindowBackgroundBlurRadius|_CGSMainConnectionID'; then
+ echo "::error::Private SkyLight API symbol found in the shipping bundle $BIN — App Store guideline 2.5.1 will reject this. See rules/macos/appstore-private-api.md."
+ printf '%s\n' "$undef" | grep -E '_CGS' || true
+ exit 1
+ fi
+ echo "OK: no private SkyLight symbols in $BIN"
+
+ - name: Package with productbuild
+ env:
+ IDENTITY: ${{ steps.identities.outputs.installer_identity }}
+ VER: ${{ steps.version.outputs.version }}
+ run: |
+ productbuild \
+ --component IRIS.app /Applications \
+ --sign "$IDENTITY" \
+ "IRIS-appstore-${VER}.pkg"
+
+ - name: Validate package
+ env:
+ VER: ${{ steps.version.outputs.version }}
+ APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
+ APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
+ APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
+ run: |
+ # altool locates the key by convention: AuthKey_.p8 under
+ # ~/.appstoreconnect/private_keys (it has no --apiPrivateKeyPath flag).
+ mkdir -p "$HOME/.appstoreconnect/private_keys"
+ KEYF="$HOME/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8"
+ echo "$APP_STORE_CONNECT_PRIVATE_KEY" | base64 --decode > "$KEYF"
+ xcrun altool --validate-app \
+ --type osx \
+ --file "IRIS-appstore-${VER}.pkg" \
+ --apiKey "$APP_STORE_CONNECT_KEY_ID" \
+ --apiIssuer "$APP_STORE_CONNECT_ISSUER_ID"
+ rm -f "$KEYF"
+
+ - name: Submit to App Store Connect
+ if: ${{ github.event.inputs.validate_only != 'true' }}
+ env:
+ VER: ${{ steps.version.outputs.version }}
+ APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
+ APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
+ APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
+ run: |
+ mkdir -p "$HOME/.appstoreconnect/private_keys"
+ KEYF="$HOME/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8"
+ echo "$APP_STORE_CONNECT_PRIVATE_KEY" | base64 --decode > "$KEYF"
+ xcrun altool --upload-app \
+ --type osx \
+ --file "IRIS-appstore-${VER}.pkg" \
+ --apiKey "$APP_STORE_CONNECT_KEY_ID" \
+ --apiIssuer "$APP_STORE_CONNECT_ISSUER_ID"
+ rm -f "$KEYF"
+ echo "Submitted. Check App Store Connect for processing status."
+
+ - name: Upload .pkg as artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: IRIS-appstore-${{ steps.version.outputs.version }}
+ path: IRIS-appstore-${{ steps.version.outputs.version }}.pkg
+
+ - name: Cleanup keychain
+ if: always()
+ run: |
+ KEYCHAIN_PATH=$RUNNER_TEMP/appstore.keychain-db
+ [ -f "$KEYCHAIN_PATH" ] && security delete-keychain "$KEYCHAIN_PATH" || true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..c228e53
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,1047 @@
+# Build and release pipeline for IRIS.
+#
+# Manual trigger only (workflow_dispatch), in one of two modes:
+#
+# publish_release UNCHECKED (the default)
+# Builds every artifact on every platform and verifies the release contents,
+# then stops. No tag, no release. Use this to prove a change builds
+# everywhere; the artifacts are attached to the run.
+#
+# publish_release CHECKED
+# The same build, then tags v (UTC, generated at dispatch)
+# at the built commit and publishes a release carrying the downloads table
+# generated below.
+#
+# Builds check out the branch the run was dispatched from (github.ref_name), so
+# dispatching from a topic branch tests that branch and dispatching from main
+# builds main. Set build_branch to override; a published tag follows the commit
+# that was actually built.
+#
+# ── What ships ────────────────────────────────────────────────────────────────
+#
+# One build variant per platform/arch: lightning + rex-jit + camera + chd.
+#
+# It is a lightning build (interpreter with breakpoint checks and the traceback
+# buffer stripped — no interactive debugging) and carries the REX3 draw-shader
+# JIT. It does not carry the v1 MIPS JIT (`jit`) or the N64 dev board
+# (`ultra64`); those are source-build opt-ins now (see iris-gui/Cargo.toml). The
+# former "standard" (non-lightning), `pcap`, `-jitv2`, and separate `-r5000`
+# variants and the chd_extract tool are no longer released — all of them remain
+# buildable from source.
+#
+# Each platform ships iris-gui (installer + portable on Windows, .dmg on macOS,
+# AppImage/deb/rpm/pkg on Linux, tarball on riscv64) and a Core tarball holding
+# the `iris` CLI + `iris-ci` automation client.
+#
+# Optional repo secrets for macOS code signing + notarization:
+# MACOS_CERTIFICATE base64-encoded Developer ID Application .p12 certificate
+# MACOS_CERTIFICATE_PWD password for the .p12 file
+# MACOS_NOTARIZE_APPLE_ID your Apple ID email
+# MACOS_NOTARIZE_PASSWORD app-specific password from appleid.apple.com
+# MACOS_TEAM_ID 10-character Apple Developer Team ID
+# Without these, builds use ad-hoc signing and are not notarized (Gatekeeper will warn).
+
+name: Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ publish_release:
+ # Unchecked: build every artifact and verify the release contents, but
+ # create no tag and no release — a dry run that still surfaces breakage.
+ # Checked: same build, then tag v and publish to Releases.
+ description: "Publish a release (tags v and posts to Releases). Leave unchecked to build artifacts only."
+ type: boolean
+ required: false
+ default: false
+ build_branch:
+ description: Override branch to check out (leave empty to build the branch this run was dispatched from)
+ required: false
+ default: ''
+ runner_image:
+ # macOS runner image for build-macos (both arches build on this one
+ # runner). Pinned rather than `macos-latest` so a run is deterministic
+ # during GitHub's macos-15 → macos-26 migration, where `macos-latest`
+ # resolves to either image unpredictably. Override here to test another
+ # image without editing the workflow. Same knob as appstore.yml.
+ description: "macOS runner image (default macos-26; override with macos-latest/macos-15)"
+ required: false
+ default: macos-26
+
+permissions:
+ contents: write
+
+env:
+ CARGO_TERM_COLOR: always
+ # Pinned quick-sharun + appimagetool versions (same as rusty-backup).
+ ANYLINUX_REF: 2affcd69e3b3fccab4507dbdbaed5d1a04bedfa9
+ APPIMAGETOOL_VERSION: "0.3.0"
+
+jobs:
+ # ── Version ──────────────────────────────────────────────────────────────────
+
+ generate-version:
+ name: Generate Version
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.version.outputs.version }}
+ tag: ${{ steps.version.outputs.tag }}
+ sha: ${{ steps.version.outputs.sha }}
+ steps:
+ # Resolve the commit the build legs will check out so a published release
+ # tags what was actually built. Without this the tag lands on the ref the
+ # run was dispatched from, which is the wrong commit whenever build_branch
+ # is set.
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ inputs.build_branch || github.ref_name }}
+
+ - name: Generate version from date
+ id: version
+ run: |
+ VERSION=$(date -u +"%Y-%m-%d-%H-%M")
+ SHA=$(git rev-parse HEAD)
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "tag=v$VERSION" >> $GITHUB_OUTPUT
+ echo "sha=$SHA" >> $GITHUB_OUTPUT
+ echo "Generated version: $VERSION (tag v$VERSION at $SHA)"
+
+ # NOTE: use `inputs.publish_release` (a real boolean), never
+ # `github.event.inputs.publish_release` — that is the STRING "false",
+ # which is truthy in an `if:` and would publish on every run.
+ - name: Announce mode
+ run: |
+ if [ "${{ inputs.publish_release }}" = "true" ]; then
+ echo "### Release run - will publish \`v${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "### Build-only run - no tag, no release" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "Artifacts are attached to this run. Re-run with **Publish a release** checked to tag and publish." >> $GITHUB_STEP_SUMMARY
+ fi
+
+ # ── Windows ──────────────────────────────────────────────────────────────────
+ #
+ # x64 + arm64: iris-gui (installer + portable, per CPU variant) and the Core
+ # zip (iris + iris-ci). arm64 builds natively on the windows-11-arm runner.
+ # (32-bit x86 dropped — earlier libchdman-rs had no i686 build; it does now, but
+ # we don't ship 32-bit.) libchdman-rs ≥ 0.288.8 ships aarch64-pc-windows-msvc
+ # prebuilts, so CHD works on arm64.
+
+ build-windows:
+ name: Build Windows (${{ matrix.arch }})
+ needs: generate-version
+ runs-on: ${{ matrix.runs-on }}
+ env:
+ RELEASE_VERSION: ${{ needs.generate-version.outputs.version }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - target: x86_64-pc-windows-msvc
+ arch: x64
+ runs-on: windows-latest
+ cli_features: "chd,camera,rex-jit,lightning"
+ - target: aarch64-pc-windows-msvc
+ arch: arm64
+ runs-on: windows-11-arm
+ cli_features: "chd,camera,rex-jit,lightning"
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ inputs.build_branch || github.ref_name }}
+
+ - name: Install Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: ${{ matrix.target }}
+
+ - name: Cache cargo
+ uses: actions/cache@v5
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Create combined license (for installer)
+ shell: bash
+ run: |
+ printf '%s\n\n\n---\n\n\n' "$(cat LICENSE)" > COMBINED-LICENSE.txt
+ cat LICENSE-libchdman-rs.txt >> COMBINED-LICENSE.txt
+
+ - name: Install Inno Setup
+ shell: pwsh
+ run: choco install innosetup --no-progress -y
+
+ # ── iris-gui (x64 + arm64) ──────────────────────────────────────────────
+
+ - name: Build iris-gui
+ run: cargo build --release --target ${{ matrix.target }} -p iris-gui --features iris/lightning,bundled
+
+ - name: Build installer
+ shell: pwsh
+ run: |
+ $iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
+ if (-not (Test-Path $iscc)) { throw "ISCC.exe not found" }
+ $ver = "${{ needs.generate-version.outputs.version }}"
+ $src = "$env:GITHUB_WORKSPACE\target\${{ matrix.target }}\release"
+ $assets = "$env:GITHUB_WORKSPACE\iris-gui\assets\icons"
+ $lic = "$env:GITHUB_WORKSPACE\COMBINED-LICENSE.txt"
+ & $iscc "/DMyAppVersion=$ver" "/DTargetArch=${{ matrix.arch }}" "/DSourceDir=$src" "/DAssetsDir=$assets" `
+ "/DLicenseFile=$lic" "/FIRIS-Setup-windows-${{ matrix.arch }}-$ver" `
+ installer\iris-gui.iss
+ if ($LASTEXITCODE -ne 0) { throw "iscc failed" }
+
+ - name: Package iris-gui (portable)
+ shell: bash
+ run: |
+ VER="${{ needs.generate-version.outputs.version }}"
+ 7z a "IRIS-gui-windows-${{ matrix.arch }}-${VER}.zip" \
+ "target/${{ matrix.target }}/release/iris-gui.exe" \
+ "iris-gui/assets/icons/icon.ico" \
+ LICENSE LICENSE-libchdman-rs.txt
+
+ # ── Core: iris CLI + iris-ci ────────────────────────────────────────────
+
+ - name: Build iris CLI
+ run: cargo build --release --bin iris --target ${{ matrix.target }} --features ${{ matrix.cli_features }}
+
+ - name: Build iris-ci
+ run: cargo build --release --bin iris-ci --target ${{ matrix.target }} --features ${{ matrix.cli_features }}
+
+ # Staging into a temp dir and zipping from inside it keeps the archive flat
+ # (iris.exe/iris-ci.exe/licenses, no target/... paths).
+ - name: Package Core
+ shell: bash
+ run: |
+ VER="${{ needs.generate-version.outputs.version }}"
+ rm -rf _pkg_tmp && mkdir -p _pkg_tmp
+ cp "target/${{ matrix.target }}/release/iris.exe" _pkg_tmp/
+ cp "target/${{ matrix.target }}/release/iris-ci.exe" _pkg_tmp/
+ cp LICENSE LICENSE-libchdman-rs.txt _pkg_tmp/
+ (cd _pkg_tmp && 7z a "../IRIS-cli-windows-${{ matrix.arch }}-${VER}.zip" *)
+ rm -rf _pkg_tmp
+
+ - name: Verify CLI archive contents
+ shell: bash
+ run: |
+ set -e
+ for f in IRIS-cli-*.zip; do
+ listing=$(7z l -ba -slt "$f" | sed -n 's/^Path = //p')
+ for want in iris.exe iris-ci.exe LICENSE LICENSE-libchdman-rs.txt; do
+ echo "$listing" | grep -qx "$want" \
+ || { echo "::error::${f} is missing flat member '${want}'"; exit 1; }
+ done
+ done
+
+ # ── Upload ──────────────────────────────────────────────────────────────
+
+ - name: Upload iris-gui portable
+ uses: actions/upload-artifact@v7
+ with:
+ name: IRIS-gui-windows-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}
+ path: IRIS-gui-windows-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}.zip
+
+ - name: Upload iris-gui installer
+ uses: actions/upload-artifact@v7
+ with:
+ name: IRIS-Setup-windows-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}
+ path: installer\Output\IRIS-Setup-windows-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}.exe
+
+ - name: Upload Core
+ uses: actions/upload-artifact@v7
+ with:
+ name: IRIS-cli-windows-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}
+ path: IRIS-cli-windows-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}.zip
+
+ # ── macOS ────────────────────────────────────────────────────────────────────
+
+ build-macos:
+ name: Build macOS (${{ matrix.arch }})
+ needs: generate-version
+ runs-on: ${{ github.event.inputs.runner_image || 'macos-26' }}
+ # Guard against a hung actions/cache step sitting for the 6h default. Three
+ # compiles (GUI + iris + iris-ci) and three notarytool submissions per leg
+ # (the DMG, the iris CLI, iris-ci — each `--wait`); the ceiling leaves room
+ # for an Apple-side queue spike without masking a hang.
+ timeout-minutes: 120
+ env:
+ HAS_SIGNING_CERT: ${{ secrets.MACOS_CERTIFICATE != '' }}
+ RELEASE_VERSION: ${{ needs.generate-version.outputs.version }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - target: aarch64-apple-darwin
+ arch: arm64
+ - target: x86_64-apple-darwin
+ arch: x64
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ inputs.build_branch || github.ref_name }}
+
+ - name: Add the target to the pinned (nightly) toolchain
+ run: |
+ # rust-toolchain.toml pins nightly, so cargo uses THAT — not stable, so
+ # installing a target via dtolnay/rust-toolchain@stable wouldn't help.
+ # `rustup show` (run in the checked-out repo) auto-installs the pinned
+ # toolchain; then add this leg's target to it. The host arm64 std comes
+ # with the toolchain, but x86_64 must be added explicitly now that it is
+ # cross-compiled here — without it rustc can't find core/std. Mirrors
+ # appstore.yml and the riscv64 job below.
+ rustup show
+ rustup target add ${{ matrix.target }}
+
+ - name: Cache cargo
+ uses: actions/cache@v5
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ # ── Build all binaries ──────────────────────────────────────────────────
+
+ - name: Build all binaries
+ run: |
+ cargo build --release --target ${{ matrix.target }} -p iris-gui --features iris/lightning,bundled
+ cargo build --release --bin iris --target ${{ matrix.target }} --features chd,camera,rex-jit,lightning
+ cargo build --release --bin iris-ci --target ${{ matrix.target }} --features chd,camera,rex-jit,lightning
+
+ # ── Code signing setup ──────────────────────────────────────────────────
+
+ - name: Import signing certificate
+ if: ${{ env.HAS_SIGNING_CERT == 'true' }}
+ env:
+ MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+ MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
+ run: |
+ KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
+ KEYCHAIN_PWD=$(openssl rand -base64 32)
+ security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"
+ security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
+ security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"
+ CERT_PATH=$RUNNER_TEMP/certificate.p12
+ echo "$MACOS_CERTIFICATE" | base64 --decode > "$CERT_PATH"
+ security import "$CERT_PATH" -P "$MACOS_CERTIFICATE_PWD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
+ rm "$CERT_PATH"
+ security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"
+ security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db
+
+ - name: Resolve signing identity
+ id: signing
+ env:
+ MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+ run: |
+ if [ -n "$MACOS_CERTIFICATE" ]; then
+ ID=$(security find-identity -v -p codesigning "$RUNNER_TEMP/app-signing.keychain-db" \
+ | grep "Developer ID Application" | head -1 | sed 's/.*"\(.*\)".*/\1/')
+ echo "identity=$ID" >> $GITHUB_OUTPUT
+ else
+ echo "identity=-" >> $GITHUB_OUTPUT
+ fi
+
+ # ── iris-gui: app bundles + DMGs ─────────────────────────────────────────
+
+ - name: Create iris-gui app bundles and DMGs
+ env:
+ VER: ${{ needs.generate-version.outputs.version }}
+ IDENTITY: ${{ steps.signing.outputs.identity }}
+ MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+ MACOS_TEAM_ID: ${{ secrets.MACOS_TEAM_ID }}
+ MACOS_NOTARIZE_APPLE_ID: ${{ secrets.MACOS_NOTARIZE_APPLE_ID }}
+ MACOS_NOTARIZE_PASSWORD: ${{ secrets.MACOS_NOTARIZE_PASSWORD }}
+ run: |
+ set -e
+ SRC="target/${{ matrix.target }}/release"
+ ARCH="${{ matrix.arch }}"
+
+ make_dmg() {
+ local bin_src="$1" # path to the built iris-gui binary
+ local dmg_name="IRIS-gui-macos-${ARCH}-${VER}.dmg"
+ local bundle="IRIS.app"
+
+ rm -rf "$bundle" dmg-staging
+ mkdir -p "${bundle}/Contents/MacOS" "${bundle}/Contents/Resources"
+ cp "$bin_src" "${bundle}/Contents/MacOS/iris-gui"
+ chmod +x "${bundle}/Contents/MacOS/iris-gui"
+ cp "iris-gui/assets/icons/icon.icns" "${bundle}/Contents/Resources/AppIcon.icns"
+ cp LICENSE "${bundle}/Contents/Resources/LICENSE"
+ cp LICENSE-libchdman-rs.txt "${bundle}/Contents/Resources/LICENSE-libchdman-rs.txt"
+
+ cat > "${bundle}/Contents/Info.plist" << PLIST
+
+
+
+
+ CFBundleNameIRIS
+ CFBundleDisplayNameIRIS
+ CFBundleIdentifierio.github.danifunker.iris
+ CFBundleVersion${VER}
+ CFBundleShortVersionString${VER}
+ CFBundleExecutableiris-gui
+ CFBundleIconFileAppIcon.icns
+ CFBundlePackageTypeAPPL
+ NSHighResolutionCapable
+ LSMinimumSystemVersion10.13
+ LSApplicationCategoryTypepublic.app-category.developer-tools
+ NSCameraUsageDescriptionProvides the IndyCam video input for SGI Indy emulation (VINO device).
+
+
+ PLIST
+
+ if [ "$IDENTITY" != "-" ]; then
+ # --entitlements grants allow-unsigned-executable-memory so the
+ # hardened runtime doesn't kill Cranelift's mmap+mprotect JIT pages
+ # (the REX3 draw-shader JIT). Permitted for Developer ID/notarization;
+ # the App Store build can't use it and runs interpreter-only instead.
+ codesign --force --options runtime \
+ --entitlements installer/iris-gui-notarized.entitlements \
+ --sign "$IDENTITY" "${bundle}"
+ else
+ codesign --force --deep --sign - "${bundle}"
+ fi
+
+ mkdir -p dmg-staging
+ cp -R "${bundle}" dmg-staging/
+ ln -sf /Applications dmg-staging/Applications
+ hdiutil create -volname "IRIS" -srcfolder dmg-staging -ov -format UDZO "${dmg_name}"
+
+ if [ "$IDENTITY" != "-" ]; then
+ codesign --force --sign "$IDENTITY" "${dmg_name}"
+ fi
+
+ if [ -n "$MACOS_NOTARIZE_APPLE_ID" ] && [ -n "$MACOS_TEAM_ID" ]; then
+ xcrun notarytool submit "${dmg_name}" \
+ --apple-id "$MACOS_NOTARIZE_APPLE_ID" \
+ --password "$MACOS_NOTARIZE_PASSWORD" \
+ --team-id "$MACOS_TEAM_ID" \
+ --wait
+ xcrun stapler staple "${dmg_name}"
+ fi
+ }
+
+ make_dmg "${SRC}/iris-gui"
+
+ # ── Core (iris + iris-ci): sign + package ───────────────────────────────
+
+ - name: Sign and package Core binaries
+ env:
+ VER: ${{ needs.generate-version.outputs.version }}
+ IDENTITY: ${{ steps.signing.outputs.identity }}
+ MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
+ MACOS_TEAM_ID: ${{ secrets.MACOS_TEAM_ID }}
+ MACOS_NOTARIZE_APPLE_ID: ${{ secrets.MACOS_NOTARIZE_APPLE_ID }}
+ MACOS_NOTARIZE_PASSWORD: ${{ secrets.MACOS_NOTARIZE_PASSWORD }}
+ run: |
+ set -e
+ SRC="target/${{ matrix.target }}/release"
+ ARCH="${{ matrix.arch }}"
+
+ sign_notarize() {
+ local bin_src="$1"
+ local tag="$2" # unique basename for the notarize zip
+
+ if [ "$IDENTITY" != "-" ]; then
+ # See the GUI bundle signing above: the JIT entitlement keeps the
+ # notarized CLI binaries (built with rex-jit) from being killed by
+ # the hardened runtime on their first JITed REX3 draw.
+ codesign --force --options runtime --timestamp \
+ --entitlements installer/iris-gui-notarized.entitlements \
+ --sign "$IDENTITY" "$bin_src"
+ else
+ codesign --force --sign - "$bin_src"
+ fi
+
+ if [ -n "$MACOS_NOTARIZE_APPLE_ID" ] && [ -n "$MACOS_TEAM_ID" ]; then
+ local zip_tmp="${tag}-notarize.zip"
+ ditto -c -k "$bin_src" "$zip_tmp"
+ xcrun notarytool submit "$zip_tmp" \
+ --apple-id "$MACOS_NOTARIZE_APPLE_ID" \
+ --password "$MACOS_NOTARIZE_PASSWORD" \
+ --team-id "$MACOS_TEAM_ID" \
+ --wait
+ rm -f "$zip_tmp"
+ fi
+ }
+
+ sign_notarize_package() {
+ local bin_src="$1"
+ local out_name="$2"
+
+ sign_notarize "$bin_src" "${out_name%.tar.gz}"
+
+ mkdir -p _pkg_tmp
+ cp "$bin_src" _pkg_tmp/iris
+ cp "${SRC}/iris-ci" _pkg_tmp/iris-ci
+ cp LICENSE "_pkg_tmp/"
+ cp LICENSE-libchdman-rs.txt "_pkg_tmp/"
+ tar -C _pkg_tmp -czf "${out_name}" iris iris-ci LICENSE LICENSE-libchdman-rs.txt
+ rm -rf _pkg_tmp
+ }
+
+ # iris-ci ships in the Core tarball; sign + notarize it first, then the
+ # pkg call below just stages the already-signed binary.
+ sign_notarize "${SRC}/iris-ci" "iris-ci-macos-${ARCH}-${VER}"
+
+ sign_notarize_package "${SRC}/iris" "IRIS-cli-macos-${ARCH}-${VER}.tar.gz"
+
+ - name: Verify CLI archive contents
+ run: |
+ set -e
+ for f in IRIS-cli-*.tar.gz; do
+ listing=$(tar tzf "$f")
+ for want in iris iris-ci LICENSE LICENSE-libchdman-rs.txt; do
+ echo "$listing" | grep -qx "$want" \
+ || { echo "::error::${f} is missing flat member '${want}'"; exit 1; }
+ done
+ done
+
+ - name: Cleanup keychain
+ if: always()
+ run: |
+ KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
+ [ -f "$KEYCHAIN_PATH" ] && security delete-keychain "$KEYCHAIN_PATH" || true
+
+ # ── Upload ──────────────────────────────────────────────────────────────
+
+ - name: Upload iris-gui DMG
+ uses: actions/upload-artifact@v7
+ with:
+ name: IRIS-gui-macos-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}
+ path: IRIS-gui-macos-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}.dmg
+
+ - name: Upload Core
+ uses: actions/upload-artifact@v7
+ with:
+ name: IRIS-cli-macos-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}
+ path: IRIS-cli-macos-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}.tar.gz
+
+ # ── Linux AppImage ────────────────────────────────────────────────────────────
+ #
+ # Arch Linux container + quick-sharun → self-contained AppImage for iris-gui.
+
+ build-linux-appimage:
+ name: Build Linux AppImage (${{ matrix.arch }})
+ needs: generate-version
+ runs-on: ${{ matrix.runs-on }}
+ container: ghcr.io/pkgforge-dev/archlinux:latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - target: x86_64-unknown-linux-gnu
+ arch: x64
+ sharun_arch: x86_64
+ runs-on: ubuntu-24.04
+ - target: aarch64-unknown-linux-gnu
+ arch: arm64
+ sharun_arch: aarch64
+ runs-on: ubuntu-24.04-arm
+ env:
+ RELEASE_VERSION: ${{ needs.generate-version.outputs.version }}
+ steps:
+ - name: Install build prerequisites
+ run: |
+ pacman -Syu --noconfirm \
+ base-devel cmake rust \
+ clang \
+ alsa-lib \
+ gtk3 \
+ openssl pkgconf \
+ v4l-utils \
+ wget curl git \
+ libxcb libxcursor libxi libxkbcommon libxkbcommon-x11 libxrandr libxtst \
+ wayland wayland-protocols \
+ mesa vulkan-icd-loader \
+ xorg-server-xvfb strace dbus file patchelf \
+ desktop-file-utils shared-mime-info zsync
+
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ inputs.build_branch || github.ref_name }}
+
+ - name: Cache cargo
+ uses: actions/cache@v5
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-${{ matrix.arch }}-appimage-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Install debloated runtime packages
+ run: |
+ wget --retry-connrefused --tries=30 \
+ "https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/${ANYLINUX_REF}/useful-tools/get-debloated-pkgs.sh" \
+ -O ./get-debloated-pkgs.sh
+ chmod +x ./get-debloated-pkgs.sh
+ ./get-debloated-pkgs.sh --add-common --add-mesa --prefer-nano
+
+ - name: Download quick-sharun
+ run: |
+ wget --retry-connrefused --tries=30 \
+ "https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/${ANYLINUX_REF}/useful-tools/quick-sharun.sh" \
+ -O ./quick-sharun
+ chmod +x ./quick-sharun
+
+ - name: Stage shared AppDir inputs (icons + desktop)
+ run: |
+ install -Dm644 "iris-gui/assets/icons/icon-256.png" \
+ /usr/share/icons/hicolor/256x256/apps/iris-gui.png
+ install -d /usr/share/applications
+ cat > /usr/share/applications/iris-gui.desktop << 'EOF'
+ [Desktop Entry]
+ Type=Application
+ Name=IRIS
+ Comment=SGI Indy (MIPS R4400) emulator
+ Exec=iris-gui
+ Icon=iris-gui
+ Categories=Emulator;Game;
+ Terminal=false
+ EOF
+
+ - name: Build and bundle iris-gui
+ env:
+ VER: ${{ needs.generate-version.outputs.version }}
+ run: |
+ cargo build --release --target ${{ matrix.target }} -p iris-gui --features iris/lightning
+ install -Dm755 "target/${{ matrix.target }}/release/iris-gui" /usr/bin/iris-gui
+
+ OUT="IRIS-gui-linux-${{ matrix.arch }}-${VER}.AppImage"
+ export OUTPATH="$PWD/dist"
+ export OUTNAME="$OUT"
+ export VERSION="${VER}"
+ export ICON=/usr/share/icons/hicolor/256x256/apps/iris-gui.png
+ export DESKTOP=/usr/share/applications/iris-gui.desktop
+ export APPIMAGETOOL_LINK="https://github.com/pkgforge-dev/appimagetool/releases/download/${APPIMAGETOOL_VERSION}/appimagetool-${{ matrix.sharun_arch }}-linux"
+ mkdir -p "$OUTPATH"
+ ./quick-sharun /usr/bin/iris-gui
+ ./quick-sharun --make-appimage
+ ls -lh "$OUTPATH"
+
+ - name: Upload AppImage
+ uses: actions/upload-artifact@v7
+ with:
+ name: IRIS-appimage-linux-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}
+ path: |
+ dist/IRIS-gui-linux-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}.AppImage
+ dist/IRIS-gui-linux-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}.AppImage.zsync
+
+ # ── Linux packages + Core tarballs ────────────────────────────────────────────
+ #
+ # deb, rpm, Arch pkg for iris-gui; tar.gz for the Core (iris + iris-ci).
+
+ build-linux-packages:
+ name: Build Linux packages (${{ matrix.arch }})
+ needs: generate-version
+ runs-on: ${{ matrix.runs-on }}
+ env:
+ RELEASE_VERSION: ${{ needs.generate-version.outputs.version }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - target: x86_64-unknown-linux-gnu
+ arch: x64
+ runs-on: ubuntu-24.04
+ - target: aarch64-unknown-linux-gnu
+ arch: arm64
+ runs-on: ubuntu-24.04-arm
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ inputs.build_branch || github.ref_name }}
+
+ - name: Install Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: ${{ matrix.target }}
+
+ - name: Install build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ pkg-config \
+ clang libclang-dev \
+ libasound2-dev \
+ libgtk-3-dev \
+ libssl-dev \
+ libv4l-dev \
+ libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
+ libxkbcommon-dev \
+ libwayland-dev \
+ libgl-dev \
+ dpkg-dev liblzma-dev rpm libarchive-tools zstd
+
+ - name: Install cargo packaging tools
+ run: |
+ cargo install cargo-deb
+ cargo install cargo-generate-rpm
+
+ - name: Cache cargo
+ uses: actions/cache@v5
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-${{ matrix.target }}-packages-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ # ── iris-gui packages ───────────────────────────────────────────────────
+
+ - name: Build iris-gui
+ run: cargo build --release --target ${{ matrix.target }} -p iris-gui --features iris/lightning
+
+ - name: Package iris-gui (deb + rpm + arch)
+ run: |
+ VER="${{ needs.generate-version.outputs.version }}"
+ ARCH="${{ matrix.arch }}"
+ TARGET="${{ matrix.target }}"
+
+ cargo deb -p iris-gui --target "$TARGET" --no-build
+ mv "target/${TARGET}/debian/"*.deb "IRIS-gui-linux-${ARCH}-${VER}.deb"
+
+ cargo generate-rpm -p iris-gui --target "$TARGET"
+ mv "target/${TARGET}/generate-rpm/"*.rpm "IRIS-gui-linux-${ARCH}-${VER}.rpm"
+
+ mkdir -p arch-pkg/usr/bin arch-pkg/usr/share/applications \
+ arch-pkg/usr/share/icons/hicolor/{16x16,32x32,48x48,64x64,128x128,256x256}/apps \
+ arch-pkg/usr/share/doc/iris-gui
+ cp "target/${TARGET}/release/iris-gui" arch-pkg/usr/bin/
+ chmod 755 arch-pkg/usr/bin/iris-gui
+ for size in 16 32 48 64 128 256; do
+ cp "iris-gui/assets/icons/icon-${size}.png" \
+ "arch-pkg/usr/share/icons/hicolor/${size}x${size}/apps/iris-gui.png"
+ done
+ cp iris-gui/iris-gui.desktop arch-pkg/usr/share/applications/
+ cp LICENSE arch-pkg/usr/share/doc/iris-gui/
+ cp LICENSE-libchdman-rs.txt arch-pkg/usr/share/doc/iris-gui/
+ (cd arch-pkg && tar -czf "../IRIS-gui-linux-${ARCH}-${VER}.pkg.tar.zst" usr)
+ rm -rf arch-pkg
+
+ # ── Core tarball ────────────────────────────────────────────────────────
+
+ - name: Build iris CLI and iris-ci
+ run: |
+ cargo build --release --bin iris --target ${{ matrix.target }} --features chd,camera,rex-jit,lightning
+ cargo build --release --bin iris-ci --target ${{ matrix.target }} --features chd,camera,rex-jit,lightning
+
+ - name: Package Core tarball
+ run: |
+ VER="${{ needs.generate-version.outputs.version }}"
+ ARCH="${{ matrix.arch }}"
+ SRC="target/${{ matrix.target }}/release"
+
+ pkg_cli() {
+ local src="$1" label="$2"
+ mkdir -p _tmp
+ cp "$src" _tmp/iris
+ cp "${SRC}/iris-ci" _tmp/iris-ci
+ cp LICENSE _tmp/
+ cp LICENSE-libchdman-rs.txt _tmp/
+ tar -C _tmp -czf "${label}" iris iris-ci LICENSE LICENSE-libchdman-rs.txt
+ rm -rf _tmp
+ }
+
+ pkg_cli "${SRC}/iris" "IRIS-cli-linux-${ARCH}-${VER}.tar.gz"
+
+ - name: Verify CLI archive contents
+ run: |
+ set -e
+ for f in IRIS-cli-*.tar.gz; do
+ listing=$(tar tzf "$f")
+ for want in iris iris-ci LICENSE LICENSE-libchdman-rs.txt; do
+ echo "$listing" | grep -qx "$want" \
+ || { echo "::error::${f} is missing flat member '${want}'"; exit 1; }
+ done
+ done
+
+ # ── Upload ──────────────────────────────────────────────────────────────
+
+ - name: Upload iris-gui packages
+ uses: actions/upload-artifact@v7
+ with:
+ name: IRIS-gui-packages-linux-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}
+ path: |
+ IRIS-gui-linux-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}.deb
+ IRIS-gui-linux-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}.rpm
+ IRIS-gui-linux-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}.pkg.tar.zst
+
+ - name: Upload Core
+ uses: actions/upload-artifact@v7
+ with:
+ name: IRIS-cli-linux-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}
+ path: IRIS-cli-linux-${{ matrix.arch }}-${{ needs.generate-version.outputs.version }}.tar.gz
+
+ # ── Linux riscv64 (cross, tarball-only) ───────────────────────────────────────
+ #
+ # No GitHub-hosted riscv64 runner exists, so we cross-compile from an x86_64
+ # Debian trixie host using multiarch: riscv64 is an official Debian 13 release
+ # arch, so the `:riscv64` -dev packages install cleanly alongside the host's.
+ # libchdman-rs ≥ 0.288.8 ships a riscv64gc-unknown-linux-gnu prebuilt, so CHD
+ # works here. Shipped as tarballs only (no deb/rpm/AppImage — the AppImage
+ # sharun tooling has no riscv64 path).
+ build-linux-riscv64:
+ name: Build Linux riscv64 (cross)
+ needs: generate-version
+ runs-on: ubuntu-24.04
+ container: debian:trixie
+ env:
+ RELEASE_VERSION: ${{ needs.generate-version.outputs.version }}
+ TARGET: riscv64gc-unknown-linux-gnu
+ ARCH: riscv64
+ # The container runs as root (euid home /root) but Actions sets
+ # HOME=/github/home; rustup hard-errors on that mismatch ("$HOME differs
+ # from euid-obtained home directory") and never installs the target std,
+ # so rustc reports "can't find crate for `core`". Force HOME to root's home
+ # and pin rustup/cargo to fixed paths so install and build agree.
+ HOME: /root
+ RUSTUP_HOME: /usr/local/rustup
+ CARGO_HOME: /usr/local/cargo
+ CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
+ CC_riscv64gc_unknown_linux_gnu: riscv64-linux-gnu-gcc
+ CXX_riscv64gc_unknown_linux_gnu: riscv64-linux-gnu-g++
+ AR_riscv64gc_unknown_linux_gnu: riscv64-linux-gnu-ar
+ PKG_CONFIG_ALLOW_CROSS: "1"
+ PKG_CONFIG_SYSROOT_DIR: "/"
+ PKG_CONFIG_PATH: /usr/lib/riscv64-linux-gnu/pkgconfig:/usr/share/pkgconfig
+ BINDGEN_EXTRA_CLANG_ARGS: "--target=riscv64-unknown-linux-gnu -I/usr/include/riscv64-linux-gnu"
+ steps:
+ - name: Install host toolchain + riscv64 cross sysroot
+ run: |
+ set -eux
+ dpkg --add-architecture riscv64
+ apt-get update
+ apt-get install -y --no-install-recommends \
+ ca-certificates curl xz-utils git pkg-config \
+ build-essential crossbuild-essential-riscv64 \
+ clang libclang-dev llvm \
+ libasound2-dev:riscv64 libgtk-3-dev:riscv64 libssl-dev:riscv64 \
+ libv4l-dev:riscv64 \
+ libxcb-render0-dev:riscv64 libxcb-shape0-dev:riscv64 libxcb-xfixes0-dev:riscv64 \
+ libxkbcommon-dev:riscv64 libwayland-dev:riscv64 libgl-dev:riscv64
+
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ inputs.build_branch || github.ref_name }}
+
+ - name: Install Rust + riscv64 std for the pinned toolchain
+ run: |
+ set -eux
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
+ | sh -s -- -y --default-toolchain none --no-modify-path
+ export PATH="$CARGO_HOME/bin:$PATH"
+ # rust-toolchain.toml pins nightly; `rustup show` (run in the checked-out
+ # repo) auto-installs it, then add the riscv64 std to THAT toolchain —
+ # adding it to stable wouldn't help since the build uses nightly.
+ rustup show
+ rustup target add "$TARGET"
+ echo "$CARGO_HOME/bin" >> "$GITHUB_PATH"
+
+ # iris-gui always pulls camera (v4l) via its iris dep, so libv4l-dev:riscv64
+ # is installed above for the bindgen cross.
+ - name: Build iris-gui (cross)
+ run: |
+ set -eux
+ cargo build --release --target "$TARGET" -p iris-gui --features iris/lightning,bundled
+
+ - name: Build iris CLI + iris-ci (cross)
+ run: |
+ set -eux
+ cargo build --release --bin iris --target "$TARGET" --features chd,camera,rex-jit,lightning
+ cargo build --release --bin iris-ci --target "$TARGET" --features chd,camera,rex-jit,lightning
+
+ - name: Package tarballs
+ run: |
+ set -eux
+ VER="${RELEASE_VERSION}"
+ REL="target/${TARGET}/release"
+
+ pkg_gui() { #