From f63343a6a07a6f0e7ca18ef9b7b257c12e41d242 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Fri, 2 Oct 2026 16:26:45 +0100 Subject: [PATCH] =?UTF-8?q?CI:=20ship=20the=20tray=20GUI=20=E2=80=94=20in?= =?UTF-8?q?=20the=20archives=20and=20as=20tsunagi-gui=20deb/zst?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitea/workflows/release.yml | 46 +++++++++++++++++++++++++++ .github/workflows/release.yml | 46 +++++++++++++++++++++++++++ crates/tsunagi/src/ipc/mod.rs | 9 +++++- dist/arch/build-pkg.sh | 48 +++++++++++++++++++--------- dist/deb/build-deb.sh | 56 +++++++++++++++++++++++++-------- dist/linux/tsunagi-tray.desktop | 11 +++++++ dist/linux/tsunagi.svg | 6 ++++ 7 files changed, 194 insertions(+), 28 deletions(-) create mode 100644 dist/linux/tsunagi-tray.desktop create mode 100644 dist/linux/tsunagi.svg diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index f7c69fd..fad726a 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -36,6 +36,7 @@ jobs: deb: true deb_arch: amd64 pkg_arch: x86_64 + gnu_target: x86_64-unknown-linux-gnu - os: ubuntu-latest target: aarch64-unknown-linux-musl asset: tsunagi-linux-aarch64 @@ -44,6 +45,7 @@ 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 @@ -102,6 +104,22 @@ jobs: cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi 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 shell: bash run: | @@ -115,6 +133,15 @@ 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. + 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 # files, so the archive carries what the platform needs to set up. cp README.md "$stage/" @@ -171,6 +198,25 @@ jobs: "$version" \ "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 # what makes the archive (and the .deb, where built) show up under the run # and hands it to `publish`. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5d829de..188c7ba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,7 @@ jobs: deb: true deb_arch: amd64 pkg_arch: x86_64 + gnu_target: x86_64-unknown-linux-gnu - os: ubuntu-latest target: aarch64-unknown-linux-musl asset: tsunagi-linux-aarch64 @@ -46,6 +47,7 @@ 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 @@ -104,6 +106,22 @@ jobs: cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi 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 shell: bash run: | @@ -117,6 +135,15 @@ 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. + 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 # files, so the archive carries what the platform needs to set up. cp README.md "$stage/" @@ -173,6 +200,25 @@ jobs: "$version" \ "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 uses: actions/upload-artifact@v7 with: diff --git a/crates/tsunagi/src/ipc/mod.rs b/crates/tsunagi/src/ipc/mod.rs index 1e78c13..de51bf9 100644 --- a/crates/tsunagi/src/ipc/mod.rs +++ b/crates/tsunagi/src/ipc/mod.rs @@ -148,10 +148,17 @@ pub fn default_control_socket_path() -> PathBuf { } #[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() { control_socket_path(&paths.state_dir) } else { - PathBuf::from("/run/tsunagi/agent.sock") + system } } } diff --git a/dist/arch/build-pkg.sh b/dist/arch/build-pkg.sh index 8c0153c..662f150 100755 --- a/dist/arch/build-pkg.sh +++ b/dist/arch/build-pkg.sh @@ -1,13 +1,15 @@ #!/usr/bin/env bash -# Assemble an Arch Linux package (.pkg.tar.zst) from a prebuilt static tsunagi -# binary, using only bsdtar + zstd on a non-Arch host — no makepkg, no Arch -# container. The binary is static, so the package has no dependencies. +# Assemble an Arch Linux package (.pkg.tar.zst) from prebuilt tsunagi binaries, +# using only bsdtar + zstd on a non-Arch host — no makepkg, no Arch container. # -# Usage: build-pkg.sh -# path to the built tsunagi executable -# pacman architecture: x86_64 or aarch64 -# package version (the tag without its leading v) -# path to write the package to +# Usage: build-pkg.sh [tray_binary] +# path to the built tsunagi (agent/CLI) executable +# pacman architecture: x86_64 or aarch64 +# package version (the tag without its leading v) +# 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. set -euo pipefail @@ -16,6 +18,7 @@ binary="$1" arch="$2" version="$3" output="$4" +tray="${5:-}" here="$(cd "$(dirname "$0")" && pwd)" 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/../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. size="$(du -sb "$root" | cut -f1)" cat > "$root/.PKGINFO" < size = ${size} arch = ${arch} -license = MIT +${relations}license = MIT license = Apache PKGINFO -# Scriptlet: create the dedicated system user the service runs as, and keep -# systemd in step. The "tsunagi" user is in the "tsunagi" group, which the -# polkit rule grants the resolver actions. +# Scriptlet: create the dedicated system user the service runs as, keep systemd +# in step, and refresh the desktop/icon caches (a no-op without a GUI payload). cat > "$root/.INSTALL" <<'INSTALL' post_install() { getent group tsunagi >/dev/null 2>&1 || groupadd -r tsunagi getent passwd tsunagi >/dev/null 2>&1 || \ useradd -r -g tsunagi -d /var/lib/tsunagi -s /usr/bin/nologin -c "tsunagi agent" tsunagi 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_install diff --git a/dist/deb/build-deb.sh b/dist/deb/build-deb.sh index 9e44b51..87ea3b2 100755 --- a/dist/deb/build-deb.sh +++ b/dist/deb/build-deb.sh @@ -1,19 +1,22 @@ #!/usr/bin/env bash -# Assemble a dependency-free Debian package from a prebuilt static tsunagi -# binary, using only dpkg-deb (no cargo-deb, no nfpm). The binary is statically -# linked, so the package declares no Depends and runs on any Debian/Ubuntu. +# Assemble a dependency-free Debian package from prebuilt tsunagi binaries, +# using only dpkg-deb (no cargo-deb, no nfpm). # -# Usage: build-deb.sh -# path to the built tsunagi executable -# Debian architecture: amd64 or arm64 -# package version (the tag without its leading v) -# path to write the .deb to +# Usage: build-deb.sh [tray_binary] +# path to the built tsunagi (agent/CLI) executable +# Debian architecture: amd64 or arm64 +# package version (the tag without its leading v) +# 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 binary="$1" arch="$2" version="$3" output="$4" +tray="${5:-}" here="$(cd "$(dirname "$0")" && pwd)" 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/../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" cat > "$root/DEBIAN/control" < Section: net Priority: optional 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 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, - and a polkit rule for the local DNS resolver. + and a polkit rule for the local DNS resolver.${desc_gui} CTRL # 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 systemctl daemon-reload >/dev/null 2>&1 || true fi -exit 0 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' #!/bin/sh set -e diff --git a/dist/linux/tsunagi-tray.desktop b/dist/linux/tsunagi-tray.desktop new file mode 100644 index 0000000..c22e764 --- /dev/null +++ b/dist/linux/tsunagi-tray.desktop @@ -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 diff --git a/dist/linux/tsunagi.svg b/dist/linux/tsunagi.svg new file mode 100644 index 0000000..68890a8 --- /dev/null +++ b/dist/linux/tsunagi.svg @@ -0,0 +1,6 @@ + + + + + +