Skip to content

Commit

Permalink
Move more features to monotrail-utils
Browse files Browse the repository at this point in the history
  • Loading branch information
konstin committed Nov 12, 2023
1 parent 9fa82c3 commit ba9f63b
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 99 deletions.
2 changes: 1 addition & 1 deletion crates/monotrail-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
pub use requirements_txt::RequirementsTxt;

mod requirements_txt;
pub mod parse_cpython_args;
mod requirements_txt;
pub mod standalone_python;
76 changes: 76 additions & 0 deletions crates/monotrail-utils/src/parse_cpython_args.rs
Original file line number Diff line number Diff line change
@@ -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.
///
Expand Down Expand Up @@ -39,3 +43,75 @@ pub fn naive_python_arg_parser<T: AsRef<str>>(args: &[T]) -> Result<Option<Strin
}
}
}

/// 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<String>, 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::<u8>()
.with_context(|| format!("Could not parse value of version_major: {}", major))?;
let minor = minor
.parse::<u8>()
.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<String>, (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))
}
13 changes: 8 additions & 5 deletions crates/monotrail-utils/src/standalone_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")?;
Expand Down Expand Up @@ -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<PathBuf>) -> (ServerGuard, Mock) {
use fs_err::File;
Expand Down
3 changes: 2 additions & 1 deletion crates/monotrail/src/cli.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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;
Expand Down
79 changes: 4 additions & 75 deletions crates/monotrail/src/inject_and_run.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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<String>, 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::<u8>()
.with_context(|| format!("Could not parse value of version_major: {}", major))?;
let minor = minor
.parse::<u8>()
.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(
Expand All @@ -314,7 +284,8 @@ pub fn run_python_args(
root: Option<&Path>,
extras: &[String],
) -> anyhow::Result<i32> {
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 {
Expand Down Expand Up @@ -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<String>, (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<Path>, link: impl AsRef<Path>) -> anyhow::Result<()> {
#[cfg(unix)]
Expand Down
3 changes: 2 additions & 1 deletion crates/monotrail/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion crates/monotrail/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
22 changes: 13 additions & 9 deletions crates/monotrail/src/monotrail.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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 {
Expand Down Expand Up @@ -672,7 +672,8 @@ pub fn run_command(
command: &str,
args: &[String],
) -> anyhow::Result<i32> {
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)?;
Expand Down Expand Up @@ -807,8 +808,11 @@ pub fn cli_from_git(
args: &[String],
) -> anyhow::Result<Option<i32>> {
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) =
Expand Down
10 changes: 6 additions & 4 deletions crates/monotrail/src/poetry_integration/run.rs
Original file line number Diff line number Diff line change
@@ -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<i32> {
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");
Expand Down
5 changes: 3 additions & 2 deletions crates/monotrail/src/ppipx.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down

0 comments on commit ba9f63b

Please sign in to comment.