From 82e4d11911d5e1c67cb4ee38cea071935ca1cb12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20M=C3=BChlfort?= Date: Tue, 24 Feb 2026 17:33:54 +0100 Subject: [PATCH 01/13] flatpak: package app as Flatpak, replacing Docker-based distribution Add Flatpak manifest (de.gonicus.Bubbles.json) with GNOME Platform 49, desktop entry, appdata, and SVG icon. Prebuilt binaries (crosvm, socat, qemu-img) and their Debian Trixie runtime libs are bundled via a compat layer using LD_LIBRARY_PATH wrappers. Replace CI build-ui + build-dist Docker jobs with build-tools (socat/qemu-img from Debian Trixie container) and build-flatpak (flatpak-builder). bubbles is built from source inside the Flatpak SDK. Add prebuild.bash for populating prebuilt/ in local builds. - Pin actions/checkout to @v5 major tag (was @v5.0.0) - Add "Building from source" section to README explaining prebuild.bash and flatpak-builder workflow LLM-assisted commit --- .github/workflows/app.yml | 136 ++++---- README.md | 36 +-- bubbles-app/Cargo.lock | 353 +++++++++++---------- bubbles-app/Cargo.toml | 4 +- bubbles-app/de.gonicus.Bubbles.appdata.xml | 15 + bubbles-app/de.gonicus.Bubbles.desktop | 8 + bubbles-app/de.gonicus.Bubbles.json | 111 +++++++ bubbles-app/de.gonicus.Bubbles.svg | 6 + bubbles-app/prebuild.bash | 113 +++++++ bubbles-app/scripts/download.bash | 15 - bubbles-app/src/main.rs | 332 ++++++++++++------- 11 files changed, 740 insertions(+), 389 deletions(-) create mode 100644 bubbles-app/de.gonicus.Bubbles.appdata.xml create mode 100644 bubbles-app/de.gonicus.Bubbles.desktop create mode 100644 bubbles-app/de.gonicus.Bubbles.json create mode 100644 bubbles-app/de.gonicus.Bubbles.svg create mode 100755 bubbles-app/prebuild.bash delete mode 100755 bubbles-app/scripts/download.bash diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index 0accecb..e521f13 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -7,31 +7,11 @@ on: - "bubbles-app/**" jobs: - build-ui: - runs-on: ubuntu-latest - container: debian:trixie - steps: - - name: Checkout - uses: actions/checkout@v5.0.0 - - name: Install deps - run: | - apt-get update - apt-get install -y libgtk-4-dev libadwaita-1-dev cargo build-essential - - name: Build - run: | - cd bubbles-app/ - cargo build --release - - name: 'Upload Artifact' - uses: actions/upload-artifact@v4 - with: - name: bubbles - path: bubbles-app/target/release/bubbles - retention-days: 1 build-crosvm: runs-on: ubuntu-latest steps: - name: Checkout crosvm - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v5 with: repository: google/crosvm fetch-depth: 0 @@ -154,57 +134,87 @@ jobs: git submodule update --init && tools/deps/install-x86_64-debs cargo build --release - name: 'Upload Artifact' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: crosvm path: target/release/crosvm retention-days: 1 - build-dist: - needs: - - build-crosvm - - build-ui + build-tools: + runs-on: ubuntu-latest + container: debian:trixie + steps: + - name: Install tools + run: apt-get update && apt-get install -y --no-install-recommends socat qemu-utils + - name: Collect binaries and libs + run: | + mkdir -p prebuilt/lib + cp /usr/bin/socat1 prebuilt/socat + cp /usr/bin/qemu-img prebuilt/qemu-img + chmod +x prebuilt/socat prebuilt/qemu-img + SYSTEM_LIBS='linux-vdso|ld-linux|libc\.so|libm\.so|libdl\.so|librt\.so|libpthread\.so|libgcc_s\.so|libstdc\+\+' + DEPS=$(ldd prebuilt/socat prebuilt/qemu-img 2>/dev/null \ + | grep '=> /' | awk '{print $3}' | sort -u) + for lib in $DEPS; do + libname=$(basename "$lib") + if ! echo "$libname" | grep -qE "$SYSTEM_LIBS"; then + cp "$lib" prebuilt/lib/ + fi + done + - name: Upload Artifact + uses: actions/upload-artifact@v5 + with: + name: prebuilt-tools + path: prebuilt/ + retention-days: 1 + + build-flatpak: + needs: [build-crosvm, build-tools] runs-on: ubuntu-latest - permissions: - contents: read - packages: write steps: - - name: Checkout - uses: actions/checkout@v5.0.0 - - name: Download bubbles and crosvm + - uses: actions/checkout@v5 + + - name: Download crosvm uses: actions/download-artifact@v5 with: - path: binaries - merge-multiple: true - - name: chmod binaries - run: | - chmod +x binaries/* - - run: | - cat > Dockerfile < ~/.local/share/applications/bubbles.desktop < + + de.gonicus.Bubbles + Bubbles + Lightweight Linux working environments + MIT + MIT + +

Lightweight Linux working environments

+
+ https://github.com/gonicus/bubbles + + + +
diff --git a/bubbles-app/de.gonicus.Bubbles.desktop b/bubbles-app/de.gonicus.Bubbles.desktop new file mode 100644 index 0000000..31148e9 --- /dev/null +++ b/bubbles-app/de.gonicus.Bubbles.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=Bubbles +Comment=Lightweight Linux working environments +Exec=bubbles +Icon=de.gonicus.Bubbles +Categories=System;Virtualization; diff --git a/bubbles-app/de.gonicus.Bubbles.json b/bubbles-app/de.gonicus.Bubbles.json new file mode 100644 index 0000000..71ffd78 --- /dev/null +++ b/bubbles-app/de.gonicus.Bubbles.json @@ -0,0 +1,111 @@ +{ + "app-id": "de.gonicus.Bubbles", + "runtime": "org.gnome.Platform", + "runtime-version": "49", + "sdk": "org.gnome.Sdk", + "sdk-extensions": ["org.freedesktop.Sdk.Extension.rust-stable"], + "command": "bubbles", + "finish-args": [ + "--share=ipc", + "--socket=wayland", + "--device=dri", + "--filesystem=home", + "--share=network", + "--talk-name=org.freedesktop.Flatpak" + ], + "build-options": { + "append-path": "/usr/lib/sdk/rust-stable/bin", + "env": { + "CARGO_HOME": "/run/build/bubbles/cargo" + } + }, + "modules": [ + { + "name": "runtime-libs-compat", + "buildsystem": "simple", + "build-commands": [ + "install -d /app/lib/compat", + "find . -maxdepth 1 -type f -exec install -m755 {} /app/lib/compat/ \\;" + ], + "sources": [ + { "type": "dir", "path": "prebuilt/lib" } + ] + }, + { + "name": "qemu-img", + "buildsystem": "simple", + "build-commands": [ + "install -Dm755 qemu-img /app/bin/qemu-img.bin", + "install -Dm755 qemu-img-wrap /app/bin/qemu-img" + ], + "sources": [ + { "type": "file", "path": "prebuilt/qemu-img" }, + { + "type": "script", + "dest-filename": "qemu-img-wrap", + "commands": [ + "exec env LD_LIBRARY_PATH=/app/lib/compat /app/bin/qemu-img.bin \"$@\"" + ] + } + ] + }, + { + "name": "oras", + "buildsystem": "simple", + "build-commands": [ + "install -Dm755 oras /app/bin/oras" + ], + "sources": [ + { + "type": "archive", + "url": "https://github.com/oras-project/oras/releases/download/v1.2.2/oras_1.2.2_linux_amd64.tar.gz", + "sha256": "bff970346470e5ef888e9f2c0bf7f8ee47283f5a45207d6e7a037da1fb0eae0d", + "strip-components": 0 + } + ] + }, + { + "name": "crosvm", + "buildsystem": "simple", + "build-commands": [ + "install -Dm755 crosvm /app/bin/crosvm" + ], + "sources": [ + { "type": "file", "path": "prebuilt/crosvm" } + ] + }, + { + "name": "socat", + "buildsystem": "simple", + "build-commands": [ + "install -Dm755 socat /app/bin/socat.bin", + "install -Dm755 socat-wrap /app/bin/socat" + ], + "sources": [ + { "type": "file", "path": "prebuilt/socat" }, + { + "type": "script", + "dest-filename": "socat-wrap", + "commands": [ + "exec env LD_LIBRARY_PATH=/app/lib/compat /app/bin/socat.bin \"$@\"" + ] + } + ] + }, + { + "name": "bubbles", + "buildsystem": "simple", + "build-commands": [ + "cargo --offline build --release", + "install -Dm755 target/release/bubbles /app/bin/bubbles", + "install -Dm644 de.gonicus.Bubbles.desktop /app/share/applications/de.gonicus.Bubbles.desktop", + "install -Dm644 de.gonicus.Bubbles.appdata.xml /app/share/metainfo/de.gonicus.Bubbles.appdata.xml", + "install -Dm644 de.gonicus.Bubbles.svg /app/share/icons/hicolor/scalable/apps/de.gonicus.Bubbles.svg" + ], + "sources": [ + { "type": "dir", "path": "." }, + "cargo-sources.json" + ] + } + ] +} diff --git a/bubbles-app/de.gonicus.Bubbles.svg b/bubbles-app/de.gonicus.Bubbles.svg new file mode 100644 index 0000000..571b801 --- /dev/null +++ b/bubbles-app/de.gonicus.Bubbles.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/bubbles-app/prebuild.bash b/bubbles-app/prebuild.bash new file mode 100755 index 0000000..af93018 --- /dev/null +++ b/bubbles-app/prebuild.bash @@ -0,0 +1,113 @@ +#!/bin/bash +# +# prebuild.bash — Populate prebuilt/ for local (non-CI) Flatpak builds +# +# Installs socat and qemu-img inside a Debian Trixie container via apt +# (which verifies package signatures), then copies the binaries and their +# runtime library dependencies out. Same approach as the build-tools +# job in .github/workflows/app.yml. +# +# Usage: +# CROSVM=~/bubbles/crosvm ./prebuild.bash +# +# Environment variables: +# CROSVM - path to a pre-built crosvm binary (required) +# +# Requirements: podman, curl + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PREBUILT_DIR="$SCRIPT_DIR/prebuilt" +CONTAINER_NAME="bubbles-prebuild-$$" + +cleanup() { + podman rm -f "$CONTAINER_NAME" 2>/dev/null || true +} +trap cleanup EXIT + +mkdir -p "$PREBUILT_DIR" "$PREBUILT_DIR/lib" + +# --------------------------------------------------------------------------- +# crosvm — must be provided; build instructions in .github/workflows/app.yml +# --------------------------------------------------------------------------- +if [ -n "${CROSVM:-}" ]; then + echo "==> crosvm: copying from ${CROSVM}" + install -m755 "$CROSVM" "$PREBUILT_DIR/crosvm" + echo " → prebuilt/crosvm" +else + echo "ERROR: crosvm binary not found." >&2 + echo " Set CROSVM=/path/to/binary, e.g.:" >&2 + echo " CROSVM=~/bubbles/crosvm $0" >&2 + echo " Build instructions: .github/workflows/app.yml (build-crosvm job)" >&2 + exit 1 +fi + +# --------------------------------------------------------------------------- +# socat, qemu-img, and runtime libraries — install in Debian Trixie container +# via apt (verifies package GPG signatures), then copy binaries and their +# non-system shared library dependencies out. +# --------------------------------------------------------------------------- +echo "==> Setting up Debian Trixie container..." + +podman run -d --name "$CONTAINER_NAME" debian:trixie sleep infinity +podman exec "$CONTAINER_NAME" sh -c \ + 'apt-get update && apt-get install -y --no-install-recommends socat qemu-utils' + +# Copy binaries +echo "==> Copying binaries..." +podman cp "$CONTAINER_NAME:/usr/bin/socat1" "$PREBUILT_DIR/socat" +podman cp "$CONTAINER_NAME:/usr/bin/qemu-img" "$PREBUILT_DIR/qemu-img" +chmod +x "$PREBUILT_DIR/socat" "$PREBUILT_DIR/qemu-img" +echo " → prebuilt/socat" +echo " → prebuilt/qemu-img" + +# Copy runtime library dependencies (excluding glibc/base system libs) +echo "==> Copying runtime libraries..." + +# Libraries that are part of glibc or universally present — skip these +SYSTEM_LIBS="linux-vdso|ld-linux|libc\.so|libm\.so|libdl\.so|librt\.so|libpthread\.so|libgcc_s\.so|libstdc\+\+" + +DEPS=$(podman exec "$CONTAINER_NAME" sh -c \ + 'ldd /usr/bin/socat1 /usr/bin/qemu-img 2>/dev/null \ + | grep "=> /" | awk "{print \$3}" | sort -u') + +for lib in $DEPS; do + libname=$(basename "$lib") + if echo "$libname" | grep -qE "$SYSTEM_LIBS"; then + continue + fi + podman cp "$CONTAINER_NAME:$lib" "$PREBUILT_DIR/lib/$libname" + echo " → prebuilt/lib/$libname" +done + +# --------------------------------------------------------------------------- +# cargo-sources.json — Flatpak needs this for offline Cargo builds +# Run generator inside the container using apt-provided Python packages +# (avoids needing pip/aiohttp on the host). +# --------------------------------------------------------------------------- +if [ -f "$SCRIPT_DIR/cargo-sources.json" ]; then + echo "==> cargo-sources.json already exists, skipping" +else + echo "==> Generating cargo-sources.json (inside container)..." + podman exec "$CONTAINER_NAME" sh -c \ + 'apt-get install -y --no-install-recommends python3 python3-aiohttp python3-tomlkit curl 2>/dev/null' + curl -fsSL -o "$SCRIPT_DIR/.flatpak-cargo-generator.py" \ + https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/4d5e760321236bd96fc1c6db9ec94c336600c114/cargo/flatpak-cargo-generator.py + podman cp "$SCRIPT_DIR/Cargo.lock" "$CONTAINER_NAME:/tmp/Cargo.lock" + podman cp "$SCRIPT_DIR/.flatpak-cargo-generator.py" "$CONTAINER_NAME:/tmp/flatpak-cargo-generator.py" + rm -f "$SCRIPT_DIR/.flatpak-cargo-generator.py" + podman exec "$CONTAINER_NAME" \ + python3 /tmp/flatpak-cargo-generator.py /tmp/Cargo.lock -o /tmp/cargo-sources.json + podman cp "$CONTAINER_NAME:/tmp/cargo-sources.json" "$SCRIPT_DIR/cargo-sources.json" + echo " → cargo-sources.json" +fi + +# --------------------------------------------------------------------------- +echo "" +echo "prebuilt/ ready:" +ls -lhR "$PREBUILT_DIR/" +echo "" +echo "To build the Flatpak:" +echo " cd $(basename "$SCRIPT_DIR")" +echo " flatpak-builder --user --install --force-clean build-dir de.gonicus.Bubbles.json" diff --git a/bubbles-app/scripts/download.bash b/bubbles-app/scripts/download.bash deleted file mode 100755 index 018572c..0000000 --- a/bubbles-app/scripts/download.bash +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -set -ex -o pipefail - -TARGET_DIRECTORY="$PWD/.bubbles/images/debian-13" -BUBBLES_DIR=$PWD - -mkdir -p $TARGET_DIRECTORY -cd $TARGET_DIRECTORY - -$BUBBLES_DIR/oras pull ghcr.io/gonicus/bubbles/vm-image:e289a3a5479817c3ffad6bb62d8214e4265e8e4b - -qemu-img convert -f qcow2 -O raw disk.qcow2 disk.img -truncate -s +15G disk.img -rm disk.qcow2 diff --git a/bubbles-app/src/main.rs b/bubbles-app/src/main.rs index 4445a65..dcd5734 100644 --- a/bubbles-app/src/main.rs +++ b/bubbles-app/src/main.rs @@ -6,8 +6,74 @@ use relm4::prelude::{AsyncFactoryComponent, AsyncFactoryVecDeque}; use relm4::{ AsyncFactorySender, Component, ComponentController, ComponentParts, ComponentSender, Controller, RelmApp, SimpleComponent, spawn }; -use std::{env, fs, path::Path, ffi::OsStr}; +use std::{env, fs, path::{Path, PathBuf}, ffi::{OsStr, OsString}}; use libc::SIGTERM; +use tokio::io::{AsyncWriteExt, AsyncReadExt}; + +fn get_data_dir() -> PathBuf { + let base = env::var("XDG_DATA_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(env::var("HOME").expect("HOME")).join(".local/share")); + base.join("bubbles") +} + +fn is_flatpak() -> bool { + Path::new("/.flatpak-info").exists() +} + +fn make_host_args(args: &[&OsStr]) -> Vec { + if is_flatpak() { + let uid = unsafe { libc::getuid() }; + let mut v: Vec = vec![ + "flatpak-spawn".into(), + "--host".into(), + format!("--env=XDG_RUNTIME_DIR=/run/user/{}", uid).into(), + ]; + v.extend(args.iter().map(|a| (*a).to_owned())); + v + } else { + args.iter().map(|a| (*a).to_owned()).collect() + } +} + +fn flatpak_host_bin(name: &str) -> PathBuf { + // /.flatpak-info is always readable inside the sandbox and contains + // app-path= for the actual installation (user or system). + if let Ok(content) = fs::read_to_string("/.flatpak-info") { + for line in content.lines() { + if let Some(path) = line.strip_prefix("app-path=") { + return PathBuf::from(path).join("bin").join(name); + } + } + } + // Fallback for non-sandbox use + PathBuf::from(name) +} + +fn wayland_sock_path() -> PathBuf { + if is_flatpak() { + let uid = unsafe { libc::getuid() }; + let display = env::var("WAYLAND_DISPLAY").expect("WAYLAND_DISPLAY"); + PathBuf::from(format!("/run/user/{}/{}", uid, display)) + } else { + let runtime_dir = env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR"); + let display = env::var("WAYLAND_DISPLAY").expect("WAYLAND_DISPLAY"); + PathBuf::from(runtime_dir).join(display) + } +} + +async fn unix_http(socket: &Path, method: &str, path: &str) -> std::io::Result { + let mut stream = tokio::net::UnixStream::connect(socket).await?; + // Content-Length: 0 included for POST correctness; harmless on GET + let req = format!( + "{} {} HTTP/1.0\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n", + method, path + ); + stream.write_all(req.as_bytes()).await?; + let mut buf = Vec::new(); + stream.read_to_end(&mut buf).await?; + Ok(String::from_utf8_lossy(&buf).into_owned()) +} struct CreateBubbleDialog { } @@ -24,9 +90,7 @@ enum ImageStatus { } fn determine_download_status() -> ImageStatus { - let images_dir = env::current_dir() - .expect("cwd to be set") - .join(Path::new(".bubbles/images")); + let images_dir = get_data_dir().join("images"); fs::create_dir_all(&images_dir).expect("directory to exist or be created"); let image_exists = images_dir.join(Path::new("debian-13")).exists(); @@ -37,68 +101,86 @@ fn determine_download_status() -> ImageStatus { }; } -pub async fn wait_until_exists(path: &OsStr) { +pub async fn wait_until_exists(path: &Path) { loop { - let process = gtk::gio::Subprocess::newv( - &[ - OsStr::new("sh"), - OsStr::new("-c"), - OsStr::new("stat $0 || (sleep 0.5 && exit 1)"), - path, - ], - SubprocessFlags::empty() - ).expect("start of process"); - process.wait_future().await.expect("probe to run"); - if process.is_successful() { - return; - } + let exists = if is_flatpak() { + // /tmp is sandbox-private; check on the host + let args = make_host_args(&[ + OsStr::new("test"), + OsStr::new("-e"), + path.as_os_str(), + ]); + let args_ref: Vec<&OsStr> = args.iter().map(OsString::as_os_str).collect(); + let p = gtk::gio::Subprocess::newv(&args_ref, SubprocessFlags::STDERR_SILENCE) + .expect("spawn host test"); + p.wait_future().await.ok(); + p.is_successful() + } else { + path.exists() + }; + if exists { return; } + tokio::time::sleep(std::time::Duration::from_millis(500)).await; } } -pub async fn wait_until_ready(vsock_socket_path: &OsStr) { +pub async fn wait_until_ready(vsock_socket_path: &Path) { loop { - let process = gtk::gio::Subprocess::newv( - &[ - OsStr::new("sh"), - OsStr::new("-c"), - OsStr::new("curl -sS --unix-socket $0 http://localhost/ready || (sleep 0.5 && exit 1)"), - vsock_socket_path, - ], - SubprocessFlags::empty() - ).expect("start of process"); - process.wait_future().await.expect("probe to run"); - if process.is_successful() { - return; + match unix_http(vsock_socket_path, "GET", "/ready").await { + Ok(response) if response.contains("200") => return, + _ => { + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + } } } } -pub async fn request_shutdown(vsock_socket_path: &OsStr) { - let process = gtk::gio::Subprocess::newv( - &[ - OsStr::new("curl"), - OsStr::new("-XPOST"), - OsStr::new("--unix-socket"), - vsock_socket_path, - OsStr::new("http://localhost/shutdown"), - ], - SubprocessFlags::empty() - ).expect("start of process"); - process.wait_future().await.expect("request to be made"); +pub async fn request_shutdown(vsock_socket_path: &Path) { + unix_http(vsock_socket_path, "POST", "/shutdown").await.ok(); } -pub async fn request_terminal(vsock_socket_path: &OsStr) { - let process = gtk::gio::Subprocess::newv( - &[ - OsStr::new("curl"), - OsStr::new("-XPOST"), - OsStr::new("--unix-socket"), - vsock_socket_path, - OsStr::new("http://localhost/spawn-terminal"), - ], - SubprocessFlags::empty() - ).expect("start of process"); - process.wait_future().await.expect("request to be made"); +pub async fn request_terminal(vsock_socket_path: &Path) { + unix_http(vsock_socket_path, "POST", "/spawn-terminal").await.ok(); +} + +async fn download_image() { + let target_dir = get_data_dir().join("images/debian-13"); + tokio::fs::create_dir_all(&target_dir).await.unwrap(); + + // Step 1: oras pull (runs inside sandbox — just needs --share=network) + // In Flatpak: bundled at /app/bin/oras; outside: resolved via PATH + let oras_bin = if is_flatpak() { "/app/bin/oras" } else { "oras" }; + gtk::gio::Subprocess::newv(&[ + OsStr::new(oras_bin), + OsStr::new("pull"), + OsStr::new("ghcr.io/gonicus/bubbles/vm-image:e289a3a5479817c3ffad6bb62d8214e4265e8e4b"), + OsStr::new("--output"), + target_dir.as_os_str(), + ], SubprocessFlags::empty()) + .expect("oras pull to start") + .wait_future().await.expect("oras pull to complete"); + + // Step 2: qemu-img convert + // In Flatpak: bundled at /app/bin/qemu-img; outside: resolved via PATH + let qemu_img = if is_flatpak() { "/app/bin/qemu-img" } else { "qemu-img" }; + let qcow2_path = target_dir.join("disk.qcow2"); + let raw_path = target_dir.join("disk.img"); + gtk::gio::Subprocess::newv(&[ + OsStr::new(qemu_img), + OsStr::new("convert"), + OsStr::new("-f"), OsStr::new("qcow2"), + OsStr::new("-O"), OsStr::new("raw"), + qcow2_path.as_os_str(), + raw_path.as_os_str(), + ], SubprocessFlags::empty()) + .expect("qemu-img to start") + .wait_future().await.expect("qemu-img to complete"); + + tokio::fs::remove_file(&qcow2_path).await.ok(); + + // Step 3: expand disk (native Rust, no truncate binary needed) + let f = tokio::fs::OpenOptions::new().write(true).open(&raw_path).await.unwrap(); + let current_size = f.metadata().await.unwrap().len(); + f.set_len(current_size + 15 * 1024 * 1024 * 1024).await.unwrap(); } #[derive(PartialEq, Debug, Clone)] @@ -211,9 +293,7 @@ struct VM { } fn load_vms() -> Vec { - let vms_dir = env::current_dir() - .expect("cwd to be set") - .join(Path::new(".bubbles/vms")); + let vms_dir = get_data_dir().join("vms"); fs::create_dir_all(&vms_dir).expect("directory to exist or be created"); let mut vms: Vec = vec![]; for dir in fs::read_dir(vms_dir).expect("to exist") { @@ -232,14 +312,9 @@ fn load_vms() -> Vec { async fn create_vm(name: String) { println!("starting copy"); - let vm_dir_path = &env::current_dir() - .expect("to be set") - .join(".bubbles/vms") - .join(name); - tokio::fs::create_dir_all(vm_dir_path).await.expect("directories to be created"); - let image_base_path = env::current_dir() - .expect("to be set") - .join(".bubbles/images/debian-13"); + let vm_dir_path = get_data_dir().join("vms").join(&name); + tokio::fs::create_dir_all(&vm_dir_path).await.expect("directories to be created"); + let image_base_path = get_data_dir().join("images/debian-13"); let image_disk_path = image_base_path.join("disk.img"); let image_linuz_path = image_base_path.join("vmlinuz"); let image_initrd_path = image_base_path.join("initrd.img"); @@ -315,19 +390,17 @@ impl AsyncFactoryComponent for VmEntry { _sender: AsyncFactorySender, ) -> Self { Self { value } - } + } async fn update(&mut self, msg: Self::Input, sender: AsyncFactorySender) { let vm_name: String = self.value.name.clone(); - let image_base_path = env::current_dir() - .expect("to be set") - .join(".bubbles/vms").join(vm_name.clone()); + let image_base_path = get_data_dir().join("vms").join(vm_name.clone()); let vsock_socket_path = image_base_path.join("vsock"); match msg { VmMsg::PowerToggle(index) => { match self.value.status { VMStatus::Running | VMStatus::InFlux => { relm4::spawn_local(async move { - request_shutdown(OsStr::new(&vsock_socket_path)).await; + request_shutdown(&vsock_socket_path).await; }); }, VMStatus::NotRunning => { @@ -338,56 +411,82 @@ impl AsyncFactoryComponent for VmEntry { let image_disk_path = image_base_path.join("disk.img"); let image_linuz_path = image_base_path.join("vmlinuz"); let image_initrd_path = image_base_path.join("initrd.img"); + + let socat_bin: OsString = if is_flatpak() { + flatpak_host_bin("socat").into_os_string() + } else { + OsString::from("socat") + }; + let socat_unix = format!("UNIX-LISTEN:{},fork", vsock_socket_path.to_str().expect("string")); + let socat_vsock = format!("VSOCK-CONNECT:{}:11111", index.current_index() + 10); + let socat_host_args = make_host_args(&[ + socat_bin.as_os_str(), + OsStr::new(&socat_unix), + OsStr::new(&socat_vsock), + ]); + let socat_host_args_ref: Vec<&OsStr> = socat_host_args.iter().map(OsString::as_os_str).collect(); let socat_process = gtk::gio::Subprocess::newv( - &[ - OsStr::new(Path::new(&env::var("HOME").expect("HOME var to be set")).join("bubbles/socat").as_os_str()), - OsStr::new(&format!("UNIX-LISTEN:{},fork", vsock_socket_path.to_str().expect("string"))), - OsStr::new(&format!("VSOCK-CONNECT:{}:11111", index.current_index() + 10)), - ], + &socat_host_args_ref, SubprocessFlags::empty() ).expect("start of socat process"); + + let passt_host_args = make_host_args(&[ + OsStr::new("passt"), + OsStr::new("-f"), + OsStr::new("--vhost-user"), + OsStr::new("--socket"), + passt_socket_path.as_os_str(), + ]); + let passt_host_args_ref: Vec<&OsStr> = passt_host_args.iter().map(OsString::as_os_str).collect(); let passt_process = gtk::gio::Subprocess::newv( - &[ - OsStr::new("passt"), - OsStr::new("-f"), - OsStr::new("--vhost-user"), - OsStr::new("--socket"), - OsStr::new(passt_socket_path.as_os_str()), - ], + &passt_host_args_ref, SubprocessFlags::empty() ).expect("start of passt process"); - wait_until_exists(passt_socket_path.as_os_str()).await; + + wait_until_exists(&passt_socket_path).await; + + let crosvm_bin: OsString = if is_flatpak() { + flatpak_host_bin("crosvm").into_os_string() + } else { + OsString::from("crosvm") + }; + let wayland_sock = wayland_sock_path(); + let vsock_cid = format!("{}", index.current_index() + 10); + let passt_socket_str = format!("net,socket={}", passt_socket_path.to_str().expect("string")); + let crosvm_host_args = make_host_args(&[ + crosvm_bin.as_os_str(), + OsStr::new("run"), + OsStr::new("--name"), + OsStr::new(&vm_name), + OsStr::new("--cpus"), + OsStr::new("num-cores=4"), + OsStr::new("-m"), + OsStr::new("7000"), + OsStr::new("--rwdisk"), + image_disk_path.as_os_str(), + OsStr::new("--initrd"), + image_initrd_path.as_os_str(), + OsStr::new("--socket"), + crosvm_socket_path.as_os_str(), + OsStr::new("--vsock"), + OsStr::new(&vsock_cid), + OsStr::new("--gpu"), + OsStr::new("context-types=cross-domain,displays=[]"), + OsStr::new("--wayland-sock"), + wayland_sock.as_os_str(), + OsStr::new("--vhost-user"), + OsStr::new(&passt_socket_str), + OsStr::new("-p"), + OsStr::new("root=/dev/vda2"), + image_linuz_path.as_os_str(), + ]); + let crosvm_host_args_ref: Vec<&OsStr> = crosvm_host_args.iter().map(OsString::as_os_str).collect(); let crosvm_process = gtk::gio::Subprocess::newv( - &[ - OsStr::new(Path::new(&env::var("HOME").expect("HOME var to be set")).join("bubbles/crosvm").as_os_str()), - OsStr::new("run"), - OsStr::new("--name"), - OsStr::new(&vm_name.clone()), - OsStr::new("--cpus"), - OsStr::new("num-cores=4"), - OsStr::new("-m"), - OsStr::new("7000"), - OsStr::new("--rwdisk"), - image_disk_path.as_os_str(), - OsStr::new("--initrd"), - image_initrd_path.as_os_str(), - OsStr::new("--socket"), - crosvm_socket_path.as_os_str(), - OsStr::new("--vsock"), - OsStr::new(&format!("{}", index.current_index() + 10)), - OsStr::new("--gpu"), - OsStr::new("context-types=cross-domain,displays=[]"), - OsStr::new("--wayland-sock"), - OsStr::new(Path::new(&env::var("XDG_RUNTIME_DIR").expect("XDG var to be set")).join(env::var("WAYLAND_DISPLAY").expect("WAYLAND_DISPLAY var to be set")).as_os_str()), - OsStr::new("--vhost-user"), - OsStr::new(&format!("net,socket={}", passt_socket_path.to_str().expect("string"))), - OsStr::new("-p"), - OsStr::new("root=/dev/vda2"), - image_linuz_path.as_os_str(), - ], + &crosvm_host_args_ref, SubprocessFlags::empty() ).expect("start of process"); - wait_until_ready(vsock_socket_path.as_os_str()).await; + + wait_until_ready(&vsock_socket_path).await; sender.output(VmStateUpdate::Update(index.clone(), VMStatus::Running)).unwrap(); crosvm_process.wait_future().await.expect("vm to stop"); socat_process.send_signal(SIGTERM); // Marker: Incompatible with Windows @@ -401,7 +500,7 @@ impl AsyncFactoryComponent for VmEntry { }, VmMsg::StartTerminal(_index) => { relm4::spawn_local(async move { - request_terminal(OsStr::new(&vsock_socket_path)).await; + request_terminal(&vsock_socket_path).await; }); } } @@ -588,10 +687,7 @@ impl SimpleComponent for App { AppMsg::DownloadImage => { self.image_status = ImageStatus::Downloading; relm4::spawn_local(async move { - gtk::gio::Subprocess::newv( - &[OsStr::new("scripts/download.bash")], - SubprocessFlags::empty() - ).expect("download").wait_future().await.expect("download to succeed"); + download_image().await; sender.input(AppMsg::FinishImageDownload); }); } @@ -620,6 +716,6 @@ impl SimpleComponent for App { } fn main() { - let app = RelmApp::new("bubbles"); + let app = RelmApp::new("de.gonicus.Bubbles"); app.run::(()); } From b7a97387ea0aa4987061ad04e58db2faf57b1340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20M=C3=BChlfort?= Date: Fri, 27 Feb 2026 21:51:12 +0100 Subject: [PATCH 02/13] ci: deduplicate build logic; move steps into prebuild.bash; fix absolute lib path LLM-assisted commit --- .github/workflows/app.yml | 189 +----------------- bubbles-app/de.gonicus.Bubbles.json | 8 +- .../crosvm/0001-passt-virtio-version.patch | 24 +++ .../crosvm/0002-display-backend-stub.patch | 27 +++ .../crosvm/0003-seccomp-madvise-guard.patch | 39 ++++ bubbles-app/prebuild.bash | 120 ++++++++--- 6 files changed, 194 insertions(+), 213 deletions(-) create mode 100644 bubbles-app/patches/crosvm/0001-passt-virtio-version.patch create mode 100644 bubbles-app/patches/crosvm/0002-display-backend-stub.patch create mode 100644 bubbles-app/patches/crosvm/0003-seccomp-madvise-guard.patch diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index e521f13..b6b8e36 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -7,201 +7,22 @@ on: - "bubbles-app/**" jobs: - build-crosvm: - runs-on: ubuntu-latest - steps: - - name: Checkout crosvm - uses: actions/checkout@v5 - with: - repository: google/crosvm - fetch-depth: 0 - - name: Revert some commits to fit passt patch - run: | - git config --global user.email "muehlfort@gonicus.de" - git config --global user.name "CI" - git checkout a96cb379acf55a75887cbba190666e7d22ff9dbf - git revert --no-edit \ - 1656a1f68296baa4313b4b46e23a6c975caa7cc9 \ - 2c6f23406c41af8432c1c1ba4e3605785e959ead \ - 806e91d2fa5416b3444257e42421e07b318e26d6 \ - ff4b721ac8b983393b0fa503000eff74ecd3de2e \ - a96cb379acf55a75887cbba190666e7d22ff9dbf - - name: Apply passt patch - run: | - git apply - </dev/null \ - | grep '=> /' | awk '{print $3}' | sort -u) - for lib in $DEPS; do - libname=$(basename "$lib") - if ! echo "$libname" | grep -qE "$SYSTEM_LIBS"; then - cp "$lib" prebuilt/lib/ - fi - done - - name: Upload Artifact - uses: actions/upload-artifact@v5 - with: - name: prebuilt-tools - path: prebuilt/ - retention-days: 1 - build-flatpak: - needs: [build-crosvm, build-tools] runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - name: Download crosvm - uses: actions/download-artifact@v5 - with: - name: crosvm - path: bubbles-app/prebuilt - - - name: Download tools - uses: actions/download-artifact@v5 - with: - name: prebuilt-tools - path: bubbles-app/prebuilt - - - name: Make binaries executable - run: chmod +x bubbles-app/prebuilt/crosvm bubbles-app/prebuilt/socat bubbles-app/prebuilt/qemu-img - - - name: Install flatpak-builder + - name: Install Flatpak tooling run: | - sudo apt-get install -y flatpak flatpak-builder + sudo apt-get update + sudo apt-get install -y flatpak flatpak-builder podman flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak install --user -y org.gnome.Platform//49 org.gnome.Sdk//49 \ org.freedesktop.Sdk.Extension.rust-stable//25.08 - - name: Generate Flatpak Cargo sources - run: | - sudo apt-get install -y python3-aiohttp python3-toml - curl -Lo flatpak-cargo-generator.py \ - https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/4d5e760321236bd96fc1c6db9ec94c336600c114/cargo/flatpak-cargo-generator.py - python3 flatpak-cargo-generator.py bubbles-app/Cargo.lock -o bubbles-app/cargo-sources.json + - name: Build prebuilt artifacts + run: cd bubbles-app && ./prebuild.bash - name: Build Flatpak run: | diff --git a/bubbles-app/de.gonicus.Bubbles.json b/bubbles-app/de.gonicus.Bubbles.json index 71ffd78..015dc01 100644 --- a/bubbles-app/de.gonicus.Bubbles.json +++ b/bubbles-app/de.gonicus.Bubbles.json @@ -44,7 +44,9 @@ "type": "script", "dest-filename": "qemu-img-wrap", "commands": [ - "exec env LD_LIBRARY_PATH=/app/lib/compat /app/bin/qemu-img.bin \"$@\"" + "SELF=$(realpath \"$0\")", + "DIR=$(dirname \"$SELF\")", + "exec env LD_LIBRARY_PATH=\"${DIR}/../lib/compat${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\" \"${DIR}/qemu-img.bin\" \"$@\"" ] } ] @@ -87,7 +89,9 @@ "type": "script", "dest-filename": "socat-wrap", "commands": [ - "exec env LD_LIBRARY_PATH=/app/lib/compat /app/bin/socat.bin \"$@\"" + "SELF=$(realpath \"$0\")", + "DIR=$(dirname \"$SELF\")", + "exec env LD_LIBRARY_PATH=\"${DIR}/../lib/compat${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\" \"${DIR}/socat.bin\" \"$@\"" ] } ] diff --git a/bubbles-app/patches/crosvm/0001-passt-virtio-version.patch b/bubbles-app/patches/crosvm/0001-passt-virtio-version.patch new file mode 100644 index 0000000..5e14fa7 --- /dev/null +++ b/bubbles-app/patches/crosvm/0001-passt-virtio-version.patch @@ -0,0 +1,24 @@ +diff --git a/devices/src/virtio/vhost_user_frontend/mod.rs b/devices/src/virtio/vhost_user_frontend/mod.rs +index 1f847409e..8d6b5e646 100644 +--- a/devices/src/virtio/vhost_user_frontend/mod.rs ++++ b/devices/src/virtio/vhost_user_frontend/mod.rs +@@ -26,6 +26,7 @@ use base::RawDescriptor; + use base::WorkerThread; + use snapshot::AnySnapshot; + use sync::Mutex; ++use virtio_sys::virtio_config::VIRTIO_F_VERSION_1; + use vm_memory::GuestMemory; + use vmm_vhost::message::VhostUserConfigFlags; + use vmm_vhost::message::VhostUserMigrationPhase; +@@ -178,9 +179,9 @@ impl VhostUserFrontend { + if avail_features & 1 << VHOST_USER_F_PROTOCOL_FEATURES != 0 { + // The vhost-user backend supports VHOST_USER_F_PROTOCOL_FEATURES; enable it. + backend_client +- .set_features(1 << VHOST_USER_F_PROTOCOL_FEATURES) ++ .set_features(1 << VHOST_USER_F_PROTOCOL_FEATURES | 1 << VIRTIO_F_VERSION_1) + .map_err(Error::SetFeatures)?; +- acked_features |= 1 << VHOST_USER_F_PROTOCOL_FEATURES; ++ acked_features |= 1 << VHOST_USER_F_PROTOCOL_FEATURES | 1 << VIRTIO_F_VERSION_1; + + let avail_protocol_features = backend_client + .get_protocol_features() diff --git a/bubbles-app/patches/crosvm/0002-display-backend-stub.patch b/bubbles-app/patches/crosvm/0002-display-backend-stub.patch new file mode 100644 index 0000000..bb6129e --- /dev/null +++ b/bubbles-app/patches/crosvm/0002-display-backend-stub.patch @@ -0,0 +1,27 @@ +diff --git a/src/crosvm/sys/linux/gpu.rs b/src/crosvm/sys/linux/gpu.rs +index 7bb3ff7af..7eba55ee7 100644 +--- a/src/crosvm/sys/linux/gpu.rs ++++ b/src/crosvm/sys/linux/gpu.rs +@@ -125,7 +125,6 @@ pub fn create_gpu_device( + gpu_params.allow_implicit_render_server_exec && !is_sandboxed; + + let mut display_backends = vec![ +- virtio::DisplayBackend::X(cfg.x_display.clone()), + virtio::DisplayBackend::Stub, + ]; + +@@ -134,14 +133,6 @@ pub fn create_gpu_device( + display_backends.insert(0, virtio::DisplayBackend::Android(service_name.to_string())); + } + +- // Use the unnamed socket for GPU display screens. +- if let Some(socket_path) = cfg.wayland_socket_paths.get("") { +- display_backends.insert( +- 0, +- virtio::DisplayBackend::Wayland(Some(socket_path.to_owned())), +- ); +- } +- + let dev = virtio::Gpu::new( + exit_evt_wrtube + .try_clone() diff --git a/bubbles-app/patches/crosvm/0003-seccomp-madvise-guard.patch b/bubbles-app/patches/crosvm/0003-seccomp-madvise-guard.patch new file mode 100644 index 0000000..cb9e9d6 --- /dev/null +++ b/bubbles-app/patches/crosvm/0003-seccomp-madvise-guard.patch @@ -0,0 +1,39 @@ +diff --git a/jail/seccomp/x86_64/common_device.policy b/jail/seccomp/x86_64/common_device.policy +index 66474e8ce..ac4d3e9e7 100644 +--- a/jail/seccomp/x86_64/common_device.policy ++++ b/jail/seccomp/x86_64/common_device.policy +@@ -35,7 +35,7 @@ io_uring_register: 1 + io_uring_enter: 1 + kill: 1 + lseek: 1 +-madvise: arg2 == MADV_DONTNEED || arg2 == MADV_DONTDUMP || arg2 == MADV_REMOVE || arg2 == MADV_MERGEABLE || arg2 == MADV_FREE || arg2 == MADV_NOHUGEPAGE ++madvise: arg2 == MADV_GUARD_INSTALL || arg2 == MADV_GUARD_REMOVE || arg2 == MADV_DONTNEED || arg2 == MADV_DONTDUMP || arg2 == MADV_REMOVE || arg2 == MADV_MERGEABLE || arg2 == MADV_FREE || arg2 == MADV_NOHUGEPAGE + membarrier: 1 + memfd_create: 1 + mmap: arg2 in ~PROT_EXEC +diff --git a/jail/seccomp/x86_64/constants.json b/jail/seccomp/x86_64/constants.json +index efbe66b58..ac25c379a 100644 +--- a/jail/seccomp/x86_64/constants.json ++++ b/jail/seccomp/x86_64/constants.json +@@ -990,6 +990,8 @@ + "LO_KEY_SIZE": 32, + "LO_NAME_SIZE": 64, + "LSMT_ROOT": -1, ++ "MADV_GUARD_INSTALL": 102, ++ "MADV_GUARD_REMOVE": 103, + "MADV_COLD": 20, + "MADV_COLLAPSE": 25, + "MADV_DODUMP": 17, +diff --git a/jail/seccomp/x86_64/gpu_common.policy b/jail/seccomp/x86_64/gpu_common.policy +index 470265099..c87148a88 100644 +--- a/jail/seccomp/x86_64/gpu_common.policy ++++ b/jail/seccomp/x86_64/gpu_common.policy +@@ -27,7 +27,7 @@ io_uring_setup: 1 + io_uring_register: 1 + io_uring_enter: 1 + kill: 1 +-madvise: arg2 == MADV_DONTNEED || arg2 == MADV_DONTDUMP || arg2 == MADV_REMOVE || arg2 == MADV_MERGEABLE || arg2 == MADV_FREE ++madvise: arg2 == MADV_GUARD_INSTALL || arg2 == MADV_GUARD_REMOVE || arg2 == MADV_DONTNEED || arg2 == MADV_DONTDUMP || arg2 == MADV_REMOVE || arg2 == MADV_MERGEABLE || arg2 == MADV_FREE + membarrier: 1 + # memfd_create is used for sharing memory with wayland. + # For normal use case, we allow arg1 == MFD_CLOEXEC|MFD_ALLOW_SEALING, with or without MFD_NOEXEC_SEAL. diff --git a/bubbles-app/prebuild.bash b/bubbles-app/prebuild.bash index af93018..d317757 100755 --- a/bubbles-app/prebuild.bash +++ b/bubbles-app/prebuild.bash @@ -1,46 +1,112 @@ #!/bin/bash # -# prebuild.bash — Populate prebuilt/ for local (non-CI) Flatpak builds +# prebuild.bash — Produce prebuilt/ artifacts for Flatpak builds # -# Installs socat and qemu-img inside a Debian Trixie container via apt -# (which verifies package signatures), then copies the binaries and their -# runtime library dependencies out. Same approach as the build-tools -# job in .github/workflows/app.yml. +# Builds crosvm (with patches), extracts socat + qemu-img from Debian Trixie, +# and generates cargo-sources.json for offline Cargo builds. This script is +# the single source of truth — both local builds and CI use it. # # Usage: -# CROSVM=~/bubbles/crosvm ./prebuild.bash +# ./prebuild.bash # Build everything (crosvm from source) +# CROSVM=~/bubbles/crosvm ./prebuild.bash # Use pre-built crosvm binary # # Environment variables: -# CROSVM - path to a pre-built crosvm binary (required) +# CROSVM - path to a pre-built crosvm binary (skips crosvm build) # -# Requirements: podman, curl +# Requirements: podman, git, sha256sum set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PREBUILT_DIR="$SCRIPT_DIR/prebuilt" -CONTAINER_NAME="bubbles-prebuild-$$" +PATCHES_DIR="$SCRIPT_DIR/patches/crosvm" + +# --- crosvm source configuration --- +CROSVM_COMMIT="a96cb379acf55a75887cbba190666e7d22ff9dbf" +CROSVM_REVERTS=( + 1656a1f68296baa4313b4b46e23a6c975caa7cc9 + 2c6f23406c41af8432c1c1ba4e3605785e959ead + 806e91d2fa5416b3444257e42421e07b318e26d6 + ff4b721ac8b983393b0fa503000eff74ecd3de2e + a96cb379acf55a75887cbba190666e7d22ff9dbf +) + +# --- Container names (for cleanup) --- +TOOLS_CONTAINER="bubbles-prebuild-tools-$$" +CROSVM_CONTAINER="bubbles-prebuild-crosvm-$$" +TMPDIR_CROSVM="" cleanup() { - podman rm -f "$CONTAINER_NAME" 2>/dev/null || true + podman rm -f "$TOOLS_CONTAINER" 2>/dev/null || true + podman rm -f "$CROSVM_CONTAINER" 2>/dev/null || true + if [ -n "$TMPDIR_CROSVM" ] && [ -d "$TMPDIR_CROSVM" ]; then + rm -rf "$TMPDIR_CROSVM" + fi } trap cleanup EXIT mkdir -p "$PREBUILT_DIR" "$PREBUILT_DIR/lib" # --------------------------------------------------------------------------- -# crosvm — must be provided; build instructions in .github/workflows/app.yml +# crosvm — build from source or copy pre-built binary # --------------------------------------------------------------------------- +crosvm_cache_key() { + local reverts_str + reverts_str=$(printf '%s\n' "${CROSVM_REVERTS[@]}") + local patches_hash + patches_hash=$(cat "$PATCHES_DIR"/*.patch 2>/dev/null | sha256sum | awk '{print $1}') + echo -n "${CROSVM_COMMIT}:${reverts_str}:${patches_hash}" | sha256sum | awk '{print $1}' +} + if [ -n "${CROSVM:-}" ]; then echo "==> crosvm: copying from ${CROSVM}" install -m755 "$CROSVM" "$PREBUILT_DIR/crosvm" echo " → prebuilt/crosvm" else - echo "ERROR: crosvm binary not found." >&2 - echo " Set CROSVM=/path/to/binary, e.g.:" >&2 - echo " CROSVM=~/bubbles/crosvm $0" >&2 - echo " Build instructions: .github/workflows/app.yml (build-crosvm job)" >&2 - exit 1 + CACHE_KEY=$(crosvm_cache_key) + CACHE_FILE="$PREBUILT_DIR/.crosvm-cache-key" + + if [ -f "$PREBUILT_DIR/crosvm" ] && [ -f "$CACHE_FILE" ] && [ "$(cat "$CACHE_FILE")" = "$CACHE_KEY" ]; then + echo "==> crosvm: cached, skipping (key: ${CACHE_KEY:0:12}…)" + else + echo "==> crosvm: building from source (commit ${CROSVM_COMMIT:0:12}…)" + TMPDIR_CROSVM=$(mktemp -d) + + echo " Cloning crosvm..." + git clone --quiet https://chromium.googlesource.com/crosvm/crosvm "$TMPDIR_CROSVM/crosvm" + cd "$TMPDIR_CROSVM/crosvm" + + git config user.email "prebuild@bubbles" + git config user.name "prebuild" + git checkout --quiet "$CROSVM_COMMIT" + + echo " Reverting commits..." + git revert --no-edit "${CROSVM_REVERTS[@]}" + + echo " Applying patches..." + for patch in "$PATCHES_DIR"/*.patch; do + echo " $(basename "$patch")" + git apply "$patch" + done + + echo " Initializing submodules..." + git submodule update --init + + echo " Building in container (this may take a while)..." + podman run -d --name "$CROSVM_CONTAINER" \ + -v "$TMPDIR_CROSVM/crosvm:/src:Z" \ + rust:trixie sleep infinity + + podman exec "$CROSVM_CONTAINER" bash -c \ + 'cd /src && apt-get update && sed -i "s/sudo //" tools/deps/install-x86_64-debs && tools/deps/install-x86_64-debs && cargo build --release' + + podman cp "$CROSVM_CONTAINER:/src/target/release/crosvm" "$PREBUILT_DIR/crosvm" + chmod +x "$PREBUILT_DIR/crosvm" + echo "$CACHE_KEY" > "$CACHE_FILE" + + cd "$SCRIPT_DIR" + echo " → prebuilt/crosvm" + fi fi # --------------------------------------------------------------------------- @@ -50,14 +116,14 @@ fi # --------------------------------------------------------------------------- echo "==> Setting up Debian Trixie container..." -podman run -d --name "$CONTAINER_NAME" debian:trixie sleep infinity -podman exec "$CONTAINER_NAME" sh -c \ +podman run -d --name "$TOOLS_CONTAINER" debian:trixie sleep infinity +podman exec "$TOOLS_CONTAINER" sh -c \ 'apt-get update && apt-get install -y --no-install-recommends socat qemu-utils' # Copy binaries echo "==> Copying binaries..." -podman cp "$CONTAINER_NAME:/usr/bin/socat1" "$PREBUILT_DIR/socat" -podman cp "$CONTAINER_NAME:/usr/bin/qemu-img" "$PREBUILT_DIR/qemu-img" +podman cp "$TOOLS_CONTAINER:/usr/bin/socat1" "$PREBUILT_DIR/socat" +podman cp "$TOOLS_CONTAINER:/usr/bin/qemu-img" "$PREBUILT_DIR/qemu-img" chmod +x "$PREBUILT_DIR/socat" "$PREBUILT_DIR/qemu-img" echo " → prebuilt/socat" echo " → prebuilt/qemu-img" @@ -68,7 +134,7 @@ echo "==> Copying runtime libraries..." # Libraries that are part of glibc or universally present — skip these SYSTEM_LIBS="linux-vdso|ld-linux|libc\.so|libm\.so|libdl\.so|librt\.so|libpthread\.so|libgcc_s\.so|libstdc\+\+" -DEPS=$(podman exec "$CONTAINER_NAME" sh -c \ +DEPS=$(podman exec "$TOOLS_CONTAINER" sh -c \ 'ldd /usr/bin/socat1 /usr/bin/qemu-img 2>/dev/null \ | grep "=> /" | awk "{print \$3}" | sort -u') @@ -77,7 +143,7 @@ for lib in $DEPS; do if echo "$libname" | grep -qE "$SYSTEM_LIBS"; then continue fi - podman cp "$CONTAINER_NAME:$lib" "$PREBUILT_DIR/lib/$libname" + podman cp "$TOOLS_CONTAINER:$lib" "$PREBUILT_DIR/lib/$libname" echo " → prebuilt/lib/$libname" done @@ -90,16 +156,16 @@ if [ -f "$SCRIPT_DIR/cargo-sources.json" ]; then echo "==> cargo-sources.json already exists, skipping" else echo "==> Generating cargo-sources.json (inside container)..." - podman exec "$CONTAINER_NAME" sh -c \ + podman exec "$TOOLS_CONTAINER" sh -c \ 'apt-get install -y --no-install-recommends python3 python3-aiohttp python3-tomlkit curl 2>/dev/null' curl -fsSL -o "$SCRIPT_DIR/.flatpak-cargo-generator.py" \ https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/4d5e760321236bd96fc1c6db9ec94c336600c114/cargo/flatpak-cargo-generator.py - podman cp "$SCRIPT_DIR/Cargo.lock" "$CONTAINER_NAME:/tmp/Cargo.lock" - podman cp "$SCRIPT_DIR/.flatpak-cargo-generator.py" "$CONTAINER_NAME:/tmp/flatpak-cargo-generator.py" + podman cp "$SCRIPT_DIR/Cargo.lock" "$TOOLS_CONTAINER:/tmp/Cargo.lock" + podman cp "$SCRIPT_DIR/.flatpak-cargo-generator.py" "$TOOLS_CONTAINER:/tmp/flatpak-cargo-generator.py" rm -f "$SCRIPT_DIR/.flatpak-cargo-generator.py" - podman exec "$CONTAINER_NAME" \ + podman exec "$TOOLS_CONTAINER" \ python3 /tmp/flatpak-cargo-generator.py /tmp/Cargo.lock -o /tmp/cargo-sources.json - podman cp "$CONTAINER_NAME:/tmp/cargo-sources.json" "$SCRIPT_DIR/cargo-sources.json" + podman cp "$TOOLS_CONTAINER:/tmp/cargo-sources.json" "$SCRIPT_DIR/cargo-sources.json" echo " → cargo-sources.json" fi From a40c89551b08a00b203514290e3c402cb8cb2e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20M=C3=BChlfort?= Date: Sun, 1 Mar 2026 20:05:23 +0100 Subject: [PATCH 03/13] gpu: enable AMD GPU acceleration via DRM native context - Build virglrenderer with amdgpu-experimental DRM renderer and crosvm with virgl_renderer feature (prebuild.bash) - Build guest Mesa with -Damdgpu-virtio=true for native radeonsi over virtio-gpu; install into VM image (vm-image.yml) - Start sommelier with --enable-linux-dmabuf for zero-copy DMA-BUF buffer forwarding (vm-image.yml) - Set crosvm GPU flags to backend=virglrenderer,context-types= drm:cross-domain on AMD hosts; fall back to cross-domain only (software rendering) on non-AMD hosts (main.rs) - Add LD_LIBRARY_PATH wrapper for crosvm to find virglrenderer - Add seccomp patches for fcntl broadening and num_scanouts=0 LLM-assisted commit --- .github/workflows/vm-image.yml | 53 +++++++- bubbles-app/de.gonicus.Bubbles.json | 14 ++- .../0004-seccomp-fcntl-broadening.patch | 14 +++ .../crosvm/0005-num-scanouts-zero.patch | 13 ++ bubbles-app/prebuild.bash | 44 ++++++- bubbles-app/src/main.rs | 115 ++++++++++++++---- 6 files changed, 220 insertions(+), 33 deletions(-) create mode 100644 bubbles-app/patches/crosvm/0004-seccomp-fcntl-broadening.patch create mode 100644 bubbles-app/patches/crosvm/0005-num-scanouts-zero.patch diff --git a/.github/workflows/vm-image.yml b/.github/workflows/vm-image.yml index 5296a14..0d80acd 100644 --- a/.github/workflows/vm-image.yml +++ b/.github/workflows/vm-image.yml @@ -49,10 +49,49 @@ jobs: name: sommelier path: /usr/local/bin/sommelier retention-days: 1 + build-mesa: + runs-on: ubuntu-latest + steps: + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y meson ninja-build pkg-config python3-mako \ + bison flex libdrm-dev libwayland-dev wayland-protocols \ + libwayland-egl-backend-dev libxcb-dri3-dev libxcb-present-dev \ + libxshmfence-dev libelf-dev libexpat1-dev libzstd-dev \ + zlib1g-dev llvm-dev libclang-dev cmake libglvnd-dev glslang-tools + - name: Build Mesa with amdgpu-virtio + run: | + git init mesa && cd mesa + git fetch --depth=1 https://gitlab.freedesktop.org/mesa/mesa.git a8baedef2905567a461191475cbd3565a21843db + git checkout FETCH_HEAD + meson setup builddir-virtio \ + -Damdgpu-virtio=true \ + -Dgallium-drivers=radeonsi \ + -Dvulkan-drivers=amd \ + -Dplatforms=wayland \ + -Dopengl=true \ + -Dglx=disabled \ + --prefix=/usr/local + ninja -C builddir-virtio + DESTDIR=$HOME/mesa-install ninja -C builddir-virtio install + - name: Create DRI driver symlink + run: | + cd $HOME/mesa-install/usr/local/lib/x86_64-linux-gnu + mkdir -p dri + cd dri + ln -sf ../libgallium-*.so virtio_gpu_dri.so + - name: 'Upload Artifact' + uses: actions/upload-artifact@v4 + with: + name: mesa-libs + path: ~/mesa-install/usr/local/lib/x86_64-linux-gnu/ + retention-days: 1 build-vm: needs: - build-agent - build-sommelier + - build-mesa runs-on: ubuntu-latest permissions: @@ -111,6 +150,11 @@ jobs: "path": "/usr/local/bin/", "source": "./usrlocalbin" }, + { + "generator": "copy", + "path": "/usr/local/lib/x86_64-linux-gnu/", + "source": "./mesa-libs" + }, { "generator": "copy", "path": "/etc/systemd/system/", @@ -169,6 +213,7 @@ jobs: #!/bin/bash set -eux chmod +x /usr/local/bin/* + ldconfig nix-channel --add https://nixos.org/channels/nixos-25.11 nixpkgs mkdir -p /etc/skel/.config starship preset nerd-font-symbols -o /etc/skel/.config/starship.toml @@ -236,7 +281,7 @@ jobs: Description=Sommelier [Service] - ExecStart=/usr/local/bin/sommelier --parent --virtgpu-channel + ExecStart=/usr/local/bin/sommelier --parent --virtgpu-channel --enable-linux-dmabuf [Install] WantedBy=default.target @@ -281,8 +326,14 @@ jobs: - name: Download bubbles-agent and sommelier uses: actions/download-artifact@v5 with: + pattern: '{bubbles-agent,sommelier}' path: usrlocalbin merge-multiple: true + - name: Download Mesa libraries + uses: actions/download-artifact@v5 + with: + name: mesa-libs + path: mesa-libs - run: | nix-shell -p distrobuilder --command 'ln -s $(which distrobuilder) distrobuilder' nix-shell -p oras --command 'ln -s $(which oras) oras' diff --git a/bubbles-app/de.gonicus.Bubbles.json b/bubbles-app/de.gonicus.Bubbles.json index 015dc01..72b7774 100644 --- a/bubbles-app/de.gonicus.Bubbles.json +++ b/bubbles-app/de.gonicus.Bubbles.json @@ -70,10 +70,20 @@ "name": "crosvm", "buildsystem": "simple", "build-commands": [ - "install -Dm755 crosvm /app/bin/crosvm" + "install -Dm755 crosvm /app/bin/crosvm.bin", + "install -Dm755 crosvm-wrap /app/bin/crosvm" ], "sources": [ - { "type": "file", "path": "prebuilt/crosvm" } + { "type": "file", "path": "prebuilt/crosvm" }, + { + "type": "script", + "dest-filename": "crosvm-wrap", + "commands": [ + "SELF=$(realpath \"$0\")", + "DIR=$(dirname \"$SELF\")", + "exec env LD_LIBRARY_PATH=\"${DIR}/../lib/compat${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\" \"${DIR}/crosvm.bin\" \"$@\"" + ] + } ] }, { diff --git a/bubbles-app/patches/crosvm/0004-seccomp-fcntl-broadening.patch b/bubbles-app/patches/crosvm/0004-seccomp-fcntl-broadening.patch new file mode 100644 index 0000000..b48a878 --- /dev/null +++ b/bubbles-app/patches/crosvm/0004-seccomp-fcntl-broadening.patch @@ -0,0 +1,14 @@ +diff --git a/jail/seccomp/x86_64/gpu_common.policy b/jail/seccomp/x86_64/gpu_common.policy +index c87148a88..98f30125b 100644 +--- a/jail/seccomp/x86_64/gpu_common.policy ++++ b/jail/seccomp/x86_64/gpu_common.policy +@@ -67,8 +67,7 @@ unlinkat: 1 + # Rules specific to gpu + connect: 1 + # 1033 is F_ADD_SEALS, 1034 is F_GET_SEALS +-fcntl: arg1 == F_DUPFD_CLOEXEC || arg1 == F_GETFD || arg1 == F_SETFD || \ +- arg1 == F_GETFL || arg1 == F_SETFL || arg1 == 1033 || arg1 == 1034 ++fcntl: 1 + fstat: 1 + getdents: 1 + getdents64: 1 diff --git a/bubbles-app/patches/crosvm/0005-num-scanouts-zero.patch b/bubbles-app/patches/crosvm/0005-num-scanouts-zero.patch new file mode 100644 index 0000000..ef542d1 --- /dev/null +++ b/bubbles-app/patches/crosvm/0005-num-scanouts-zero.patch @@ -0,0 +1,13 @@ +diff --git a/devices/src/virtio/gpu/mod.rs b/devices/src/virtio/gpu/mod.rs +index c5d6977be..4f32c4bdd 100644 +--- a/devices/src/virtio/gpu/mod.rs ++++ b/devices/src/virtio/gpu/mod.rs +@@ -1755,7 +1755,7 @@ impl Gpu { + virtio_gpu_config { + events_read: Le32::from(events_read), + events_clear: Le32::from(0), +- num_scanouts: Le32::from(VIRTIO_GPU_MAX_SCANOUTS as u32), ++ num_scanouts: Le32::from(0), + num_capsets: Le32::from(num_capsets), + } + } diff --git a/bubbles-app/prebuild.bash b/bubbles-app/prebuild.bash index d317757..682030f 100755 --- a/bubbles-app/prebuild.bash +++ b/bubbles-app/prebuild.bash @@ -23,6 +23,7 @@ PATCHES_DIR="$SCRIPT_DIR/patches/crosvm" # --- crosvm source configuration --- CROSVM_COMMIT="a96cb379acf55a75887cbba190666e7d22ff9dbf" +VIRGLRENDERER_COMMIT="ca50e008863837e094747a69974dde3ae148aeaa" CROSVM_REVERTS=( 1656a1f68296baa4313b4b46e23a6c975caa7cc9 2c6f23406c41af8432c1c1ba4e3605785e959ead @@ -55,7 +56,7 @@ crosvm_cache_key() { reverts_str=$(printf '%s\n' "${CROSVM_REVERTS[@]}") local patches_hash patches_hash=$(cat "$PATCHES_DIR"/*.patch 2>/dev/null | sha256sum | awk '{print $1}') - echo -n "${CROSVM_COMMIT}:${reverts_str}:${patches_hash}" | sha256sum | awk '{print $1}' + echo -n "${CROSVM_COMMIT}:${VIRGLRENDERER_COMMIT}:${reverts_str}:${patches_hash}" | sha256sum | awk '{print $1}' } if [ -n "${CROSVM:-}" ]; then @@ -92,15 +93,52 @@ else echo " Initializing submodules..." git submodule update --init + echo " Cloning virglrenderer..." + git init "$TMPDIR_CROSVM/virglrenderer" + git -C "$TMPDIR_CROSVM/virglrenderer" fetch --depth=1 \ + https://gitlab.freedesktop.org/virgl/virglrenderer.git "$VIRGLRENDERER_COMMIT" + git -C "$TMPDIR_CROSVM/virglrenderer" checkout FETCH_HEAD + echo " Building in container (this may take a while)..." podman run -d --name "$CROSVM_CONTAINER" \ -v "$TMPDIR_CROSVM/crosvm:/src:Z" \ + -v "$TMPDIR_CROSVM/virglrenderer:/virglrenderer:Z" \ rust:trixie sleep infinity - podman exec "$CROSVM_CONTAINER" bash -c \ - 'cd /src && apt-get update && sed -i "s/sudo //" tools/deps/install-x86_64-debs && tools/deps/install-x86_64-debs && cargo build --release' + # Build virglrenderer first (with amdgpu-experimental DRM renderer) + podman exec "$CROSVM_CONTAINER" bash -c ' + apt-get update + apt-get install -y meson ninja-build libgbm-dev libdrm-dev libepoxy-dev pkg-config python3-yaml + cd /virglrenderer + meson setup builddir --prefix=/usr/local \ + -Dvenus=true -Dplatforms=egl -Ddrm-renderers=amdgpu-experimental + ninja -C builddir + DESTDIR=/opt/virglrenderer-install ninja -C builddir install + find /opt/virglrenderer-install -name virglrenderer.pc \ + -exec sed -i "s|^prefix=.*|prefix=/opt/virglrenderer-install/usr/local|" {} \; + ' + + # Detect the library directory virglrenderer was installed into + VIRGL_LIBDIR=$(podman exec "$CROSVM_CONTAINER" \ + find /opt/virglrenderer-install/usr/local -name 'libvirglrenderer.so' -printf '%h' -quit) + + # Build crosvm with virgl_renderer feature + podman exec "$CROSVM_CONTAINER" bash -c " + cd /src + sed -i 's/sudo //' tools/deps/install-x86_64-debs && tools/deps/install-x86_64-debs + PKG_CONFIG_PATH=${VIRGL_LIBDIR}/pkgconfig \ + LD_LIBRARY_PATH=${VIRGL_LIBDIR} \ + cargo build --release --features virgl_renderer + " podman cp "$CROSVM_CONTAINER:/src/target/release/crosvm" "$PREBUILT_DIR/crosvm" + + # Copy virglrenderer libraries + echo " Copying virglrenderer libraries..." + podman exec "$CROSVM_CONTAINER" bash -c "ls ${VIRGL_LIBDIR}/" | while read -r f; do + podman cp "$CROSVM_CONTAINER:${VIRGL_LIBDIR}/$f" "$PREBUILT_DIR/lib/$f" + echo " → prebuilt/lib/$f" + done chmod +x "$PREBUILT_DIR/crosvm" echo "$CACHE_KEY" > "$CACHE_FILE" diff --git a/bubbles-app/src/main.rs b/bubbles-app/src/main.rs index dcd5734..0f97654 100644 --- a/bubbles-app/src/main.rs +++ b/bubbles-app/src/main.rs @@ -36,6 +36,53 @@ fn make_host_args(args: &[&OsStr]) -> Vec { } } +fn make_host_args_with_env(env_vars: &[&str], args: &[&OsStr]) -> Vec { + if is_flatpak() { + let uid = unsafe { libc::getuid() }; + let mut v: Vec = vec![ + "flatpak-spawn".into(), + "--host".into(), + format!("--env=XDG_RUNTIME_DIR=/run/user/{}", uid).into(), + ]; + for var in env_vars { + v.push(format!("--env={}", var).into()); + } + v.extend(args.iter().map(|a| (*a).to_owned())); + v + } else { + // For non-Flatpak, set env vars in current process (inherited by child) + for var in env_vars { + if let Some((key, val)) = var.split_once('=') { + env::set_var(key, val); + } + } + args.iter().map(|a| (*a).to_owned()).collect() + } +} + +/// Detect whether the host has an AMD GPU by checking PCI vendor IDs. +/// Returns true if any render node belongs to AMD (vendor 0x1002). +/// In Flatpak, /dev/dri is accessible via --device=dri and sysfs is readable. +fn host_has_amd_gpu() -> bool { + let dri_path = Path::new("/sys/class/drm"); + if let Ok(entries) = fs::read_dir(dri_path) { + for entry in entries.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if !name_str.starts_with("renderD") { + continue; + } + let vendor_path = entry.path().join("device/vendor"); + if let Ok(vendor) = fs::read_to_string(&vendor_path) { + if vendor.trim() == "0x1002" { + return true; + } + } + } + } + false +} + fn flatpak_host_bin(name: &str) -> PathBuf { // /.flatpak-info is always readable inside the sandbox and contains // app-path= for the actual installation (user or system). @@ -453,33 +500,47 @@ impl AsyncFactoryComponent for VmEntry { let wayland_sock = wayland_sock_path(); let vsock_cid = format!("{}", index.current_index() + 10); let passt_socket_str = format!("net,socket={}", passt_socket_path.to_str().expect("string")); - let crosvm_host_args = make_host_args(&[ - crosvm_bin.as_os_str(), - OsStr::new("run"), - OsStr::new("--name"), - OsStr::new(&vm_name), - OsStr::new("--cpus"), - OsStr::new("num-cores=4"), - OsStr::new("-m"), - OsStr::new("7000"), - OsStr::new("--rwdisk"), - image_disk_path.as_os_str(), - OsStr::new("--initrd"), - image_initrd_path.as_os_str(), - OsStr::new("--socket"), - crosvm_socket_path.as_os_str(), - OsStr::new("--vsock"), - OsStr::new(&vsock_cid), - OsStr::new("--gpu"), - OsStr::new("context-types=cross-domain,displays=[]"), - OsStr::new("--wayland-sock"), - wayland_sock.as_os_str(), - OsStr::new("--vhost-user"), - OsStr::new(&passt_socket_str), - OsStr::new("-p"), - OsStr::new("root=/dev/vda2"), - image_linuz_path.as_os_str(), - ]); + let is_amd = host_has_amd_gpu(); + let gpu_flag = if is_amd { + "backend=virglrenderer,context-types=drm:cross-domain,displays=[]" + } else { + "context-types=cross-domain,displays=[]" + }; + let env_vars: Vec<&str> = if is_amd { + vec!["VIRGL_GBM_LAYOUT_FORCE_ENABLE=true"] + } else { + vec![] + }; + let crosvm_host_args = make_host_args_with_env( + &env_vars, + &[ + crosvm_bin.as_os_str(), + OsStr::new("run"), + OsStr::new("--name"), + OsStr::new(&vm_name), + OsStr::new("--cpus"), + OsStr::new("num-cores=4"), + OsStr::new("-m"), + OsStr::new("7000"), + OsStr::new("--rwdisk"), + image_disk_path.as_os_str(), + OsStr::new("--initrd"), + image_initrd_path.as_os_str(), + OsStr::new("--socket"), + crosvm_socket_path.as_os_str(), + OsStr::new("--vsock"), + OsStr::new(&vsock_cid), + OsStr::new("--gpu"), + OsStr::new(gpu_flag), + OsStr::new("--wayland-sock"), + wayland_sock.as_os_str(), + OsStr::new("--vhost-user"), + OsStr::new(&passt_socket_str), + OsStr::new("-p"), + OsStr::new("root=/dev/vda2"), + image_linuz_path.as_os_str(), + ], + ); let crosvm_host_args_ref: Vec<&OsStr> = crosvm_host_args.iter().map(OsString::as_os_str).collect(); let crosvm_process = gtk::gio::Subprocess::newv( &crosvm_host_args_ref, From b655fe274c24d6d35bc5386aa68b64398e4454a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20M=C3=BChlfort?= Date: Mon, 2 Mar 2026 10:15:00 +0000 Subject: [PATCH 04/13] Add LICENSE for crosvm patches --- bubbles-app/patches/crosvm/LICENSE | 27 +++++++++++++++++++++++++++ bubbles-app/patches/crosvm/README.md | 5 +++++ 2 files changed, 32 insertions(+) create mode 100644 bubbles-app/patches/crosvm/LICENSE create mode 100644 bubbles-app/patches/crosvm/README.md diff --git a/bubbles-app/patches/crosvm/LICENSE b/bubbles-app/patches/crosvm/LICENSE new file mode 100644 index 0000000..bd43331 --- /dev/null +++ b/bubbles-app/patches/crosvm/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017 The ChromiumOS Authors +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/bubbles-app/patches/crosvm/README.md b/bubbles-app/patches/crosvm/README.md new file mode 100644 index 0000000..da7eee3 --- /dev/null +++ b/bubbles-app/patches/crosvm/README.md @@ -0,0 +1,5 @@ +These patches modify crosvm source code. + +`crosvm` is distributed under the terms of its [LICENSE notice](./LICENSE) of their original authors. The license notice is taken directly from https://chromium.googlesource.com/crosvm/crosvm/+/refs/heads/main/LICENSE + +The [patch](./0002-display-backend-stub.patch) for disabling the display is derived from https://gitlab.com/talex5/crosvm/-/commit/2e71ed5243ff1e484b6cb14c515805ed69b8ece2 \ No newline at end of file From 8f3e1e1c0907a60ee251e603646a3d216b37a003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20M=C3=BChlfort?= Date: Mon, 2 Mar 2026 11:29:20 +0100 Subject: [PATCH 05/13] Add LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..33f0352 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 GONICUS GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 962922dd74cba40f007de6426b197195dc3ebd12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20M=C3=BChlfort?= Date: Mon, 2 Mar 2026 11:59:22 +0000 Subject: [PATCH 06/13] readme: postpone README.md changes --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index febfa7a..fc64417 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,13 @@ ## Getting started -Bubbles is distributed as a Flatpak. +Right now, bubbles is distributed via a container outputting the required binaries into `$HOME/bubbles`. Requirements: -- `flatpak` -- `passt` (must be installed on the host: `dnf install passt` or `apt install passt`) +- `podman`/`docker` for installation +- `passt` +- `qemu-img` +- `curl` Loose Recommendation: - `btrfs` as backing filesystem (seems to optimize for disk image deduplication under the hood) @@ -30,23 +32,20 @@ Loose Recommendation: ### Install ``` -flatpak install de.gonicus.Bubbles.flatpak +mkdir $HOME/bubbles +# May be different for non-SELinux systems: skip ":Z" +# May be different for docker: You may need to chown files afterwards +podman run -v "$HOME/bubbles:/output:Z" ghcr.io/gonicus/bubbles/bubbles:841f165307e5d15b789cd8fc1aab40b7ecef6f3e +# For .desktop file: +cat > ~/.local/share/applications/bubbles.desktop < Date: Tue, 3 Mar 2026 16:57:27 +0100 Subject: [PATCH 07/13] flatpak: add licenses LLM-assisted commit --- bubbles-app/de.gonicus.Bubbles.json | 29 +++++++++---- bubbles-app/prebuild.bash | 64 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/bubbles-app/de.gonicus.Bubbles.json b/bubbles-app/de.gonicus.Bubbles.json index 015dc01..7bec533 100644 --- a/bubbles-app/de.gonicus.Bubbles.json +++ b/bubbles-app/de.gonicus.Bubbles.json @@ -25,10 +25,12 @@ "buildsystem": "simple", "build-commands": [ "install -d /app/lib/compat", - "find . -maxdepth 1 -type f -exec install -m755 {} /app/lib/compat/ \\;" + "find . -maxdepth 1 -type f -exec install -m755 {} /app/lib/compat/ \\;", + "for pkg in licenses/debian/*/; do pkgname=$(basename \"$pkg\"); install -d \"/app/share/licenses/de.gonicus.Bubbles/${pkgname}\"; install -m644 \"${pkg}\"* \"/app/share/licenses/de.gonicus.Bubbles/${pkgname}/\"; done" ], "sources": [ - { "type": "dir", "path": "prebuilt/lib" } + { "type": "dir", "path": "prebuilt/lib" }, + { "type": "dir", "path": "prebuilt/licenses", "dest": "licenses" } ] }, { @@ -36,7 +38,9 @@ "buildsystem": "simple", "build-commands": [ "install -Dm755 qemu-img /app/bin/qemu-img.bin", - "install -Dm755 qemu-img-wrap /app/bin/qemu-img" + "install -Dm755 qemu-img-wrap /app/bin/qemu-img", + "install -d /app/share/licenses/de.gonicus.Bubbles/qemu-img", + "install -m644 qemu-img-licenses/* /app/share/licenses/de.gonicus.Bubbles/qemu-img/" ], "sources": [ { "type": "file", "path": "prebuilt/qemu-img" }, @@ -48,14 +52,16 @@ "DIR=$(dirname \"$SELF\")", "exec env LD_LIBRARY_PATH=\"${DIR}/../lib/compat${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\" \"${DIR}/qemu-img.bin\" \"$@\"" ] - } + }, + { "type": "dir", "path": "prebuilt/licenses/debian/qemu-img", "dest": "qemu-img-licenses" } ] }, { "name": "oras", "buildsystem": "simple", "build-commands": [ - "install -Dm755 oras /app/bin/oras" + "install -Dm755 oras /app/bin/oras", + "install -Dm644 LICENSE /app/share/licenses/de.gonicus.Bubbles/oras/LICENSE" ], "sources": [ { @@ -70,10 +76,12 @@ "name": "crosvm", "buildsystem": "simple", "build-commands": [ - "install -Dm755 crosvm /app/bin/crosvm" + "install -Dm755 crosvm /app/bin/crosvm", + "install -Dm644 LICENSE /app/share/licenses/de.gonicus.Bubbles/crosvm/LICENSE" ], "sources": [ - { "type": "file", "path": "prebuilt/crosvm" } + { "type": "file", "path": "prebuilt/crosvm" }, + { "type": "file", "path": "patches/crosvm/LICENSE" } ] }, { @@ -81,7 +89,9 @@ "buildsystem": "simple", "build-commands": [ "install -Dm755 socat /app/bin/socat.bin", - "install -Dm755 socat-wrap /app/bin/socat" + "install -Dm755 socat-wrap /app/bin/socat", + "install -d /app/share/licenses/de.gonicus.Bubbles/socat", + "install -m644 socat-licenses/* /app/share/licenses/de.gonicus.Bubbles/socat/" ], "sources": [ { "type": "file", "path": "prebuilt/socat" }, @@ -93,7 +103,8 @@ "DIR=$(dirname \"$SELF\")", "exec env LD_LIBRARY_PATH=\"${DIR}/../lib/compat${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\" \"${DIR}/socat.bin\" \"$@\"" ] - } + }, + { "type": "dir", "path": "prebuilt/licenses/debian/socat", "dest": "socat-licenses" } ] }, { diff --git a/bubbles-app/prebuild.bash b/bubbles-app/prebuild.bash index d317757..1554b08 100755 --- a/bubbles-app/prebuild.bash +++ b/bubbles-app/prebuild.bash @@ -147,6 +147,70 @@ for lib in $DEPS; do echo " → prebuilt/lib/$libname" done +# --------------------------------------------------------------------------- +# License collection — gather license/copyright files for all bundled components +# --------------------------------------------------------------------------- +echo "==> Collecting licenses..." + +LICENSES_DIR="$PREBUILT_DIR/licenses" +rm -rf "$LICENSES_DIR" +mkdir -p "$LICENSES_DIR/crosvm" "$LICENSES_DIR/debian" + +# crosvm (BSD-3-Clause) — already checked into the repo +cp "$PATCHES_DIR/LICENSE" "$LICENSES_DIR/crosvm/LICENSE" +echo " → licenses/crosvm/LICENSE" + +# socat and qemu-img — extract copyright + source info +# Map binary names to their Debian package names (they don't always match) +declare -A BIN_TO_PKG=( [socat]=socat [qemu-img]=qemu-utils ) + +for bin_name in socat qemu-img; do + deb_pkg="${BIN_TO_PKG[$bin_name]}" + pkg_dir="$LICENSES_DIR/debian/$bin_name" + mkdir -p "$pkg_dir" + + # Extract Debian copyright file (keyed by Debian package name) + podman cp "$TOOLS_CONTAINER:/usr/share/doc/$deb_pkg/copyright" "$pkg_dir/copyright" 2>/dev/null || \ + podman exec "$TOOLS_CONTAINER" sh -c "cat /usr/share/doc/${deb_pkg}/copyright" > "$pkg_dir/copyright" + + # Get package version for the source offer + pkg_version=$(podman exec "$TOOLS_CONTAINER" dpkg-query -W -f '${Version}' "$deb_pkg") + + cat > "$pkg_dir/SOURCE-INFO" </dev/null + done | grep "=> /" | awk "{print \$3}" | sort -u | while read -r libpath; do + dpkg -S "$libpath" 2>/dev/null | cut -d: -f1 + done | sort -u +') + +for pkg in $LIB_PKGS; do + # Skip packages we already handle directly + case "$pkg" in + socat|qemu-utils|libc6|libc-bin) continue ;; + esac + + pkg_dir="$LICENSES_DIR/debian/$pkg" + mkdir -p "$pkg_dir" + podman cp "$TOOLS_CONTAINER:/usr/share/doc/$pkg/copyright" "$pkg_dir/copyright" 2>/dev/null || \ + podman exec "$TOOLS_CONTAINER" sh -c "cat /usr/share/doc/${pkg}/copyright" > "$pkg_dir/copyright" 2>/dev/null || \ + echo " Warning: no copyright file found for $pkg" + echo " → licenses/debian/$pkg/copyright" +done + # --------------------------------------------------------------------------- # cargo-sources.json — Flatpak needs this for offline Cargo builds # Run generator inside the container using apt-provided Python packages From 6e7dafdae13ec66c7696cc4165ad8cf428f32aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20M=C3=BChlfort?= Date: Tue, 3 Mar 2026 18:15:21 +0100 Subject: [PATCH 08/13] ci: release .flatpak as release artifact upon pushing of tags LLM-assisted commit --- .github/workflows/app.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/app.yml b/.github/workflows/app.yml index b6b8e36..093d665 100644 --- a/.github/workflows/app.yml +++ b/.github/workflows/app.yml @@ -2,9 +2,10 @@ name: AppBuild on: push: - branches: [ "main" ] - paths: - - "bubbles-app/**" + tags: [ "v*" ] + +permissions: + contents: write jobs: build-flatpak: @@ -34,8 +35,11 @@ jobs: flatpak build-bundle ~/.local/share/flatpak/repo \ de.gonicus.Bubbles.flatpak de.gonicus.Bubbles - - name: Upload bundle - uses: actions/upload-artifact@v5 - with: - name: de.gonicus.Bubbles.flatpak - path: de.gonicus.Bubbles.flatpak + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ github.ref_name }}" \ + --title "${{ github.ref_name }}" \ + --generate-notes \ + de.gonicus.Bubbles.flatpak From 8db0bb31cb2203710c45801578bdb8905e4580eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20M=C3=BChlfort?= Date: Tue, 3 Mar 2026 18:25:32 +0100 Subject: [PATCH 09/13] cargo update --- bubbles-app/Cargo.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/bubbles-app/Cargo.lock b/bubbles-app/Cargo.lock index 4a221bc..eacd2cd 100644 --- a/bubbles-app/Cargo.lock +++ b/bubbles-app/Cargo.lock @@ -491,9 +491,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.90" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -794,9 +794,9 @@ checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -908,9 +908,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -921,9 +921,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -931,9 +931,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -944,9 +944,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] From bf299b29bad9ef445e161c7b207f093d210a7b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20M=C3=BChlfort?= Date: Wed, 4 Mar 2026 16:45:27 +0100 Subject: [PATCH 10/13] flatpak: bundle all runtime libs LLM-assisted commit --- bubbles-app/de.gonicus.Bubbles.json | 16 ++++++++++++++-- bubbles-app/prebuild.bash | 14 ++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/bubbles-app/de.gonicus.Bubbles.json b/bubbles-app/de.gonicus.Bubbles.json index 7bec533..53a2e34 100644 --- a/bubbles-app/de.gonicus.Bubbles.json +++ b/bubbles-app/de.gonicus.Bubbles.json @@ -50,7 +50,8 @@ "commands": [ "SELF=$(realpath \"$0\")", "DIR=$(dirname \"$SELF\")", - "exec env LD_LIBRARY_PATH=\"${DIR}/../lib/compat${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\" \"${DIR}/qemu-img.bin\" \"$@\"" + "LIB=\"${DIR}/../lib/compat\"", + "exec \"$LIB/ld-linux-x86-64.so.2\" --library-path \"$LIB\" \"${DIR}/qemu-img.bin\" \"$@\"" ] }, { "type": "dir", "path": "prebuilt/licenses/debian/qemu-img", "dest": "qemu-img-licenses" } @@ -81,6 +82,16 @@ ], "sources": [ { "type": "file", "path": "prebuilt/crosvm" }, + { + "type": "script", + "dest-filename": "crosvm-wrap", + "commands": [ + "SELF=$(realpath \"$0\")", + "DIR=$(dirname \"$SELF\")", + "LIB=\"${DIR}/../lib/compat\"", + "exec \"$LIB/ld-linux-x86-64.so.2\" --library-path \"$LIB\" \"${DIR}/crosvm.bin\" \"$@\"" + ] + }, { "type": "file", "path": "patches/crosvm/LICENSE" } ] }, @@ -101,7 +112,8 @@ "commands": [ "SELF=$(realpath \"$0\")", "DIR=$(dirname \"$SELF\")", - "exec env LD_LIBRARY_PATH=\"${DIR}/../lib/compat${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\" \"${DIR}/socat.bin\" \"$@\"" + "LIB=\"${DIR}/../lib/compat\"", + "exec \"$LIB/ld-linux-x86-64.so.2\" --library-path \"$LIB\" \"${DIR}/socat.bin\" \"$@\"" ] }, { "type": "dir", "path": "prebuilt/licenses/debian/socat", "dest": "socat-licenses" } diff --git a/bubbles-app/prebuild.bash b/bubbles-app/prebuild.bash index 1554b08..02fe6ab 100755 --- a/bubbles-app/prebuild.bash +++ b/bubbles-app/prebuild.bash @@ -128,11 +128,12 @@ chmod +x "$PREBUILT_DIR/socat" "$PREBUILT_DIR/qemu-img" echo " → prebuilt/socat" echo " → prebuilt/qemu-img" -# Copy runtime library dependencies (excluding glibc/base system libs) +# Copy runtime library dependencies (including glibc for non-FHS distro support) echo "==> Copying runtime libraries..." -# Libraries that are part of glibc or universally present — skip these -SYSTEM_LIBS="linux-vdso|ld-linux|libc\.so|libm\.so|libdl\.so|librt\.so|libpthread\.so|libgcc_s\.so|libstdc\+\+" +# Only skip the kernel-injected vDSO — bundle everything else including glibc, +# so binaries work on non-FHS distros (NixOS, Guix) where /lib64/ doesn't exist. +SYSTEM_LIBS="linux-vdso" DEPS=$(podman exec "$TOOLS_CONTAINER" sh -c \ 'ldd /usr/bin/socat1 /usr/bin/qemu-img 2>/dev/null \ @@ -147,6 +148,11 @@ for lib in $DEPS; do echo " → prebuilt/lib/$libname" done +# Bundle the dynamic linker itself (not captured by ldd grep pattern) +podman cp "$TOOLS_CONTAINER:/lib64/ld-linux-x86-64.so.2" "$PREBUILT_DIR/lib/ld-linux-x86-64.so.2" +chmod +x "$PREBUILT_DIR/lib/ld-linux-x86-64.so.2" +echo " → prebuilt/lib/ld-linux-x86-64.so.2" + # --------------------------------------------------------------------------- # License collection — gather license/copyright files for all bundled components # --------------------------------------------------------------------------- @@ -200,7 +206,7 @@ LIB_PKGS=$(podman exec "$TOOLS_CONTAINER" sh -c ' for pkg in $LIB_PKGS; do # Skip packages we already handle directly case "$pkg" in - socat|qemu-utils|libc6|libc-bin) continue ;; + socat|qemu-utils) continue ;; esac pkg_dir="$LICENSES_DIR/debian/$pkg" From 99a7c5ddef6e33ccad3b74a23065cff38a2a78c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20M=C3=BChlfort?= Date: Wed, 4 Mar 2026 16:45:27 +0100 Subject: [PATCH 11/13] flatpak: bundle all runtime libs LLM-assisted commit --- bubbles-app/de.gonicus.Bubbles.json | 19 ++++++++++++++++--- bubbles-app/prebuild.bash | 14 ++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/bubbles-app/de.gonicus.Bubbles.json b/bubbles-app/de.gonicus.Bubbles.json index 7bec533..fd37b74 100644 --- a/bubbles-app/de.gonicus.Bubbles.json +++ b/bubbles-app/de.gonicus.Bubbles.json @@ -50,7 +50,8 @@ "commands": [ "SELF=$(realpath \"$0\")", "DIR=$(dirname \"$SELF\")", - "exec env LD_LIBRARY_PATH=\"${DIR}/../lib/compat${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\" \"${DIR}/qemu-img.bin\" \"$@\"" + "LIB=\"${DIR}/../lib/compat\"", + "exec \"$LIB/ld-linux-x86-64.so.2\" --library-path \"$LIB\" \"${DIR}/qemu-img.bin\" \"$@\"" ] }, { "type": "dir", "path": "prebuilt/licenses/debian/qemu-img", "dest": "qemu-img-licenses" } @@ -76,11 +77,22 @@ "name": "crosvm", "buildsystem": "simple", "build-commands": [ - "install -Dm755 crosvm /app/bin/crosvm", + "install -Dm755 crosvm /app/bin/crosvm.bin", + "install -Dm755 crosvm-wrap /app/bin/crosvm", "install -Dm644 LICENSE /app/share/licenses/de.gonicus.Bubbles/crosvm/LICENSE" ], "sources": [ { "type": "file", "path": "prebuilt/crosvm" }, + { + "type": "script", + "dest-filename": "crosvm-wrap", + "commands": [ + "SELF=$(realpath \"$0\")", + "DIR=$(dirname \"$SELF\")", + "LIB=\"${DIR}/../lib/compat\"", + "exec \"$LIB/ld-linux-x86-64.so.2\" --library-path \"$LIB\" \"${DIR}/crosvm.bin\" \"$@\"" + ] + }, { "type": "file", "path": "patches/crosvm/LICENSE" } ] }, @@ -101,7 +113,8 @@ "commands": [ "SELF=$(realpath \"$0\")", "DIR=$(dirname \"$SELF\")", - "exec env LD_LIBRARY_PATH=\"${DIR}/../lib/compat${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}\" \"${DIR}/socat.bin\" \"$@\"" + "LIB=\"${DIR}/../lib/compat\"", + "exec \"$LIB/ld-linux-x86-64.so.2\" --library-path \"$LIB\" \"${DIR}/socat.bin\" \"$@\"" ] }, { "type": "dir", "path": "prebuilt/licenses/debian/socat", "dest": "socat-licenses" } diff --git a/bubbles-app/prebuild.bash b/bubbles-app/prebuild.bash index 1554b08..02fe6ab 100755 --- a/bubbles-app/prebuild.bash +++ b/bubbles-app/prebuild.bash @@ -128,11 +128,12 @@ chmod +x "$PREBUILT_DIR/socat" "$PREBUILT_DIR/qemu-img" echo " → prebuilt/socat" echo " → prebuilt/qemu-img" -# Copy runtime library dependencies (excluding glibc/base system libs) +# Copy runtime library dependencies (including glibc for non-FHS distro support) echo "==> Copying runtime libraries..." -# Libraries that are part of glibc or universally present — skip these -SYSTEM_LIBS="linux-vdso|ld-linux|libc\.so|libm\.so|libdl\.so|librt\.so|libpthread\.so|libgcc_s\.so|libstdc\+\+" +# Only skip the kernel-injected vDSO — bundle everything else including glibc, +# so binaries work on non-FHS distros (NixOS, Guix) where /lib64/ doesn't exist. +SYSTEM_LIBS="linux-vdso" DEPS=$(podman exec "$TOOLS_CONTAINER" sh -c \ 'ldd /usr/bin/socat1 /usr/bin/qemu-img 2>/dev/null \ @@ -147,6 +148,11 @@ for lib in $DEPS; do echo " → prebuilt/lib/$libname" done +# Bundle the dynamic linker itself (not captured by ldd grep pattern) +podman cp "$TOOLS_CONTAINER:/lib64/ld-linux-x86-64.so.2" "$PREBUILT_DIR/lib/ld-linux-x86-64.so.2" +chmod +x "$PREBUILT_DIR/lib/ld-linux-x86-64.so.2" +echo " → prebuilt/lib/ld-linux-x86-64.so.2" + # --------------------------------------------------------------------------- # License collection — gather license/copyright files for all bundled components # --------------------------------------------------------------------------- @@ -200,7 +206,7 @@ LIB_PKGS=$(podman exec "$TOOLS_CONTAINER" sh -c ' for pkg in $LIB_PKGS; do # Skip packages we already handle directly case "$pkg" in - socat|qemu-utils|libc6|libc-bin) continue ;; + socat|qemu-utils) continue ;; esac pkg_dir="$LICENSES_DIR/debian/$pkg" From a9761fc889aebfcf5943052ec271e3d71dd84f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20M=C3=BChlfort?= Date: Wed, 4 Mar 2026 20:40:56 +0100 Subject: [PATCH 12/13] flatpak: also add crosvm libs LLM-assisted commit --- bubbles-app/prebuild.bash | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bubbles-app/prebuild.bash b/bubbles-app/prebuild.bash index 8ddc83c..21a43e3 100755 --- a/bubbles-app/prebuild.bash +++ b/bubbles-app/prebuild.bash @@ -139,6 +139,22 @@ else podman cp "$CROSVM_CONTAINER:${VIRGL_LIBDIR}/$f" "$PREBUILT_DIR/lib/$f" echo " → prebuilt/lib/$f" done + + # Copy crosvm runtime library dependencies (e.g. libwayland-client, libcap) + echo " Copying crosvm runtime libraries..." + CROSVM_DEPS=$(podman exec "$CROSVM_CONTAINER" bash -c " + ldd /src/target/release/crosvm 2>/dev/null \ + | grep '=> /' | awk '{print \$3}' | sort -u") + for lib in $CROSVM_DEPS; do + libname=$(basename "$lib") + # Skip libs already copied (virglrenderer, vDSO, libc basics already bundled by tools step) + if [ -f "$PREBUILT_DIR/lib/$libname" ]; then + continue + fi + podman cp "$CROSVM_CONTAINER:$lib" "$PREBUILT_DIR/lib/$libname" + echo " → prebuilt/lib/$libname" + done + chmod +x "$PREBUILT_DIR/crosvm" echo "$CACHE_KEY" > "$CACHE_FILE" From d83ddee51804358ff851e5515910f523e1620956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20M=C3=BChlfort?= Date: Wed, 4 Mar 2026 20:57:04 +0100 Subject: [PATCH 13/13] flatpak: include transitive libraries LLM-assisted commit --- bubbles-app/prebuild.bash | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bubbles-app/prebuild.bash b/bubbles-app/prebuild.bash index 21a43e3..2eb1e6f 100755 --- a/bubbles-app/prebuild.bash +++ b/bubbles-app/prebuild.bash @@ -140,14 +140,18 @@ else echo " → prebuilt/lib/$f" done - # Copy crosvm runtime library dependencies (e.g. libwayland-client, libcap) - echo " Copying crosvm runtime libraries..." + # Copy the full transitive runtime library closure for crosvm. + # ldd already resolves the complete dependency tree (including libs + # needed by other libs, e.g. libepoxy pulled in by libvirglrenderer), + # so a single ldd call with LD_LIBRARY_PATH covering virglrenderer + # captures everything. + echo " Copying crosvm runtime libraries (full closure)..." CROSVM_DEPS=$(podman exec "$CROSVM_CONTAINER" bash -c " + LD_LIBRARY_PATH=${VIRGL_LIBDIR} \ ldd /src/target/release/crosvm 2>/dev/null \ | grep '=> /' | awk '{print \$3}' | sort -u") for lib in $CROSVM_DEPS; do libname=$(basename "$lib") - # Skip libs already copied (virglrenderer, vDSO, libc basics already bundled by tools step) if [ -f "$PREBUILT_DIR/lib/$libname" ]; then continue fi