Skip to content

Commit b923035

Browse files
karthiknadigCopilot
andcommitted
perf: resolve macOS Python shims without spawning (#504)
Map Apple /usr/bin/python3 aliases through the active developer directory, remove duplicate Xcode and CommandLineTools probes, and skip unresolved shims before the spawn fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f9485d0 commit b923035

5 files changed

Lines changed: 289 additions & 47 deletions

File tree

crates/pet-mac-commandlinetools/src/lib.rs

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use pet_core::{
99
Locator, LocatorKind,
1010
};
1111
use pet_fs::path::resolve_symlink;
12+
use pet_python_utils::macos::{
13+
add_macos_system_python_alias, is_macos_system_python, resolve_macos_system_python_env,
14+
};
1215
use pet_python_utils::version;
1316
use pet_python_utils::{env::ResolvedPythonEnv, executable::find_executables};
1417
use pet_virtualenv::is_virtualenv;
@@ -107,6 +110,13 @@ impl Locator for MacCmdLineTools {
107110
if std::env::consts::OS != "macos" {
108111
return None;
109112
}
113+
114+
let resolved_system_alias = if is_macos_system_python(&env.executable) {
115+
Some(resolve_macos_system_python_env(env)?)
116+
} else {
117+
None
118+
};
119+
let env = resolved_system_alias.as_ref().unwrap_or(env);
110120
// Assume we create a virtual env from a python install,
111121
// Then the exe in the virtual env bin will be a symlink to the homebrew python install.
112122
// Hence the first part of the condition will be true, but the second part will be false.
@@ -165,29 +175,6 @@ impl Locator for MacCmdLineTools {
165175

166176
let mut resolved_environments = vec![];
167177

168-
// We know /usr/bin/python3 can end up pointing to this same Python exe as well
169-
// Hence look for those symlinks as well.
170-
// Unfortunately /usr/bin/python3 is not a real symlink
171-
// Hence we must spawn and verify it points to the same Python exe.
172-
for possible_exes in [PathBuf::from("/usr/bin/python3")] {
173-
if !symlinks.contains(&possible_exes) {
174-
if let Some(resolved_env) = ResolvedPythonEnv::from(&possible_exes) {
175-
if symlinks.contains(&resolved_env.executable) {
176-
resolved_environments.push(resolved_env.clone());
177-
178-
symlinks.push(possible_exes);
179-
// Use the latest accurate information we have.
180-
version = Some(resolved_env.version);
181-
prefix = Some(resolved_env.prefix);
182-
arch = if resolved_env.is64_bit {
183-
Some(Architecture::X64)
184-
} else {
185-
Some(Architecture::X86)
186-
};
187-
}
188-
}
189-
}
190-
}
191178
// Similarly the final exe can be /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9
192179
// & we might have another file `python3` in that bin directory which would point to the same exe.
193180
// Lets get those as well.
@@ -205,6 +192,7 @@ impl Locator for MacCmdLineTools {
205192
}
206193
}
207194

195+
add_macos_system_python_alias(&mut symlinks);
208196
symlinks.sort();
209197
symlinks.dedup();
210198

crates/pet-mac-xcode/src/lib.rs

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use pet_core::{
99
Locator, LocatorKind,
1010
};
1111
use pet_fs::path::resolve_symlink;
12+
use pet_python_utils::macos::{
13+
add_macos_system_python_alias, is_macos_system_python, resolve_macos_system_python_env,
14+
};
1215
use pet_python_utils::version;
1316
use pet_python_utils::{env::ResolvedPythonEnv, executable::find_executables};
1417
use pet_virtualenv::is_virtualenv;
@@ -38,6 +41,13 @@ impl Locator for MacXCode {
3841
if std::env::consts::OS != "macos" {
3942
return None;
4043
}
44+
45+
let resolved_system_alias = if is_macos_system_python(&env.executable) {
46+
Some(resolve_macos_system_python_env(env)?)
47+
} else {
48+
None
49+
};
50+
let env = resolved_system_alias.as_ref().unwrap_or(env);
4151
// Assume we create a virtual env from a python install,
4252
// Then the exe in the virtual env bin will be a symlink to the homebrew python install.
4353
// Hence the first part of the condition will be true, but the second part will be false.
@@ -98,28 +108,6 @@ impl Locator for MacXCode {
98108

99109
let mut resolved_environments = vec![];
100110

101-
// We know /usr/bin/python3 can end up pointing to this same Python exe as well
102-
// Hence look for those symlinks as well.
103-
// Unfortunately /usr/bin/python3 is not a real symlink
104-
// Hence we must spawn and verify it points to the same Python exe.
105-
for possible_exes in [PathBuf::from("/usr/bin/python3")] {
106-
if !symlinks.contains(&possible_exes) {
107-
if let Some(resolved_env) = ResolvedPythonEnv::from(&possible_exes) {
108-
if symlinks.contains(&resolved_env.executable) {
109-
resolved_environments.push(resolved_env.clone());
110-
symlinks.push(possible_exes);
111-
// Use the latest accurate information we have.
112-
version = Some(resolved_env.version);
113-
prefix = Some(resolved_env.prefix);
114-
arch = if resolved_env.is64_bit {
115-
Some(Architecture::X64)
116-
} else {
117-
Some(Architecture::X86)
118-
};
119-
}
120-
}
121-
}
122-
}
123111
// Similarly the final exe can be /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9
124112
// & we might have another file `python3` in that bin directory which would point to the same exe.
125113
// Lets get those as well.
@@ -137,6 +125,7 @@ impl Locator for MacXCode {
137125
}
138126
}
139127

128+
add_macos_system_python_alias(&mut symlinks);
140129
symlinks.sort();
141130
symlinks.dedup();
142131

crates/pet-python-utils/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ pub mod env;
77
pub mod executable;
88
pub mod fs_cache;
99
mod headers;
10+
pub mod macos;
1011
pub mod platform_dirs;
1112
pub mod version;
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use std::{
5+
env,
6+
path::{Path, PathBuf},
7+
};
8+
9+
use pet_core::env::PythonEnv;
10+
use pet_fs::path::{resolve_any_symlink, resolve_symlink};
11+
12+
const SYSTEM_PYTHON_DIR: &str = "/usr/bin";
13+
const XCODE_SELECT_LINK: &str = "/var/db/xcode_select_link";
14+
const DEFAULT_XCODE_DEVELOPER_DIR: &str = "/Applications/Xcode.app/Contents/Developer";
15+
const DEFAULT_COMMAND_LINE_TOOLS_DIR: &str = "/Library/Developer/CommandLineTools";
16+
17+
pub fn is_macos_system_python(executable: &Path) -> bool {
18+
let mut components = executable.components();
19+
matches!(components.next(), Some(std::path::Component::RootDir))
20+
&& matches!(components.next(), Some(std::path::Component::Normal(part)) if part == "usr")
21+
&& matches!(components.next(), Some(std::path::Component::Normal(part)) if part == "bin")
22+
&& matches!(components.next(), Some(std::path::Component::Normal(name)) if is_macos_python_name(name))
23+
&& components.next().is_none()
24+
}
25+
26+
fn is_macos_python_name(name: &std::ffi::OsStr) -> bool {
27+
let Some(name) = name.to_str() else {
28+
return false;
29+
};
30+
if name == "python3" {
31+
return true;
32+
}
33+
let Some(minor) = name.strip_prefix("python3.") else {
34+
return false;
35+
};
36+
!minor.is_empty() && minor.bytes().all(|byte| byte.is_ascii_digit())
37+
}
38+
39+
pub fn resolve_macos_system_python(executable: &Path) -> Option<PathBuf> {
40+
if std::env::consts::OS != "macos" || !is_macos_system_python(executable) {
41+
return None;
42+
}
43+
let developer_dir = active_developer_dir()?;
44+
selected_python_with(executable, &developer_dir, Path::is_file)
45+
}
46+
47+
pub fn resolve_macos_system_python_env(env: &PythonEnv) -> Option<PythonEnv> {
48+
let executable = resolve_macos_system_python(&env.executable)?;
49+
let mut resolved = PythonEnv::new(executable, env.prefix.clone(), env.version.clone());
50+
let mut aliases = env.symlinks.clone().unwrap_or_default();
51+
aliases.push(env.executable.clone());
52+
aliases.sort();
53+
aliases.dedup();
54+
resolved.symlinks = Some(aliases);
55+
Some(resolved)
56+
}
57+
58+
pub fn add_macos_system_python_alias(symlinks: &mut Vec<PathBuf>) {
59+
let alias = PathBuf::from(SYSTEM_PYTHON_DIR).join("python3");
60+
let Some(selected) = resolve_macos_system_python(&alias) else {
61+
return;
62+
};
63+
let resolved = resolve_symlink(&selected).unwrap_or_else(|| selected.clone());
64+
add_alias_if_target_matches(symlinks, alias, &selected, &resolved);
65+
}
66+
67+
fn active_developer_dir() -> Option<PathBuf> {
68+
let environment = env::var_os("DEVELOPER_DIR").map(PathBuf::from);
69+
let selected = resolve_any_symlink(&PathBuf::from(XCODE_SELECT_LINK));
70+
active_developer_dir_with(environment, selected, Path::is_dir)
71+
}
72+
73+
fn active_developer_dir_with(
74+
environment: Option<PathBuf>,
75+
selected: Option<PathBuf>,
76+
is_dir: impl Fn(&Path) -> bool,
77+
) -> Option<PathBuf> {
78+
environment
79+
.into_iter()
80+
.chain(selected)
81+
.chain([
82+
PathBuf::from(DEFAULT_XCODE_DEVELOPER_DIR),
83+
PathBuf::from(DEFAULT_COMMAND_LINE_TOOLS_DIR),
84+
])
85+
.map(normalize_developer_dir)
86+
.find(|path| is_dir(path))
87+
}
88+
89+
fn normalize_developer_dir(path: PathBuf) -> PathBuf {
90+
if path.extension().is_some_and(|extension| extension == "app") {
91+
path.join("Contents").join("Developer")
92+
} else {
93+
path
94+
}
95+
}
96+
97+
fn selected_python_with(
98+
alias: &Path,
99+
developer_dir: &Path,
100+
mut is_file: impl FnMut(&Path) -> bool,
101+
) -> Option<PathBuf> {
102+
if !is_macos_system_python(alias) {
103+
return None;
104+
}
105+
let candidate = developer_dir
106+
.join("usr")
107+
.join("bin")
108+
.join(alias.file_name()?);
109+
is_file(&candidate).then_some(candidate)
110+
}
111+
112+
fn add_alias_if_target_matches(
113+
symlinks: &mut Vec<PathBuf>,
114+
alias: PathBuf,
115+
selected: &Path,
116+
resolved: &Path,
117+
) {
118+
if symlinks
119+
.iter()
120+
.any(|path| path == selected || path == resolved)
121+
{
122+
symlinks.push(alias);
123+
symlinks.sort();
124+
symlinks.dedup();
125+
}
126+
}
127+
128+
#[cfg(test)]
129+
mod tests {
130+
use super::*;
131+
132+
#[test]
133+
fn system_python_requires_a_python_name_directly_under_usr_bin() {
134+
assert!(is_macos_system_python(Path::new("/usr/bin/python3")));
135+
assert!(is_macos_system_python(Path::new("/usr/bin/python3.12")));
136+
assert!(!is_macos_system_python(Path::new("/usr/bin/python")));
137+
assert!(!is_macos_system_python(Path::new("/usr/local/bin/python3")));
138+
assert!(!is_macos_system_python(Path::new(
139+
"/usr/bin/python3-config"
140+
)));
141+
}
142+
143+
#[test]
144+
fn developer_dir_prefers_environment_and_normalizes_app_bundle() {
145+
let selected = active_developer_dir_with(
146+
Some(PathBuf::from("/Applications/Xcode_16.app")),
147+
Some(PathBuf::from(DEFAULT_COMMAND_LINE_TOOLS_DIR)),
148+
|_| true,
149+
);
150+
151+
assert_eq!(
152+
selected,
153+
Some(PathBuf::from(
154+
"/Applications/Xcode_16.app/Contents/Developer"
155+
))
156+
);
157+
}
158+
159+
#[test]
160+
fn developer_dir_falls_back_to_selected_link_then_standard_locations() {
161+
let selected = active_developer_dir_with(
162+
None,
163+
Some(PathBuf::from(
164+
"/Applications/Xcode_Beta.app/Contents/Developer",
165+
)),
166+
|_| true,
167+
);
168+
assert_eq!(
169+
selected,
170+
Some(PathBuf::from(
171+
"/Applications/Xcode_Beta.app/Contents/Developer"
172+
))
173+
);
174+
175+
let fallback = active_developer_dir_with(None, None, |path| {
176+
path == Path::new(DEFAULT_COMMAND_LINE_TOOLS_DIR)
177+
});
178+
assert_eq!(
179+
fallback,
180+
Some(PathBuf::from(DEFAULT_COMMAND_LINE_TOOLS_DIR))
181+
);
182+
}
183+
184+
#[test]
185+
fn selected_python_maps_alias_without_spawning() {
186+
let developer_dir = Path::new(DEFAULT_COMMAND_LINE_TOOLS_DIR);
187+
let expected = developer_dir.join("usr/bin/python3");
188+
let mut file_checks = 0;
189+
190+
let selected = selected_python_with(Path::new("/usr/bin/python3"), developer_dir, |path| {
191+
file_checks += 1;
192+
path == expected
193+
});
194+
195+
assert_eq!(selected, Some(expected));
196+
assert_eq!(file_checks, 1);
197+
}
198+
199+
#[test]
200+
fn alias_is_added_only_for_a_matching_selected_target() {
201+
let selected = PathBuf::from("/Library/Developer/CommandLineTools/usr/bin/python3");
202+
let resolved = PathBuf::from(
203+
"/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/bin/python3.9",
204+
);
205+
let alias = PathBuf::from("/usr/bin/python3");
206+
let mut symlinks = vec![resolved.clone()];
207+
208+
add_alias_if_target_matches(&mut symlinks, alias.clone(), &selected, &resolved);
209+
assert!(symlinks.contains(&alias));
210+
211+
let mut unrelated = vec![PathBuf::from("/opt/homebrew/bin/python3")];
212+
add_alias_if_target_matches(&mut unrelated, alias.clone(), &selected, &resolved);
213+
assert!(!unrelated.contains(&alias));
214+
}
215+
}

0 commit comments

Comments
 (0)