Merge pull request #6575 from Hmbown/fix/homebrew-install-detection

fix(update): detect Homebrew installs launched through the bin symlink on macOS
This commit is contained in:
Hunter Bown authored and GitHub committed 2026-09-25 12:46:35 -07:00
commit 079cbdcb5a
2 files changed
+42 -5

No files matched your search

+39 -5
View File
@@ -59,13 +59,23 @@ pub enum InstallMethod {
impl InstallMethod {
/// Detect from an executable path, honouring [`INSTALL_METHOD_ENV`].
///
/// Pass the *resolved* path — `std::env::current_exe()` already follows
/// symlinks on the platforms we ship, which is what puts a globally
/// npm-installed binary inside `node_modules` and a Homebrew one inside
/// `Cellar` rather than in the manager's flat `bin` shim directory.
/// `std::env::current_exe()` follows symlinks on Linux but not on macOS,
/// where a Homebrew launch reports `/opt/homebrew/bin/codewhale` instead
/// of the `Cellar` file it points at. When the path as given looks
/// self-updatable, the symlink target is checked too, and a package
/// manager that owns the real file wins.
#[must_use]
pub fn detect(exe: &Path) -> Self {
let detected = Self::detect_with_omarchy_probe(exe, omarchy_package_owns);
let mut detected = Self::detect_with_omarchy_probe(exe, omarchy_package_owns);
if detected.supports_self_update()
&& let Ok(real) = std::fs::canonicalize(exe)
&& real != exe
{
let owner = Self::detect_with_omarchy_probe(&real, omarchy_package_owns);
if !owner.supports_self_update() {
detected = owner;
}
}
// An override can identify a relocated package, but cannot authorize
// replacing a file that a known package manager owns.
if !detected.supports_self_update() {
@@ -249,6 +259,30 @@ mod tests {
assert!(!InstallMethod::Homebrew.supports_self_update());
}
/// macOS reports the launcher symlink from `current_exe()`, so detection
/// must follow it into `Cellar` before allowing an in-place update.
#[cfg(unix)]
#[test]
fn homebrew_symlink_launcher_is_detected_through_its_target() {
let root = tempfile::tempdir().unwrap();
let cellar_bin = root.path().join("Cellar/codewhale/0.10.0/bin");
std::fs::create_dir_all(&cellar_bin).unwrap();
let real = cellar_bin.join("codewhale");
std::fs::write(&real, b"binary").unwrap();
let bin = root.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
let launcher = bin.join("codewhale");
std::os::unix::fs::symlink(&real, &launcher).unwrap();
assert_eq!(InstallMethod::from_path(&launcher), InstallMethod::Binary);
assert_eq!(InstallMethod::detect(&launcher), InstallMethod::Homebrew);
// A plain release binary with no package owner stays self-updatable.
let plain = bin.join("codew");
std::fs::write(&plain, b"binary").unwrap();
assert!(InstallMethod::detect(&plain).supports_self_update());
}
#[test]
fn cargo_install_requires_the_cargo_bin_pair() {
assert_eq!(
+3
View File
@@ -77,6 +77,9 @@
"crates/release/src/check.rs": {
"std_fs": 4
},
"crates/release/src/install.rs": {
"std_fs": 1
},
"crates/release/src/launch.rs": {
"std_fs": 2
},