Compare commits

...
3 Commits
Author SHA1 Message Date
Aleksandr BogomiakovandClaude a55ea883a1 Tray: initialise GTK and run its main loop on Linux
Release / tsunagi-linux-aarch64 (push) Successful in 3m14s
Release / tsunagi-linux-x86_64 (push) Successful in 5m21s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
tray-icon needs gtk::init() and a running GTK loop on Linux, which
eframe/winit never provide, so the tray panicked at startup with "GTK has
not been initialized". Build the tray on a dedicated GTK thread instead.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-02 17:54:38 +01:00
UltradesuandClaude Opus 4.8 3e066f991f CI: install libxdo-dev for the Linux tray link
Release / tsunagi-linux-aarch64 (push) Successful in 3m23s
Release / tsunagi-linux-x86_64 (push) Successful in 6m58s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
The x86_64 Linux tray failed at link with "unable to find library -lxdo":
tray-icon pulls libxdo, whose .so was not present. Add libxdo-dev to the GUI
build's apt install, alongside libgtk-3-dev and libayatana-appindicator3-dev.
Everything else (aarch64 agent, macOS, Windows) already built.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 16:51:53 +01:00
UltradesuandClaude Opus 4.8 3752419075 CI: build the Linux tray natively with GTK; drop the aarch64 GUI
Release / tsunagi-linux-aarch64 (push) Successful in 4m14s
Release / tsunagi-linux-x86_64 (push) Failing after 4m29s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
The aarch64 GUI cross-build failed: tray-icon links GTK and libappindicator
through pkg-config, which zig cross-compilation cannot satisfy without a target
sysroot. Rather than set one up, the Linux tray is now built natively on the
x86_64 runner with libgtk-3-dev and libayatana-appindicator3-dev installed, so
pkg-config just works. The agent is still static musl for both arches.

The aarch64 Linux GUI is dropped for now: that archive stays agent-only and no
aarch64 tsunagi-gui package is produced. x86_64 gets the tray in its archive
and the tsunagi-gui .deb/.pkg.tar.zst. macOS and Windows build the tray
natively as before. Guarded on matrix.gnu_target (set only on x86_64 Linux).

Note: the native build links the runner's glibc, so the Linux GUI binary needs a
reasonably recent glibc; fine for current desktops. A lower baseline would need
a GTK sysroot or an older runner image.

YAML validated; the Linux GUI build is exercised only by a real CI run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 16:38:58 +01:00
5 changed files with 109 additions and 37 deletions

No files matched your search

