CI: ship the tray GUI — in the archives and as tsunagi-gui deb/zst
Release / tsunagi-linux-aarch64 (push) Failing after 2m40s
Release / tsunagi-linux-x86_64 (push) Failing after 2m40s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s

Adds the tray application to the release:

- Every target's archive now also contains the tsunagi-tray binary. On Linux it
  is built against an old glibc baseline (via cargo-zigbuild) rather than static
  musl, because a GUI must dlopen libGL/X11/Wayland at runtime and a static musl
  binary cannot; the agent stays static musl. macOS and Windows build the tray
  natively.
- New GUI package variants for Linux: `tsunagi-gui` .deb and .pkg.tar.zst, built
  by the existing scripts with an extra tray argument. They install the agent
  (service, polkit, user — same as the headless packages) plus the tray binary,
  a desktop launcher (dist/linux/tsunagi-tray.desktop) and an icon
  (dist/linux/tsunagi.svg), so the GUI appears in the applications menu. The GUI
  package conflicts/replaces/provides the headless tsunagi so the two do not
  coexist.
- The tray finds the system service with no configuration: the library's default
  control-socket path now prefers /run/tsunagi/agent.sock when present (both the
  tray and CLI use this), falling back to the per-user path.

No control-protocol change. Builds, fmt and clippy clean on macOS; the Linux GUI
cross-build and the packages are exercised only by a real CI run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
UltradesuandClaude Opus 4.8 committed 2026-10-02 16:26:45 +01:00
1 parent d13bb00623
commit f63343a6a0
7 files changed
+194 -28

No files matched your search

