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
80    .ancestors()
81    .nth(3)
82    .filter(|p| p.extension().is_some_and(|e| e == "app"))
83    .map(|p| p.to_path_buf())
84}
85
86/// Detect the install method from the running executable and environment.
87///
88/// This only decides *how* to install an update — whether one exists is
89/// [`super::UpdateConfig::check`], which asks the release feed. No package
90/// manager is consulted.
91pub fn detect() -> InstallKind {
92  // A Linux AppImage exports APPIMAGE pointing at the running image.
93  if let Some(image) = std::env::var_os("APPIMAGE") {
94    return InstallKind::AppImage(PathBuf::from(image));
95  }
96  #[cfg(target_os = "macos")]
97  {
98    // Any macOS .app self-updates; Homebrew is never asked whether it owns it.
99    let exe = std::env::current_exe().unwrap_or_default();
100    if let Some(app) = bundle_of(&exe) {
101      return InstallKind::MacApp(app);
102    }
103  }
104  // A Linux distro package under a system prefix is root-owned, and Windows
105  // installs update through their own package flow; neither can be swapped in
106  // place, so both fall through to Unknown (open the download page).
107  InstallKind::Unknown
108}
109
110#[cfg(test)]
111mod tests {
112  use super::*;
113
114  #[test]
115  fn only_swappable_installs_update_in_place() {
116    // A macOS .app and a Linux AppImage are rewritten in place; everything
117    // else (a root-owned distro package, Windows, a dev build) opens the page.
118    assert!(InstallKind::MacApp(PathBuf::from("/Applications/Acme.app")).is_in_place());
119    assert!(InstallKind::AppImage(PathBuf::from("/x/Acme.AppImage")).is_in_place());
120    assert!(!InstallKind::Unknown.is_in_place());
121  }
122
123  #[test]
124  fn bundle_is_three_levels_above_the_executable() {
125    assert_eq!(
126      bundle_of(Path::new("/Applications/Acme.app/Contents/MacOS/acme")),
127      Some(PathBuf::from("/Applications/Acme.app"))
128    );
129  }
130
131  #[test]
132  fn unbundled_executables_have_no_bundle() {
133    // A dev build under target/ must not be mistaken for an installable .app.
134    assert_eq!(bundle_of(Path::new("/dev/acme/target/release/acme")), None);
135    assert_eq!(bundle_of(Path::new("/usr/local/bin/acme")), None);
136    assert_eq!(bundle_of(Path::new("acme")), None);
137  }
138
139  #[test]
140  fn every_stage_has_a_label() {
141    // The UI renders these verbatim, so an empty one is a blank status line.
142    for stage in [
143      UpdateStage::Downloading { done: 0, total: 0 },
144      UpdateStage::Preparing,
145      UpdateStage::Installing,
146      UpdateStage::Verifying,
147    ] {
148      assert!(!stage.label().is_empty());
149    }
150  }
151}