Updater: recognise an MSI install outside Program Files (#1870) (#1873)

Thanks @SychPL.
This commit is contained in:
Mateusz Sych authored and GitHub committed 2026-10-17 04:08:46 -04:00
1 parent a2245b2f51
commit ff7f3204d5
5 files changed
+63 -6

No files matched your search

Generated
+1
View File
@@ -4255,6 +4255,7 @@ dependencies = [
"toml",
"ttf-parser",
"web-time",
"winreg",
"zip 3.0.0",
]
+5
View File
@@ -28,6 +28,11 @@ zip = { workspace = true }
# Rasterises the icon SVGs (assets/icons/ui/design/svg) at the size they are drawn.
tiny-skia = { workspace = true }
[target.'cfg(windows)'.dependencies]
# Updates: the folder the Windows installer recorded, so an install outside Program Files is still
# updated by its installer (MIT; already in the tree as a build dependency).
winreg = "0.56"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
# The offscreen viewport render test opens a device without a window.
pollster = { workspace = true }
+27 -3
View File
@@ -144,7 +144,7 @@ pub enum InstallKind {
Rpm,
/// The plain `.tar.gz`, unpacked somewhere.
LinuxTarball,
/// Installed by the `.msi` (under Program Files).
/// Installed by the `.msi` (under Program Files, or in the folder the installer recorded).
WindowsMsi,
/// The portable `.zip`, unpacked somewhere.
WindowsPortable,
@@ -165,6 +165,9 @@ pub struct Probe<'a> {
/// The running executable.
pub exe: Option<PathBuf>,
pub exists: &'a dyn Fn(&Path) -> bool,
/// Where the Windows installer put SolidCraft (`HKLM\Software\SolidCraft`, `InstallDir`): a
/// folder chosen in its wizard can be anywhere, not only under Program Files (#1870).
pub msi_dir: Option<PathBuf>,
}
pub fn detect(p: &Probe) -> InstallKind {
@@ -202,7 +205,8 @@ pub fn detect(p: &Probe) -> InstallKind {
}
"windows" => {
let under = |k: &str| env(k).is_some_and(|dir| starts_with_ci(&exe, Path::new(&dir)));
if under("ProgramFiles") || under("ProgramW6432") || under("ProgramFiles(x86)") {
let installed = p.msi_dir.as_deref().is_some_and(|dir| starts_with_ci(&exe, dir));
if installed || under("ProgramFiles") || under("ProgramW6432") || under("ProgramFiles(x86)") {
return InstallKind::WindowsMsi;
}
if exe.parent().is_some_and(|d| (p.exists)(&d.join("solidcraft-cli.exe"))) {
@@ -240,10 +244,30 @@ pub fn this_install() -> InstallKind {
{
let env = |k: &str| std::env::var(k).ok();
let exists = |p: &Path| p.exists();
detect(&Probe { os: std::env::consts::OS, release_build: release_build(), env: &env, exe: std::env::current_exe().ok(), exists: &exists })
let msi_dir = msi_install_dir();
detect(&Probe {
os: std::env::consts::OS,
release_build: release_build(),
env: &env,
exe: std::env::current_exe().ok(),
exists: &exists,
msi_dir,
})
}
}
/// The folder the Windows installer recorded (solidcraft.wxs writes it for upgrades), if any.
#[cfg(windows)]
fn msi_install_dir() -> Option<PathBuf> {
let key = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE).open_subkey(r"Software\SolidCraft").ok()?;
key.get_value::<String, _>("InstallDir").ok().map(PathBuf::from)
}
#[cfg(all(not(windows), not(target_arch = "wasm32")))]
fn msi_install_dir() -> Option<PathBuf> {
None
}
impl InstallKind {
/// The package manager updates it: Download isn't the way (the window shows the command).
pub fn system_managed(&self) -> bool {
+29 -2
View File
@@ -32,7 +32,7 @@ fn probe(os: &str, release: bool, env: &[(&str, &str)], exe: &str, files: &[&str
let files: Vec<PathBuf> = files.iter().map(PathBuf::from).collect();
let get = |k: &str| env.get(k).cloned();
let exists = |p: &Path| files.iter().any(|f| f == p);
detect(&Probe { os, release_build: release, env: &get, exe: Some(PathBuf::from(exe)), exists: &exists })
detect(&Probe { os, release_build: release, env: &get, exe: Some(PathBuf::from(exe)), exists: &exists, msi_dir: None })
}
#[test]
@@ -92,7 +92,34 @@ fn portable_windows_layout_is_recognised() {
let exe = PathBuf::from("/portable/solidcraft.exe");
let get = |k: &str| (k == "ProgramFiles").then(|| "C:\\Program Files".to_string());
let exists = |p: &Path| p == Path::new("/portable/solidcraft-cli.exe");
assert_eq!(detect(&Probe { os: "windows", release_build: true, env: &get, exe: Some(exe), exists: &exists }), InstallKind::WindowsPortable);
assert_eq!(
detect(&Probe { os: "windows", release_build: true, env: &get, exe: Some(exe), exists: &exists, msi_dir: None }),
InstallKind::WindowsPortable
);
}
/// #1870: the installer's wizard can put SolidCraft anywhere, and it puts solidcraft-cli.exe
/// beside it as the portable zip does. The folder it recorded is what tells its copy apart.
#[test]
fn an_msi_install_outside_program_files_is_recognised() {
let exe = PathBuf::from(r"D:\Program Files\SolidCraft\solidcraft.exe");
let get = |k: &str| match k {
"ProgramFiles" | "ProgramW6432" => Some(r"C:\Program Files".to_string()),
"ProgramFiles(x86)" => Some(r"C:\Program Files (x86)".to_string()),
_ => None,
};
// solidcraft-cli.exe is beside it.
let exists = |_: &Path| true;
let kind = |msi_dir: Option<&str>| {
detect(&Probe { os: "windows", release_build: true, env: &get, exe: Some(exe.clone()), exists: &exists, msi_dir: msi_dir.map(PathBuf::from) })
};
// The installer records [INSTALLFOLDER], which ends in a backslash; Windows paths ignore case.
assert_eq!(kind(Some(r"D:\Program Files\SolidCraft\")), InstallKind::WindowsMsi);
assert_eq!(kind(Some(r"d:\program files\solidcraft")), InstallKind::WindowsMsi);
// No installer copy here: the portable zip, as before.
assert_eq!(kind(None), InstallKind::WindowsPortable);
assert_eq!(kind(Some("")), InstallKind::WindowsPortable);
assert_eq!(kind(Some(r"D:\Program Files\SolidCraft2\")), InstallKind::WindowsPortable, "a folder whose name only starts the same");
}
/// The assets of v0.10.0, as published.
+1 -1
View File
@@ -88,7 +88,7 @@ and SolidCraft picks the one matching how it was installed:
| `.deb` | under `/usr`, dpkg knows `solidcraft` | `solidcraft-linux-<arch>.deb` | `sudo apt install ./solidcraft-linux-<arch>.deb` |
| `.rpm` | under `/usr`, an rpm database exists | `solidcraft-linux-<arch>.rpm` | `sudo dnf install ./solidcraft-linux-<arch>.rpm` |
| `.tar.gz` | anywhere else on Linux | `solidcraft-<version>-linux-<arch>.tar.gz` | Unpack over the old copy |
| Windows installer | under Program Files | `solidcraft-windows-<arch>.msi` | Run the installer |
| Windows installer | under Program Files, or in the folder the installer recorded | `solidcraft-windows-<arch>.msi` | Run the installer |
| Windows portable | `solidcraft-cli.exe` beside it | `solidcraft-windows-<arch>-portable.zip` | Unzip over the old copy |
| Windows `.exe` | anywhere else on Windows | `SolidCraft-<version>-windows-x64.exe` | Replace the old `.exe` |
| macOS | inside `SolidCraft.app` | `solidcraft-macos-universal.dmg` | Drag the new app to Applications |