#![doc = include_str!("../../docs/install.md")]
use std::path::{Path, PathBuf};
use crate::error::Result;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
pub(crate) mod macos;
#[cfg(target_os = "windows")]
mod windows;
mod ownership;
pub use ownership::{Ownership, OwnershipHints, PackageManager, ownership};
#[derive(Debug, Clone)]
pub struct Installed {
pub restart_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum MsiUi {
#[default]
Passive,
Basic,
Quiet,
}
impl MsiUi {
#[cfg_attr(
not(any(target_os = "windows", test)),
expect(dead_code, reason = "msiexec runs on Windows only")
)]
pub(crate) fn flag(self) -> &'static str {
match self {
Self::Passive => "/passive",
Self::Basic => "/qb",
Self::Quiet => "/quiet",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct InstallOptions {
pub msi_ui: MsiUi,
}
impl InstallOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn msi_ui(mut self, ui: MsiUi) -> Self {
self.msi_ui = ui;
self
}
}
pub fn install(artifact: &Path, install_root: &Path) -> Result<Installed> {
install_with(artifact, install_root, &InstallOptions::new())
}
pub fn install_with(
artifact: &Path,
install_root: &Path,
options: &InstallOptions,
) -> Result<Installed> {
#[cfg(target_os = "macos")]
{
let _ = options;
macos::install(artifact, install_root)
}
#[cfg(target_os = "linux")]
{
let _ = options;
linux::install(artifact, install_root)
}
#[cfg(target_os = "windows")]
{
windows::install(artifact, install_root, options)
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
let _ = (artifact, install_root, options);
Err(crate::Error::UnsupportedPlatform(std::env::consts::OS))
}
}
pub fn remove_leftovers(install_root: &Path) {
#[cfg(target_os = "windows")]
windows::remove_leftovers(install_root);
#[cfg(not(target_os = "windows"))]
let _ = install_root;
}
#[cfg(feature = "delta")]
pub(crate) fn accepts_rebuilt(install_root: &Path, artifact_name: &str) -> bool {
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
let _ = (install_root, artifact_name);
true
}
#[cfg(target_os = "windows")]
{
windows::accepts_rebuilt(install_root, artifact_name)
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
let _ = (install_root, artifact_name);
false
}
}
pub fn current_install_root() -> Result<PathBuf> {
let exe = std::env::current_exe()?;
#[cfg(target_os = "macos")]
{
Ok(macos::bundle_root(&exe).unwrap_or(exe))
}
#[cfg(target_os = "linux")]
{
Ok(linux::appimage_root(&exe).unwrap_or(exe))
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
Ok(exe)
}
}
pub fn check_location(install_root: &Path, artifact_name: &str) -> Result<()> {
if cfg!(target_os = "macos")
&& install_root
.components()
.any(|c| c.as_os_str() == "AppTranslocation")
{
return Err(crate::Error::Location(
"the app runs from a read-only App Translocation copy; move it to /Applications and open it again"
.into(),
));
}
let installer_elevates =
cfg!(target_os = "windows") && artifact_name.to_ascii_lowercase().ends_with(".msi");
if installer_elevates {
return Ok(());
}
match install_root.parent() {
Some(dir) if !dir.as_os_str().is_empty() => probe_write(dir),
_ => Ok(()),
}
}
fn probe_write(dir: &Path) -> Result<()> {
use std::io::ErrorKind;
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.subsec_nanos());
let probe = dir.join(format!(
".gpui-updater-probe-{}-{nanos}",
std::process::id()
));
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&probe)
{
Ok(file) => {
drop(file);
let _ = std::fs::remove_file(&probe);
Ok(())
}
Err(e) if e.kind() == ErrorKind::ReadOnlyFilesystem => {
Err(crate::Error::Location(format!(
"{} is on a read-only volume, such as a mounted disk image; move the app to a writable folder",
dir.display()
)))
}
Err(e) if e.kind() == ErrorKind::PermissionDenied => Err(crate::Error::Permission(
format!("no permission to write to {}", dir.display()),
)),
Err(e) => {
tracing::debug!(dir = %dir.display(), error = %e, "could not probe the install directory");
Ok(())
}
}
}
#[cfg(test)]
mod location_tests {
use super::*;
#[cfg(unix)]
use crate::error::{Error, ErrorKind};
#[test]
fn accepts_a_writable_folder_and_leaves_nothing_behind() {
let dir = tempfile::tempdir().unwrap();
check_location(&dir.path().join("App.app"), "App.dmg").unwrap();
assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 0);
}
#[test]
fn leaves_a_missing_folder_for_the_install_to_report() {
let dir = tempfile::tempdir().unwrap();
check_location(&dir.path().join("gone/App.app"), "App.dmg").unwrap();
}
#[cfg(target_os = "macos")]
#[test]
fn rejects_app_translocation() {
let root = Path::new("/private/var/folders/x/T/AppTranslocation/1A2B/d/App.app");
let err = check_location(root, "App.dmg").unwrap_err();
assert!(matches!(err, Error::Location(_)), "{err:?}");
assert_eq!(err.kind(), ErrorKind::Location);
}
#[cfg(unix)]
#[test]
fn rejects_a_folder_the_user_may_not_write() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let locked = dir.path().join("locked");
std::fs::create_dir(&locked).unwrap();
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o555)).unwrap();
let privileged = std::fs::write(locked.join("probe"), b"").is_ok();
let result = check_location(&locked.join("App.app"), "App.dmg");
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).unwrap();
if privileged {
return;
}
let err = result.unwrap_err();
assert!(matches!(err, Error::Permission(_)), "{err:?}");
assert_eq!(err.kind(), ErrorKind::Permission);
}
}
#[cfg(test)]
mod options_tests {
use super::*;
#[test]
fn msi_ui_levels_map_to_msiexec_options() {
assert_eq!(InstallOptions::new().msi_ui, MsiUi::Passive);
assert_eq!(
InstallOptions::new().msi_ui(MsiUi::Basic).msi_ui,
MsiUi::Basic
);
assert_eq!(MsiUi::Passive.flag(), "/passive");
assert_eq!(MsiUi::Basic.flag(), "/qb");
assert_eq!(MsiUi::Quiet.flag(), "/quiet");
}
}