Locate app bundle based on location of CLI binary

Antonio Scandurra and Nathan Sobo created

The app bundle can also be specified via `-b` or `--bundle-path`.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

Cargo.lock             |  2 -
crates/cli/Cargo.toml  |  2 -
crates/cli/src/main.rs | 56 +++++++++++++++----------------------------
3 files changed, 20 insertions(+), 40 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -998,12 +998,10 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "clap 3.1.8",
- "cocoa",
  "core-foundation",
  "core-services",
  "dirs 3.0.1",
  "ipc-channel",
- "objc",
  "plist",
  "serde",
 ]

crates/cli/Cargo.toml 🔗

@@ -19,8 +19,6 @@ ipc-channel = "0.16"
 serde = { version = "1.0", features = ["derive"] }
 
 [target.'cfg(target_os = "macos")'.dependencies]
-cocoa = "0.24"
 core-foundation = "0.9"
 core-services = "0.2"
-objc = "0.2"
 plist = "1.3"

crates/cli/src/main.rs 🔗

@@ -8,9 +8,8 @@ use core_foundation::{
 };
 use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
 use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
-use objc::{class, msg_send, sel, sel_impl};
 use serde::Deserialize;
-use std::{ffi::CStr, fs, path::PathBuf, ptr};
+use std::{ffi::OsStr, fs, path::PathBuf, ptr};
 
 #[derive(Parser)]
 #[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
@@ -24,6 +23,9 @@ struct Args {
     /// Print Zed's version and the app path.
     #[clap(short, long)]
     version: bool,
+    /// Custom Zed.app path
+    #[clap(short, long)]
+    bundle_path: Option<PathBuf>,
 }
 
 #[derive(Debug, Deserialize)]
@@ -35,19 +37,24 @@ struct InfoPlist {
 fn main() -> Result<()> {
     let args = Args::parse();
 
-    let app_path = locate_app()?;
+    let bundle_path = if let Some(bundle_path) = args.bundle_path {
+        bundle_path.canonicalize()?
+    } else {
+        locate_bundle()?
+    };
+
     if args.version {
-        let plist_path = app_path.join("Contents/Info.plist");
+        let plist_path = bundle_path.join("Contents/Info.plist");
         let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
         println!(
             "Zed {} – {}",
             plist.bundle_short_version_string,
-            app_path.to_string_lossy()
+            bundle_path.to_string_lossy()
         );
         return Ok(());
     }
 
-    let (tx, rx) = launch_app(app_path)?;
+    let (tx, rx) = launch_app(bundle_path)?;
 
     tx.send(CliRequest::Open {
         paths: args
@@ -70,38 +77,15 @@ fn main() -> Result<()> {
     Ok(())
 }
 
-fn locate_app() -> Result<PathBuf> {
-    if cfg!(debug_assertions) {
-        Ok(std::env::current_exe()?
-            .parent()
-            .unwrap()
-            .join("bundle/osx/Zed.app"))
-    } else {
-        Ok(path_to_app_with_bundle_identifier("dev.zed.Zed")
-            .unwrap_or_else(|| "/Applications/Zed.dev".into()))
-    }
-}
-
-fn path_to_app_with_bundle_identifier(bundle_id: &str) -> Option<PathBuf> {
-    use cocoa::{
-        base::{id, nil},
-        foundation::{NSString, NSURL as _},
-    };
-
-    unsafe {
-        let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
-        let bundle_id = NSString::alloc(nil).init_str(bundle_id);
-        let app_url: id = msg_send![workspace, URLForApplicationWithBundleIdentifier: bundle_id];
-        if !app_url.is_null() {
-            Some(PathBuf::from(
-                CStr::from_ptr(app_url.path().UTF8String())
-                    .to_string_lossy()
-                    .to_string(),
-            ))
-        } else {
-            None
+fn locate_bundle() -> Result<PathBuf> {
+    let cli_path = std::env::current_exe()?.canonicalize()?;
+    let mut app_path = cli_path.clone();
+    while app_path.extension() != Some(OsStr::new("app")) {
+        if !app_path.pop() {
+            return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
         }
     }
+    Ok(app_path)
 }
 
 fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {