Skip to content

Commit d6d0420

Browse files
committed
fix(macos): launch the detected Spotify bundle, not a hardcoded one
spawn_macos ran 'open -a /Applications/Spotify.app' and ignored the context entirely, so on a ~/Applications install the whole apply succeeded and then opened nothing, failing with 'Spotify did not start within 30 seconds'. It now launches the bundle derived from the resolved executable. The version-detect fallback had the same hardcoded path and now derives it too; its primary mdls path already used the real bundle, which is why version detection worked while the launch did not.
1 parent 920d32e commit d6d0420

2 files changed

Lines changed: 64 additions & 11 deletions

File tree

rust/crates/spicetify/src/hooks/version_detect.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::path::Path;
1+
use std::path::{Path, PathBuf};
22
use std::process::Command;
33
use std::sync::LazyLock;
44

@@ -46,13 +46,19 @@ fn detect_version(exec_path: &Path) -> Result<String> {
4646
}
4747
}
4848

49-
let output = Command::new("sh")
50-
.args([
51-
"-c",
52-
"defaults read /Applications/Spotify.app/Contents/Info CFBundleShortVersionString",
53-
])
49+
// Same bundle the caller resolved, not a hardcoded one: on an install in
50+
// ~/Applications a fixed /Applications path reads nothing and the version
51+
// comes back undetectable.
52+
let info_plist = app_base
53+
.map_or_else(|| PathBuf::from("/Applications/Spotify.app"), Path::to_path_buf)
54+
.join("Contents")
55+
.join("Info");
56+
let output = Command::new("defaults")
57+
.arg("read")
58+
.arg(&info_plist)
59+
.arg("CFBundleShortVersionString")
5460
.output()
55-
.map_err(|e| anyhow::anyhow!("failed to read Info.plist: {e}"))?;
61+
.map_err(|e| anyhow::anyhow!("failed to read {}: {e}", info_plist.display()))?;
5662

5763
if output.status.success() {
5864
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();

rust/crates/spicetify/src/process.rs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,39 @@ pub fn spawn_detached(ctx: &AppContext) -> Result<()> {
7878
}
7979

8080
#[cfg(target_os = "macos")]
81-
fn spawn_macos(_ctx: &AppContext) -> Result<()> {
81+
fn spawn_macos(ctx: &AppContext) -> Result<()> {
82+
// Launch the bundle that was actually resolved, not a hardcoded one:
83+
// `open -a` on a path that does not exist starts nothing, and the caller
84+
// then waits out its timeout with no idea why.
85+
let bundle = macos_bundle(&ctx.spotify_exec);
8286
let child = Command::new("open")
83-
.args(["-a", "/Applications/Spotify.app", "--args"])
87+
.arg("-a")
88+
.arg(&bundle)
89+
.arg("--args")
8490
.stdin(Stdio::null())
8591
.stdout(Stdio::null())
8692
.stderr(Stdio::null())
8793
.spawn()
88-
.map_err(|e| anyhow::anyhow!("failed to launch Spotify via open: {e}"))?;
94+
.map_err(|e| {
95+
anyhow::anyhow!("failed to launch Spotify via open -a {}: {e}", bundle.display())
96+
})?;
8997
std::mem::forget(child);
90-
tracing::info!("Spotify launched via open -a /Applications/Spotify.app");
98+
tracing::info!("Spotify launched via open -a {}", bundle.display());
9199
Ok(())
92100
}
93101

102+
/// `<bundle>/Contents/MacOS/Spotify` back up to `<bundle>`. Falls back to the
103+
/// executable itself, which `open -a` also accepts, when the path is not
104+
/// shaped like a bundle.
105+
#[cfg(target_os = "macos")]
106+
fn macos_bundle(exec: &std::path::Path) -> std::path::PathBuf {
107+
exec.parent()
108+
.and_then(std::path::Path::parent)
109+
.and_then(std::path::Path::parent)
110+
.filter(|p| p.extension().is_some_and(|ext| ext == "app"))
111+
.map_or_else(|| exec.to_path_buf(), std::path::Path::to_path_buf)
112+
}
113+
94114
#[cfg(windows)]
95115
fn spawn_windows(ctx: &AppContext) -> Result<()> {
96116
use std::path::PathBuf;
@@ -186,3 +206,30 @@ pub fn force_kill_spotify(ctx: &AppContext) {
186206
tracing::info!("force-killing Spotify processes");
187207
kill_image(image);
188208
}
209+
210+
#[cfg(all(test, target_os = "macos"))]
211+
mod tests {
212+
use super::macos_bundle;
213+
use std::path::{Path, PathBuf};
214+
215+
#[test]
216+
fn bundle_is_derived_from_the_resolved_executable() {
217+
// The launch path must follow detection: a hardcoded /Applications
218+
// "launches" nothing on a ~/Applications install and the caller then
219+
// waits out its start timeout.
220+
assert_eq!(
221+
macos_bundle(Path::new("/Users/me/Applications/Spotify.app/Contents/MacOS/Spotify")),
222+
PathBuf::from("/Users/me/Applications/Spotify.app")
223+
);
224+
assert_eq!(
225+
macos_bundle(Path::new("/Applications/Spotify.app/Contents/MacOS/Spotify")),
226+
PathBuf::from("/Applications/Spotify.app")
227+
);
228+
}
229+
230+
#[test]
231+
fn a_non_bundle_executable_is_used_as_is() {
232+
let exec = Path::new("/opt/spotify/spotify");
233+
assert_eq!(macos_bundle(exec), exec.to_path_buf(), "open -a accepts the binary too");
234+
}
235+
}

0 commit comments

Comments
 (0)