Skip to content

Commit b40789a

Browse files
authored
Fix detection of Python on Codespaces (#56)
For #53
1 parent b710945 commit b40789a

File tree

4 files changed

+155
-47
lines changed

4 files changed

+155
-47
lines changed

crates/pet-linux-global-python/src/lib.rs

Lines changed: 86 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,51 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use std::fs;
4+
use std::{
5+
collections::HashMap,
6+
fs,
7+
path::{Path, PathBuf},
8+
sync::{Arc, Mutex},
9+
thread,
10+
};
511

6-
use log::warn;
712
use pet_core::{
813
python_environment::{PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentCategory},
914
reporter::Reporter,
1015
Locator,
1116
};
1217
use pet_fs::path::resolve_symlink;
13-
use pet_python_utils::{env::PythonEnv, executable::find_executables};
18+
use pet_python_utils::{
19+
env::{PythonEnv, ResolvedPythonEnv},
20+
executable::find_executables,
21+
};
1422
use pet_virtualenv::is_virtualenv;
1523

16-
pub struct LinuxGlobalPython {}
24+
pub struct LinuxGlobalPython {
25+
reported_executables: Arc<Mutex<HashMap<PathBuf, PythonEnvironment>>>,
26+
}
1727

1828
impl LinuxGlobalPython {
1929
pub fn new() -> LinuxGlobalPython {
20-
LinuxGlobalPython {}
30+
LinuxGlobalPython {
31+
reported_executables: Arc::new(
32+
Mutex::new(HashMap::<PathBuf, PythonEnvironment>::new()),
33+
),
34+
}
35+
}
36+
37+
fn find_cached(&self, reporter: Option<&dyn Reporter>) {
38+
if std::env::consts::OS == "macos" || std::env::consts::OS == "windows" {
39+
return;
40+
}
41+
// Look through the /bin, /usr/bin, /usr/local/bin directories
42+
thread::scope(|s| {
43+
for bin in ["/bin", "/usr/bin", "/usr/local/bin"] {
44+
s.spawn(move || {
45+
find_and_report_global_pythons_in(bin, reporter, &self.reported_executables);
46+
});
47+
}
48+
});
2149
}
2250
}
2351
impl Default for LinuxGlobalPython {
@@ -47,41 +75,62 @@ impl Locator for LinuxGlobalPython {
4775
// If we do not have a version, then we cannot use this method.
4876
// Without version means we have not spawned the Python exe, thus do not have the real info.
4977
env.version.clone()?;
50-
let prefix = env.prefix.clone()?;
5178
let executable = env.executable.clone();
5279

53-
// If prefix or version is not available, then we cannot use this method.
54-
// 1. For files in /bin or /usr/bin, the prefix is always /usr
55-
// 2. For files in /usr/local/bin, the prefix is always /usr/local
80+
self.find_cached(None);
81+
82+
// We only support python environments in /bin, /usr/bin, /usr/local/bin
5683
if !executable.starts_with("/bin")
5784
&& !executable.starts_with("/usr/bin")
5885
&& !executable.starts_with("/usr/local/bin")
59-
&& !prefix.starts_with("/usr")
60-
&& !prefix.starts_with("/usr/local")
6186
{
6287
return None;
6388
}
6489

65-
// All known global linux are always installed in `/bin` or `/usr/bin` or `/usr/local/bin`
66-
if executable.starts_with("/bin")
67-
|| executable.starts_with("/usr/bin")
68-
|| executable.starts_with("/usr/local/bin")
69-
{
70-
get_python_in_bin(env)
71-
} else {
72-
warn!(
73-
"Unknown Python exe ({:?}), not in any of the known locations /bin, /usr/bin, /usr/local/bin",
74-
executable
75-
);
76-
None
90+
self.reported_executables
91+
.lock()
92+
.unwrap()
93+
.get(&executable)
94+
.cloned()
95+
}
96+
97+
fn find(&self, reporter: &dyn Reporter) {
98+
if std::env::consts::OS == "macos" || std::env::consts::OS == "windows" {
99+
return;
77100
}
101+
self.reported_executables.lock().unwrap().clear();
102+
self.find_cached(Some(reporter))
78103
}
104+
}
79105

80-
fn find(&self, _reporter: &dyn Reporter) {
81-
// No point looking in /usr/bin or /bin folder.
82-
// We will end up looking in these global locations and spawning them in other parts.
83-
// Here we cannot assume that anything in /usr/bin is a global Python, it could be a symlink or other.
84-
// Safer approach is to just spawn it which we need to do to get the `sys.prefix`
106+
fn find_and_report_global_pythons_in(
107+
bin: &str,
108+
reporter: Option<&dyn Reporter>,
109+
reported_executables: &Arc<Mutex<HashMap<PathBuf, PythonEnvironment>>>,
110+
) {
111+
let python_executables = find_executables(Path::new(bin));
112+
113+
for exe in python_executables.clone().iter() {
114+
if reported_executables.lock().unwrap().contains_key(exe) {
115+
continue;
116+
}
117+
if let Some(resolved) = ResolvedPythonEnv::from(exe) {
118+
if let Some(env) = get_python_in_bin(&resolved.to_python_env()) {
119+
let mut reported_executables = reported_executables.lock().unwrap();
120+
// env.symlinks = Some([symlinks, env.symlinks.clone().unwrap_or_default()].concat());
121+
if let Some(symlinks) = &env.symlinks {
122+
for symlink in symlinks {
123+
reported_executables.insert(symlink.clone(), env.clone());
124+
}
125+
}
126+
if let Some(exe) = env.executable.clone() {
127+
reported_executables.insert(exe, env.clone());
128+
}
129+
if let Some(reporter) = reporter {
130+
reporter.report_environment(&env);
131+
}
132+
}
133+
}
85134
}
86135
}
87136

@@ -100,6 +149,7 @@ fn get_python_in_bin(env: &PythonEnv) -> Option<PythonEnvironment> {
100149
// Keep track of what the exe resolves to.
101150
// Will have a value only if the exe is in another dir
102151
// E.g. /bin/python3 might be a symlink to /usr/bin/python3.12
152+
// Similarly /usr/local/python/current/bin/python might point to something like /usr/local/python/3.10.13/bin/python3.10
103153
// However due to legacy reasons we'll be treating these two as separate exes.
104154
// Hence they will be separate Python environments.
105155
let mut resolved_exe_is_from_another_dir = None;
@@ -119,6 +169,14 @@ fn get_python_in_bin(env: &PythonEnv) -> Option<PythonEnvironment> {
119169
resolved_exe_is_from_another_dir = Some(symlink);
120170
}
121171
}
172+
if let Ok(symlink) = fs::canonicalize(&executable) {
173+
// Ensure this is a symlink in the bin or usr/bin directory.
174+
if symlink.starts_with(bin) {
175+
symlinks.push(symlink);
176+
} else {
177+
resolved_exe_is_from_another_dir = Some(symlink);
178+
}
179+
}
122180

123181
// Look for other symlinks in the same folder
124182
// We know that on linux there are sym links in the same folder as the exe.

crates/pet-poetry/src/config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
path::{Path, PathBuf},
77
};
88

9-
use log::trace;
9+
use log::{error, trace};
1010
use pet_python_utils::platform_dirs::Platformdirs;
1111

1212
use crate::env_variables::EnvVariables;
@@ -152,7 +152,7 @@ fn parse_contents(contents: &str) -> Option<ConfigToml> {
152152
})
153153
}
154154
Err(e) => {
155-
eprintln!("Error parsing poetry toml file: {:?}", e);
155+
error!("Error parsing poetry toml file: {:?}", e);
156156
None
157157
}
158158
}

