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
69 changes: 66 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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 }}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 13 additions & 2 deletions crates/engine-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,18 +176,29 @@ pub fn current() -> Option<Record> {
}

/// 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")
}

Expand Down
8 changes: 5 additions & 3 deletions docs/LOCAL-ENGINE-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 78 additions & 0 deletions installer/macos/make-dmg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/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 <signed redrob binary> <version> <macos-aarch64|macos-x86_64> <out dir>
# 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: <out dir>/redrob-code-<version>-<token>.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" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key><string>ai.redrob.code</string>
<key>CFBundleName</key><string>$name</string>
<key>CFBundleDisplayName</key><string>$name</string>
<key>CFBundleExecutable</key><string>redrob</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>$version</string>
<key>CFBundleVersion</key><string>$numeric</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>LSMinimumSystemVersion</key><string>12.0</string>
<key>LSUIElement</key><true/>
<key>NSHumanReadableCopyright</key><string>Copyright Janghoon Lee (Redrob) and contributors</string>
</dict>
</plist>
PLIST

if [ -n "${APPLE_SIGNING_IDENTITY:-}" ]; then
codesign --force --options runtime --timestamp \
--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

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 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"
71 changes: 71 additions & 0 deletions installer/windows/redrob-code.wxs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Redrob Code MSI (WiX Toolset v5: `dotnet tool install -g wix` (5.0.2); no extensions).
Built by the release workflow:
wix build installer/windows/redrob-code.wxs -arch x64|arm64
-d Version=0.6.6 -d SourceBin=dist/redrob-windows-x64/bin -o dist/redrob-code-0.6.6-windows-x64.msi
Same install as the Inno Setup `redrob-code.iss` beside it, in the one package format Redrob
Launcher installs on Windows: per user (no UAC prompt), into %LOCALAPPDATA%\Redrob\bin, the engine
as `redrob.exe` with `redrob-code.exe` beside it (the launcher looks for `<app id>.exe`), and the
folder appended to the user's PATH. The launcher reads it back from Add/Remove Programs by
DisplayName "Redrob Code" and Publisher "Redrob", and finds the exe through InstallLocation.
Per-user packages can't use a file as a component's KeyPath (ICE38), so each component is
keyed on an HKCU value, and the folders we create are removed on uninstall (ICE64). MSI versions
are numeric: a prerelease tag's `-rc.1` is dropped before it gets here.
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package
Name="Redrob Code"
Manufacturer="Redrob"
Version="$(var.Version)"
UpgradeCode="8F4C6A21-9E3B-4D77-A5C8-2B1E7F0D9A64"
Scope="perUser"
Compressed="yes"
Language="1033">
<SummaryInformation Description="Redrob Code: the engine every Redrob app runs its AI through" Manufacturer="Redrob" />
<MajorUpgrade AllowSameVersionUpgrades="yes"
DowngradeErrorMessage="A newer version of Redrob Code is already installed." />
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />

<Property Id="ARPURLINFOABOUT" Value="https://redrob.ai" />
<Property Id="ARPURLUPDATEINFO" Value="https://github.com/redrob-labs/redrob-code/releases" />
<Property Id="ARPHELPLINK" Value="https://github.com/redrob-labs/redrob-code" />
<!-- Add/Remove Programs' InstallLocation: the launcher reads it to find redrob-code.exe. -->
<Property Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" />
<Property Id="ARPNOMODIFY" Value="1" />

<StandardDirectory Id="LocalAppDataFolder">
<Directory Id="RedrobFolder" Name="Redrob">
<Directory Id="INSTALLFOLDER" Name="bin" />
</Directory>
</StandardDirectory>

<Feature Id="Main" Title="Redrob Code" Level="1">
<ComponentGroupRef Id="RedrobCodeFiles" />
</Feature>
</Package>

<Fragment>
<ComponentGroup Id="RedrobCodeFiles" Directory="INSTALLFOLDER">
<Component Id="RedrobExe" Guid="7D3F2E51-0C8A-4B6E-9F21-5A6C3D8E1B40">
<File Id="RedrobExe" Source="$(var.SourceBin)\redrob.exe" Name="redrob.exe" />
<RegistryValue Root="HKCU" Key="Software\Redrob\Redrob Code" Name="InstallDir" Type="string" Value="[INSTALLFOLDER]" KeyPath="yes" />
<RemoveFolder Id="RemoveInstallFolder" Directory="INSTALLFOLDER" On="uninstall" />
<RemoveFolder Id="RemoveRedrobFolder" Directory="RedrobFolder" On="uninstall" />
</Component>
<!-- The same binary under the app's id, so the launcher (and a shell) can call it by name. -->
<Component Id="RedrobCodeExe" Guid="2B9E7C04-6F1D-4A3B-8E5C-0D7A9F2B6C13">
<File Id="RedrobCodeExe" Source="$(var.SourceBin)\redrob.exe" Name="redrob-code.exe" />
<RegistryValue Root="HKCU" Key="Software\Redrob\Redrob Code" Name="Version" Type="string" Value="$(var.Version)" KeyPath="yes" />
</Component>
<!-- The user's PATH gains the folder; removed again on uninstall. -->
<Component Id="RedrobCodePath" Guid="C4A1D8F6-3E2B-4C7D-9A0E-6B5F1D2C8E97">
<RegistryValue Root="HKCU" Key="Software\Redrob\Redrob Code" Name="OnPath" Type="integer" Value="1" KeyPath="yes" />
<Environment Id="RedrobCodeOnPath" Name="PATH" Value="[INSTALLFOLDER]" Part="last" Action="set" Permanent="no" System="no" Separator=";" />
</Component>
</ComponentGroup>
</Fragment>
</Wix>
Loading