fix(workbuddy): detect macOS executable from Info.plist

This commit is contained in:
lzt404 committed 2026-09-29 15:25:14 +08:00
1 parent aae649dd0f
commit 21b3f85dca
2 files changed
+139 -5

No files matched your search

+54 -5
View File
@@ -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)
+85
View File
@@ -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() {