+17 -8
View File
@@ -45,7 +45,6 @@ jobs:
deb: true
deb_arch: arm64
pkg_arch: aarch64
gnu_target: aarch64-unknown-linux-gnu
- os: macos-latest
target: aarch64-apple-darwin
asset: tsunagi-macos-aarch64
@@ -111,11 +110,19 @@ jobs:
shell: bash
run: |
set -euo pipefail
export RUSTFLAGS="${RUSTFLAGS:-} -A linker_messages"
if [[ "${{ runner.os }}" == "Linux" ]]; then
rustup target add "${{ matrix.gnu_target }}"
cargo zigbuild --release --locked \
--target "${{ matrix.gnu_target }}.2.28" --bin tsunagi-tray
if [[ -n "${{ matrix.gnu_target }}" ]]; then
# tray-icon links GTK and libappindicator through pkg-config, which
# has no cross story under zig, so the Linux GUI is built natively
# (x86_64 only) against the runner's glibc.
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
cargo build --release --locked \
--target "${{ matrix.gnu_target }}" --bin tsunagi-tray
else
echo "no GUI build for ${{ matrix.target }}"
fi
else
cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi-tray
fi
@@ -133,11 +140,13 @@ jobs:
[[ "${{ runner.os }}" == "Windows" ]] && bin="tsunagi.exe"
cp "target/${{ matrix.target }}/release/${bin}" "$stage/"
# The tray GUI alongside it: glibc build on Linux, native elsewhere.
# The tray GUI alongside it, where it was built (Linux: x86_64 only).
tray="tsunagi-tray"
[[ "${{ runner.os }}" == "Windows" ]] && tray="tsunagi-tray.exe"
if [[ "${{ runner.os }}" == "Linux" ]]; then
cp "target/${{ matrix.gnu_target }}/release/${tray}" "$stage/"
if [[ -n "${{ matrix.gnu_target }}" ]]; then
cp "target/${{ matrix.gnu_target }}/release/${tray}" "$stage/"
fi
else
cp "target/${{ matrix.target }}/release/${tray}" "$stage/"
fi
@@ -201,7 +210,7 @@ jobs:
# The GUI variants: the same agent plus the tray binary and a desktop
# launcher, named tsunagi-gui (conflicting with the headless packages).
- name: Build the GUI packages
if: matrix.deb
if: matrix.gnu_target
shell: bash
run: |
set -euo pipefail
+17 -8
View File
@@ -47,7 +47,6 @@ jobs:
deb: true
deb_arch: arm64
pkg_arch: aarch64
gnu_target: aarch64-unknown-linux-gnu
- os: macos-latest
target: aarch64-apple-darwin
asset: tsunagi-macos-aarch64
@@ -113,11 +112,19 @@ jobs:
shell: bash
run: |
set -euo pipefail
export RUSTFLAGS="${RUSTFLAGS:-} -A linker_messages"
if [[ "${{ runner.os }}" == "Linux" ]]; then
rustup target add "${{ matrix.gnu_target }}"
cargo zigbuild --release --locked \
--target "${{ matrix.gnu_target }}.2.28" --bin tsunagi-tray
if [[ -n "${{ matrix.gnu_target }}" ]]; then
# tray-icon links GTK and libappindicator through pkg-config, which
# has no cross story under zig, so the Linux GUI is built natively
# (x86_64 only) against the runner's glibc.
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
cargo build --release --locked \
--target "${{ matrix.gnu_target }}" --bin tsunagi-tray
else
echo "no GUI build for ${{ matrix.target }}"
fi
else
cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi-tray
fi
@@ -135,11 +142,13 @@ jobs:
[[ "${{ runner.os }}" == "Windows" ]] && bin="tsunagi.exe"
cp "target/${{ matrix.target }}/release/${bin}" "$stage/"
# The tray GUI alongside it: glibc build on Linux, native elsewhere.
# The tray GUI alongside it, where it was built (Linux: x86_64 only).
tray="tsunagi-tray"
[[ "${{ runner.os }}" == "Windows" ]] && tray="tsunagi-tray.exe"
if [[ "${{ runner.os }}" == "Linux" ]]; then
cp "target/${{ matrix.gnu_target }}/release/${tray}" "$stage/"
if [[ -n "${{ matrix.gnu_target }}" ]]; then
cp "target/${{ matrix.gnu_target }}/release/${tray}" "$stage/"
fi
else
cp "target/${{ matrix.target }}/release/${tray}" "$stage/"
fi
@@ -203,7 +212,7 @@ jobs:
# The GUI variants: the same agent plus the tray binary and a desktop
# launcher, named tsunagi-gui (conflicting with the headless packages).
- name: Build the GUI packages
if: matrix.deb
if: matrix.gnu_target
shell: bash
run: |
set -euo pipefail
Generated
+1
View File
@@ -6329,6 +6329,7 @@ version = "0.1.0"
dependencies = [
"eframe",
"egui-phosphor",
"gtk",
"tokio",
"tracing",
"tracing-subscriber",
+5
View File
@@ -26,6 +26,11 @@ tracing.workspace = true
tracing-subscriber.workspace = true
egui-phosphor = "0.9"
# Linux only: tray-icon (already GTK-based there) needs the app to initialise
# GTK and run its main loop, which eframe/winit do not do.
[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18"
# macOS only: set the app's activation policy to Accessory so it lives in the
# menu bar with no Dock icon. Pinned to eframe's winit so the types unify.
[target.'cfg(target_os = "macos")'.dependencies]
+69 -21
View File
@@ -8,7 +8,7 @@
use std::sync::mpsc;
use eframe::egui;
use tray_icon::menu::{Menu, MenuEvent, MenuItem, PredefinedMenuItem};
use tray_icon::menu::{Menu, MenuEvent, MenuId, MenuItem, PredefinedMenuItem};
use tray_icon::{Icon, TrayIcon, TrayIconBuilder};
/// What the user picked from the tray menu.
@@ -22,34 +22,81 @@ pub(crate) enum Action {
/// The live tray icon plus the channel of menu actions.
pub(crate) struct Tray {
/// Kept alive for as long as the app runs; dropping it removes the icon.
/// On Linux the icon lives on the GTK thread instead (it is not `Send`).
#[cfg(not(target_os = "linux"))]
_icon: TrayIcon,
actions: mpsc::Receiver<Action>,
}
/// A built tray icon and the ids of its menu items.
struct Built {
icon: TrayIcon,
open: MenuId,
quit: MenuId,
}
fn build() -> Result<Built, String> {
let open = MenuItem::new("Open tsunagi", true, None);
let quit = MenuItem::new("Quit", true, None);
let menu = Menu::new();
menu.append(&open).map_err(|err| err.to_string())?;
menu.append(&PredefinedMenuItem::separator())
.map_err(|err| err.to_string())?;
menu.append(&quit).map_err(|err| err.to_string())?;
let icon = TrayIconBuilder::new()
.with_tooltip("tsunagi")
.with_menu(Box::new(menu))
.with_icon(make_icon()?)
.build()
.map_err(|err| err.to_string())?;
Ok(Built {
icon,
open: open.id().clone(),
quit: quit.id().clone(),
})
}
impl Tray {
/// Builds the tray and starts reading its menu events.
///
/// Created on the main thread (required on macOS); `ctx` is used to wake the
/// UI when a menu item is chosen.
/// Created on the main thread on macOS and Windows (required on macOS). On
/// Linux the tray needs GTK, which eframe/winit never initialise, so it is
/// built on a dedicated thread that initialises GTK and runs its main loop.
/// `ctx` is used to wake the UI when a menu item is chosen.
pub(crate) fn new(ctx: egui::Context) -> Result<Self, String> {
let open = MenuItem::new("Open tsunagi", true, None);
let quit = MenuItem::new("Quit", true, None);
let menu = Menu::new();
menu.append(&open).map_err(|err| err.to_string())?;
menu.append(&PredefinedMenuItem::separator())
.map_err(|err| err.to_string())?;
menu.append(&quit).map_err(|err| err.to_string())?;
#[cfg(target_os = "linux")]
let (open_id, quit_id) = {
let (ready_tx, ready_rx) = mpsc::sync_channel(1);
std::thread::Builder::new()
.name("tray-gtk".into())
.spawn(move || {
if let Err(err) = gtk::init() {
let _ = ready_tx.send(Err(format!("cannot initialise GTK: {err}")));
return;
}
match build() {
Ok(built) => {
let _ = ready_tx.send(Ok((built.open, built.quit)));
gtk::main();
drop(built.icon);
}
Err(err) => {
let _ = ready_tx.send(Err(err));
}
}
})
.map_err(|err| err.to_string())?;
ready_rx
.recv()
.map_err(|_| "the tray thread exited before it was ready".to_string())??
};
#[cfg(not(target_os = "linux"))]
let (icon, open_id, quit_id) = {
let built = build()?;
(built.icon, built.open, built.quit)
};
let icon = make_icon()?;
let tray = TrayIconBuilder::new()
.with_tooltip("tsunagi")
.with_menu(Box::new(menu))
.with_icon(icon)
.build()
.map_err(|err| err.to_string())?;
let open_id = open.id().clone();
let quit_id = quit.id().clone();
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
// The global menu-event channel is fed by the OS event loop.
@@ -69,7 +116,8 @@ impl Tray {
});
Ok(Self {
_icon: tray,
#[cfg(not(target_os = "linux"))]
_icon: icon,
actions: rx,
})
}