Skip to content

Commit 8dc1aa5

Browse files
authored
helper function for resolving the site package path across platforms (#23)
* helper function for resolving the site package path across platforms * work in progress * refactored initialization.rs, consolidated to BridgeError. * platform specific import * modifed package_resolver::find_package_path to not panic * added platform specific str for r2x executable to fix test. cfg(not(windows))->cfg(unix) * ensure python executable is not equivalent before symlink * fixed missing space in bash conditional * added another file check before symlink * added 'allow-dirty' for dist init in cargo.toml. * changed allow-dirty=['ci'] to true. in dist-workspace.toml, revert cargo.toml * throwing darts blindfolded at this point. * added allow-dirty to [dist] section of dist-workspace.toml
1 parent 3201678 commit 8dc1aa5

8 files changed

Lines changed: 139 additions & 60 deletions

File tree

.github/workflows/release.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,22 @@ jobs:
6363
- name: Setup Python 3.12
6464
uses: actions/setup-python@v5
6565
with:
66-
python-version: '3.12'
66+
python-version: "3.12"
6767
- name: Ensure python3.12 alias (unix)
6868
if: runner.os != 'Windows'
6969
shell: bash
70-
run: ln -sf "$pythonLocation/bin/python" "$pythonLocation/bin/python3.12"
70+
# Check that files are not equivalent before linking.
71+
run: |
72+
if ! [ "$pythonLocation/bin/python" -ef "$pythonLocation/bin/python3.12" ]; then
73+
ln -sf "$pythonLocation/bin/python" "$pythonLocation/bin/python3.12"
74+
fi
7175
- name: Ensure python3.12 alias (windows)
7276
if: runner.os == 'Windows'
7377
shell: pwsh
74-
run: Copy-Item "$env:pythonLocation\\python.exe" "$env:pythonLocation\\python3.12.exe" -Force
78+
run: |
79+
if (!(Test-Path "$env:pythonLocation\\python3.12.exe") -or ((Get-Item "$env:pythonLocation\\python.exe").FullName -ne (Get-Item "$env:pythonLocation\\python3.12.exe").FullName)) {
80+
Copy-Item "$env:pythonLocation\\python.exe" "$env:pythonLocation\\python3.12.exe" -Force
81+
}
7582
- name: Install dist
7683
# we specify bash to get pipefail; it guards against the `curl` command
7784
# failing. otherwise `sh` won't catch that `curl` returned non-0
@@ -194,10 +201,13 @@ jobs:
194201
- name: Setup Python 3.12
195202
uses: actions/setup-python@v5
196203
with:
197-
python-version: '3.12'
204+
python-version: "3.12"
198205
- name: Ensure python3.12 alias (unix)
199206
shell: bash
200-
run: ln -sf "$pythonLocation/bin/python" "$pythonLocation/bin/python3.12"
207+
run: |
208+
if ! [ "$pythonLocation/bin/python" -ef "$pythonLocation/bin/python3.12" ]; then
209+
ln -sf "$pythonLocation/bin/python" "$pythonLocation/bin/python3.12"
210+
fi
201211
- name: Install cached dist
202212
uses: actions/download-artifact@v4
203213
with:

crates/r2x-cli/src/plugins/package_resolver.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
//! Handles locating installed packages in virtual environments,
44
//! including support for UV editable installs via .pth files.
55
6+
use r2x_python::resolve_site_package_path;
67
use std::path::PathBuf;
7-
88
/// Find the path to an installed package
99
pub fn find_package_path(package_name_full: &str) -> Result<PathBuf, String> {
1010
let config = crate::config_manager::Config::load()
@@ -19,15 +19,10 @@ pub fn find_package_path(package_name_full: &str) -> Result<PathBuf, String> {
1919

2020
// Fallback: search in site-packages (for normally installed packages)
2121
let venv_path = PathBuf::from(config.get_venv_path());
22-
let lib_dir = venv_path.join("lib");
23-
24-
let python_version_dir = std::fs::read_dir(&lib_dir)
25-
.map_err(|e| format!("Failed to read lib directory: {}", e))?
26-
.filter_map(|e| e.ok())
27-
.find(|e| e.file_name().to_string_lossy().starts_with("python"))
28-
.ok_or_else(|| "No python directory found in venv".to_string())?;
2922

30-
let site_packages = python_version_dir.path().join("site-packages");
23+
// FIXME, properly handle error propagation.
24+
let site_packages = resolve_site_package_path(&venv_path)
25+
.map_err(|e| format!("failed to resolve path to python packages: {}", e))?;
3126

3227
let package_dir = std::fs::read_dir(&site_packages)
3328
.map_err(|e| format!("Failed to read site-packages: {}", e))?

crates/r2x-cli/tests/integration.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ use assert_cmd::{cargo::cargo_bin_cmd, Command};
44
use predicates::prelude::*;
55
use std::path::PathBuf;
66

7+
#[cfg(unix)]
8+
const EXECUTABLE_NAME: &str = "r2x";
9+
10+
#[cfg(windows)]
11+
const EXECUTABLE_NAME: &str = "r2x.exe";
12+
713
fn fixture_config_path() -> PathBuf {
814
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
915
.join("tests")
@@ -51,7 +57,10 @@ fn test_plugins_help() {
5157
.args(["run", "plugin", "--help"])
5258
.assert()
5359
.success()
54-
.stdout(predicate::str::contains("Usage: r2x run plugin"));
60+
.stdout(predicate::str::contains(format!(
61+
"Usage: {} run plugin",
62+
EXECUTABLE_NAME
63+
)));
5564
}
5665

5766
#[test]

crates/r2x-config/src/lib.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use serde::{Deserialize, Serialize};
22
use std::fs;
33
use std::path::PathBuf;
4-
use std::process::Command;
54
use which::which;
65

6+
#[cfg(unix)]
7+
use std::process::Command;
8+
79
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
810
pub struct Config {
911
#[serde(skip_serializing_if = "Option::is_none")]
@@ -290,11 +292,6 @@ impl Config {
290292

291293
return Err("Failed to locate uv after installation. Verify that ~/.local/bin or ~/.cargo/bin is in your PATH".into());
292294
}
293-
294-
#[cfg(target_os = "windows")]
295-
{
296-
Err("uv is not installed. Please install it from: https://docs.astral.sh/uv/getting-started/installation/".into())
297-
}
298295
}
299296

300297
pub fn ensure_cache_path(&mut self) -> Result<String, Box<dyn std::error::Error>> {

crates/r2x-python/src/initialization.rs

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! This module handles all Python interpreter initialization, virtual environment
44
//! configuration, and environment setup required before the bridge can be used.
55
6-
use super::utils::*;
6+
use super::utils::{resolve_python_path, resolve_site_package_path};
77
use crate::errors::BridgeError;
88
use once_cell::sync::OnceCell;
99
use pyo3::prelude::*;
@@ -69,30 +69,8 @@ impl Bridge {
6969

7070
// Add site-packages from venv to sys.path so imports work as expected
7171
let venv_path = PathBuf::from(config.get_venv_path());
72+
let site_packages = resolve_site_package_path(&venv_path)?;
7273

73-
let lib_dir = venv_path.join(PYTHON_LIB_DIR);
74-
logger::debug(&format!(
75-
"lib_dir: {}, exists: {}",
76-
lib_dir.display(),
77-
lib_dir.exists()
78-
));
79-
if !lib_dir.exists() {
80-
return Err(BridgeError::VenvNotFound(venv_path.to_path_buf()));
81-
}
82-
83-
// Find the python3.X directory inside lib/
84-
use std::fs;
85-
let python_version_dir = fs::read_dir(&lib_dir)
86-
.map_err(|e| {
87-
BridgeError::Initialization(format!("Failed to read lib directory: {}", e))
88-
})?
89-
.filter_map(|e| e.ok())
90-
.find(|e| e.file_name().to_string_lossy().starts_with("python"))
91-
.ok_or_else(|| {
92-
BridgeError::Initialization("No python3.X directory found in venv/lib".to_string())
93-
})?;
94-
95-
let site_packages = python_version_dir.path().join(SITE_PACKAGES);
9674
logger::debug(&format!(
9775
"site_packages: {}, exists: {}",
9876
site_packages.display(),
@@ -317,7 +295,13 @@ pub fn configure_python_venv() -> Result<PathBuf, BridgeError> {
317295

318296
let venv_path = PathBuf::from(config.get_venv_path());
319297

320-
let python_path = venv_path.join(PYTHON_BIN_DIR).join(PYTHON_EXE);
298+
let python_path_result = resolve_python_path(&venv_path);
299+
300+
if python_path_result.is_err() {
301+
logger::debug("Could not resolve Python path");
302+
}
303+
304+
let python_path = python_path_result.unwrap_or_else(|_| PathBuf::new());
321305

322306
// Create venv if it doesn't exist
323307
if !venv_path.exists() || !python_path.exists() {

crates/r2x-python/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ mod utils;
1414

1515
pub use errors::BridgeError;
1616
pub use initialization::{configure_python_venv, Bridge};
17-
pub use utils::{PYTHON_BIN_DIR, PYTHON_EXE, PYTHON_LIB_DIR, SITE_PACKAGES};
17+
pub use utils::{resolve_python_path, resolve_site_package_path};
1818

1919
#[cfg(test)]
2020
mod tests {

crates/r2x-python/src/utils.rs

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,106 @@
33
//! This module provides compile-time constants for directories and files that differ
44
//! between Windows and Unix-like systems in Python virtual environments.
55
6+
use super::errors::BridgeError;
7+
use std::path::PathBuf;
8+
9+
#[cfg(unix)]
10+
use std::fs;
11+
612
/// The name of the library directory in a Python venv (e.g., "Lib" on Windows, "lib" on Unix)
713
#[cfg(windows)]
8-
pub const PYTHON_LIB_DIR: &str = "Lib";
9-
#[cfg(not(windows))]
10-
pub const PYTHON_LIB_DIR: &str = "lib";
14+
const PYTHON_LIB_DIR: &str = "Lib";
15+
#[cfg(unix)]
16+
const PYTHON_LIB_DIR: &str = "lib";
1117

1218
/// The name of the binaries/scripts directory in a Python venv (e.g., "Scripts" on Windows, "bin" on Unix)
1319
#[cfg(windows)]
14-
pub const PYTHON_BIN_DIR: &str = "Scripts";
15-
#[cfg(not(windows))]
16-
pub const PYTHON_BIN_DIR: &str = "bin";
20+
const PYTHON_BIN_DIR: &str = "Scripts";
21+
#[cfg(unix)]
22+
const PYTHON_BIN_DIR: &str = "bin";
1723

1824
/// The name of the Python executable in a venv (e.g., "python.exe" on Windows, "python" on Unix)
1925
#[cfg(windows)]
20-
pub const PYTHON_EXE: &str = "python.exe";
21-
#[cfg(not(windows))]
22-
pub const PYTHON_EXE: &str = "python";
26+
const PYTHON_EXE: &str = "python.exe";
27+
#[cfg(unix)]
28+
const PYTHON_EXE: &str = "python";
29+
30+
// Site Packages differences.
31+
//
32+
// MacOS
33+
// .venv/lib/python {version}/site-packages
34+
//
35+
// Windows
36+
// .venv/Lib/site-packages
37+
38+
pub fn resolve_site_package_path(venv_path: &PathBuf) -> Result<PathBuf, BridgeError> {
39+
// Verify the venv_path exists and is a directory.
40+
if !venv_path.is_dir() {
41+
return Err(BridgeError::VenvNotFound(venv_path.to_path_buf()));
42+
}
43+
44+
#[cfg(windows)]
45+
{
46+
let site_packages = venv_path.join(PYTHON_LIB_DIR).join("site-packages");
47+
48+
// verify site_package_path exists
49+
if !site_packages.is_dir() {
50+
return Err(BridgeError::Initialization(format!(
51+
"unable to locate package directory: {}",
52+
site_packages.display()
53+
)));
54+
}
55+
Ok(site_packages)
56+
}
57+
58+
#[cfg(not(windows))]
59+
{
60+
let lib_dir = venv_path.join(PYTHON_LIB_DIR);
61+
62+
if !lib_dir.is_dir() {
63+
return Err(BridgeError::Initialization(format!(
64+
"unable to locate lib directory: {}",
65+
lib_dir.display()
66+
)));
67+
}
68+
69+
let python_version_dir = fs::read_dir(&lib_dir)
70+
.map_err(|e| {
71+
BridgeError::Initialization(format!("Failed to read lib directory: {}", e))
72+
})?
73+
.filter_map(|e| e.ok())
74+
.find(|e| e.file_name().to_string_lossy().starts_with("python"))
75+
.ok_or_else(|| {
76+
BridgeError::Initialization("No python3.X directory found in venv/lib".to_string())
77+
})?;
78+
79+
let site_packages = python_version_dir.path().join("site-packages");
80+
81+
if !site_packages.is_dir() {
82+
return Err(BridgeError::Initialization(format!(
83+
"unable to locate package directory: {}",
84+
site_packages.display()
85+
)));
86+
}
87+
88+
Ok(site_packages)
89+
}
90+
}
91+
92+
pub fn resolve_python_path(venv_path: &PathBuf) -> Result<PathBuf, BridgeError> {
93+
// validate venv path is a valid directory
94+
if !venv_path.is_dir() {
95+
return Err(BridgeError::VenvNotFound(venv_path.to_path_buf()));
96+
}
97+
98+
let python_path = venv_path.join(PYTHON_BIN_DIR).join(PYTHON_EXE);
99+
// validate the interpreter path is valid
100+
if !python_path.is_file() {
101+
return Err(BridgeError::Initialization(format!(
102+
"Path to python binary is not valid: {}",
103+
python_path.display()
104+
)));
105+
}
23106

24-
/// The subdirectory name for site-packages within the lib directory
25-
pub const SITE_PACKAGES: &str = "site-packages";
107+
return Ok(python_path);
108+
}

dist-workspace.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-da
1515
install-path = "CARGO_HOME"
1616
# Whether to install an updater program
1717
install-updater = false
18-
19-
[workspace.metadata.dist]
18+
# prevents errors for they python parts of the release
2019
allow-dirty = ["ci"]
2120

21+
[workspace.metadata.dist]
22+
allow-dirty = true

0 commit comments

Comments
 (0)