feat(linux): extend companion self-update to macOS/Windows test builds (#109244)

Platform-aware install kinds: Linux AppImage and macOS self-install in place;
Windows defers — the update downloads and verifies in the background, then the
NSIS installer runs only from the user-confirmed restart (Tauri's installer
launch exits the process, so it must never fire behind a silent auto-check).
Linux system packages stay notify-only.

The release workflow gains dispatch-gated unsigned macOS/Windows test bundles
signed with the same minisign key, and a separate desktop-test update channel:
latest.json stays Linux-only on every release, while an opt-in fixed
'desktop-test' prerelease hosts latest-desktop-test.json (monotonic version
guard) so Linux-only releases can never strand distributed test builds. Build
jobs check out the SHA the validation job resolved, closing the tag-move race.
This commit is contained in:
Peter Steinberger
2026-07-16 10:55:19 -07:00
committed by GitHub
parent d1153e4ad8
commit b35fc165ee
4 changed files with 432 additions and 59 deletions
+306 -26
View File
@@ -7,6 +7,10 @@ on:
description: Existing OpenClaw release tag to receive Linux companion bundles, for example v2026.7.1
required: true
type: string
desktop-test-bundles:
description: Also build unsigned macOS/Windows test bundles
default: false
type: boolean
permissions:
contents: write
@@ -19,12 +23,14 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
build_and_attach:
name: Build and attach Linux companion bundles
# Oldest supported build base: bundles link against this glibc, so newer
# runners would silently drop Ubuntu 22.04/Debian 12 users.
validate_release:
name: Validate release tag
runs-on: ubuntu-22.04
timeout-minutes: 45
timeout-minutes: 10
# Build jobs check out this exact SHA so a tag force-moved mid-run cannot
# swap in code the ancestry guard never validated.
outputs:
tag_sha: ${{ steps.ancestry.outputs.tag_sha }}
steps:
- name: Validate tag input format
env:
@@ -47,6 +53,7 @@ jobs:
persist-credentials: false
- name: Ensure tag commit is reachable from main
id: ancestry
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
@@ -57,6 +64,7 @@ jobs:
echo "Tag ${RELEASE_TAG} (${tag_sha}) is not reachable from main; Linux bundles ship for main-based releases only."
exit 1
fi
echo "tag_sha=${tag_sha}" >> "$GITHUB_OUTPUT"
- name: Ensure matching GitHub release exists
env:
@@ -64,6 +72,20 @@ jobs:
RELEASE_TAG: ${{ inputs.tag }}
run: gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json tagName --jq .tagName
build_linux:
name: Build Linux companion bundles
needs: validate_release
# Oldest supported build base: bundles link against this glibc, so newer
# runners would silently drop Ubuntu 22.04/Debian 12 users.
runs-on: ubuntu-22.04
timeout-minutes: 45
steps:
- name: Checkout selected tag
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_release.outputs.tag_sha }}
persist-credentials: false
- name: Install Tauri system dependencies
run: |
sudo apt-get update
@@ -104,51 +126,266 @@ jobs:
pnpm dlx @tauri-apps/cli@2.11.4 build --bundles deb,appimage \
--config "{\"version\":\"${version}\"}"
- name: Verify and rename bundles for the release tag
- name: Verify and rename Linux bundles
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
shopt -s nullglob
version="${RELEASE_TAG#v}"
deb=$(ls apps/linux/src-tauri/target/release/bundle/deb/*.deb)
deb_version=$(dpkg-deb -f "${deb}" Version)
debs=(apps/linux/src-tauri/target/release/bundle/deb/*.deb)
appimages=(apps/linux/src-tauri/target/release/bundle/appimage/*.AppImage)
if [[ ${#debs[@]} -ne 1 || ${#appimages[@]} -ne 1 || ! -f "${appimages[0]}.sig" ]]; then
echo "Expected one deb, one AppImage, and its updater signature"
exit 1
fi
deb_version=$(dpkg-deb -f "${debs[0]}" Version)
if [[ "${deb_version}" != "${version}"* ]]; then
echo "Debian package version '${deb_version}' does not match release version '${version}'"
exit 1
fi
mkdir -p dist/linux-app
cp "${deb}" "dist/linux-app/OpenClaw-${version}-amd64.deb"
cp apps/linux/src-tauri/target/release/bundle/appimage/*.AppImage \
"dist/linux-app/OpenClaw-${version}-amd64.AppImage"
(cd dist/linux-app && sha256sum ./* > SHA256SUMS.linux-app.txt)
cat dist/linux-app/SHA256SUMS.linux-app.txt
mkdir -p dist/linux-app/release dist/linux-app/signatures
cp "${debs[0]}" "dist/linux-app/release/OpenClaw-${version}-amd64.deb"
cp "${appimages[0]}" "dist/linux-app/release/OpenClaw-${version}-amd64.AppImage"
cp "${appimages[0]}.sig" \
"dist/linux-app/signatures/OpenClaw-${version}-amd64.AppImage.sig"
- name: Generate updater manifest (latest.json)
- name: Upload Linux bundles
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: linux-app-release
path: dist/linux-app
if-no-files-found: error
# TEST-ONLY bundles: no Apple codesigning/notarization or Authenticode.
# Users must bypass Gatekeeper or SmartScreen before running them.
build_macos:
name: Build unsigned macOS test bundles
if: ${{ inputs['desktop-test-bundles'] }}
needs: validate_release
runs-on: macos-14
timeout-minutes: 45
steps:
- name: Checkout selected tag
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_release.outputs.tag_sha }}
persist-credentials: false
- name: Install Rust
run: rustup toolchain install stable --profile minimal
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
install-deps: "false"
- name: Build macOS test bundles
working-directory: apps/linux/src-tauri
env:
RELEASE_TAG: ${{ inputs.tag }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
pnpm dlx @tauri-apps/cli@2.11.4 build --bundles app,dmg \
--config "{\"version\":\"${version}\"}"
- name: Verify and rename macOS bundles
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
shopt -s nullglob
version="${RELEASE_TAG#v}"
apps=(apps/linux/src-tauri/target/release/bundle/macos/*.app)
archives=(apps/linux/src-tauri/target/release/bundle/macos/*.app.tar.gz)
dmgs=(apps/linux/src-tauri/target/release/bundle/dmg/*.dmg)
if [[ ${#apps[@]} -ne 1 || ${#archives[@]} -ne 1 || ${#dmgs[@]} -ne 1 || ! -f "${archives[0]}.sig" ]]; then
echo "Expected one app, updater archive, updater signature, and dmg"
exit 1
fi
bundle_version=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \
"${apps[0]}/Contents/Info.plist")
if [[ "${bundle_version}" != "${version}" ]]; then
echo "macOS bundle version '${bundle_version}' does not match release version '${version}'"
exit 1
fi
mkdir -p dist/macos-app/release dist/macos-app/signatures
cp "${dmgs[0]}" "dist/macos-app/release/OpenClaw-${version}-darwin-aarch64.dmg"
cp "${archives[0]}" \
"dist/macos-app/release/OpenClaw-${version}-darwin-aarch64.app.tar.gz"
cp "${archives[0]}.sig" \
"dist/macos-app/signatures/OpenClaw-${version}-darwin-aarch64.app.tar.gz.sig"
- name: Upload macOS test bundles
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: macos-app-release
path: dist/macos-app
if-no-files-found: error
build_windows:
name: Build unsigned Windows test bundle
if: ${{ inputs['desktop-test-bundles'] }}
needs: validate_release
runs-on: windows-2022
timeout-minutes: 45
steps:
- name: Checkout selected tag
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.validate_release.outputs.tag_sha }}
persist-credentials: false
- name: Install Rust
run: rustup toolchain install stable --profile minimal
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
install-deps: "false"
- name: Build Windows test bundle
working-directory: apps/linux/src-tauri
shell: bash
env:
RELEASE_TAG: ${{ inputs.tag }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
pnpm dlx @tauri-apps/cli@2.11.4 build --bundles nsis \
--config "{\"version\":\"${version}\"}"
- name: Verify and rename Windows bundle
shell: pwsh
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
$ErrorActionPreference = "Stop"
$version = $env:RELEASE_TAG.Substring(1)
$installers = @(Get-ChildItem "apps/linux/src-tauri/target/release/bundle/nsis/*.exe")
if ($installers.Count -ne 1) {
throw "Expected one NSIS installer; found $($installers.Count)"
}
$installer = $installers[0]
$signature = "$($installer.FullName).sig"
if (-not (Test-Path -LiteralPath $signature)) {
throw "Missing NSIS updater signature: $signature"
}
if (-not $installer.VersionInfo.ProductVersion.StartsWith($version)) {
throw "Windows bundle version '$($installer.VersionInfo.ProductVersion)' does not match release version '$version'"
}
New-Item -ItemType Directory -Force -Path "dist/windows-app/release", "dist/windows-app/signatures" | Out-Null
Copy-Item -LiteralPath $installer.FullName -Destination "dist/windows-app/release/OpenClaw-$version-windows-x86_64.exe"
Copy-Item -LiteralPath $signature -Destination "dist/windows-app/signatures/OpenClaw-$version-windows-x86_64.exe.sig"
- name: Upload Windows test bundle
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: windows-app-release
path: dist/windows-app
if-no-files-found: error
publish:
name: Publish companion bundles and updater manifest
if: >-
${{
always() &&
needs.build_linux.result == 'success' &&
(!inputs['desktop-test-bundles'] ||
(needs.build_macos.result == 'success' && needs.build_windows.result == 'success'))
}}
needs:
- validate_release
- build_linux
- build_macos
- build_windows
# One shared desktop-test channel asset must not race across release tags.
concurrency:
group: linux-app-release-publish
cancel-in-progress: false
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- name: Download Linux bundles
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: linux-app-release
path: dist/input/linux
- name: Download macOS test bundles
if: ${{ inputs['desktop-test-bundles'] }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: macos-app-release
path: dist/input/macos
- name: Download Windows test bundle
if: ${{ inputs['desktop-test-bundles'] }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: windows-app-release
path: dist/input/windows
- name: Assemble release assets and updater manifest
env:
DESKTOP_TEST_BUNDLES: ${{ inputs['desktop-test-bundles'] }}
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
# The signature is over the AppImage bytes, so renaming the file does
# not invalidate it. The committed pubkey verifies it in the app.
sig_file=$(ls apps/linux/src-tauri/target/release/bundle/appimage/*.AppImage.sig)
signature=$(cat "${sig_file}")
mkdir -p dist/release
cp dist/input/linux/release/* dist/release/
if [[ "${DESKTOP_TEST_BUNDLES}" == "true" ]]; then
cp dist/input/macos/release/* dist/release/
cp dist/input/windows/release/* dist/release/
fi
# Generate this before latest.json so it covers only downloadable bundles.
(cd dist/release && sha256sum ./* > SHA256SUMS.linux-app.txt)
cat dist/release/SHA256SUMS.linux-app.txt
linux_signature=$(cat "dist/input/linux/signatures/OpenClaw-${version}-amd64.AppImage.sig")
url_base="https://github.com/${GITHUB_REPOSITORY}/releases/download/${RELEASE_TAG}"
pub_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# Capture the full body (no early-closing pipe under pipefail), then
# truncate to 2000 Unicode chars inside jq so we never split a
# multibyte character or SIGPIPE the release-view command.
notes=$(gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json body --jq '.body // ""')
url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${RELEASE_TAG}/OpenClaw-${version}-amd64.AppImage"
# latest.json is always the stable Linux channel. Desktop test builds
# use a separate manifest that Linux-only releases leave untouched.
jq -n \
--arg version "${version}" \
--arg notes "${notes}" \
--arg pub_date "${pub_date}" \
--arg signature "${signature}" \
--arg url "${url}" \
'{version: $version, notes: ($notes | .[0:2000]), pub_date: $pub_date, platforms: {"linux-x86_64": {signature: $signature, url: $url}}}' \
> dist/linux-app/latest.json
cat dist/linux-app/latest.json
--arg linux_signature "${linux_signature}" \
--arg linux_url "${url_base}/OpenClaw-${version}-amd64.AppImage" \
'{version: $version, notes: ($notes | .[0:2000]), pub_date: $pub_date, platforms: {"linux-x86_64": {signature: $linux_signature, url: $linux_url}}}' \
> dist/release/latest.json
cat dist/release/latest.json
if [[ "${DESKTOP_TEST_BUNDLES}" == "true" ]]; then
macos_signature=$(cat "dist/input/macos/signatures/OpenClaw-${version}-darwin-aarch64.app.tar.gz.sig")
windows_signature=$(cat "dist/input/windows/signatures/OpenClaw-${version}-windows-x86_64.exe.sig")
jq -n \
--arg version "${version}" \
--arg notes "${notes}" \
--arg pub_date "${pub_date}" \
--arg macos_signature "${macos_signature}" \
--arg macos_url "${url_base}/OpenClaw-${version}-darwin-aarch64.app.tar.gz" \
--arg windows_signature "${windows_signature}" \
--arg windows_url "${url_base}/OpenClaw-${version}-windows-x86_64.exe" \
'{version: $version, notes: ($notes | .[0:2000]), pub_date: $pub_date, platforms: {"darwin-aarch64": {signature: $macos_signature, url: $macos_url}, "windows-x86_64": {signature: $windows_signature, url: $windows_url}}}' \
> dist/release/latest-desktop-test.json
cat dist/release/latest-desktop-test.json
fi
- name: Attach bundles to the release
env:
@@ -159,4 +396,47 @@ jobs:
gh release upload "${RELEASE_TAG}" \
--repo "${GITHUB_REPOSITORY}" \
--clobber \
dist/linux-app/*
dist/release/*
- name: Publish desktop test update channel
if: ${{ inputs['desktop-test-bundles'] }}
env:
DESKTOP_TEST_CHANNEL_TAG: desktop-test
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
TAG_SHA: ${{ needs.validate_release.outputs.tag_sha }}
run: |
set -euo pipefail
channel_dir="${RUNNER_TEMP}/desktop-test-channel"
candidate_version="${RELEASE_TAG#v}"
mkdir -p "${channel_dir}"
if gh release view "${DESKTOP_TEST_CHANNEL_TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
has_manifest=$(gh release view "${DESKTOP_TEST_CHANNEL_TAG}" \
--repo "${GITHUB_REPOSITORY}" \
--json assets \
--jq '[.assets[].name] | index("latest-desktop-test.json") != null')
if [[ "${has_manifest}" == "true" ]]; then
gh release download "${DESKTOP_TEST_CHANNEL_TAG}" \
--repo "${GITHUB_REPOSITORY}" \
--pattern latest-desktop-test.json \
--dir "${channel_dir}"
current_version=$(jq -er '.version | strings' "${channel_dir}/latest-desktop-test.json")
newest_version=$(printf '%s\n' "${current_version}" "${candidate_version}" | LC_ALL=C sort -V | tail -n 1)
if [[ "${newest_version}" != "${candidate_version}" ]]; then
echo "Desktop test channel is already newer (${current_version}); leaving it unchanged."
exit 0
fi
fi
else
gh release create "${DESKTOP_TEST_CHANNEL_TAG}" \
--repo "${GITHUB_REPOSITORY}" \
--target "${TAG_SHA}" \
--prerelease \
--title "OpenClaw desktop test update channel" \
--notes "Opt-in updater manifest for unsigned macOS and Windows Tauri test builds."
fi
gh release upload "${DESKTOP_TEST_CHANNEL_TAG}" \
--repo "${GITHUB_REPOSITORY}" \
--clobber \
dist/release/latest-desktop-test.json
+1 -1
View File
@@ -30,7 +30,7 @@ On first run, release builds automatically install the stable CLI channel, while
## Updates
The companion checks the latest GitHub release shortly after launch and from **Check for Updates** in the tray menu. AppImage installs download and verify the signed update in place, then wait for **Restart to update**. Package-managed installs such as `.deb` stay owned by the system package manager and link to the release download page instead of replacing installed files.
The companion checks the latest GitHub release shortly after launch and from **Check for Updates** in the tray menu. AppImage installs download and verify the signed update in place, then wait for **Restart to update**. Package-managed installs such as `.deb` stay owned by the system package manager and link to the release download page instead of replacing installed files. The macOS and Windows test builds use a separate opt-in desktop-test update channel; macOS self-updates like the AppImage build, while Windows downloads the update first and runs its installer only after **Restart to update**.
## Canvas bridge
+1 -1
View File
@@ -10,7 +10,7 @@ name = "openclaw-desktop"
path = "src/main.rs"
[build-dependencies]
tauri-build = "2.6.3"
tauri-build = { version = "2.6.3", features = [] }
[dependencies]
base64 = "0.22.1"
+124 -31
View File
@@ -1,11 +1,11 @@
use serde::Serialize;
use std::ffi::OsString;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tauri::{AppHandle, Emitter, Manager, WebviewWindow};
use tauri_plugin_opener::OpenerExt;
use tauri_plugin_updater::UpdaterExt;
use tauri_plugin_updater::{Update, UpdaterExt};
pub(crate) const NOT_AVAILABLE_EVENT: &str = "updater://not-available";
pub(crate) const AVAILABLE_EVENT: &str = "updater://available";
@@ -15,18 +15,33 @@ pub(crate) const READY_EVENT: &str = "updater://ready";
pub(crate) const ERROR_EVENT: &str = "updater://error";
const RELEASE_URL: &str = "https://github.com/openclaw/openclaw/releases/latest";
#[cfg(any(target_os = "macos", target_os = "windows"))]
// Test desktop builds need a channel that Linux-only releases never replace.
const DESKTOP_TEST_UPDATE_ENDPOINT: &str =
"https://github.com/openclaw/openclaw/releases/download/desktop-test/latest-desktop-test.json";
const AUTO_CHECK_DELAY: Duration = Duration::from_secs(3);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum InstallKind {
AppImage,
SystemPackage,
SelfInstall,
DeferredInstall,
NotifyOnly,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
// Keep every discriminator available so one host can test all platform rules.
#[allow(dead_code)]
enum Platform {
Linux,
Macos,
Windows,
}
#[derive(Default)]
pub struct UpdaterState {
auto_check_started: AtomicBool,
check_in_progress: Arc<AtomicBool>,
deferred_update: Mutex<Option<DeferredUpdate>>,
// Set when a manual (tray/command) check is requested. The one in-flight
// check reads this at emit time so a manual click that lands while the
// silent startup auto-check is running still surfaces a result instead of
@@ -34,6 +49,11 @@ pub struct UpdaterState {
manual_pending: Arc<AtomicBool>,
}
struct DeferredUpdate {
update: Update,
bytes: Vec<u8>,
}
struct CheckGuard {
in_progress: Arc<AtomicBool>,
manual_pending: Arc<AtomicBool>,
@@ -103,7 +123,28 @@ pub fn updater_ready(app: AppHandle) {
#[tauri::command]
pub fn relaunch(app: AppHandle) {
app.restart();
let state = app.state::<UpdaterState>();
let deferred = state
.deferred_update
.lock()
.expect("deferred updater state lock poisoned")
.take();
let Some(deferred) = deferred else {
app.restart();
};
let result = deferred.update.install(&deferred.bytes);
match result {
Ok(()) => app.restart(),
Err(error) => {
state
.deferred_update
.lock()
.expect("deferred updater state lock poisoned")
.replace(deferred);
emit_error(&app, error);
}
}
}
#[tauri::command]
@@ -129,7 +170,16 @@ async fn run_check(app: AppHandle, manual: bool) {
return;
};
let should_notify = || manual_pending.load(Ordering::Acquire);
let updater = match app.updater() {
#[cfg(target_os = "linux")]
let updater = app.updater();
#[cfg(any(target_os = "macos", target_os = "windows"))]
let updater = app
.updater_builder()
.endpoints(vec![DESKTOP_TEST_UPDATE_ENDPOINT
.parse()
.expect("desktop test updater endpoint is valid")])
.and_then(|builder| builder.build());
let updater = match updater {
Ok(updater) => updater,
Err(error) => {
if should_notify() {
@@ -158,7 +208,8 @@ async fn run_check(app: AppHandle, manual: bool) {
notes: update.body.clone(),
};
if install_kind() == InstallKind::SystemPackage {
let install_kind = install_kind();
if install_kind == InstallKind::NotifyOnly {
emit(
&app,
AVAILABLE_MANUAL_EVENT,
@@ -175,19 +226,26 @@ async fn run_check(app: AppHandle, manual: bool) {
let Some(window) = main_window(&app) else {
return;
};
let progress_window = window.clone();
let mut downloaded = 0_u64;
let result = update
.download_and_install(
move |chunk_size, total| {
downloaded = downloaded.saturating_add(chunk_size as u64);
let _ = progress_window.emit(PROGRESS_EVENT, Progress { downloaded, total });
},
|| {},
)
.await;
let result = match install_kind {
InstallKind::SelfInstall => update
.download_and_install(progress_callback(window.clone()), || {})
.await
.map(|()| None),
InstallKind::DeferredInstall => update
.download(progress_callback(window.clone()), || {})
.await
.map(Some),
InstallKind::NotifyOnly => unreachable!("notify-only updates return before downloading"),
};
match result {
Ok(()) => {
Ok(deferred_bytes) => {
if let Some(bytes) = deferred_bytes {
app.state::<UpdaterState>()
.deferred_update
.lock()
.expect("deferred updater state lock poisoned")
.replace(DeferredUpdate { update, bytes });
}
let _ = window.emit(READY_EVENT, info);
}
Err(error) => emit_error(&app, error),
@@ -208,15 +266,31 @@ fn begin_check(app: &AppHandle) -> Option<CheckGuard> {
}
fn install_kind() -> InstallKind {
install_kind_from_appimage_env(std::env::var_os("APPIMAGE"))
#[cfg(target_os = "linux")]
let platform = Platform::Linux;
#[cfg(target_os = "macos")]
let platform = Platform::Macos;
#[cfg(target_os = "windows")]
let platform = Platform::Windows;
install_kind_from_appimage_env(std::env::var_os("APPIMAGE"), platform)
}
fn install_kind_from_appimage_env(appimage: Option<OsString>) -> InstallKind {
if appimage.is_some() {
InstallKind::AppImage
} else {
// Package managers own deb/rpm installs; replacing their files would corrupt that contract.
InstallKind::SystemPackage
fn install_kind_from_appimage_env(appimage: Option<OsString>, platform: Platform) -> InstallKind {
match platform {
Platform::Linux if appimage.is_some() => InstallKind::SelfInstall,
Platform::Linux => {
// Package managers own deb/rpm files, so replacing them would corrupt their contract.
InstallKind::NotifyOnly
}
Platform::Macos => {
// Tauri owns .app replacement and returns after installing, like the AppImage path.
InstallKind::SelfInstall
}
Platform::Windows => {
// Tauri's NSIS install exits the process, so wait for user-confirmed relaunch.
InstallKind::DeferredInstall
}
}
}
@@ -224,6 +298,14 @@ fn main_window(app: &AppHandle) -> Option<WebviewWindow> {
app.get_webview_window("main")
}
fn progress_callback(window: WebviewWindow) -> impl FnMut(usize, Option<u64>) {
let mut downloaded = 0_u64;
move |chunk_size, total| {
downloaded = downloaded.saturating_add(chunk_size as u64);
let _ = window.emit(PROGRESS_EVENT, Progress { downloaded, total });
}
}
fn emit<S: Serialize + Clone>(app: &AppHandle, event: &str, payload: S) {
if let Some(window) = main_window(app) {
let _ = window.emit(event, payload);
@@ -245,14 +327,25 @@ mod tests {
use super::*;
#[test]
fn install_kind_follows_appimage_env_presence() {
fn install_kind_covers_every_platform_path() {
assert_eq!(
install_kind_from_appimage_env(None),
InstallKind::SystemPackage
install_kind_from_appimage_env(None, Platform::Linux),
InstallKind::NotifyOnly
);
assert_eq!(
install_kind_from_appimage_env(Some(OsString::from("/tmp/OpenClaw.AppImage"))),
InstallKind::AppImage
install_kind_from_appimage_env(
Some(OsString::from("/tmp/OpenClaw.AppImage")),
Platform::Linux,
),
InstallKind::SelfInstall
);
assert_eq!(
install_kind_from_appimage_env(None, Platform::Macos),
InstallKind::SelfInstall
);
assert_eq!(
install_kind_from_appimage_env(None, Platform::Windows),
InstallKind::DeferredInstall
);
}