gpui_updater_core/install/
mod.rs1#![doc = include_str!("../../docs/install.md")]
3
4use std::path::{Path, PathBuf};
5
6use crate::error::Result;
7
8#[cfg(target_os = "linux")]
9mod linux;
10#[cfg(target_os = "macos")]
11pub(crate) mod macos;
12#[cfg(target_os = "windows")]
13mod windows;
14
15mod ownership;
16pub use ownership::{Ownership, OwnershipHints, PackageManager, ownership};
17
18#[derive(Debug, Clone)]
20pub struct Installed {
21 pub restart_path: Option<PathBuf>,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37#[non_exhaustive]
38pub enum MsiUi {
39 #[default]
41 Passive,
42 Basic,
44 Quiet,
47}
48
49impl MsiUi {
50 #[cfg_attr(
52 not(any(target_os = "windows", test)),
53 expect(dead_code, reason = "msiexec runs on Windows only")
54 )]
55 pub(crate) fn flag(self) -> &'static str {
56 match self {
57 Self::Passive => "/passive",
58 Self::Basic => "/qb",
59 Self::Quiet => "/quiet",
60 }
61 }
62}
63
64#[derive(Debug, Clone, Default, PartialEq, Eq)]
66#[non_exhaustive]
67pub struct InstallOptions {
68 pub msi_ui: MsiUi,
70}
71
72impl InstallOptions {
73 #[must_use]
75 pub fn new() -> Self {
76 Self::default()
77 }
78
79 #[must_use]
81 pub fn msi_ui(mut self, ui: MsiUi) -> Self {
82 self.msi_ui = ui;
83 self
84 }
85}
86
87pub fn install(artifact: &Path, install_root: &Path) -> Result<Installed> {
108 install_with(artifact, install_root, &InstallOptions::new())
109}
110
111pub fn install_with(
117 artifact: &Path,
118 install_root: &Path,
119 options: &InstallOptions,
120) -> Result<Installed> {
121 #[cfg(target_os = "macos")]
122 {
123 let _ = options;
124 macos::install(artifact, install_root)
125 }
126 #[cfg(target_os = "linux")]
127 {
128 let _ = options;
129 linux::install(artifact, install_root)
130 }
131 #[cfg(target_os = "windows")]
132 {
133 windows::install(artifact, install_root, options)
134 }
135 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
136 {
137 let _ = (artifact, install_root, options);
138 Err(crate::Error::UnsupportedPlatform(std::env::consts::OS))
139 }
140}
141
142pub fn remove_leftovers(install_root: &Path) {
148 #[cfg(target_os = "windows")]
149 windows::remove_leftovers(install_root);
150 #[cfg(not(target_os = "windows"))]
151 let _ = install_root;
152}
153
154pub fn current_install_root() -> Result<PathBuf> {
165 let exe = std::env::current_exe()?;
166 #[cfg(target_os = "macos")]
167 {
168 Ok(macos::bundle_root(&exe).unwrap_or(exe))
169 }
170 #[cfg(target_os = "linux")]
171 {
172 Ok(linux::appimage_root(&exe).unwrap_or(exe))
173 }
174 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
175 {
176 Ok(exe)
177 }
178}
179
180pub fn check_location(install_root: &Path, artifact_name: &str) -> Result<()> {
197 if cfg!(target_os = "macos")
198 && install_root
199 .components()
200 .any(|c| c.as_os_str() == "AppTranslocation")
201 {
202 return Err(crate::Error::Location(
203 "the app runs from a read-only App Translocation copy; move it to /Applications and open it again"
204 .into(),
205 ));
206 }
207 let installer_elevates =
208 cfg!(target_os = "windows") && artifact_name.to_ascii_lowercase().ends_with(".msi");
209 if installer_elevates {
210 return Ok(());
211 }
212 match install_root.parent() {
213 Some(dir) if !dir.as_os_str().is_empty() => probe_write(dir),
214 _ => Ok(()),
215 }
216}
217
218fn probe_write(dir: &Path) -> Result<()> {
221 use std::io::ErrorKind;
222 use std::time::{SystemTime, UNIX_EPOCH};
223
224 let nanos = SystemTime::now()
225 .duration_since(UNIX_EPOCH)
226 .map_or(0, |d| d.subsec_nanos());
227 let probe = dir.join(format!(
228 ".gpui-updater-probe-{}-{nanos}",
229 std::process::id()
230 ));
231 match std::fs::OpenOptions::new()
232 .write(true)
233 .create_new(true)
234 .open(&probe)
235 {
236 Ok(file) => {
237 drop(file);
238 let _ = std::fs::remove_file(&probe);
239 Ok(())
240 }
241 Err(e) if e.kind() == ErrorKind::ReadOnlyFilesystem => {
242 Err(crate::Error::Location(format!(
243 "{} is on a read-only volume, such as a mounted disk image; move the app to a writable folder",
244 dir.display()
245 )))
246 }
247 Err(e) if e.kind() == ErrorKind::PermissionDenied => Err(crate::Error::Permission(
248 format!("no permission to write to {}", dir.display()),
249 )),
250 Err(e) => {
251 tracing::debug!(dir = %dir.display(), error = %e, "could not probe the install directory");
252 Ok(())
253 }
254 }
255}
256
257#[cfg(test)]
258mod location_tests {
259 use super::*;
260 #[cfg(unix)]
261 use crate::error::{Error, ErrorKind};
262
263 #[test]
264 fn accepts_a_writable_folder_and_leaves_nothing_behind() {
265 let dir = tempfile::tempdir().unwrap();
266 check_location(&dir.path().join("App.app"), "App.dmg").unwrap();
267 assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 0);
268 }
269
270 #[test]
271 fn leaves_a_missing_folder_for_the_install_to_report() {
272 let dir = tempfile::tempdir().unwrap();
273 check_location(&dir.path().join("gone/App.app"), "App.dmg").unwrap();
274 }
275
276 #[cfg(target_os = "macos")]
277 #[test]
278 fn rejects_app_translocation() {
279 let root = Path::new("/private/var/folders/x/T/AppTranslocation/1A2B/d/App.app");
280 let err = check_location(root, "App.dmg").unwrap_err();
281 assert!(matches!(err, Error::Location(_)), "{err:?}");
282 assert_eq!(err.kind(), ErrorKind::Location);
283 }
284
285 #[cfg(unix)]
286 #[test]
287 fn rejects_a_folder_the_user_may_not_write() {
288 use std::os::unix::fs::PermissionsExt;
289
290 let dir = tempfile::tempdir().unwrap();
291 let locked = dir.path().join("locked");
292 std::fs::create_dir(&locked).unwrap();
293 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o555)).unwrap();
294 let privileged = std::fs::write(locked.join("probe"), b"").is_ok();
296 let result = check_location(&locked.join("App.app"), "App.dmg");
297 std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).unwrap();
298 if privileged {
299 return;
300 }
301 let err = result.unwrap_err();
302 assert!(matches!(err, Error::Permission(_)), "{err:?}");
303 assert_eq!(err.kind(), ErrorKind::Permission);
304 }
305}
306
307#[cfg(test)]
308mod options_tests {
309 use super::*;
310
311 #[test]
312 fn msi_ui_levels_map_to_msiexec_options() {
313 assert_eq!(InstallOptions::new().msi_ui, MsiUi::Passive);
314 assert_eq!(
315 InstallOptions::new().msi_ui(MsiUi::Basic).msi_ui,
316 MsiUi::Basic
317 );
318 assert_eq!(MsiUi::Passive.flag(), "/passive");
319 assert_eq!(MsiUi::Basic.flag(), "/qb");
320 assert_eq!(MsiUi::Quiet.flag(), "/quiet");
321 }
322}