build(linux): allow macOS and Windows test builds of the desktop app (#108004)

The desktop companion crate was Linux-only. All platform-specific code
lives in canvas.rs (WebKitGTK, cairo, unix sockets, SO_PEERCRED), so:
move webkit2gtk/cairo-rs/libc under linux target dependencies, gate the
canvas module, its protocol registration, invoke handler, and exit
shutdown to Linux, add a USERPROFILE home fallback and an explicit
no-op installer error for Windows, and add the multi-size icon.ico
Windows resource. Linux behavior is unchanged; macOS/Windows builds are
for testing (macOS cargo build+test green locally; Windows aarch64-msvc
built and launched in a Windows 11 ARM VM).
This commit is contained in:
Peter Steinberger
2026-07-14 22:17:05 -07:00
committed by GitHub
parent 51d85769b3
commit 9384a40393
7 changed files with 74 additions and 26 deletions
+1
View File
@@ -50,6 +50,7 @@ magick 32x32.png -background none -gravity center -extent 32x32 PNG32:32x32.png
rsvg-convert -w 128 -h 128 icon-tile.svg -o 128x128.png
rsvg-convert -w 256 -h 256 icon-tile.svg -o 128x128@2x.png
rsvg-convert -w 512 -h 512 icon-tile.svg -o icon.png
magick icon.png -define icon:auto-resize=256,128,64,48,32,16 icon.ico
```
## Packaging
+4 -2
View File
@@ -14,10 +14,12 @@ tauri-build = "2.6.3"
[dependencies]
base64 = "0.22.1"
cairo-rs = { version = "0.18.5", features = ["png"] }
image = { version = "0.25.10", default-features = false, features = ["jpeg", "png"] }
libc = "0.2.186"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150"
tauri = { version = "2.11.5", features = ["image-png", "tray-icon"] }
[target.'cfg(target_os = "linux")'.dependencies]
cairo-rs = { version = "0.18.5", features = ["png"] }
libc = "0.2.186"
webkit2gtk = "2.0.2"
+2
View File
@@ -1,4 +1,6 @@
fn main() {
// Command metadata generates capability permissions independently of the
// target's invoke handler, so keep the Linux-only command permission known.
const COMMANDS: &[&str] = &[
"bootstrap",
"canvas_a2ui_action",
Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

+5 -1
View File
@@ -122,8 +122,12 @@ impl OpenClawCli {
}
pub fn openclaw_home() -> Result<PathBuf, CliError> {
#[cfg(target_os = "windows")]
let home = env::var_os("HOME")
.filter(|value| !value.is_empty())
.ok_or_else(|| CliError::Environment("HOME is not set".to_string()))?;
.or_else(|| env::var_os("USERPROFILE").filter(|value| !value.is_empty()));
#[cfg(not(target_os = "windows"))]
let home = env::var_os("HOME").filter(|value| !value.is_empty());
let home = home.ok_or_else(|| CliError::Environment("HOME is not set".to_string()))?;
Ok(PathBuf::from(home).join(".openclaw"))
}
+24 -2
View File
@@ -1,14 +1,27 @@
#[cfg(not(target_os = "windows"))]
use crate::cli::openclaw_home;
use serde::{Deserialize, Serialize};
use serde::Deserialize;
#[cfg(not(target_os = "windows"))]
use serde::Serialize;
#[cfg(not(target_os = "windows"))]
use std::collections::VecDeque;
#[cfg(not(target_os = "windows"))]
use std::io::{BufRead, BufReader};
#[cfg(not(target_os = "windows"))]
use std::process::{Command, Stdio};
#[cfg(not(target_os = "windows"))]
use std::sync::mpsc;
#[cfg(not(target_os = "windows"))]
use std::thread;
#[cfg(not(target_os = "windows"))]
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Emitter, Manager};
use tauri::AppHandle;
#[cfg(not(target_os = "windows"))]
use tauri::{Emitter, Manager};
#[cfg(not(target_os = "windows"))]
const INSTALL_EVENT: &str = "install-progress";
#[cfg(not(target_os = "windows"))]
const ERROR_TAIL_LINES: usize = 24;
#[derive(Clone, Copy, Debug, Deserialize)]
@@ -20,6 +33,7 @@ pub enum InstallChannel {
}
impl InstallChannel {
#[cfg(not(target_os = "windows"))]
fn version(self) -> &'static str {
match self {
Self::Stable => "latest",
@@ -29,6 +43,7 @@ impl InstallChannel {
}
}
#[cfg(not(target_os = "windows"))]
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct InstallProgress<'a> {
@@ -36,6 +51,12 @@ struct InstallProgress<'a> {
line: &'a str,
}
#[cfg(target_os = "windows")]
pub fn install(_app: &AppHandle, _channel: InstallChannel) -> Result<(), String> {
Err("CLI installation is unavailable in this Windows test build.".to_string())
}
#[cfg(not(target_os = "windows"))]
pub fn install(app: &AppHandle, channel: InstallChannel) -> Result<(), String> {
let script = app
.path()
@@ -107,6 +128,7 @@ pub fn install(app: &AppHandle, channel: InstallChannel) -> Result<(), String> {
}
}
#[cfg(not(target_os = "windows"))]
fn stream_lines<R>(
stream: &'static str,
reader: R,
+38 -21
View File
@@ -1,3 +1,4 @@
#[cfg(target_os = "linux")]
mod canvas;
mod cli;
mod gateway;
@@ -280,28 +281,41 @@ async fn gateway_action(
}
fn main() {
let app = canvas::register_protocol(tauri::Builder::default())
.setup(|app| {
let window = app
.get_webview_window("main")
.expect("tauri.conf.json must define the main window");
let state = DesktopState::new(window.url()?);
app.manage(state.clone());
match canvas::CanvasBridge::start(app.handle().clone()) {
Ok(bridge) => {
app.manage(bridge);
}
Err(error) => eprintln!("Canvas bridge unavailable: {error}"),
let builder = tauri::Builder::default();
#[cfg(target_os = "linux")]
let builder = canvas::register_protocol(builder);
let builder = builder.setup(|app| {
let window = app
.get_webview_window("main")
.expect("tauri.conf.json must define the main window");
let state = DesktopState::new(window.url()?);
app.manage(state.clone());
#[cfg(target_os = "linux")]
match canvas::CanvasBridge::start(app.handle().clone()) {
Ok(bridge) => {
app.manage(bridge);
}
state.set_tray(tray::build(app, state.clone())?);
Ok(())
})
.invoke_handler(tauri::generate_handler![
bootstrap,
canvas::canvas_a2ui_action,
install_cli,
gateway_action
])
Err(error) => eprintln!("Canvas bridge unavailable: {error}"),
}
state.set_tray(tray::build(app, state.clone())?);
Ok(())
});
#[cfg(target_os = "linux")]
let builder = builder.invoke_handler(tauri::generate_handler![
bootstrap,
canvas::canvas_a2ui_action,
install_cli,
gateway_action
]);
#[cfg(not(target_os = "linux"))]
let builder = builder.invoke_handler(tauri::generate_handler![
bootstrap,
install_cli,
gateway_action
]);
let app = builder
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
let state = window.app_handle().state::<DesktopState>();
@@ -314,10 +328,13 @@ fn main() {
.build(tauri::generate_context!())
.expect("OpenClaw desktop app failed");
app.run(|app, event| {
#[cfg(target_os = "linux")]
if matches!(event, tauri::RunEvent::Exit) {
if let Some(bridge) = app.try_state::<canvas::CanvasBridge>() {
bridge.shutdown();
}
}
#[cfg(not(target_os = "linux"))]
let _ = (app, event);
});
}