fix(workbuddy): detect macOS executable from Info.plist
This commit is contained in:
1 parent
aae649dd0f
commit
21b3f85dca
2 files changed
+139
-5
No files matched your search
@@ -60,6 +60,7 @@ fn workbuddy_resources(executable: &Path) -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
pub(crate) fn find_workbuddy_desktop_executable(home: &Path) -> Option<PathBuf> {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let mut candidates = Vec::new();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -85,11 +86,10 @@ pub(crate) fn find_workbuddy_desktop_executable(home: &Path) -> Option<PathBuf>
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
for root in [PathBuf::from("/Applications"), home.join("Applications")] {
|
||||
for (app, executable) in [("WorkBuddy AI", "WorkBuddy AI"), ("WorkBuddy", "WorkBuddy")] {
|
||||
candidates.push(root.join(format!("{app}.app/Contents/MacOS/{executable}")));
|
||||
}
|
||||
}
|
||||
let found = find_workbuddy_macos_executable_in_roots(
|
||||
&[PathBuf::from("/Applications"), home.join("Applications")],
|
||||
read_workbuddy_bundle_executable,
|
||||
);
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
{
|
||||
candidates.extend([
|
||||
@@ -98,12 +98,61 @@ pub(crate) fn find_workbuddy_desktop_executable(home: &Path) -> Option<PathBuf>
|
||||
PathBuf::from("/opt/WorkBuddyAI/workbuddy"),
|
||||
]);
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let found = candidates.into_iter().find(|path| path.is_file());
|
||||
#[cfg(all(target_os = "windows", not(test)))]
|
||||
let found = found.or_else(find_windows_registered_workbuddy_executable);
|
||||
found
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
fn find_workbuddy_macos_executable_in_roots(
|
||||
roots: &[PathBuf],
|
||||
mut read_bundle_executable: impl FnMut(&Path) -> Option<String>,
|
||||
) -> Option<PathBuf> {
|
||||
for root in roots {
|
||||
for (app, fallback) in [("WorkBuddy AI", "WorkBuddy AI"), ("WorkBuddy", "WorkBuddy")] {
|
||||
let application = root.join(format!("{app}.app"));
|
||||
let macos = application.join("Contents/MacOS");
|
||||
let info_plist = application.join("Contents/Info.plist");
|
||||
if info_plist.is_file() {
|
||||
if let Some(name) = read_bundle_executable(&info_plist) {
|
||||
let name = name.trim();
|
||||
let mut components = Path::new(name).components();
|
||||
if matches!(components.next(), Some(Component::Normal(_)))
|
||||
&& components.next().is_none()
|
||||
{
|
||||
let executable = macos.join(name);
|
||||
if executable.is_file() {
|
||||
return Some(executable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let executable = macos.join(fallback);
|
||||
if executable.is_file() {
|
||||
return Some(executable);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read_workbuddy_bundle_executable(info_plist: &Path) -> Option<String> {
|
||||
let mut command = Command::new("/usr/bin/plutil");
|
||||
command
|
||||
.args(["-extract", "CFBundleExecutable", "raw", "-o", "-"])
|
||||
.arg(info_plist);
|
||||
let output = command_output_with_timeout(&mut command, Duration::from_secs(5)).ok()??;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
let name = String::from_utf8(output.stdout).ok()?;
|
||||
let name = name.trim();
|
||||
(!name.is_empty()).then(|| name.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn read_workbuddy_app_version(executable: &Path) -> Option<String> {
|
||||
// Use file metadata, never run an Electron application with --version during detection.
|
||||
read_zcode_app_version(executable)
|
||||
|
||||
@@ -478,6 +478,91 @@ fn workbuddy_paths_use_environment_then_installed_product_data_folder() {
|
||||
assert_eq!(resolve(Some(&home.0), Some(Path::new("unused"))), home.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workbuddy_macos_finds_plist_executable_before_legacy_name() {
|
||||
let home = Home::new();
|
||||
for (index, app) in ["WorkBuddy", "WorkBuddy AI"].into_iter().enumerate() {
|
||||
let root = home.0.join(format!("install-{index}"));
|
||||
let application = root.join(format!("{app}.app"));
|
||||
let macos = application.join("Contents/MacOS");
|
||||
fs::create_dir_all(&macos).unwrap();
|
||||
fs::write(application.join("Contents/Info.plist"), "Electron").unwrap();
|
||||
fs::write(macos.join(app), "legacy").unwrap();
|
||||
let electron = macos.join("Electron");
|
||||
fs::write(&electron, "electron").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
find_workbuddy_macos_executable_in_roots(&[root], |info| {
|
||||
fs::read_to_string(info).ok()
|
||||
}),
|
||||
Some(electron)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workbuddy_macos_falls_back_for_missing_or_invalid_plist_executable() {
|
||||
let home = Home::new();
|
||||
let outside = home.0.join("outside");
|
||||
fs::write(&outside, "outside").unwrap();
|
||||
for (index, app, declared) in [
|
||||
(0, "WorkBuddy", None),
|
||||
(1, "WorkBuddy AI", Some("Electron".to_string())),
|
||||
(2, "WorkBuddy", Some("../outside".to_string())),
|
||||
(3, "WorkBuddy AI", Some(".".to_string())),
|
||||
(4, "WorkBuddy", Some(outside.to_string_lossy().into_owned())),
|
||||
] {
|
||||
let root = home.0.join(format!("install-{index}"));
|
||||
let application = root.join(format!("{app}.app"));
|
||||
let macos = application.join("Contents/MacOS");
|
||||
fs::create_dir_all(&macos).unwrap();
|
||||
if let Some(declared) = declared {
|
||||
fs::write(application.join("Contents/Info.plist"), declared).unwrap();
|
||||
}
|
||||
fs::write(application.join("Contents/outside"), "outside").unwrap();
|
||||
let fallback = macos.join(app);
|
||||
fs::write(&fallback, "legacy").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
find_workbuddy_macos_executable_in_roots(&[root], |info| {
|
||||
fs::read_to_string(info).ok()
|
||||
}),
|
||||
Some(fallback)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn workbuddy_reads_xml_and_binary_bundle_executable() {
|
||||
let home = Home::new();
|
||||
let xml = home.0.join("Info.xml.plist");
|
||||
fs::write(
|
||||
&xml,
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict><key>CFBundleExecutable</key><string>Electron</string></dict></plist>"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
read_workbuddy_bundle_executable(&xml).as_deref(),
|
||||
Some("Electron")
|
||||
);
|
||||
|
||||
let binary = home.0.join("Info.binary.plist");
|
||||
let status = Command::new("/usr/bin/plutil")
|
||||
.args(["-convert", "binary1", "-o"])
|
||||
.arg(&binary)
|
||||
.arg(&xml)
|
||||
.status()
|
||||
.unwrap();
|
||||
assert!(status.success());
|
||||
assert_eq!(
|
||||
read_workbuddy_bundle_executable(&binary).as_deref(),
|
||||
Some("Electron")
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn workbuddy_detects_local_install_without_launching_it() {
|
||||
|
||||
Reference in new issue
Block a user