crates/pet-poetry/src/pyproject_toml.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
path::{Path, PathBuf},
77
};
88

9-
use log::trace;
9+
use log::{error, trace};
1010

1111
pub struct PyProjectToml {
1212
pub name: String,
@@ -41,7 +41,7 @@ fn parse_contents(contents: &str, file: &Path) -> Option<PyProjectToml> {
4141
name.map(|name| PyProjectToml::new(name, file.into()))
4242
}
4343
Err(e) => {
44-
eprintln!("Error parsing toml file: {:?}", e);
44+
error!("Error parsing toml file: {:?}", e);
4545
None
4646
}
4747
}

crates/pet/src/locators.rs

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use pet_python_utils::env::{PythonEnv, ResolvedPythonEnv};
2020
use pet_venv::Venv;
2121
use pet_virtualenv::VirtualEnv;
2222
use pet_virtualenvwrapper::VirtualEnvWrapper;
23+
use std::path::PathBuf;
2324
use std::sync::Arc;
2425

2526
pub fn create_locators(conda_locator: Arc<Conda>) -> Arc<Vec<Arc<dyn Locator>>> {
@@ -114,23 +115,72 @@ pub fn identify_python_environment_using_locators(
114115
// We have check all of the resolvers.
115116
// Telemetry point, failed to identify env here.
116117
warn!(
117-
"Unknown Env ({:?}) in Path resolved as {:?} and reported as Unknown",
118-
executable, resolved_env
118+
"Unknown Env ({:?}) in Path resolved as {:?} and reported as {:?}",
119+
executable,
120+
resolved_env,
121+
fallback_category.unwrap_or(PythonEnvironmentCategory::Unknown)
119122
);
120-
let env = PythonEnvironmentBuilder::new(
121-
fallback_category.unwrap_or(PythonEnvironmentCategory::Unknown),
122-
)
123-
.executable(Some(resolved_env.executable))
124-
.prefix(Some(resolved_env.prefix))
125-
.arch(Some(if resolved_env.is64_bit {
126-
Architecture::X64
127-
} else {
128-
Architecture::X86
129-
}))
130-
.version(Some(resolved_env.version))
131-
.build();
132-
return Some(env);
123+
return Some(create_unknown_env(resolved_env, fallback_category));
133124
}
134125
}
135126
None
136127
}
128+
129+
fn create_unknown_env(
130+
resolved_env: ResolvedPythonEnv,
131+
fallback_category: Option<PythonEnvironmentCategory>,
132+
) -> PythonEnvironment {
133+
// Find all the python exes in the same bin directory.
134+
135+
PythonEnvironmentBuilder::new(fallback_category.unwrap_or(PythonEnvironmentCategory::Unknown))
136+
.symlinks(find_symlinks(&resolved_env.executable))
137+
.executable(Some(resolved_env.executable))
138+
.prefix(Some(resolved_env.prefix))
139+
.arch(Some(if resolved_env.is64_bit {
140+
Architecture::X64
141+
} else {
142+
Architecture::X86
143+
}))
144+
.version(Some(resolved_env.version))
145+
.build()
146+
}
147+
148+
#[cfg(unix)]
149+
fn find_symlinks(executable: &PathBuf) -> Option<Vec<PathBuf>> {
150+
// Assume this is a python environment in /usr/bin/python.
151+
// Now we know there can be other exes in the same directory as well, such as /usr/bin/python3.12 and that could be the same as /usr/bin/python
152+
// However its possible /usr/bin/python is a symlink to /usr/local/bin/python3.12
153+
// Either way, if both /usr/bin/python and /usr/bin/python3.12 point to the same exe (what ever it may be),
154+
// then we know that both /usr/bin/python and /usr/bin/python3.12 are the same python environment.
155+
// We use canonicalize to get the real path of the symlink.
156+
// Only used in this case, see notes for resolve_symlink.
157+
158+
use pet_fs::path::resolve_symlink;
159+
use pet_python_utils::executable::find_executables;
160+
use std::fs;
161+
162+
let real_exe = resolve_symlink(executable).or(fs::canonicalize(executable).ok());
163+
164+
let bin = executable.parent()?;
165+
// Make no assumptions that bin is always where exes are in linux
166+
// No harm in supporting scripts as well.
167+
if !bin.ends_with("bin") && !bin.ends_with("Scripts") && !bin.ends_with("scripts") {
168+
return None;
169+
}
170+
171+
let mut symlinks = vec![];
172+
for exe in find_executables(bin) {
173+
let symlink = resolve_symlink(&exe).or(fs::canonicalize(&exe).ok());
174+
if symlink == real_exe {
175+
symlinks.push(exe);
176+
}
177+
}
178+
Some(symlinks)
179+
}
180+
181+
#[cfg(windows)]
182+
fn find_symlinks(executable: &PathBuf) -> Option<Vec<PathBuf>> {
183+
// In windows we will need to spawn the Python exe and then get the exes.
184+
// Lets wait and see if this is necessary.
185+
None
186+
}

0 commit comments

Comments
 (0)