Install the CLI with an overwriting symbolic link.

This commit is contained in:
2026-08-03 11:04:02 +02:00
parent 36cbaa0cc5
commit 3b1a65359c
5 changed files with 69 additions and 79 deletions

View File

@@ -1859,11 +1859,15 @@ mod tests {
assert!(fixture.run(&["install"], "").is_err());
}
#[cfg(unix)]
#[test]
fn launcher_install_is_idempotent_and_refuses_overwrite() {
fn launcher_install_is_idempotent_and_overwrites_existing_file() {
let fixture = Fixture::new(false);
let executable = fixture._root.path().join("packaged-bds-cli");
std::fs::write(&executable, b"binary").unwrap();
let target = fixture.home_dir.join(".local/bin/bds-cli");
std::fs::create_dir_all(target.parent().unwrap()).unwrap();
std::fs::write(&target, b"old launcher").unwrap();
let context = RunContext {
executable_path: executable.clone(),
..fixture.context("")
@@ -1878,12 +1882,10 @@ mod tests {
context,
)
.unwrap();
let target = fixture.home_dir.join(".local/bin/bds-cli");
assert!(!target.is_symlink());
assert!(
std::fs::read_to_string(target)
.unwrap()
.contains(executable.canonicalize().unwrap().to_str().unwrap())
assert!(target.is_symlink());
assert_eq!(
std::fs::read_link(target).unwrap(),
executable.canonicalize().unwrap()
);
}
}

View File

@@ -2,8 +2,7 @@ use std::path::{Path, PathBuf};
use crate::engine::{EngineError, EngineResult};
/// Install a recoverable launcher pointing at a packaged `bds-cli` binary.
/// Existing unrelated files are never overwritten.
/// Link the user-facing command to a packaged `bds-cli` binary.
pub fn install_launcher(executable: &Path, home_dir: &Path) -> EngineResult<PathBuf> {
if !executable.is_file() {
return Err(EngineError::Validation(format!(
@@ -14,49 +13,29 @@ pub fn install_launcher(executable: &Path, home_dir: &Path) -> EngineResult<Path
let bin_dir = home_dir.join(".local/bin");
std::fs::create_dir_all(&bin_dir)?;
let target = bin_dir.join(if cfg!(windows) {
"bds-cli.cmd"
"bds-cli.exe"
} else {
"bds-cli"
});
let source = executable.canonicalize()?;
let launcher = launcher_contents(&source);
#[cfg(unix)]
if target.is_symlink() && target.canonicalize().ok().as_ref() == Some(&source) {
std::fs::remove_file(&target)?;
}
if target.exists() {
if std::fs::read(&target).ok().as_deref() != Some(launcher.as_bytes()) {
return Err(EngineError::Conflict(format!(
"refusing to overwrite existing launcher at {}",
target.display()
)));
}
return Ok(target);
}
std::fs::write(&target, launcher)?;
#[cfg(unix)]
if let Err(error) = std::fs::remove_file(&target)
&& error.kind() != std::io::ErrorKind::NotFound
{
use std::os::unix::fs::PermissionsExt as _;
let mut permissions = std::fs::metadata(&target)?.permissions();
permissions.set_mode(0o755);
std::fs::set_permissions(&target, permissions)?;
return Err(error.into());
}
#[cfg(unix)]
std::os::unix::fs::symlink(source, &target)?;
#[cfg(windows)]
std::os::windows::fs::symlink_file(source, &target)?;
#[cfg(not(any(unix, windows)))]
return Err(EngineError::Validation(
"installing the CLI is not supported on this platform".to_string(),
));
Ok(target)
}
#[cfg(unix)]
fn launcher_contents(executable: &Path) -> String {
let quoted = executable.to_string_lossy().replace('\'', "'\"'\"'");
format!("#!/bin/sh\nexec '{quoted}' \"$@\"\n")
}
#[cfg(windows)]
fn launcher_contents(executable: &Path) -> String {
let escaped = executable.to_string_lossy().replace('%', "%%");
format!("@echo off\r\n\"{escaped}\" %*\r\n")
}
/// Resolve the CLI shipped beside the desktop executable and install it.
pub fn install_packaged_launcher(home_dir: &Path) -> EngineResult<PathBuf> {
let app = std::env::current_exe()?;
@@ -72,41 +51,50 @@ pub fn install_packaged_launcher(home_dir: &Path) -> EngineResult<PathBuf> {
mod tests {
use super::*;
#[test]
fn install_is_idempotent_and_never_overwrites_an_unrelated_file() {
let root = tempfile::tempdir().unwrap();
let executable = root.path().join("packaged-bds-cli");
std::fs::write(&executable, b"binary").unwrap();
let home = root.path().join("home");
let target = install_launcher(&executable, &home).unwrap();
assert!(!target.is_symlink());
let launcher = std::fs::read_to_string(&target).unwrap();
assert!(launcher.contains(executable.canonicalize().unwrap().to_str().unwrap()));
assert_eq!(install_launcher(&executable, &home).unwrap(), target);
std::fs::remove_file(&target).unwrap();
std::fs::write(&target, b"mine").unwrap();
assert!(install_launcher(&executable, &home).is_err());
assert_eq!(std::fs::read(&target).unwrap(), b"mine");
}
#[cfg(unix)]
#[test]
fn replaces_the_previous_installer_symlink_with_a_forwarding_launcher() {
fn install_replaces_existing_launcher_with_symlink() {
let root = tempfile::tempdir().unwrap();
let executable = root.path().join("packaged-bds-cli");
std::fs::write(&executable, b"binary").unwrap();
let home = root.path().join("home");
let target = home.join(".local/bin/bds-cli");
std::fs::create_dir_all(target.parent().unwrap()).unwrap();
std::os::unix::fs::symlink(&executable, &target).unwrap();
std::fs::write(&target, b"old launcher").unwrap();
let target = install_launcher(&executable, &home).unwrap();
assert!(target.is_symlink());
assert_eq!(
std::fs::read_link(&target).unwrap(),
executable.canonicalize().unwrap()
);
assert_eq!(install_launcher(&executable, &home).unwrap(), target);
let replacement = root.path().join("replacement-bds-cli");
std::fs::write(&replacement, b"new binary").unwrap();
assert_eq!(install_launcher(&replacement, &home).unwrap(), target);
assert_eq!(
std::fs::read_link(target).unwrap(),
replacement.canonicalize().unwrap()
);
}
#[cfg(unix)]
#[test]
fn install_replaces_broken_symlink() {
let root = tempfile::tempdir().unwrap();
let executable = root.path().join("packaged-bds-cli");
std::fs::write(&executable, b"binary").unwrap();
let home = root.path().join("home");
let target = home.join(".local/bin/bds-cli");
std::fs::create_dir_all(target.parent().unwrap()).unwrap();
std::os::unix::fs::symlink(root.path().join("missing-bds-cli"), &target).unwrap();
assert_eq!(install_launcher(&executable, &home).unwrap(), target);
assert!(!target.is_symlink());
assert!(
std::fs::read_to_string(target)
.unwrap()
.starts_with("#!/bin/sh")
assert!(target.is_symlink());
assert_eq!(
std::fs::read_link(target).unwrap(),
executable.canonicalize().unwrap()
);
}
}