Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 82 additions & 6 deletions .github/workflows/voltage-buzz-gpui-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ on:
- apps/buzz-gpui/.github/actions/**
- apps/buzz-gpui/scripts/package-*
- apps/buzz-gpui/scripts/build-package.sh
- apps/buzz-gpui/scripts/*-appimage.sh
# What the AppImage and --gst-check lanes exercise in the app.
- apps/buzz-gpui/crates/app/src/appimage.rs
- apps/buzz-gpui/crates/app/src/gst_bundle.rs
- apps/buzz-gpui/scripts/check-remote-provider.mjs
- apps/buzz-gpui/scripts/check-remote-provider.test.mjs
- apps/buzz-gpui/scripts/test-package-build.py
Expand Down Expand Up @@ -79,23 +83,40 @@ jobs:
echo "version=$upstream+voltage.$(( ${last:-0} + 1 ))" >> "$GITHUB_OUTPUT"

linux:
name: GPUI Linux x86_64 (.deb)
name: GPUI Linux ${{ matrix.arch }} (.deb, .AppImage)
needs: [version, package-contract]
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
arch: x86_64
- runner: ubuntu-24.04-arm
arch: aarch64
env:
BUZZ_GPUI_RELEASE_VERSION: ${{ needs.version.outputs.version }}
BUZZ_GPUI_APPIMAGE: "1"
defaults:
run:
working-directory: apps/buzz-gpui
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
# Builds inside ubuntu:26.04, so the package links Ubuntu's libraries.
# Builds inside ubuntu:24.04, so the packages link Ubuntu's libraries
# and need only its glibc.
- run: scripts/package-deb.sh
working-directory: apps/buzz-gpui
# The AppImage runs on other distributions, with only what it carries
# and the desktop libraries every system has.
- run: scripts/check-appimage.sh dist/*.AppImage
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: buzz-gpui-linux-x86_64
path: apps/buzz-gpui/dist/*.deb
name: buzz-gpui-linux-${{ matrix.arch }}
path: |
apps/buzz-gpui/dist/*.deb
apps/buzz-gpui/dist/*.AppImage
if-no-files-found: error

macos:
Expand Down Expand Up @@ -214,3 +235,58 @@ jobs:
--title "Buzz GPUI ${TAG#v}" --notes "$notes" --draft
gh release upload "$TAG" dist/* --repo "$GITHUB_REPOSITORY"
gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --draft=false --latest

# Nix users install a release through the flake's buzz-gpui-voltage-bin,
# which pins the release's AppImages in apps/buzz-gpui/nix/release.json.
# main takes changes only through reviewed pull requests, so this proposes
# the new pin as one. A job of its own, so a failure here can be re-run
# without publishing the release again.
nix-pin:
name: GPUI pin the release for Nix
if: github.event_name == 'workflow_dispatch'
needs: [version, release]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: dist
pattern: buzz-gpui-linux-*
merge-multiple: true
- run: python3 apps/buzz-gpui/scripts/pin-nix-release.py "$VERSION" "$GITHUB_REPOSITORY" dist
env:
VERSION: ${{ needs.version.outputs.version }}
# A commit made through the API is signed by GitHub, as main requires.
- env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.version.outputs.version }}
run: |
branch="chore/gpui-nix-pin-v$VERSION"
title="chore(gpui): pin Buzz GPUI $VERSION for Nix"
# A re-run starts the branch, which only this job writes, again.
gh api -X DELETE "repos/$GITHUB_REPOSITORY/git/refs/heads/$branch" >/dev/null 2>&1 || true
gh api "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/heads/$branch" -f sha="$GITHUB_SHA" >/dev/null
jq -n \
--arg repository "$GITHUB_REPOSITORY" --arg branch "$branch" --arg head "$GITHUB_SHA" \
--arg headline "$title" \
--arg contents "$(base64 -w0 apps/buzz-gpui/nix/release.json)" \
'{
query: "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid } } }",
variables: { input: {
branch: { repositoryNameWithOwner: $repository, branchName: $branch },
expectedHeadOid: $head,
message: {
headline: $headline,
body: "Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
},
fileChanges: { additions: [ { path: "apps/buzz-gpui/nix/release.json", contents: $contents } ] }
} }
}' | gh api graphql --input - >/dev/null
gh pr create --repo "$GITHUB_REPOSITORY" --base main --head "$branch" --title "$title" \
--body "Points the flake's \`buzz-gpui-voltage-bin\` at the AppImages of [v$VERSION]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/v$VERSION). Generated by the release workflow."
48 changes: 44 additions & 4 deletions apps/buzz-gpui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ See [Move existing agents to Remote workspace](docs/remote-agent-migration.md) f
sudo apt install ./buzz-gpui_0.1.0-1_amd64.deb
```

Build the package with `scripts/package-deb.sh`. It builds inside an
`ubuntu:26.04` container and writes `dist/buzz-gpui_<version>_amd64.deb`.
The package installs on Ubuntu 24.04, Debian 13 and later, on x86_64
(`amd64`) or ARM (`arm64`). Build it with `scripts/package-deb.sh`. It builds
inside an `ubuntu:24.04` container for the machine's own architecture and
writes `dist/buzz-gpui_<version>_<arch>.deb`.

On first start the app reads your identity from the keyring. It uses the
`dev.local.buzz.foundation.identity` entry. If that entry is empty, it copies
Expand All @@ -35,6 +37,43 @@ tray icons through the AppIndicator extension, which Ubuntu turns on.
Agents on the laptop run inside the app. Their keys are kept in the system
keyring, and they stop when the app quits.

## Laptop: install the app (other Linux)

`Buzz-<version>-linux-x86_64.AppImage` (or `-aarch64`) runs on any Linux
with glibc 2.39 or later, for example Fedora 40 and later. It carries the
GStreamer libraries and plugins it plays audio and video with, plus the
GIO TLS backend for HTTPS media. It takes
the desktop libraries every system has (X11, fontconfig, ALSA) from the
system. Its static runtime does not require the host `libfuse2` library.

```sh
chmod +x Buzz-<version>-linux-x86_64.AppImage
./Buzz-<version>-linux-x86_64.AppImage
```

If FUSE mounting is unavailable, extract and run it instead:

```sh
./Buzz-<version>-linux-x86_64.AppImage --appimage-extract-and-run
```

It registers itself for `buzz://` links, and its update notice downloads
the next AppImage. Ubuntu's GStreamer has no AV1 decoder, so the AppImage
does not play AV1 video.

Build it with `BUZZ_GPUI_APPIMAGE=1 scripts/package-deb.sh`, which writes
the AppImage beside the `.deb` from the same build.
`scripts/check-appimage.sh <AppImage>` then runs it on Ubuntu, Debian and
Fedora containers that have no GStreamer or GIO TLS backend: every library
it loads must be in the image or a desktop library, and it must play local
and HTTPS media.

On NixOS, which has no `/lib64` loader, install the flake's
`buzz-gpui-voltage-bin` instead. It runs the release AppImage in nixpkgs'
`appimageTools` environment (`nix/appimage.nix`). `nix/release.json` pins
the AppImages; after each release, Voltage's release workflow opens a pull
request with the new pin (`scripts/pin-nix-release.py`).

## Laptop: install the app (macOS)

The macOS build is not signed or notarized. It carries the GStreamer
Expand Down Expand Up @@ -87,8 +126,9 @@ The ones you may notice:
Code there; install them yourself and Buzz finds them on `PATH`.
- On Windows, hosts and `buzzd` listen and connect over `tcp://` only, not
`unix://`.
- HEIC avatars need `ffmpeg`: `brew install ffmpeg` on macOS, and on
Windows an `ffmpeg.exe` on `PATH`.
- HEIC avatars need `ffmpeg`: `brew install ffmpeg` on macOS, on Windows
an `ffmpeg.exe` on `PATH`, and with the AppImage the distribution's
`ffmpeg` package.

### Builds

Expand Down
78 changes: 78 additions & 0 deletions apps/buzz-gpui/crates/app/src/appimage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Whether this copy runs from an AppImage, and which file that is.
//!
//! The AppImage runtime mounts the image at a temporary directory, sets
//! `APPDIR` to it and `APPIMAGE` to the image file, then starts the binary
//! inside. The mount goes away when the app exits, so whatever outlives this
//! process (the `buzz://` handler, the next version's download) names the
//! image, never the running binary.

use std::ffi::OsString;
use std::path::{Path, PathBuf};

/// The AppImage file this process runs from, if it runs from one.
pub fn path() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
// The executable's path is canonical; the mount's is as the runtime
// built it from TMPDIR, which can run through a symlink.
let appdir = std::env::var_os("APPDIR")
.map(|dir| std::fs::canonicalize(&dir).map_or(dir, PathBuf::into_os_string));
from_env(std::env::var_os("APPIMAGE"), appdir, &exe)
}

/// `appimage`, when `exe` lies inside `appdir`. A program started from
/// inside some other AppImage (a terminal, say) inherits both variables,
/// but its binary is elsewhere.
fn from_env(appimage: Option<OsString>, appdir: Option<OsString>, exe: &Path) -> Option<PathBuf> {
let appimage = PathBuf::from(appimage?);
let appdir = PathBuf::from(appdir?);
(appimage.is_absolute() && appdir.is_absolute() && exe.starts_with(&appdir)).then_some(appimage)
}

#[cfg(test)]
mod tests {
use super::*;

fn env(appimage: Option<&str>, appdir: Option<&str>, exe: &str) -> Option<PathBuf> {
from_env(
appimage.map(Into::into),
appdir.map(Into::into),
Path::new(exe),
)
}

#[test]
fn names_the_image_the_binary_runs_from() {
assert_eq!(
env(
Some("/home/me/Apps/Buzz.AppImage"),
Some("/tmp/.mount_BuzzAb12"),
"/tmp/.mount_BuzzAb12/usr/bin/buzz-gpui",
),
Some(PathBuf::from("/home/me/Apps/Buzz.AppImage"))
);
}

#[test]
fn ignores_variables_inherited_from_another_image() {
assert_eq!(
env(
Some("/home/me/Apps/Terminal.AppImage"),
Some("/tmp/.mount_TermCd34"),
"/usr/bin/buzz-gpui",
),
None
);
}

#[test]
fn needs_both_variables_as_absolute_paths() {
let exe = "/tmp/.mount_BuzzAb12/usr/bin/buzz-gpui";
assert_eq!(env(None, Some("/tmp/.mount_BuzzAb12"), exe), None);
assert_eq!(env(Some("/home/me/Buzz.AppImage"), None, exe), None);
assert_eq!(
env(Some("Buzz.AppImage"), Some("/tmp/.mount_BuzzAb12"), exe),
None
);
assert_eq!(env(Some("/home/me/Buzz.AppImage"), Some(""), exe), None);
}
}
50 changes: 46 additions & 4 deletions apps/buzz-gpui/crates/app/src/deep_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,9 @@ pub fn register_scheme() {
/// Packaged installs register through their desktop file.
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn register_desktop_file() {
let Ok(exe) = std::env::current_exe() else {
let Some(exe) = handler_target(crate::appimage::path(), std::env::current_exe().ok()) else {
return;
};
if exe.starts_with("/usr") || exe.starts_with("/opt") || exe.starts_with("/nix/store") {
return;
}
let Some(applications) = dirs::data_dir().map(|dir| dir.join("applications")) else {
return;
};
Expand Down Expand Up @@ -144,6 +141,25 @@ fn register_desktop_file() {
}
}

/// What `buzz://` links should start: the AppImage file when this copy runs
/// from one, as its binary is in a mount that goes away when it exits;
/// otherwise this binary, unless a package installed it with a desktop file
/// of its own.
#[cfg(any(target_os = "linux", target_os = "freebsd", test))]
fn handler_target(
appimage: Option<std::path::PathBuf>,
exe: Option<std::path::PathBuf>,
) -> Option<std::path::PathBuf> {
if appimage.is_some() {
return appimage;
}
let exe = exe?;
let packaged = ["/usr", "/opt", "/nix/store"]
.iter()
.any(|dir| exe.starts_with(dir));
(!packaged).then_some(exe)
}

/// The user desktop file that hands `buzz://` links to this binary.
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
const HANDLER: &str = "buzz-gpui-handler.desktop";
Expand Down Expand Up @@ -252,6 +268,32 @@ mod tests {
assert!(handler_entry("/a/100%$x").contains("Exec=\"/a/100%%\\$x\" %u"));
}

#[test]
fn links_start_the_appimage_rather_than_its_mount() {
use std::path::PathBuf;
let path = |p: &str| Some(PathBuf::from(p));
let mount = path("/tmp/.mount_BuzzAb12/usr/bin/buzz-gpui");
assert_eq!(
handler_target(path("/home/me/Buzz.AppImage"), mount.clone()),
path("/home/me/Buzz.AppImage")
);
// A package's desktop file handles links, but not for an AppImage.
assert_eq!(
handler_target(path("/opt/Buzz.AppImage"), mount),
path("/opt/Buzz.AppImage")
);
assert_eq!(handler_target(None, path("/usr/bin/buzz-gpui")), None);
assert_eq!(
handler_target(None, path("/nix/store/x/bin/buzz-gpui")),
None
);
assert_eq!(
handler_target(None, path("/home/me/bin/buzz-gpui")),
path("/home/me/bin/buzz-gpui")
);
assert_eq!(handler_target(None, None), None);
}

#[test]
fn the_windows_protocol_runs_this_binary() {
assert_eq!(
Expand Down
3 changes: 3 additions & 0 deletions apps/buzz-gpui/crates/app/src/gst_bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ const ELEMENTS: &[&str] = &[
"mpg123audiodec",
"vp8dec",
"vp9dec",
// Ubuntu 24.04, which the Linux packages build on, packages no GStreamer
// AV1 decoder; AV1 plays on Linux only where the system adds one.
#[cfg(not(target_os = "linux"))]
"dav1ddec",
"opusdec",
"vorbisdec",
Expand Down
1 change: 1 addition & 0 deletions apps/buzz-gpui/crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod agent_activity;
mod agent_plugins;
mod agents2;
mod appearance;
mod appimage;
mod atomic_file;
mod buzz_mcp;
mod channel_templates;
Expand Down
9 changes: 9 additions & 0 deletions apps/buzz-gpui/crates/app/src/updates/fixtures/release.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@
"size": 24117248,
"browser_download_url": "https://github.com/benthecarman/buzz-gpui/releases/download/v0.1.1/buzz-gpui_0.1.1-1_amd64.deb"
},
{
"url": "https://api.github.com/repos/benthecarman/buzz-gpui/releases/assets/300000007",
"id": 300000007,
"name": "Buzz-0.1.1-linux-x86_64.AppImage",
"content_type": "application/vnd.appimage",
"state": "uploaded",
"size": 98566144,
"browser_download_url": "https://github.com/benthecarman/buzz-gpui/releases/download/v0.1.1/Buzz-0.1.1-linux-x86_64.AppImage"
},
{
"url": "https://api.github.com/repos/benthecarman/buzz-gpui/releases/assets/300000005",
"id": 300000005,
Expand Down
Loading
Loading