Skip to content

Commit 2d383e0

Browse files
authored
Add native homebrew locator on mac (#23363)
1 parent 33dfe29 commit 2d383e0

File tree

4 files changed

+69
-1
lines changed

4 files changed

+69
-1
lines changed

native_locator/src/homebrew.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use std::{collections::HashSet, fs::DirEntry, io::Error, path::PathBuf};
5+
6+
use crate::{known::Environment, messaging::MessageDispatcher};
7+
use regex::Regex;
8+
9+
fn is_symlinked_python_executable(path: Result<DirEntry, Error>) -> Option<PathBuf> {
10+
let path = path.ok()?.path();
11+
let name = path.file_name()?.to_string_lossy();
12+
if !name.starts_with("python") || name.ends_with("-config") {
13+
return None;
14+
}
15+
let metadata = std::fs::symlink_metadata(&path).ok()?;
16+
if metadata.is_file() || !metadata.file_type().is_symlink() {
17+
return None;
18+
}
19+
Some(std::fs::canonicalize(path).ok()?)
20+
}
21+
22+
pub fn find_and_report(
23+
dispatcher: &mut impl MessageDispatcher,
24+
environment: &impl Environment,
25+
) -> Option<()> {
26+
// https://docs.brew.sh/Homebrew-and-Python#brewed-python-modules
27+
// Executable Python scripts will be in $(brew --prefix)/bin.
28+
// They are always symlinks, hence we will only look for symlinks.
29+
30+
let homebrew_prefix = environment.get_env_var("HOMEBREW_PREFIX".to_string())?;
31+
let homebrew_prefix_bin = PathBuf::from(homebrew_prefix).join("bin");
32+
let mut reported: HashSet<String> = HashSet::new();
33+
for file in std::fs::read_dir(homebrew_prefix_bin).ok()? {
34+
if let Some(exe) = is_symlinked_python_executable(file) {
35+
let python_regex = Regex::new(r"/(\d+\.\d+\.\d+)/").unwrap();
36+
let python_version = exe.to_string_lossy().to_string();
37+
let version = match python_regex.captures(&python_version) {
38+
Some(captures) => match captures.get(1) {
39+
Some(version) => Some(version.as_str().to_string()),
40+
None => None,
41+
},
42+
None => None,
43+
};
44+
if reported.contains(&exe.to_string_lossy().to_string()) {
45+
continue;
46+
}
47+
reported.insert(exe.to_string_lossy().to_string());
48+
49+
let env = crate::messaging::PythonEnvironment::new(
50+
"Python".to_string(),
51+
vec![exe.to_string_lossy().to_string()],
52+
crate::messaging::PythonEnvironmentCategory::Homebrew,
53+
version,
54+
None,
55+
None,
56+
None,
57+
);
58+
dispatcher.report_environment(env);
59+
}
60+
}
61+
62+
None
63+
}

native_locator/src/main.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ use messaging::{create_dispatcher, MessageDispatcher};
88

99
mod common_python;
1010
mod conda;
11+
mod homebrew;
1112
mod known;
1213
mod logging;
1314
mod messaging;
15+
mod pyenv;
1416
mod utils;
1517
mod windows_python;
16-
mod pyenv;
1718

1819
fn main() {
1920
let mut dispatcher = create_dispatcher();
@@ -34,6 +35,8 @@ fn main() {
3435

3536
pyenv::find_and_report(&mut dispatcher, &environment);
3637

38+
homebrew::find_and_report(&mut dispatcher, &environment);
39+
3740
match now.elapsed() {
3841
Ok(elapsed) => {
3942
dispatcher.log_info(&format!(

native_locator/src/messaging.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ impl EnvManagerMessage {
5252
#[serde(rename_all = "camelCase")]
5353
pub enum PythonEnvironmentCategory {
5454
System,
55+
Homebrew,
5556
Conda,
5657
Pyenv,
5758
WindowsStore,

src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function categoryToKind(category: string): PythonEnvKind {
3939
case 'conda':
4040
return PythonEnvKind.Conda;
4141
case 'system':
42+
case 'homebrew':
4243
return PythonEnvKind.System;
4344
case 'pyenv':
4445
return PythonEnvKind.Pyenv;

0 commit comments

Comments
 (0)