+46
View File
@@ -36,6 +36,7 @@ jobs:
deb: true deb: true
deb_arch: amd64 deb_arch: amd64
pkg_arch: x86_64 pkg_arch: x86_64
gnu_target: x86_64-unknown-linux-gnu
- os: ubuntu-latest - os: ubuntu-latest
target: aarch64-unknown-linux-musl target: aarch64-unknown-linux-musl
asset: tsunagi-linux-aarch64 asset: tsunagi-linux-aarch64
@@ -44,6 +45,7 @@ jobs:
deb: true deb: true
deb_arch: arm64 deb_arch: arm64
pkg_arch: aarch64 pkg_arch: aarch64
gnu_target: aarch64-unknown-linux-gnu
- os: macos-latest - os: macos-latest
target: aarch64-apple-darwin target: aarch64-apple-darwin
asset: tsunagi-macos-aarch64 asset: tsunagi-macos-aarch64
@@ -102,6 +104,22 @@ jobs:
cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi
fi fi
# The tray GUI. On Linux it is built against glibc (an old baseline for
# portability), not musl: a GUI must dlopen libGL/X11/Wayland at runtime,
# which a static musl binary cannot. macOS and Windows build it natively.
- name: Build the tray GUI
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
else
cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi-tray
fi
- name: Package - name: Package
shell: bash shell: bash
run: | run: |
@@ -115,6 +133,15 @@ jobs:
[[ "${{ runner.os }}" == "Windows" ]] && bin="tsunagi.exe" [[ "${{ runner.os }}" == "Windows" ]] && bin="tsunagi.exe"
cp "target/${{ matrix.target }}/release/${bin}" "$stage/" cp "target/${{ matrix.target }}/release/${bin}" "$stage/"
# The tray GUI alongside it: glibc build on Linux, native elsewhere.
tray="tsunagi-tray"
[[ "${{ runner.os }}" == "Windows" ]] && tray="tsunagi-tray.exe"
if [[ "${{ runner.os }}" == "Linux" ]]; then
cp "target/${{ matrix.gnu_target }}/release/${tray}" "$stage/"
else
cp "target/${{ matrix.target }}/release/${tray}" "$stage/"
fi
# The top-level README, plus the per-OS install notes and integration # The top-level README, plus the per-OS install notes and integration
# files, so the archive carries what the platform needs to set up. # files, so the archive carries what the platform needs to set up.
cp README.md "$stage/" cp README.md "$stage/"
@@ -171,6 +198,25 @@ jobs:
"$version" \ "$version" \
"tsunagi-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst" "tsunagi-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst"
# 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
shell: bash
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
pkgver="${version//-/_}"
tray="target/${{ matrix.gnu_target }}/release/tsunagi-tray"
bash dist/deb/build-deb.sh \
"target/${{ matrix.target }}/release/tsunagi" \
"${{ matrix.deb_arch }}" "$version" \
"tsunagi-gui_${version}_${{ matrix.deb_arch }}.deb" "$tray"
bash dist/arch/build-pkg.sh \
"target/${{ matrix.target }}/release/tsunagi" \
"${{ matrix.pkg_arch }}" "$version" \
"tsunagi-gui-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst" "$tray"
# @v3 on purpose: Gitea does not support the v4 artifact backend. This is # @v3 on purpose: Gitea does not support the v4 artifact backend. This is
# what makes the archive (and the .deb, where built) show up under the run # what makes the archive (and the .deb, where built) show up under the run
# and hands it to `publish`. # and hands it to `publish`.
+46
View File
@@ -38,6 +38,7 @@ jobs:
deb: true deb: true
deb_arch: amd64 deb_arch: amd64
pkg_arch: x86_64 pkg_arch: x86_64
gnu_target: x86_64-unknown-linux-gnu
- os: ubuntu-latest - os: ubuntu-latest
target: aarch64-unknown-linux-musl target: aarch64-unknown-linux-musl
asset: tsunagi-linux-aarch64 asset: tsunagi-linux-aarch64
@@ -46,6 +47,7 @@ jobs:
deb: true deb: true
deb_arch: arm64 deb_arch: arm64
pkg_arch: aarch64 pkg_arch: aarch64
gnu_target: aarch64-unknown-linux-gnu
- os: macos-latest - os: macos-latest
target: aarch64-apple-darwin target: aarch64-apple-darwin
asset: tsunagi-macos-aarch64 asset: tsunagi-macos-aarch64
@@ -104,6 +106,22 @@ jobs:
cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi
fi fi
# The tray GUI. On Linux it is built against glibc (an old baseline for
# portability), not musl: a GUI must dlopen libGL/X11/Wayland at runtime,
# which a static musl binary cannot. macOS and Windows build it natively.
- name: Build the tray GUI
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
else
cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi-tray
fi
- name: Package - name: Package
shell: bash shell: bash
run: | run: |
@@ -117,6 +135,15 @@ jobs:
[[ "${{ runner.os }}" == "Windows" ]] && bin="tsunagi.exe" [[ "${{ runner.os }}" == "Windows" ]] && bin="tsunagi.exe"
cp "target/${{ matrix.target }}/release/${bin}" "$stage/" cp "target/${{ matrix.target }}/release/${bin}" "$stage/"
# The tray GUI alongside it: glibc build on Linux, native elsewhere.
tray="tsunagi-tray"
[[ "${{ runner.os }}" == "Windows" ]] && tray="tsunagi-tray.exe"
if [[ "${{ runner.os }}" == "Linux" ]]; then
cp "target/${{ matrix.gnu_target }}/release/${tray}" "$stage/"
else
cp "target/${{ matrix.target }}/release/${tray}" "$stage/"
fi
# The top-level README, plus the per-OS install notes and integration # The top-level README, plus the per-OS install notes and integration
# files, so the archive carries what the platform needs to set up. # files, so the archive carries what the platform needs to set up.
cp README.md "$stage/" cp README.md "$stage/"
@@ -173,6 +200,25 @@ jobs:
"$version" \ "$version" \
"tsunagi-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst" "tsunagi-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst"
# 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
shell: bash
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
pkgver="${version//-/_}"
tray="target/${{ matrix.gnu_target }}/release/tsunagi-tray"
bash dist/deb/build-deb.sh \
"target/${{ matrix.target }}/release/tsunagi" \
"${{ matrix.deb_arch }}" "$version" \
"tsunagi-gui_${version}_${{ matrix.deb_arch }}.deb" "$tray"
bash dist/arch/build-pkg.sh \
"target/${{ matrix.target }}/release/tsunagi" \
"${{ matrix.pkg_arch }}" "$version" \
"tsunagi-gui-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst" "$tray"
- name: Upload the build outputs as run artifacts - name: Upload the build outputs as run artifacts
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
+8 -1
View File
@@ -148,10 +148,17 @@ pub fn default_control_socket_path() -> PathBuf {
} }
#[cfg(all(unix, not(target_os = "macos")))] #[cfg(all(unix, not(target_os = "macos")))]
{ {
// Prefer a running system service's socket when it is present, so a
// GUI or CLI reaches the packaged daemon with no configuration; fall
// back to the per-user location otherwise.
let system = PathBuf::from("/run/tsunagi/agent.sock");
if system.exists() {
return system;
}
if let Ok(paths) = crate::config::StoragePaths::user_default() { if let Ok(paths) = crate::config::StoragePaths::user_default() {
control_socket_path(&paths.state_dir) control_socket_path(&paths.state_dir)
} else { } else {
PathBuf::from("/run/tsunagi/agent.sock") system
} }
} }
} }
+34 -14
View File
@@ -1,13 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Assemble an Arch Linux package (.pkg.tar.zst) from a prebuilt static tsunagi # Assemble an Arch Linux package (.pkg.tar.zst) from prebuilt tsunagi binaries,
# binary, using only bsdtar + zstd on a non-Arch host — no makepkg, no Arch # using only bsdtar + zstd on a non-Arch host — no makepkg, no Arch container.
# container. The binary is static, so the package has no dependencies.
# #
# Usage: build-pkg.sh <binary> <arch> <version> <output.pkg.tar.zst> # Usage: build-pkg.sh <binary> <arch> <version> <output.pkg.tar.zst> [tray_binary]
# <binary> path to the built tsunagi executable # <binary> path to the built tsunagi (agent/CLI) executable
# <arch> pacman architecture: x86_64 or aarch64 # <arch> pacman architecture: x86_64 or aarch64
# <version> package version (the tag without its leading v) # <version> package version (the tag without its leading v)
# <output> path to write the package to # <output> path to write the package to
# [tray_binary] optional tsunagi-tray GUI executable; when given, the package
# also installs the tray and a desktop launcher and is named
# tsunagi-gui (conflicting with the headless tsunagi).
# #
# Needs bsdtar (libarchive-tools) and zstd on the builder. # Needs bsdtar (libarchive-tools) and zstd on the builder.
set -euo pipefail set -euo pipefail
@@ -16,6 +18,7 @@ binary="$1"
arch="$2" arch="$2"
version="$3" version="$3"
output="$4" output="$4"
tray="${5:-}"
here="$(cd "$(dirname "$0")" && pwd)" here="$(cd "$(dirname "$0")" && pwd)"
root="$(mktemp -d)" root="$(mktemp -d)"
@@ -40,31 +43,48 @@ install -D -m 0644 "$here/../linux/50-tsunagi-resolved.rules" \
install -D -m 0644 "$here/../../README.md" "$root/usr/share/doc/tsunagi/README.md" install -D -m 0644 "$here/../../README.md" "$root/usr/share/doc/tsunagi/README.md"
install -D -m 0644 "$here/../linux/README.md" "$root/usr/share/doc/tsunagi/INSTALL.md" install -D -m 0644 "$here/../linux/README.md" "$root/usr/share/doc/tsunagi/INSTALL.md"
pkgname=tsunagi
pkgdesc="Serverless private mesh networking agent"
relations=""
if [ -n "$tray" ]; then
pkgname=tsunagi-gui
pkgdesc="Serverless private mesh networking agent, with the tray GUI"
relations=$'conflict = tsunagi\nprovides = tsunagi\nreplaces = tsunagi\n'
install -D -m 0755 "$tray" "$root/usr/bin/tsunagi-tray"
install -D -m 0644 "$here/../linux/tsunagi-tray.desktop" \
"$root/usr/share/applications/tsunagi-tray.desktop"
install -D -m 0644 "$here/../linux/tsunagi.svg" \
"$root/usr/share/icons/hicolor/scalable/apps/tsunagi.svg"
fi
# Installed size (payload only), before the metadata files are written. # Installed size (payload only), before the metadata files are written.
size="$(du -sb "$root" | cut -f1)" size="$(du -sb "$root" | cut -f1)"
cat > "$root/.PKGINFO" <<PKGINFO cat > "$root/.PKGINFO" <<PKGINFO
pkgname = tsunagi pkgname = ${pkgname}
pkgver = ${pkgver}-${pkgrel} pkgver = ${pkgver}-${pkgrel}
pkgdesc = Serverless private mesh networking agent pkgdesc = ${pkgdesc}
url = https://github.com/Ultradesu/tsunagi url = https://github.com/Ultradesu/tsunagi
builddate = $(date +%s) builddate = $(date +%s)
packager = AB <ab@hexor.cy> packager = AB <ab@hexor.cy>
size = ${size} size = ${size}
arch = ${arch} arch = ${arch}
license = MIT ${relations}license = MIT
license = Apache license = Apache
PKGINFO PKGINFO
# Scriptlet: create the dedicated system user the service runs as, and keep # Scriptlet: create the dedicated system user the service runs as, keep systemd
# systemd in step. The "tsunagi" user is in the "tsunagi" group, which the # in step, and refresh the desktop/icon caches (a no-op without a GUI payload).
# polkit rule grants the resolver actions.
cat > "$root/.INSTALL" <<'INSTALL' cat > "$root/.INSTALL" <<'INSTALL'
post_install() { post_install() {
getent group tsunagi >/dev/null 2>&1 || groupadd -r tsunagi getent group tsunagi >/dev/null 2>&1 || groupadd -r tsunagi
getent passwd tsunagi >/dev/null 2>&1 || \ getent passwd tsunagi >/dev/null 2>&1 || \
useradd -r -g tsunagi -d /var/lib/tsunagi -s /usr/bin/nologin -c "tsunagi agent" tsunagi useradd -r -g tsunagi -d /var/lib/tsunagi -s /usr/bin/nologin -c "tsunagi agent" tsunagi
systemctl daemon-reload >/dev/null 2>&1 || true systemctl daemon-reload >/dev/null 2>&1 || true
command -v update-desktop-database >/dev/null 2>&1 && \
update-desktop-database -q /usr/share/applications >/dev/null 2>&1 || true
command -v gtk-update-icon-cache >/dev/null 2>&1 && \
gtk-update-icon-cache -q -t /usr/share/icons/hicolor >/dev/null 2>&1 || true
} }
post_upgrade() { post_upgrade() {
post_install post_install
+43 -13
View File
@@ -1,19 +1,22 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Assemble a dependency-free Debian package from a prebuilt static tsunagi # Assemble a dependency-free Debian package from prebuilt tsunagi binaries,
# binary, using only dpkg-deb (no cargo-deb, no nfpm). The binary is statically # using only dpkg-deb (no cargo-deb, no nfpm).
# linked, so the package declares no Depends and runs on any Debian/Ubuntu.
# #
# Usage: build-deb.sh <binary> <deb_arch> <version> <output.deb> # Usage: build-deb.sh <binary> <deb_arch> <version> <output.deb> [tray_binary]
# <binary> path to the built tsunagi executable # <binary> path to the built tsunagi (agent/CLI) executable
# <deb_arch> Debian architecture: amd64 or arm64 # <deb_arch> Debian architecture: amd64 or arm64
# <version> package version (the tag without its leading v) # <version> package version (the tag without its leading v)
# <output> path to write the .deb to # <output> path to write the .deb to
# [tray_binary] optional path to the tsunagi-tray GUI executable; when given,
# the package also installs the tray and a desktop launcher and
# is named tsunagi-gui (conflicting with the headless tsunagi).
set -euo pipefail set -euo pipefail
binary="$1" binary="$1"
arch="$2" arch="$2"
version="$3" version="$3"
output="$4" output="$4"
tray="${5:-}"
here="$(cd "$(dirname "$0")" && pwd)" here="$(cd "$(dirname "$0")" && pwd)"
root="$(mktemp -d)" root="$(mktemp -d)"
@@ -34,22 +37,38 @@ install -D -m 0644 "$here/../linux/50-tsunagi-resolved.rules" \
install -D -m 0644 "$here/../../README.md" "$root/usr/share/doc/tsunagi/README.md" install -D -m 0644 "$here/../../README.md" "$root/usr/share/doc/tsunagi/README.md"
install -D -m 0644 "$here/../linux/README.md" "$root/usr/share/doc/tsunagi/INSTALL.md" install -D -m 0644 "$here/../linux/README.md" "$root/usr/share/doc/tsunagi/INSTALL.md"
pkgname=tsunagi
extra_control=""
desc_gui=""
if [ -n "$tray" ]; then
pkgname=tsunagi-gui
# The GUI package is a superset of the headless one and must not coexist
# with it (both ship /usr/bin/tsunagi).
extra_control=$'Conflicts: tsunagi\nReplaces: tsunagi\nProvides: tsunagi\n'
desc_gui=" It also installs the tray GUI (tsunagi-tray) and a desktop launcher so it can be started from the applications menu."
install -D -m 0755 "$tray" "$root/usr/bin/tsunagi-tray"
install -D -m 0644 "$here/../linux/tsunagi-tray.desktop" \
"$root/usr/share/applications/tsunagi-tray.desktop"
install -D -m 0644 "$here/../linux/tsunagi.svg" \
"$root/usr/share/icons/hicolor/scalable/apps/tsunagi.svg"
fi
mkdir -p "$root/DEBIAN" mkdir -p "$root/DEBIAN"
cat > "$root/DEBIAN/control" <<CTRL cat > "$root/DEBIAN/control" <<CTRL
Package: tsunagi Package: ${pkgname}
Version: ${version} Version: ${version}
Architecture: ${arch} Architecture: ${arch}
Maintainer: AB <ab@hexor.cy> Maintainer: AB <ab@hexor.cy>
Section: net Section: net
Priority: optional Priority: optional
Homepage: https://github.com/Ultradesu/tsunagi Homepage: https://github.com/Ultradesu/tsunagi
Description: Serverless private mesh networking agent ${extra_control}Description: Serverless private mesh networking agent
tsunagi forms small private mesh IP networks between peers, with NAT tsunagi forms small private mesh IP networks between peers, with NAT
traversal and no central server. This package installs a static, traversal and no central server. This package installs a static,
dependency-free binary, a systemd service that runs the agent as a dedicated dependency-free agent, a systemd service that runs it as a dedicated
unprivileged "tsunagi" user with only CAP_NET_ADMIN and CAP_NET_BIND_SERVICE, unprivileged "tsunagi" user with only CAP_NET_ADMIN and CAP_NET_BIND_SERVICE,
and a polkit rule for the local DNS resolver. and a polkit rule for the local DNS resolver.${desc_gui}
CTRL CTRL
# The dedicated system user the service runs as. It is in the "tsunagi" group, # The dedicated system user the service runs as. It is in the "tsunagi" group,
@@ -68,9 +87,20 @@ fi
if [ -d /run/systemd/system ]; then if [ -d /run/systemd/system ]; then
systemctl daemon-reload >/dev/null 2>&1 || true systemctl daemon-reload >/dev/null 2>&1 || true
fi fi
exit 0
POSTINST POSTINST
if [ -n "$tray" ]; then
cat >> "$root/DEBIAN/postinst" <<'POSTINST_GUI'
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q /usr/share/applications >/dev/null 2>&1 || true
fi
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -q -t /usr/share/icons/hicolor >/dev/null 2>&1 || true
fi
POSTINST_GUI
fi
echo "exit 0" >> "$root/DEBIAN/postinst"
cat > "$root/DEBIAN/prerm" <<'PRERM' cat > "$root/DEBIAN/prerm" <<'PRERM'
#!/bin/sh #!/bin/sh
set -e set -e
+11
View File
@@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=tsunagi
GenericName=Mesh network manager
Comment=Manage tsunagi networks from the tray
Exec=tsunagi-tray
Icon=tsunagi
Terminal=false
Categories=Network;Utility;
Keywords=mesh;vpn;overlay;network;
StartupNotify=false
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<circle cx="64" cy="64" r="56" fill="#2f80d8"/>
<circle cx="64" cy="64" r="30" fill="none" stroke="#ffffff" stroke-width="7" opacity="0.9"/>
<circle cx="64" cy="64" r="9" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 328 B