diff --git a/crates/monotrail-utils/src/lib.rs b/crates/monotrail-utils/src/lib.rs index 30c23df..5f1088e 100644 --- a/crates/monotrail-utils/src/lib.rs +++ b/crates/monotrail-utils/src/lib.rs @@ -2,6 +2,6 @@ pub use requirements_txt::RequirementsTxt; -mod requirements_txt; pub mod parse_cpython_args; +mod requirements_txt; pub mod standalone_python; diff --git a/crates/monotrail-utils/src/parse_cpython_args.rs b/crates/monotrail-utils/src/parse_cpython_args.rs index 080d044..636c2ae 100644 --- a/crates/monotrail-utils/src/parse_cpython_args.rs +++ b/crates/monotrail-utils/src/parse_cpython_args.rs @@ -1,3 +1,7 @@ +use anyhow::{bail, Context}; +use std::env; +use tracing::trace; + /// python has idiosyncratic cli options that are hard to replicate with clap, so we roll our own. /// Takes args without the first-is-current-program (i.e. python) convention. /// @@ -39,3 +43,75 @@ pub fn naive_python_arg_parser>(args: &[T]) -> Result anyhow::Result<(Vec, Option<(u8, u8)>)> { + if let Some(first_arg) = python_args.get(0) { + if first_arg.starts_with('+') { + let python_version = parse_major_minor(first_arg)?; + return Ok((python_args[1..].to_vec(), Some(python_version))); + } + } + Ok((python_args.to_vec(), None)) +} + +/// Parses "3.8" to (3, 8) +pub fn parse_major_minor(version: &str) -> anyhow::Result<(u8, u8)> { + let python_version = + if let Some((major, minor)) = version.trim_start_matches('+').split_once('.') { + let major = major + .parse::() + .with_context(|| format!("Could not parse value of version_major: {}", major))?; + let minor = minor + .parse::() + .with_context(|| format!("Could not parse value of version_minor: {}", minor))?; + (major, minor) + } else { + bail!("Expect +x.y as first argument (missing dot)"); + }; + Ok(python_version) +} + +/// There are three possible sources of a python version: +/// - explicitly as cli argument +/// - as +x.y in the python args +/// - through MONOTRAIL_PYTHON_VERSION, as forwarding through calling our python hook (TODO: give +/// version info to the python hook, maybe with /usr/bin/env, but i don't know how) +/// We ensure that only one is set a time +pub fn determine_python_version( + python_args: &[String], + python_version: Option<&str>, + default_python_version: (u8, u8), +) -> anyhow::Result<(Vec, (u8, u8))> { + let (args, python_version_plus) = parse_plus_arg(python_args)?; + let python_version_arg = python_version.map(parse_major_minor).transpose()?; + let env_var = format!("{}_PYTHON_VERSION", env!("CARGO_PKG_NAME").to_uppercase()); + let python_version_env = env::var_os(&env_var) + .map(|x| parse_major_minor(x.to_string_lossy().as_ref())) + .transpose() + .with_context(|| format!("Couldn't parse {}", env_var))?; + trace!( + "python versions: as argument: {:?}, with plus: {:?}, with {}: {:?}", + python_version_plus, + python_version_arg, + env_var, + python_version_env + ); + let python_version = match (python_version_plus, python_version_arg, python_version_env) { + (None, None, None) => default_python_version, + (Some(python_version_plus), None, None) => python_version_plus, + (None, Some(python_version_arg), None) => python_version_arg, + (None, None, Some(python_version_env)) => python_version_env, + (python_version_plus, python_version_arg, python_version_env) => { + bail!( + "Conflicting python versions: as argument {:?}, with plus: {:?}, with {}: {:?}", + python_version_plus, + python_version_arg, + env_var, + python_version_env + ); + } + }; + Ok((args, python_version)) +} diff --git a/crates/monotrail-utils/src/standalone_python.rs b/crates/monotrail-utils/src/standalone_python.rs index 804fc02..db601a2 100644 --- a/crates/monotrail-utils/src/standalone_python.rs +++ b/crates/monotrail-utils/src/standalone_python.rs @@ -189,7 +189,11 @@ fn provision_python_inner( Ok(()) } -pub fn provision_python(python_version: (u8, u8), cache_dir: &Path) -> anyhow::Result<(PathBuf, PathBuf)> { +/// Returns `(python_binary, python_home)` +pub fn provision_python( + python_version: (u8, u8), + cache_dir: &Path, +) -> anyhow::Result<(PathBuf, PathBuf)> { let python_parent_dir = cache_dir.join("python-build-standalone"); // We need this here for the locking logic fs::create_dir_all(&python_parent_dir).context("Failed to create cache dir")?; @@ -286,15 +290,14 @@ pub fn filename_regex(major: u8, minor: u8) -> Regex { .unwrap() } - #[cfg(test)] mod test { - use std::path::PathBuf; - use tempfile::tempdir; - use mockito::{Mock, ServerGuard}; #[cfg(all(target_os = "linux", target_arch = "x86_64"))] use crate::standalone_python::provision_python; use crate::standalone_python::{find_python, PYTHON_STANDALONE_LATEST_RELEASE}; + use mockito::{Mock, ServerGuard}; + use std::path::PathBuf; + use tempfile::tempdir; pub fn zstd_json_mock(url: &str, fixture: impl Into) -> (ServerGuard, Mock) { use fs_err::File; diff --git a/crates/monotrail/src/cli.rs b/crates/monotrail/src/cli.rs index ebed359..bb85e66 100644 --- a/crates/monotrail/src/cli.rs +++ b/crates/monotrail/src/cli.rs @@ -1,4 +1,4 @@ -use crate::inject_and_run::{parse_plus_arg, run_python_args}; +use crate::inject_and_run::run_python_args; use crate::install::{filter_installed, install_all}; use crate::markers::marker_environment_from_python; use crate::monotrail::{cli_from_git, monotrail_root, run_command}; @@ -13,6 +13,7 @@ use crate::verify_installation::verify_installation; use anyhow::{bail, Context}; use clap::Parser; use install_wheel_rs::{CompatibleTags, Error, InstallLocation}; +use monotrail_utils::parse_cpython_args::parse_plus_arg; use monotrail_utils::RequirementsTxt; use pep440_rs::Operator; use pep508_rs::VersionOrUrl; diff --git a/crates/monotrail/src/inject_and_run.rs b/crates/monotrail/src/inject_and_run.rs index 9aef3ae..19b02ed 100644 --- a/crates/monotrail/src/inject_and_run.rs +++ b/crates/monotrail/src/inject_and_run.rs @@ -1,14 +1,14 @@ //! Communication with libpython -use monotrail_utils::parse_cpython_args::naive_python_arg_parser; -use crate::monotrail::{find_scripts, install, load_specs, FinderData, InjectData, PythonContext}; use crate::monotrail::provision_python_env; +use crate::monotrail::{find_scripts, install, load_specs, FinderData, InjectData, PythonContext}; use crate::DEFAULT_PYTHON_VERSION; use anyhow::{bail, format_err, Context}; use fs_err as fs; use install_wheel_rs::{get_script_launcher, Script, SHEBANG_PYTHON}; use libc::{c_int, c_void, wchar_t}; use libloading::Library; +use monotrail_utils::parse_cpython_args::{determine_python_version, naive_python_arg_parser}; use std::collections::BTreeMap; use std::env; use std::env::current_exe; @@ -98,7 +98,6 @@ unsafe fn pre_init(lib: &Library) -> anyhow::Result<()> { Ok(()) } - /// The way we're using to load symbol by symbol with the type generic is really ugly and cumbersome /// If you know how to do this with `extern` or even pyo3-ffi directly please tell me. /// @@ -277,35 +276,6 @@ pub fn inject_and_run_python( } } -/// Allows linking monotrail as python and then doing `python +3.10 -m say.hello` -#[allow(clippy::type_complexity)] -pub fn parse_plus_arg(python_args: &[String]) -> anyhow::Result<(Vec, Option<(u8, u8)>)> { - if let Some(first_arg) = python_args.get(0) { - if first_arg.starts_with('+') { - let python_version = parse_major_minor(first_arg)?; - return Ok((python_args[1..].to_vec(), Some(python_version))); - } - } - Ok((python_args.to_vec(), None)) -} - -/// Parses "3.8" to (3, 8) -pub fn parse_major_minor(version: &str) -> anyhow::Result<(u8, u8)> { - let python_version = - if let Some((major, minor)) = version.trim_start_matches('+').split_once('.') { - let major = major - .parse::() - .with_context(|| format!("Could not parse value of version_major: {}", major))?; - let minor = minor - .parse::() - .with_context(|| format!("Could not parse value of version_minor: {}", minor))?; - (major, minor) - } else { - bail!("Expect +x.y as first argument (missing dot)"); - }; - Ok(python_version) -} - /// `monotrail run python` implementation. Injects the dependencies and runs the python interpreter /// with the specified arguments. pub fn run_python_args( @@ -314,7 +284,8 @@ pub fn run_python_args( root: Option<&Path>, extras: &[String], ) -> anyhow::Result { - let (args, python_version) = determine_python_version(args, python_version)?; + let (args, python_version) = + determine_python_version(args, python_version, DEFAULT_PYTHON_VERSION)?; let (python_context, python_home) = provision_python_env(python_version)?; let script = if let Some(root) = root { @@ -379,48 +350,6 @@ pub fn run_python_args_finder_data( Ok(exit_code) } -/// There are three possible sources of a python version: -/// - explicitly as cli argument -/// - as +x.y in the python args -/// - through MONOTRAIL_PYTHON_VERSION, as forwarding through calling our python hook (TODO: give -/// version info to the python hook, maybe with /usr/bin/env, but i don't know how) -/// We ensure that only one is set a time -pub fn determine_python_version( - python_args: &[String], - python_version: Option<&str>, -) -> anyhow::Result<(Vec, (u8, u8))> { - let (args, python_version_plus) = parse_plus_arg(&python_args)?; - let python_version_arg = python_version.map(parse_major_minor).transpose()?; - let env_var = format!("{}_PYTHON_VERSION", env!("CARGO_PKG_NAME").to_uppercase()); - let python_version_env = env::var_os(&env_var) - .map(|x| parse_major_minor(x.to_string_lossy().as_ref())) - .transpose() - .with_context(|| format!("Couldn't parse {}", env_var))?; - trace!( - "python versions: as argument: {:?}, with plus: {:?}, with {}: {:?}", - python_version_plus, - python_version_arg, - env_var, - python_version_env - ); - let python_version = match (python_version_plus, python_version_arg, python_version_env) { - (None, None, None) => DEFAULT_PYTHON_VERSION, - (Some(python_version_plus), None, None) => python_version_plus, - (None, Some(python_version_arg), None) => python_version_arg, - (None, None, Some(python_version_env)) => python_version_env, - (python_version_plus, python_version_arg, python_version_env) => { - bail!( - "Conflicting python versions: as argument {:?}, with plus: {:?}, with {}: {:?}", - python_version_plus, - python_version_arg, - env_var, - python_version_env - ); - } - }; - Ok((args, python_version)) -} - /// On unix, we can just symlink to the binary, on windows we need to use a batch file as redirect fn launcher_indirection(original: impl AsRef, link: impl AsRef) -> anyhow::Result<()> { #[cfg(unix)] diff --git a/crates/monotrail/src/lib.rs b/crates/monotrail/src/lib.rs index cbc7316..d3b15fe 100644 --- a/crates/monotrail/src/lib.rs +++ b/crates/monotrail/src/lib.rs @@ -16,7 +16,8 @@ //! installations. pub use cli::{run_cli, Cli}; -pub use inject_and_run::{parse_major_minor, run_python_args}; +pub use inject_and_run::run_python_args; +pub use monotrail_utils::parse_cpython_args::parse_major_minor; use poetry_integration::read_dependencies::read_poetry_specs; #[doc(hidden)] pub use utils::assert_cli_error; diff --git a/crates/monotrail/src/main.rs b/crates/monotrail/src/main.rs index 83d01d0..0b04af4 100644 --- a/crates/monotrail/src/main.rs +++ b/crates/monotrail/src/main.rs @@ -2,7 +2,8 @@ use anyhow::Context; use clap::Parser; -use monotrail::{parse_major_minor, run_cli, run_python_args, Cli}; +use monotrail::{run_cli, run_python_args, Cli}; +use monotrail_utils::parse_cpython_args::parse_major_minor; use std::env; use std::env::args; use std::path::{Path, PathBuf}; diff --git a/crates/monotrail/src/monotrail.rs b/crates/monotrail/src/monotrail.rs index b1506c7..9b84696 100644 --- a/crates/monotrail/src/monotrail.rs +++ b/crates/monotrail/src/monotrail.rs @@ -1,20 +1,23 @@ use crate::inject_and_run::{ - determine_python_version, inject_and_run_python, prepare_execve_environment, - run_python_args_finder_data, + inject_and_run_python, prepare_execve_environment, run_python_args_finder_data, }; use crate::install::{install_all, InstalledPackage}; +use crate::markers::marker_environment_from_python; use crate::poetry_integration::lock::poetry_resolve; use crate::poetry_integration::read_dependencies::{ poetry_spec_from_dir, read_requirements_for_poetry, specs_from_git, }; -use crate::read_poetry_specs; use crate::spec::RequestedSpec; use crate::utils::{cache_dir, get_dir_content}; +use crate::{read_poetry_specs, DEFAULT_PYTHON_VERSION}; use anyhow::{bail, Context}; use fs_err as fs; use fs_err::{DirEntry, File}; use install_wheel_rs::{CompatibleTags, InstallLocation, Script, SHEBANG_PYTHON}; +use monotrail_utils::parse_cpython_args::determine_python_version; +use monotrail_utils::standalone_python::provision_python; use pep508_rs::MarkerEnvironment; +use serde::Serialize; use std::collections::{BTreeMap, HashMap}; use std::env::{current_dir, current_exe}; #[cfg(unix)] @@ -25,9 +28,6 @@ use std::process::Command; use std::{env, io}; use tempfile::TempDir; use tracing::{debug, info, trace, warn}; -use monotrail_utils::standalone_python::provision_python; -use crate::markers::marker_environment_from_python; -use serde::Serialize; #[derive(Debug, Clone, Copy, Eq, PartialEq)] enum LockfileType { @@ -672,7 +672,8 @@ pub fn run_command( command: &str, args: &[String], ) -> anyhow::Result { - let (args, python_version) = determine_python_version(args, python_version)?; + let (args, python_version) = + determine_python_version(args, python_version, DEFAULT_PYTHON_VERSION)?; let (python_context, python_home) = provision_python_env(python_version)?; let (specs, root_scripts, lockfile, root) = load_specs(root, extras, &python_context)?; let finder_data = install(&specs, root_scripts, lockfile, Some(root), &python_context)?; @@ -807,8 +808,11 @@ pub fn cli_from_git( args: &[String], ) -> anyhow::Result> { let trail_args = args[1..].to_vec(); - let (trail_args, python_version) = - determine_python_version(&trail_args, python_version.as_deref())?; + let (trail_args, python_version) = determine_python_version( + &trail_args, + python_version.as_deref(), + DEFAULT_PYTHON_VERSION, + )?; let (python_context, python_home) = provision_python_env(python_version)?; let (specs, repo_dir, lockfile) = diff --git a/crates/monotrail/src/poetry_integration/run.rs b/crates/monotrail/src/poetry_integration/run.rs index e53656e..5f97be8 100644 --- a/crates/monotrail/src/poetry_integration/run.rs +++ b/crates/monotrail/src/poetry_integration/run.rs @@ -1,19 +1,21 @@ //! Runs poetry after installing it from a bundle lockfile -use crate::inject_and_run::{determine_python_version, inject_and_run_python}; +use crate::inject_and_run::inject_and_run_python; use crate::monotrail::install; +use crate::monotrail::provision_python_env; use crate::poetry_integration::poetry_lock::PoetryLock; use crate::poetry_integration::poetry_toml::PoetryPyprojectToml; -use crate::read_poetry_specs; -use crate::monotrail::provision_python_env; +use crate::{read_poetry_specs, DEFAULT_PYTHON_VERSION}; use anyhow::Context; +use monotrail_utils::parse_cpython_args::determine_python_version; use std::collections::BTreeMap; use std::path::PathBuf; /// Use the libpython.so to run a poetry command on python 3.8, unless you give +x.y as first /// argument pub fn poetry_run(args: &[String], python_version: Option<&str>) -> anyhow::Result { - let (args, python_version) = determine_python_version(&args, python_version)?; + let (args, python_version) = + determine_python_version(&args, python_version, DEFAULT_PYTHON_VERSION)?; let (python_context, python_home) = provision_python_env(python_version)?; let pyproject_toml = include_str!("../../../../resources/poetry_boostrap_lock/pyproject.toml"); diff --git a/crates/monotrail/src/ppipx.rs b/crates/monotrail/src/ppipx.rs index bb97097..57e4bf5 100644 --- a/crates/monotrail/src/ppipx.rs +++ b/crates/monotrail/src/ppipx.rs @@ -1,13 +1,14 @@ +use crate::monotrail::provision_python_env; use crate::monotrail::{install, run_command_finder_data, PythonContext}; use crate::poetry_integration::lock::poetry_resolve_from_dir; use crate::poetry_integration::poetry_toml; use crate::poetry_integration::poetry_toml::PoetryPyprojectToml; use crate::poetry_integration::read_dependencies::read_toml_files; -use crate::monotrail::provision_python_env; use crate::utils::data_local_dir; -use crate::{parse_major_minor, read_poetry_specs, DEFAULT_PYTHON_VERSION}; +use crate::{read_poetry_specs, DEFAULT_PYTHON_VERSION}; use anyhow::Context; use fs_err as fs; +use monotrail_utils::parse_cpython_args::parse_major_minor; use std::collections::BTreeMap; use std::path::PathBuf; use tempfile::TempDir;