diff --git a/crates/auto_update/src/lib.rs b/crates/auto_update/src/lib.rs index be2643b5..1173d99d 100644 --- a/crates/auto_update/src/lib.rs +++ b/crates/auto_update/src/lib.rs @@ -5,6 +5,7 @@ use gpui_updater::{EngineConfig, GitHubSource, UpdateStatus, Updater, Version}; use instant::{Duration, Instant}; const COOP_UPDATE_EXPLANATION: &str = "COOP_UPDATE_EXPLANATION"; +const COOP_BUNDLE_TYPE: &str = "COOP_BUNDLE_TYPE"; fn get_github_repo_owner() -> String { std::env::var("COOP_GITHUB_REPO_OWNER").unwrap_or_else(|_| "reyakov".to_string()) @@ -14,18 +15,27 @@ fn get_github_repo_name() -> String { std::env::var("COOP_GITHUB_REPO_NAME").unwrap_or_else(|_| "coop".to_string()) } -fn is_flatpak_installation() -> bool { - std::env::var("FLATPAK_ID").is_ok() || std::env::var(COOP_UPDATE_EXPLANATION).is_ok() +/// Whether updates are managed by an external distribution channel +/// (Flatpak/Snap), in which case the in-app updater must not run. +fn uses_managed_updates() -> bool { + // The Flatpak runtime exports `FLATPAK_ID` inside the sandbox. + std::env::var("FLATPAK_ID").is_ok() + // Allow opting out of in-app updates via an explicit environment variable. + || std::env::var(COOP_UPDATE_EXPLANATION).is_ok() + // The Snap package sets `COOP_BUNDLE_TYPE=snap` (see snapcraft.yaml.in). + || std::env::var(COOP_BUNDLE_TYPE).is_ok_and(|value| value == "snap") } /// Initialize the auto-update system. /// -/// Skips initialization when running as a Flatpak (updates are handled by the -/// Flatpak distribution channel). Otherwise creates the global [`AutoUpdater`] +/// Skips initialization when updates are handled by an external distribution +/// channel (Flatpak/Snap). Otherwise creates the global [`AutoUpdater`] /// entity and schedules a check for updates after a 2-minute delay. pub fn init(window: &mut Window, cx: &mut App) { - if is_flatpak_installation() { - log::info!("Skipping auto-update initialization: App is installed via Flatpak"); + if uses_managed_updates() { + log::info!( + "Skipping auto-update initialization: App is installed via a managed distribution channel (Flatpak/Snap)" + ); return; } @@ -57,7 +67,27 @@ pub struct AutoUpdater { } impl AutoUpdater { + /// Whether auto-update is available for this installation. + /// + /// Returns `false` on managed distribution channels (Flatpak/Snap), where + /// updates are handled by the channel and no global updater is created. + pub fn is_available(cx: &App) -> bool { + cx.try_global::().is_some() + } + + /// Retrieve the global auto updater instance, if one was initialized. + pub fn try_global(cx: &App) -> Option> { + cx.try_global::() + .map(|global| global.0.clone()) + } + /// Retrieve the global auto updater instance. + /// + /// # Panics + /// + /// Panics when auto-update is not available for this installation. Prefer + /// [`AutoUpdater::try_global`] when the installation type is not known at + /// compile time (e.g. Flatpak/Snap). pub fn global(cx: &App) -> Entity { cx.global::().0.clone() } diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 668426e1..9d306f62 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -380,12 +380,15 @@ impl Workspace { self.import_encryption(window, cx); } Command::Update => { - let auto_updater = AutoUpdater::global(cx); - auto_updater.update(cx, |this, cx| { - this.updater.update(cx, |updater, cx| { - updater.check(cx); + // No-op on managed distribution channels (Flatpak/Snap) where + // the in-app updater is never initialized. + if let Some(auto_updater) = AutoUpdater::try_global(cx) { + auto_updater.update(cx, |this, cx| { + this.updater.update(cx, |updater, cx| { + updater.check(cx); + }); }); - }); + } } } } @@ -559,7 +562,7 @@ impl Workspace { .caret() .compact() .transparent() - .dropdown_menu(move |this, _window, _cx| { + .dropdown_menu(move |this, _window, cx| { let avatar = avatar.clone(); let name = name.clone(); @@ -593,12 +596,15 @@ impl Workspace { IconName::Sun, Box::new(Command::ToggleTheme), ) - .separator() - .menu_with_icon( - "Check for Updates", - IconName::Device, - Box::new(Command::Update), - ) + // Only offer in-app updates when auto-update is + // enabled (managed channels update themselves). + .when(AutoUpdater::is_available(cx), |this| { + this.separator().menu_with_icon( + "Check for Updates", + IconName::Device, + Box::new(Command::Update), + ) + }) .menu_with_icon( "Settings", IconName::Settings, @@ -610,7 +616,6 @@ impl Workspace { } fn titlebar_right(&mut self, cx: &mut Context) -> impl IntoElement { - let updater = AutoUpdater::global(cx); let chat = ChatRegistry::global(cx); let nip4e_enabled = AppSettings::get_nip4e(cx); let nostr = NostrRegistry::global(cx); @@ -622,13 +627,19 @@ impl Workspace { let persons = PersonRegistry::global(cx); let profile = persons.read(cx).get(&public_key, cx); let announcement = profile.announcement(); - let updater_idle = updater.read(cx).idle(cx); + + // Update status is only shown when auto-update is available. On + // managed distribution channels (Flatpak/Snap) no updater exists, so + // nothing is rendered. + let updater_status = AutoUpdater::try_global(cx).and_then(|updater| { + let updater = updater.read(cx); + (!updater.idle(cx)).then(|| updater.status(cx)) + }); h_flex() .when(!cx.theme().platform.is_mac(), |this| this.pr_2()) .gap_2() - .when(!updater_idle, |this| { - let status = updater.read(cx).status(cx); + .when_some(updater_status, |this, status| { this.child(div().text_xs().italic().child(status)) }) .when(nip4e_enabled, |this| { diff --git a/desktop/resources/snap/snapcraft.yaml.in b/desktop/resources/snap/snapcraft.yaml.in index d0aa0032..6c829e4d 100644 --- a/desktop/resources/snap/snapcraft.yaml.in +++ b/desktop/resources/snap/snapcraft.yaml.in @@ -1,3 +1,7 @@ +# Snaps built by snapcraft without Snap Store credentials are unsigned and +# cannot be installed without bypassing signature checks. Use: +# sudo snap install --dangerous ./coop__.snap +# For signed installs (`snap install coop`), publish via the Snap Store. name: coop title: Coop base: core24 diff --git a/script/bundle-snap b/script/bundle-snap index 75a52d0a..aa182ead 100755 --- a/script/bundle-snap +++ b/script/bundle-snap @@ -35,3 +35,7 @@ SNAP_NAME="coop_${1}_${ARCH_SUFFIX}.snap" snapcraft --destructive-mode --output "$SNAP_NAME" echo "Created snap package: $SNAP_NAME" +echo "" +echo "This snap is unsigned (built without Snap Store credentials)." +echo "To install it locally, use:" +echo " sudo snap install --dangerous ./$SNAP_NAME" diff --git a/script/try-snap b/script/try-snap index 70cebe13..33164e6c 100755 --- a/script/try-snap +++ b/script/try-snap @@ -15,11 +15,26 @@ if [ "$#" -ne 1 ]; then exit 1 fi +# Get system architecture (same mapping as script/bundle-snap) +ARCH=$(uname -m) +case "$ARCH" in + x86_64) ARCH_SUFFIX="x86_64" ;; + aarch64) ARCH_SUFFIX="aarch64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; +esac + +snap_file="coop_${1}_${ARCH_SUFFIX}.snap" +if [ ! -f "$snap_file" ]; then + echo "Snap file not found: $snap_file" + echo "Build it first with: script/bundle-snap $1" + exit 1 +fi + # Rerun as root [ "$UID" -eq 0 ] || exec sudo bash -e "$0" "$@" snap remove coop || true mkdir -p snap rm -rf snap/unpacked -unsquashfs -dest snap/unpacked "coop_$1_amd64.snap" +unsquashfs -dest snap/unpacked "$snap_file" snap try --classic snap/unpacked