From 9f4f4f4a50879e594ca831d7e9e795ae7115c392 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+savagemanage@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:53:37 +0000 Subject: [PATCH 1/3] Ship Windows MSIs and macOS disk images so Redrob Launcher can install the engine --- .github/workflows/release.yml | 69 ++++++++++++++++++++++++++++-- installer/macos/make-dmg.sh | 67 +++++++++++++++++++++++++++++ installer/windows/redrob-code.wxs | 71 +++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 3 deletions(-) create mode 100755 installer/macos/make-dmg.sh create mode 100644 installer/windows/redrob-code.wxs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1af8c8b817..2792f73cc9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -155,6 +155,13 @@ jobs: APPLE_NOTARY_API_KEY_ID: ${{ secrets.APPLE_NOTARY_API_KEY_ID }} APPLE_NOTARY_API_ISSUER_ID: ${{ secrets.APPLE_NOTARY_API_ISSUER_ID }} steps: + # Only the installer scripts are needed from the repository (sparse: one directory). + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + sparse-checkout: | + installer + sparse-checkout-cone-mode: true + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: cli-unsigned @@ -238,10 +245,37 @@ jobs: (cd "$directory/bin" && zip -qr "../../${directory}.zip" .) done + # ── The disk images ─────────────────────────────────────────────────────────────────── + # + # Redrob Launcher installs macOS apps only from a DMG holding a signed `.app`, so the signed + # binary of each architecture is wrapped in `Redrob Code.app` (installer/macos/make-dmg.sh), + # the bundle signed, the image notarized and stapled. The launcher then links + # ~/.redrob/bin/redrob to the binary inside, where every app looks for the engine. + - name: Build, notarize and staple the disk images + env: + VERSION: ${{ needs.version.outputs.version }} + run: | + set -euo pipefail + for pair in "redrob-darwin-arm64:macos-aarch64" "redrob-darwin-x64:macos-x86_64"; do + directory="${pair%%:*}"; token="${pair##*:}" + dmg="$(bash installer/macos/make-dmg.sh "dist/$directory/bin/redrob" "$VERSION" "$token" dist)" + xcrun notarytool submit "$dmg" \ + --key "$RUNNER_TEMP/AuthKey.p8" \ + --key-id "$APPLE_NOTARY_API_KEY_ID" \ + --issuer "$APPLE_NOTARY_API_ISSUER_ID" \ + --wait + xcrun stapler staple "$dmg" + xcrun stapler validate "$dmg" + echo "$dmg notarized and stapled" + done + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: release-macos - path: dist/redrob-darwin-*.zip + path: | + dist/redrob-darwin-*.zip + dist/redrob-code-*-macos-*.dmg + if-no-files-found: error # `if: always()` so a failed notarization still takes the private key off the runner. - name: Delete the signing keychain @@ -266,7 +300,10 @@ jobs: dist/redrob-windows-arm64/bin/redrob.exe dist/redrob-windows-x64/bin/redrob.exe dist/redrob-windows-x64-baseline/bin/redrob.exe - REDROB_WINDOWS_INSTALLER: dist/redrob-code-x64-setup.exe + REDROB_WINDOWS_INSTALLER: >- + dist/redrob-code-x64-setup.exe + dist/redrob-code-${{ needs.version.outputs.version }}-windows-x64.msi + dist/redrob-code-${{ needs.version.outputs.version }}-windows-arm64.msi steps: # Only the installer definition is needed from the repository; the signing recipe is the # organization's (redrob-labs/.github/actions/sign-windows), not a copy kept here. Sparse, @@ -335,6 +372,29 @@ jobs: if (-not (Test-Path $setup)) { throw "the installer was not produced at $setup" } Write-Host "built $((Get-Item $setup).Length) bytes" + # The MSIs: the one package format Redrob Launcher installs on Windows (per user, into + # %LOCALAPPDATA%\Redrob\bin, PATH appended; installer/windows/redrob-code.wxs). One per + # architecture, each wrapping the binary signed above; signed themselves in the next step. + - name: Build the Windows MSIs + shell: pwsh + env: + VERSION: ${{ needs.version.outputs.version }} + run: | + $ErrorActionPreference = "Stop" + dotnet tool install --global wix --version 5.0.2 + # MSI versions are numeric: a prerelease suffix is dropped. + $msiVersion = ($env:VERSION -split '-')[0] + foreach ($pair in @(@('x64', 'redrob-windows-x64'), @('arm64', 'redrob-windows-arm64'))) { + $arch = $pair[0]; $dir = $pair[1] + $out = "${{ github.workspace }}\dist\redrob-code-$env:VERSION-windows-$arch.msi" + wix build "${{ github.workspace }}\installer\windows\redrob-code.wxs" -arch $arch ` + -d "Version=$msiVersion" -d "SourceBin=${{ github.workspace }}\dist\$dir\bin" -o $out + if ($LASTEXITCODE -ne 0) { throw "wix build ($arch) failed with $LASTEXITCODE" } + Remove-Item -Force -ErrorAction SilentlyContinue ([IO.Path]::ChangeExtension($out, '.wixpdb')) + if (-not (Test-Path $out)) { throw "the MSI was not produced at $out" } + Write-Host "built $out ($((Get-Item $out).Length) bytes)" + } + # A fresh login right before the second signing point, for the same reason as the first: # the installer build above takes minutes, and the credential must not be assumed to have # outlived it. @@ -345,7 +405,7 @@ jobs: tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - name: Sign the installer + - name: Sign the installers uses: redrob-labs/.github/actions/sign-windows@main with: files: ${{ env.REDROB_WINDOWS_INSTALLER }} @@ -371,6 +431,7 @@ jobs: path: | dist/redrob-windows-*.zip dist/redrob-code-x64-setup.exe + dist/redrob-code-*-windows-*.msi if-no-files-found: error # The assets only this repository knows how to shape: the Linux archives, the Redrob Launcher @@ -453,6 +514,8 @@ jobs: with: subject-path: | signed/redrob-*.zip + signed/redrob-code-*.msi + signed/redrob-code-*.dmg assets/redrob-*.zip assets/redrob-*.tar.gz diff --git a/installer/macos/make-dmg.sh b/installer/macos/make-dmg.sh new file mode 100755 index 0000000000..287b78f2be --- /dev/null +++ b/installer/macos/make-dmg.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Wrap a signed `redrob` binary in `Redrob Code.app` and ship it as a DMG. +# +# Redrob Launcher installs macOS apps from a DMG holding an `.app` whose Info.plist names the +# suite's bundle id (`ai.redrob.code`) and version, signed by the organization's Developer ID. The +# engine is a command-line program, so the bundle is a shell for it: `Contents/MacOS/redrob` is the +# very binary `sign-macos` already signed and notarized, and nothing else is added. The launcher +# then points `~/.redrob/bin/redrob` at that binary (its `engine_link`), which is where every +# Redrob app and the engine's own installer look. +# +# Usage: installer/macos/make-dmg.sh +# Env: APPLE_SIGNING_IDENTITY, APPLE_SIGNING_KEYCHAIN -- to sign the bundle (the binary inside +# keeps its own signature); unset = unsigned bundle, for a local look at the layout. +# Out: /redrob-code--.dmg (NOT notarized or stapled: the workflow does +# that, with the credentials it already holds). +set -euo pipefail + +binary="${1:?signed redrob binary}" +version="${2:?version, e.g. 0.6.6}" +token="${3:?macos-aarch64 or macos-x86_64}" +out="${4:?output directory}" +case "$token" in macos-aarch64|macos-x86_64) ;; *) echo "unknown macOS token: $token" >&2; exit 2 ;; esac + +name="Redrob Code" +stage="$(mktemp -d)" +trap 'rm -rf "$stage"' EXIT +app="$stage/$name.app" +mkdir -p "$app/Contents/MacOS" +cp "$binary" "$app/Contents/MacOS/redrob" +chmod 755 "$app/Contents/MacOS/redrob" +# A numeric CFBundleVersion: MSI-style, the prerelease part is dropped. +numeric="${version%%-*}" +cat > "$app/Contents/Info.plist" < + + + + CFBundleIdentifierai.redrob.code + CFBundleName$name + CFBundleDisplayName$name + CFBundleExecutableredrob + CFBundlePackageTypeAPPL + CFBundleShortVersionString$version + CFBundleVersion$numeric + CFBundleInfoDictionaryVersion6.0 + LSMinimumSystemVersion12.0 + LSUIElement + NSHumanReadableCopyrightCopyright Janghoon Lee (Redrob) and contributors + + +PLIST + +if [ -n "${APPLE_SIGNING_IDENTITY:-}" ]; then + codesign --force --options runtime --timestamp \ + --keychain "${APPLE_SIGNING_KEYCHAIN:?}" --sign "$APPLE_SIGNING_IDENTITY" "$app" + codesign --verify --strict --deep --verbose=2 "$app" +else + echo "APPLE_SIGNING_IDENTITY unset: bundle left unsigned" >&2 +fi + +mkdir -p "$out" +dmg="$out/redrob-code-$version-$token.dmg" +rm -f "$dmg" +# A plain compressed image of the bundle; the launcher mounts it and copies the .app out, and a +# person can drag it to Applications. +hdiutil create -quiet -format UDZO -volname "$name $version" -srcfolder "$app" "$dmg" +echo "$dmg" diff --git a/installer/windows/redrob-code.wxs b/installer/windows/redrob-code.wxs new file mode 100644 index 0000000000..b86e28b0e0 --- /dev/null +++ b/installer/windows/redrob-code.wxs @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 7275dc33f8d2b30405146cc972fdcf130e496e74 Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+savagemanage@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:58:38 +0000 Subject: [PATCH 2/3] engine-client: resolve the Windows installer's LOCALAPPDATA folder --- crates/engine-client/src/lib.rs | 15 +++++++++++++-- docs/LOCAL-ENGINE-API.md | 8 +++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/engine-client/src/lib.rs b/crates/engine-client/src/lib.rs index 32b1f28361..210207662e 100644 --- a/crates/engine-client/src/lib.rs +++ b/crates/engine-client/src/lib.rs @@ -176,18 +176,29 @@ pub fn current() -> Option { } /// Where the engine binary is, by the engine's own resolution order: `REDROB_CODE_BIN`, -/// `~/.redrob/bin/redrob`, then `redrob` on PATH. +/// `~/.redrob/bin/redrob`, then (Windows) the installer's folder `%LOCALAPPDATA%\Redrob\bin`, then +/// `redrob` on PATH. The Windows installers (the setup.exe and the MSI) put the engine in that +/// LOCALAPPDATA folder and append it to the user's PATH; a process started before the install +/// still has the old PATH, so the folder is tried by name. pub fn redrob_bin() -> PathBuf { if let Some(b) = std::env::var_os("REDROB_CODE_BIN").filter(|v| !v.is_empty()) { return PathBuf::from(b); } + let name = if cfg!(windows) { "redrob.exe" } else { "redrob" }; if let Some(home) = dirs::home_dir() { - let name = if cfg!(windows) { "redrob.exe" } else { "redrob" }; let p = home.join(".redrob").join("bin").join(name); if p.is_file() { return p; } } + if cfg!(windows) + && let Some(local) = dirs::data_local_dir() + { + let p = local.join("Redrob").join("bin").join(name); + if p.is_file() { + return p; + } + } PathBuf::from("redrob") } diff --git a/docs/LOCAL-ENGINE-API.md b/docs/LOCAL-ENGINE-API.md index 7a58cca0a3..f38cbeb0f2 100644 --- a/docs/LOCAL-ENGINE-API.md +++ b/docs/LOCAL-ENGINE-API.md @@ -142,9 +142,11 @@ step. ### Resolution order 1. `REDROB_CODE_BIN` — explicit override, development -2. `$HOME/.redrob/bin/redrob` — where `install.sh` already installs -3. `PATH` -4. absent → the product offers a one-click install that runs the official installer +2. `$HOME/.redrob/bin/redrob` — where `install.sh` already installs (and where Redrob Launcher + links the copy it installs) +3. Windows: `%LOCALAPPDATA%\Redrob\bin\redrob.exe` — where the setup.exe and the MSI install +4. `PATH` +5. absent → the product offers a one-click install that runs the official installer (GitHub Releases, verified against `SHA256SUMS.txt`) ### One daemon, not one per product From 6779ad4d858bfc898cb21913ed1917a258b836ae Mon Sep 17 00:00:00 2001 From: Janghoon Lee <44862514+savagemanage@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:14:39 +0000 Subject: [PATCH 3/3] make-dmg: retry hdiutil and keep its output visible --- installer/macos/make-dmg.sh | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/installer/macos/make-dmg.sh b/installer/macos/make-dmg.sh index 287b78f2be..afe4d406b6 100755 --- a/installer/macos/make-dmg.sh +++ b/installer/macos/make-dmg.sh @@ -52,8 +52,8 @@ PLIST if [ -n "${APPLE_SIGNING_IDENTITY:-}" ]; then codesign --force --options runtime --timestamp \ - --keychain "${APPLE_SIGNING_KEYCHAIN:?}" --sign "$APPLE_SIGNING_IDENTITY" "$app" - codesign --verify --strict --deep --verbose=2 "$app" + --keychain "${APPLE_SIGNING_KEYCHAIN:?}" --sign "$APPLE_SIGNING_IDENTITY" "$app" 1>&2 + codesign --verify --strict --deep --verbose=2 "$app" 1>&2 else echo "APPLE_SIGNING_IDENTITY unset: bundle left unsigned" >&2 fi @@ -62,6 +62,17 @@ mkdir -p "$out" dmg="$out/redrob-code-$version-$token.dmg" rm -f "$dmg" # A plain compressed image of the bundle; the launcher mounts it and copies the .app out, and a -# person can drag it to Applications. -hdiutil create -quiet -format UDZO -volname "$name $version" -srcfolder "$app" "$dmg" +# person can drag it to Applications. hdiutil on the hosted macOS runners fails now and then with +# "Resource busy" right after another image operation, so it gets a few tries, and its output goes +# to stderr so a failure says why (stdout is the path, for the caller). +attempt=0 +until hdiutil create -format UDZO -ov -volname "$name $version" -srcfolder "$app" "$dmg" 1>&2; do + attempt=$((attempt + 1)) + if [ "$attempt" -ge 4 ]; then + echo "hdiutil create failed $attempt times" >&2 + exit 1 + fi + echo "hdiutil create failed (attempt $attempt); retrying" >&2 + sleep 5 +done echo "$dmg"