mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-08 19:42:12 +00:00
update: a package manager's copy and another app's bundle do not self-update on macOS and Windows
self_update::detect fell back to Install::Standalone for any executable it could not place, so on macOS a Homebrew, MacPorts or Nix copy, or a program inside another app's bundle, was replaced in place, and on Windows so was a scoop, winget, Chocolatey or Store copy. The package manager's record then stays on the old version, and the bundle belongs to another app. macOS now treats executables under /opt/homebrew, /usr/local/Cellar, /usr/local/Homebrew, /usr/local/opt, /opt/local and /nix as owned by a package manager, and any executable inside an .app bundle that is not this app's own as not ours; none of them self-update. Windows does the same for scoop, winget, Chocolatey and WindowsApps folders. The rules are plain path functions, so they are tested on every platform. Tested: three new tests failed before the change (the macOS and Windows folder rules, and detect on an executable in another app's bundle) and pass after it. cargo test -p craft-update (209 tests), clippy -D warnings and fmt pass on macOS; the Windows branch was not compiled here. Fixes #390 for macOS and Windows; the Linux side is #388.
This commit is contained in:
1 parent
2b8bd856ce
commit
61bde471fd
1 file changed
+92
@@ -176,6 +176,24 @@ pub fn is_self_updating(app: &App) -> bool {
|
||||
current(app).is_some()
|
||||
}
|
||||
|
||||
/// Whether `exe` sits in a folder a macOS package manager owns (Homebrew, MacPorts, Nix).
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
fn macos_package_manager_owns(exe: &Path) -> bool {
|
||||
// Homebrew (both prefixes), MacPorts and the Nix store; component-wise, so `/opt/homebrewed` is not one
|
||||
["/opt/homebrew", "/usr/local/Cellar", "/usr/local/Homebrew", "/usr/local/opt", "/opt/local", "/nix"].iter().any(|root| exe.starts_with(root))
|
||||
}
|
||||
|
||||
/// Whether `exe` sits in a folder a Windows package manager owns (scoop, winget, Chocolatey, the
|
||||
/// Store).
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
fn windows_package_manager_owns(exe: &Path) -> bool {
|
||||
// Compared as text so the rule reads the same wherever it is tested: either separator, any case.
|
||||
let text = exe.to_string_lossy().to_lowercase();
|
||||
let parts: Vec<&str> = text.split(['\\', '/']).collect();
|
||||
parts.contains(&"windowsapps")
|
||||
|| parts.windows(2).any(|w| matches!((w[0], w[1]), ("scoop", "apps" | "shims") | ("winget", "packages") | ("chocolatey", "lib" | "bin")))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn detect(app: &App, exe: &Path) -> Option<Install> {
|
||||
if let Some(bundle) = exe.ancestors().find(|p| p.extension().is_some_and(|e| e == "app"))
|
||||
@@ -185,6 +203,11 @@ fn detect(app: &App, exe: &Path) -> Option<Install> {
|
||||
{
|
||||
return Some(Install::MacApp(bundle.to_path_buf()));
|
||||
}
|
||||
// A package manager's copy is its to update (replacing it in place leaves its record stale), and
|
||||
// a program inside another app's bundle is that app's file.
|
||||
if macos_package_manager_owns(exe) || exe.ancestors().any(|p| p.extension().is_some_and(|e| e == "app")) {
|
||||
return None;
|
||||
}
|
||||
if exe.is_file() {
|
||||
return Some(Install::Standalone(exe.to_path_buf()));
|
||||
}
|
||||
@@ -204,6 +227,10 @@ fn detect(app: &App, exe: &Path) -> Option<Install> {
|
||||
if crate::msi_copy::windows::find(app, exe).is_some() {
|
||||
return Some(Install::Msi(exe.to_path_buf()));
|
||||
}
|
||||
// scoop, winget, Chocolatey and the Store own their copies; an in-place replace leaves their record stale
|
||||
if windows_package_manager_owns(exe) {
|
||||
return None;
|
||||
}
|
||||
if exe.is_file() {
|
||||
return Some(Install::Standalone(exe.to_path_buf()));
|
||||
}
|
||||
@@ -896,6 +923,71 @@ fn run_step(app: &App, reference: &Path, pid: u32, exe: &Path, step: &Finish, wa
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn macos_package_manager_folders_are_recognised() {
|
||||
for owned in [
|
||||
"/opt/homebrew/bin/artcraft-launcher",
|
||||
"/opt/homebrew/Cellar/artcraft/1.0/bin/artcraft-launcher",
|
||||
"/usr/local/Cellar/artcraft/1.0/bin/artcraft-launcher",
|
||||
"/usr/local/Homebrew/bin/artcraft-launcher",
|
||||
"/usr/local/opt/artcraft/bin/artcraft-launcher",
|
||||
"/opt/local/bin/artcraft-launcher",
|
||||
"/nix/store/abc123-artcraft/bin/artcraft-launcher",
|
||||
] {
|
||||
assert!(macos_package_manager_owns(Path::new(owned)), "{owned}");
|
||||
}
|
||||
for loose in [
|
||||
"/Users/ada/bin/artcraft-launcher",
|
||||
"/usr/local/bin/artcraft-launcher",
|
||||
"/Users/ada/opt/homebrew/bin/artcraft-launcher",
|
||||
"/opt/homebrewed/artcraft-launcher",
|
||||
"/Applications/Tools/artcraft-launcher",
|
||||
] {
|
||||
assert!(!macos_package_manager_owns(Path::new(loose)), "{loose}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_package_manager_folders_are_recognised() {
|
||||
for owned in [
|
||||
r"C:\Users\Ada\scoop\apps\artcraft-launcher\current\artcraft-launcher.exe",
|
||||
r"C:\Users\Ada\scoop\shims\artcraft-launcher.exe",
|
||||
r"C:\ProgramData\scoop\apps\artcraft-launcher\current\artcraft-launcher.exe",
|
||||
r"C:\Users\Ada\AppData\Local\Microsoft\WinGet\Packages\Storytold.ArtCraftLauncher_x\artcraft-launcher.exe",
|
||||
r"C:\ProgramData\chocolatey\lib\artcraft-launcher\tools\artcraft-launcher.exe",
|
||||
r"C:\Program Files\WindowsApps\Storytold.ArtCraftLauncher_1.0_x64\artcraft-launcher.exe",
|
||||
] {
|
||||
assert!(windows_package_manager_owns(Path::new(owned)), "{owned}");
|
||||
}
|
||||
for loose in [
|
||||
r"C:\Tools\ArtCraft Launcher\artcraft-launcher.exe",
|
||||
r"D:\myscoopy\apps\artcraft-launcher.exe",
|
||||
r"C:\Users\Ada\Downloads\artcraft-launcher.exe",
|
||||
] {
|
||||
assert!(!windows_package_manager_owns(Path::new(loose)), "{loose}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A program inside an `.app` bundle that is not this app's own (a different bundle id) is
|
||||
/// another app's file, not a loose copy of ours to replace.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn an_executable_in_another_apps_bundle_is_not_self_updating() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let macos = dir.path().join("Other.app/Contents/MacOS");
|
||||
std::fs::create_dir_all(&macos).unwrap();
|
||||
let mut info = plist::Dictionary::new();
|
||||
info.insert("CFBundleIdentifier".into(), plist::Value::String("com.example.other".into()));
|
||||
plist::Value::Dictionary(info).to_file_xml(dir.path().join("Other.app/Contents/Info.plist")).unwrap();
|
||||
let exe = macos.join("other");
|
||||
std::fs::write(&exe, b"binary").unwrap();
|
||||
assert_eq!(detect(&crate::suite::LAUNCHER, &exe), None);
|
||||
// a loose copy outside any bundle is still a standalone one
|
||||
let loose = dir.path().join("artcraft-launcher");
|
||||
std::fs::write(&loose, b"binary").unwrap();
|
||||
assert_eq!(detect(&crate::suite::LAUNCHER, &loose), Some(Install::Standalone(loose.clone())));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn msi_self_update_uses_the_target_of_a_symlinked_download() {
|
||||
|
||||
Reference in new issue
Block a user