Skip to content

Commit

Permalink
Fixes for known path search
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne committed May 27, 2024
1 parent 3ddcdb7 commit 26a7ac1
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 43 deletions.
60 changes: 40 additions & 20 deletions native_locator/src/common_python.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

use regex::Regex;

use crate::known::Environment;
use crate::locator::{Locator, LocatorResult};
use crate::messaging::PythonEnvironment;
use crate::utils::{self, PythonEnv};
use crate::utils::{
self, find_python_binary_path, get_version_from_header_files, is_symlinked_python_executable,
PythonEnv,
};
use std::env;
use std::path::{Path, PathBuf};

Expand All @@ -29,32 +34,44 @@ impl PythonOnPath<'_> {

impl Locator for PythonOnPath<'_> {
fn resolve(&self, env: &PythonEnv) -> Option<PythonEnvironment> {
let bin = if cfg!(windows) {
"python.exe"
} else {
"python"
};
if env.executable.file_name().unwrap().to_ascii_lowercase() != bin {
return None;
}
Some(PythonEnvironment {
let exe = &env.executable;
let mut env = PythonEnvironment {
display_name: None,
python_executable_path: Some(env.executable.clone()),
python_executable_path: Some(exe.clone()),
version: env.version.clone(),
category: crate::messaging::PythonEnvironmentCategory::System,
env_path: env.path.clone(),
python_run_command: Some(vec![env.executable.to_str().unwrap().to_string()]),
python_run_command: Some(vec![exe.clone().to_str().unwrap().to_string()]),
..Default::default()
})
};

if let Some(symlink) = is_symlinked_python_executable(&exe) {
env.symlinks = Some(vec![symlink.clone(), exe.clone()]);
// Getting version this way is more accurate than the above regex.
// Sample paths
// /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10
if symlink.starts_with("/Library/Frameworks/Python.framework/Versions") {
let python_regex = Regex::new(r"/Versions/((\d+\.?)*)/bin").unwrap();
if let Some(captures) = python_regex.captures(symlink.to_str().unwrap()) {
let version = captures.get(1).map_or("", |m| m.as_str());
if version.len() > 0 {
env.version = Some(version.to_string());
}
}
// Sample paths
// /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10
if let Some(parent) = symlink.ancestors().nth(2) {
if let Some(version) = get_version_from_header_files(parent) {
env.version = Some(version);
}
}
}
}
Some(env)
}

fn find(&mut self) -> Option<LocatorResult> {
let paths = self.environment.get_env_var("PATH".to_string())?;
let bin = if cfg!(windows) {
"python.exe"
} else {
"python"
};

// Exclude files from this folder, as they would have been discovered elsewhere (widows_store)
// Also the exe is merely a pointer to another file.
Expand All @@ -67,8 +84,11 @@ impl Locator for PythonOnPath<'_> {
let mut environments: Vec<PythonEnvironment> = vec![];
env::split_paths(&paths)
.filter(|p| !p.starts_with(apps_path.clone()))
.map(|p| p.join(bin))
.filter(|p| p.exists())
// Paths like /Library/Frameworks/Python.framework/Versions/3.10/bin can end up in the current PATH variable.
// Hence do not just look for files in a bin directory of the path.
.map(|p| find_python_binary_path(&p))
.filter(Option::is_some)
.map(Option::unwrap)
.for_each(|full_path| {
let version = utils::get_version(&full_path);
let env_path = get_env_path(&full_path);
Expand Down
14 changes: 1 addition & 13 deletions native_locator/src/homebrew.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,11 @@ use crate::{
known::Environment,
locator::{Locator, LocatorResult},
messaging::PythonEnvironment,
utils::PythonEnv,
utils::{is_symlinked_python_executable, PythonEnv},
};
use regex::Regex;
use std::{collections::HashSet, env, path::PathBuf};

fn is_symlinked_python_executable(path: &PathBuf) -> Option<PathBuf> {
let name = path.file_name()?.to_string_lossy();
if !name.starts_with("python") || name.ends_with("-config") || name.ends_with("-build") {
return None;
}
let metadata = std::fs::symlink_metadata(&path).ok()?;
if metadata.is_file() || !metadata.file_type().is_symlink() {
return None;
}
Some(std::fs::canonicalize(path).ok()?)
}

fn get_homebrew_prefix_env_var(environment: &dyn Environment) -> Option<PathBuf> {
if let Some(homebrew_prefix) = environment.get_env_var("HOMEBREW_PREFIX".to_string()) {
let homebrew_prefix_bin = PathBuf::from(homebrew_prefix).join("bin");
Expand Down
68 changes: 58 additions & 10 deletions native_locator/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,34 @@ pub fn get_version(python_executable: &PathBuf) -> Option<String> {
None
}

#[cfg(windows)]
pub fn find_python_binary_path(env_path: &Path) -> Option<PathBuf> {
let python_bin_name = if cfg!(windows) {
"python.exe"
} else {
"python"
};
let path_1 = env_path.join("bin").join(python_bin_name);
let path_2 = env_path.join("Scripts").join(python_bin_name);
let path_3 = env_path.join(python_bin_name);
let paths = vec![path_1, path_2, path_3];
paths.into_iter().find(|path| path.exists())
for path in vec![
env_path.join("Scripts").join("python.exe"),
env_path.join("Scripts").join("python3.exe"),
env_path.join("python.exe"),
env_path.join("python3.exe"),
] {
if path.exists() {
return Some(path);
}
}
None
}

#[cfg(unix)]
pub fn find_python_binary_path(env_path: &Path) -> Option<PathBuf> {
for path in vec![
env_path.join("bin").join("python"),
env_path.join("bin").join("python3"),
env_path.join("python"),
env_path.join("python3"),
] {
if path.exists() {
return Some(path);
}
}
None
}

pub fn list_python_environments(path: &PathBuf) -> Option<Vec<PythonEnv>> {
Expand Down Expand Up @@ -146,3 +163,34 @@ pub fn get_environment_key(env: &PythonEnvironment) -> Option<String> {
pub fn get_environment_manager_key(env: &EnvManager) -> String {
return env.executable_path.to_string_lossy().to_string();
}

pub fn is_symlinked_python_executable(path: &PathBuf) -> Option<PathBuf> {
let name = path.file_name()?.to_string_lossy();
if !name.starts_with("python") || name.ends_with("-config") || name.ends_with("-build") {
return None;
}
let metadata = std::fs::symlink_metadata(&path).ok()?;
if metadata.is_file() || !metadata.file_type().is_symlink() {
return None;
}
Some(std::fs::canonicalize(path).ok()?)
}

// Get the python version from the `Headers/patchlevel.h` file
// The lines we are looking for are:
// /* Version as a string */
// #define PY_VERSION "3.10.2"
// /*--end constants--*/
pub fn get_version_from_header_files(path: &Path) -> Option<String> {
let version_regex = Regex::new(r#"#define\s+PY_VERSION\s+"((\d+\.?)*)"#).unwrap();
let patchlevel_h = path.join("Headers").join("patchlevel.h");
let contents = fs::read_to_string(&patchlevel_h).ok()?;
for line in contents.lines() {
if let Some(captures) = version_regex.captures(line) {
if let Some(value) = captures.get(1) {
return Some(value.as_str().to_string());
}
}
}
None
}

0 comments on commit 26a7ac1

Please sign in to comment.