From f46b2cacc723a0d8a231eba73266fca1013f1088 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Fri, 14 Aug 2026 18:05:00 -0400 Subject: [PATCH] build(appimage): harden thin bundle workflow Keep host multimedia libraries out of the AppImage, document the exact Ubuntu build inputs, and resolve Rust 1.97 release-gate warnings. --- packaging/appimage/README.md | 23 ++++++++++++++++++----- packaging/appimage/build-appimage.sh | 10 +++++++--- src/app/mod.rs | 28 ++++++++++++++-------------- src/widget/selectable_text.rs | 11 ++++------- 4 files changed, 43 insertions(+), 29 deletions(-) diff --git a/packaging/appimage/README.md b/packaging/appimage/README.md index 1b06810..ff5b0e3 100644 --- a/packaging/appimage/README.md +++ b/packaging/appimage/README.md @@ -24,7 +24,10 @@ The AppImage runs on any reasonably current glibc-based distro that has: - **A Vulkan-capable GPU + driver** (peerspeak's iced/wgpu renderer). Mesa/RADV on AMD/Intel or the NVIDIA driver all work. -- **PipeWire** (with the PulseAudio shim, for `pactl`). +- **PipeWire** with the PulseAudio shim, `pactl`, and the host `libpulse.so.0` + client library. The Pulse client stack is deliberately not bundled because + PixelPass launches host GStreamer tools that must keep using the host's + matching multimedia libraries. - For **screen-share only** — pixelpass shells out to these on the host `PATH`; it prints the exact package names for your distro if any are missing: - **GStreamer + plugins** (`gst-launch-1.0`/`gst-inspect-1.0`, base, @@ -58,13 +61,23 @@ distrobox create --yes --image ubuntu:24.04 --name peerspeak-appimage distrobox enter peerspeak-appimage -- sudo apt-get update distrobox enter peerspeak-appimage -- sudo apt-get install -y \ build-essential cmake clang libclang-dev pkg-config \ - libpipewire-0.3-dev libspa-0.2-dev libasound2-dev libxcb1-dev \ + libpipewire-0.3-dev libspa-0.2-dev libpulse-dev libasound2-dev libxcb1-dev \ curl ca-certificates file patchelf git +rustup toolchain install 1.97.1 --profile default -# Build (the host's ~/.rustup toolchain is glibc-2.17-baseline, so it runs in the -# box; isolated CARGO_TARGET_DIRs keep it off the host target/): +# PeerSpeak's ownership validator needs the modern SPA-JSON parser headers it +# is tested against. Distrobox exposes the host's headers under /run/host; +# pkg-config still links Ubuntu's older ABI-compatible libraries, preserving +# the glibc baseline. This checkout's verified host-header version is 1.6.8: +test -f /run/host/usr/include/spa-0.2/spa/utils/json-core.h + +# Build with the repository's pinned Rust version (the host's ~/.rustup +# toolchain is glibc-2.17-baseline, so it runs in the box; isolated +# CARGO_TARGET_DIRs keep it off the host target/): distrobox enter peerspeak-appimage -- env \ - PATH="$HOME/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:$PATH" \ + PATH="$HOME/.rustup/toolchains/1.97.1-x86_64-unknown-linux-gnu/bin:$PATH" \ + SYSTEM_DEPS_LIBSPA_INCLUDE="/run/host/usr/include/spa-0.2" \ + SYSTEM_DEPS_LIBPIPEWIRE_INCLUDE="/run/host/usr/include/pipewire-0.3:/run/host/usr/include/spa-0.2" \ ./packaging/appimage/build-appimage.sh ``` diff --git a/packaging/appimage/build-appimage.sh b/packaging/appimage/build-appimage.sh index f3d42bc..e2d1650 100755 --- a/packaging/appimage/build-appimage.sh +++ b/packaging/appimage/build-appimage.sh @@ -8,9 +8,12 @@ # X11) is dlopen'd at runtime and is on the AppImage excludelist because it must # match the host driver, and pixelpass's capture/encode tools (gst-launch-1.0, # pactl, mpv) are expected on the host PATH. So the AppImage carries just the two -# binaries plus their handful of non-excludelisted libs. The custom AppRun -# prepends usr/bin to PATH so peerspeak's own $PATH lookup finds the bundled -# pixelpass, while the host's tools stay reachable. +# binaries plus their handful of non-excludelisted libs. PulseAudio's client +# stack is also excluded: pixelpass shells out to host GStreamer, and letting +# those subprocesses inherit Ubuntu's bundled libsndfile/libmpg123 stack can +# override incompatible host multimedia libraries. The custom AppRun prepends +# usr/bin to PATH so peerspeak's own $PATH lookup finds the bundled pixelpass, +# while the host's tools and matching audio stack stay reachable. # # All runtime assets (notification WAVs, avatar presets, window icon, fonts) are # include_bytes!-embedded in the peerspeak binary, so nothing else is bundled. @@ -80,6 +83,7 @@ echo ">> running linuxdeploy (bundles libs, builds the AppImage)" --appdir "$appdir" \ -e "$appdir/usr/bin/peerspeak" \ -e "$appdir/usr/bin/pixelpass" \ + --exclude-library 'lib*.so*' \ -d "$repo/packaging/peerspeak.desktop" \ -i "$repo/assets/icons/peerspeak-256.png" \ --icon-filename peerspeak \ diff --git a/src/app/mod.rs b/src/app/mod.rs index 1375437..b610c71 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -5568,7 +5568,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { .size(11) .color(color_subtext), slider(0.0..=1.0, state.config.background_dim, AppMessage::SetBackgroundDim) - .step(0.05), + .step(0.05_f32), text("Set a picture from your computer as the app background. Auto-resized; a dimming overlay keeps text readable. Applies live.") .size(11) .color(color_subtext), @@ -5996,7 +5996,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { ).width(iced::Length::Fill), text(format!("Input Volume (mic): {:.0}%", state.config.input_volume * 100.0)).size(11).color(color_subtext), slider(0.0..=2.0, state.config.input_volume, AppMessage::InputVolumeChanged) - .step(0.05) + .step(0.05_f32) .on_release(AppMessage::PersistConfig), ].spacing(8).width(iced::Length::Fill), column![ @@ -6008,7 +6008,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { ).width(iced::Length::Fill), text(format!("Output Volume: {:.0}%", state.config.output_volume * 100.0)).size(11).color(color_subtext), slider(0.0..=2.0, state.config.output_volume, AppMessage::OutputVolumeChanged) - .step(0.05) + .step(0.05_f32) .on_release(AppMessage::PersistConfig), ].spacing(8).width(iced::Length::Fill), ].spacing(20).align_y(iced::alignment::Vertical::Top).width(iced::Length::Fill), @@ -6618,7 +6618,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { column![ row![ avatar_view(&state.config.avatar, &state.name, &state.self_id, 34.0), - text(format!("{} (You)", &state.name)).size(16).color(color_text), + text(format!("{} (You)", state.name)).size(16).color(color_text), horizontal_space(), if state.is_muted { text("[Muted]").size(14).color(color_red) @@ -6933,7 +6933,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { slider(0.0..=2.0, current_vol, move |v| { AppMessage::PeerVolumeChanged(peer_id_clone, v) }) - .step(0.01) + .step(0.01_f32) .on_release(AppMessage::PersistConfig) ] .spacing(8) @@ -6946,7 +6946,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { slider(-1.0..=1.0, current_pan, move |v| { AppMessage::PeerPanChanged(peer_id_clone, v) }) - .step(0.05) + .step(0.05_f32) .on_release(AppMessage::PersistConfig), ] .spacing(8) @@ -6961,7 +6961,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { slider(0.0..=METER_MAX, current_gate, move |v| { AppMessage::PeerGateChanged(peer_id_clone, v) }) - .step(0.001) + .step(0.001_f32) .on_release(AppMessage::PersistConfig), ] .spacing(8) @@ -6980,7 +6980,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { slider(EQ_GAIN_DB_MIN..=EQ_GAIN_DB_MAX, value, move |v| { AppMessage::PeerEqChanged(peer_id_clone, band, v) }) - .step(0.5) + .step(0.5_f32) .on_release(AppMessage::PersistConfig), ] .spacing(8) @@ -7215,7 +7215,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { clip_progress(music_status.position, music_status.total), AppMessage::MusicSeek, ) - .step(0.001), + .step(0.001_f32), text(format!("{elapsed} / {duration}")) .size(11) .color(color_subtext), @@ -7225,7 +7225,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { state.config.music_volume, AppMessage::MusicSetVolume ) - .step(0.01), + .step(0.01_f32), button( text("Browse") .size(12) @@ -7319,7 +7319,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { effective_music_volume(state), AppMessage::MusicSetSourceVolume ) - .step(0.01), + .step(0.01_f32), ] .spacing(8) .into() @@ -7815,7 +7815,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { }, move |fraction| AppMessage::SeekAudio(att.id, fraction), ) - .step(0.001) + .step(0.001_f32) .width(iced::Length::Fixed(180.0)), text(format!("{elapsed} / {duration}")) .size(11) @@ -7827,7 +7827,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { slider(0.0..=2.0, effective_clip_volume(state, att.id), move |v| { AppMessage::SetClipVolumeFor(att.id, v) },) - .step(0.01) + .step(0.01_f32) .width(iced::Length::Fixed(80.0)), ] .spacing(8) @@ -7945,7 +7945,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { state.config.clip_volume, AppMessage::SetClipVolume ) - .step(0.01) + .step(0.01_f32) .width(iced::Length::Fixed(110.0)), ] .spacing(10) diff --git a/src/widget/selectable_text.rs b/src/widget/selectable_text.rs index d49d91a..a6944ed 100644 --- a/src/widget/selectable_text.rs +++ b/src/widget/selectable_text.rs @@ -435,13 +435,10 @@ where let was_hovered = self.hovered_link.is_some(); self.hovered_link = local_position.and_then(|position| { - state.paragraph.hit_span(position).and_then(|span| { - if spans.get(span)?.link.is_some() { - Some(span) - } else { - None - } - }) + state + .paragraph + .hit_span(position) + .filter(|&span| spans.get(span).is_some_and(|span| span.link.is_some())) }); if was_hovered != self.hovered_link.is_some() {