Skip to main content

guise/update/
install.rs

1//! Install detection and the vocabulary the installer reports back with.
2
3#[cfg(any(target_os = "macos", test))]
4use std::path::Path;
5use std::path::PathBuf;
6
7/// How this copy of the app was installed, which decides the update path. An
8/// app self-updates where it can rewrite its own install; anything else opens
9/// the download page. (Some variants are only ever constructed on their
10/// platform.)
11#[allow(dead_code)]
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub enum InstallKind {
14    /// A macOS `.app` bundle at this path — rewrite its contents in place.
15    /// Covers every macOS install, Homebrew casks included; how it got there
16    /// doesn't matter.
17    MacApp(PathBuf),
18    /// A running AppImage at this path (replace the file).
19    AppImage(PathBuf),
20    /// An install that can't be rewritten from inside the app — a root-owned
21    /// distro package (`.deb`/`.rpm`), a Windows install, or a dev build. Falls
22    /// back to opening the release page.
23    Unknown,
24}
25
26impl InstallKind {
27    /// Whether this install can be updated in place (vs. opening the page).
28    pub fn is_in_place(&self) -> bool {
29        matches!(self, InstallKind::MacApp(_) | InstallKind::AppImage(_))
30    }
31}
32
33/// What the installer is doing, reported as it happens so the UI can show real
34/// progress. Without this the whole install is one opaque blocking call, and a
35/// failure that arrives in microseconds — a missing asset, say — never renders a
36/// single frame of feedback.
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub enum UpdateStage {
39    /// Fetching the asset: bytes written of the total (0 total = unknown).
40    Downloading { done: u64, total: u64 },
41    /// Opening what was downloaded (macOS mounts the `.dmg`).
42    Preparing,
43    /// Writing the new version over the install.
44    Installing,
45    /// Checking the result before relaunching into it.
46    Verifying,
47}
48
49impl UpdateStage {
50    /// Short present-tense label for the UI. Lives here so the stages and the
51    /// words describing them can't drift apart.
52    pub fn label(&self) -> &'static str {
53        match self {
54            UpdateStage::Downloading { .. } => "Downloading update…",
55            UpdateStage::Preparing => "Preparing…",
56            UpdateStage::Installing => "Installing…",
57            UpdateStage::Verifying => "Verifying…",
58        }
59    }
60}
61
62/// How to relaunch after a successful install.
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub enum Relaunch {
65    /// The install was rewritten in place at its existing path: restart with
66    /// **no** explicit binary path, so gpui reopens the running bundle via
67    /// `NSBundle`. Never hand the restart an explicit path here — `open` on a
68    /// path whose LaunchServices registration is stale can fall back to running
69    /// the inner Mach-O inside Terminal.app.
70    Current,
71    /// Restart by launching this binary ([`gpui::App::set_restart_path`]).
72    Binary(PathBuf),
73}
74
75/// The `.app` bundle three levels above a macOS executable
76/// (`…/Acme.app/Contents/MacOS/acme`), if there is one.
77#[cfg(any(target_os = "macos", test))]
78pub(crate) fn bundle_of(exe: &Path) -> Option<PathBuf> {
79    exe.ancestors()
80        .nth(3)
81        .filter(|p| p.extension().is_some_and(|e| e == "app"))
82        .map(|p| p.to_path_buf())
83}
84
85/// Detect the install method from the running executable and environment.
86///
87/// This only decides *how* to install an update — whether one exists is
88/// [`super::UpdateConfig::check`], which asks the release feed. No package
89/// manager is consulted.
90pub fn detect() -> InstallKind {
91    // A Linux AppImage exports APPIMAGE pointing at the running image.
92    if let Some(image) = std::env::var_os("APPIMAGE") {
93        return InstallKind::AppImage(PathBuf::from(image));
94    }
95    #[cfg(target_os = "macos")]
96    {
97        // Any macOS .app self-updates; Homebrew is never asked whether it owns it.
98        let exe = std::env::current_exe().unwrap_or_default();
99        if let Some(app) = bundle_of(&exe) {
100            return InstallKind::MacApp(app);
101        }
102    }
103    // A Linux distro package under a system prefix is root-owned, and Windows
104    // installs update through their own package flow; neither can be swapped in
105    // place, so both fall through to Unknown (open the download page).
106    InstallKind::Unknown
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn only_swappable_installs_update_in_place() {
115        // A macOS .app and a Linux AppImage are rewritten in place; everything
116        // else (a root-owned distro package, Windows, a dev build) opens the page.
117        assert!(InstallKind::MacApp(PathBuf::from("/Applications/Acme.app")).is_in_place());
118        assert!(InstallKind::AppImage(PathBuf::from("/x/Acme.AppImage")).is_in_place());
119        assert!(!InstallKind::Unknown.is_in_place());
120    }
121
122    #[test]
123    fn bundle_is_three_levels_above_the_executable() {
124        assert_eq!(
125            bundle_of(Path::new("/Applications/Acme.app/Contents/MacOS/acme")),
126            Some(PathBuf::from("/Applications/Acme.app"))
127        );
128    }
129
130    #[test]
131    fn unbundled_executables_have_no_bundle() {
132        // A dev build under target/ must not be mistaken for an installable .app.
133        assert_eq!(bundle_of(Path::new("/dev/acme/target/release/acme")), None);
134        assert_eq!(bundle_of(Path::new("/usr/local/bin/acme")), None);
135        assert_eq!(bundle_of(Path::new("acme")), None);
136    }
137
138    #[test]
139    fn every_stage_has_a_label() {
140        // The UI renders these verbatim, so an empty one is a blank status line.
141        for stage in [
142            UpdateStage::Downloading { done: 0, total: 0 },
143            UpdateStage::Preparing,
144            UpdateStage::Installing,
145            UpdateStage::Verifying,
146        ] {
147            assert!(!stage.label().is_empty());
148        }
149    }
150}