Skip to content

Commit

Permalink
feat: env markers (#37)
Browse files Browse the repository at this point in the history
Adds support for detecting and evaluating environment markers.
Environment markers can be extracted from a python executable by running
a simple python script.

Fixes #34 as the proper python markers are now parsed and evaluated:

```
>rip  "scipy>=1.4.0"
2023-10-02T13:34:12.758759Z  INFO rip: extracted the following environment markers from the system python interpreter:
Pep508EnvMakers {
    os_name: "nt",
    sys_platform: "win32",
    platform_machine: "AMD64",
    platform_python_implementation: "CPython",
    platform_release: "10",
    platform_system: "Windows",
    platform_version: "10.0.22621",
    python_version: "3.8",
    python_full_version: "3.8.17",
    implementation_name: "cpython",
    implementation_version: "3.8.17",
}
2023-10-02T13:34:12.759610Z  INFO rattler_installs_packages::resolve: collecting scipy
2023-10-02T13:34:12.759831Z  INFO rattler_installs_packages::http: executing request url=https://pypi.org/simple/scipy/ cache_mode=Default
2023-10-02T13:34:12.830482Z  WARN rattler_installs_packages::resolve: Not considering scipy 1.11.0rc2, 1.11.0rc1, 1.10.0rc2, 1.10.0rc1, 1.9.0rc3, 1.9.0rc2, 1.9.0rc1, 1.8.0rc4, 1.8.0rc3, 1.8.0rc2, 1.8.0rc1, 0.8.0 because there are no wheel artifacts available
2023-10-02T13:34:12.830699Z  WARN rattler_installs_packages::resolve: Not considering scipy 1.11.3, 1.11.2, 1.11.1, 1.11.0 because none of the artifacts are compatible with Python 3.8.17
2023-10-02T13:34:12.831023Z  INFO rattler_installs_packages::resolve: obtaining dependency information from scipy=1.10.1
2023-10-02T13:34:12.831685Z  INFO rattler_installs_packages::resolve: collecting numpy
2023-10-02T13:34:12.831821Z  INFO rattler_installs_packages::http: executing request url=https://pypi.org/simple/numpy/ cache_mode=Default
2023-10-02T13:34:12.874279Z  WARN rattler_installs_packages::resolve: Not considering numpy 1.26.0rc1, 1.26.0b1, 1.25.0rc1, 1.24.0rc2, 1.24.0rc1, 1.23.0rc3, 1.23.0rc2, 1.23.0rc1, 1.10.0.post2, 1.5.0, 1.4.1, 1.3.0 because there are no wheel artifacts available
2023-10-02T13:34:12.874502Z  WARN rattler_installs_packages::resolve: Not considering numpy 1.26.0, 1.25.2, 1.25.1, 1.25.0 because none of the artifacts are compatible with Python 3.8.17
2023-10-02T13:34:12.874934Z  INFO rattler_installs_packages::resolve: collecting scipy[test]
2023-10-02T13:34:12.876837Z  INFO rattler_installs_packages::resolve: collecting scipy[dev]
2023-10-02T13:34:12.878637Z  INFO rattler_installs_packages::resolve: collecting scipy[doc]
2023-10-02T13:34:12.880546Z  INFO rattler_installs_packages::resolve: obtaining dependency information from numpy=1.24.4
Resolved environment:
- scipy >= 1.4.0

Name   Version
numpy  1.24.4
scipy  1.10.1
```

@notatallshaw You might be interested in this! :)

Fix #2
  • Loading branch information
baszalmstra authored Oct 2, 2023
1 parent a7ad26c commit ab1e94b
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 38 deletions.
26 changes: 26 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/rattler_installs_packages/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ data-encoding = "2.4.0"
elsa = "1.9.0"
fs4 = "0.6.6"
futures = "0.3.28"
html-escape = "0.2.13"
http = "0.2.9"
http-cache-semantics = { version = "1.0.1", default-features = false, features = ["with_serde", "reqwest"] }
indexmap = "2.0.1"
Expand All @@ -45,7 +46,7 @@ smallvec = { version = "1.11.1", features = ["const_generics", "const_new"] }
tempfile = "3.8.0"
thiserror = "1.0.49"
tl = "0.7.7"
tokio = { version = "1.32.0" }
tokio = { version = "1.32.0", features = ["process"] }
tokio-util = { version = "0.7.9", features = ["compat"] }
tracing = { version = "0.1.37", default-features = false, features = ["attributes"] }
url = { version = "2.4.1", features = ["serde"] }
Expand Down
123 changes: 123 additions & 0 deletions crates/rattler_installs_packages/src/env_markers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use crate::marker::Env;
use serde::{Deserialize, Serialize};
use std::io;
use std::io::ErrorKind;
use std::path::Path;
use std::process::ExitStatus;
use thiserror::Error;

/// Describes the environment markers that can be used in dependency specifications to enable or
/// disable certain dependencies based on runtime environment.
///
/// Exactly the markers defined in this struct must be present during version resolution. Unknown
/// variables should raise an error.
///
/// Note that the "extra" variable is not defined in this struct because it depends on the wheel
/// that is being inspected.
///
/// The behavior and the names of the markers are described in PEP 508.
#[derive(Default, Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct Pep508EnvMakers {
pub os_name: String,
pub sys_platform: String,
pub platform_machine: String,
pub platform_python_implementation: String,
pub platform_release: String,
pub platform_system: String,
pub platform_version: String,
pub python_version: String,
pub python_full_version: String,
pub implementation_name: String,
pub implementation_version: String,
}

#[derive(Debug, Error)]
pub enum FromPythonError {
#[error("could not find python executable")]
CouldNotFindPythonExecutable,

#[error(transparent)]
FailedToExecute(#[from] io::Error),

#[error(transparent)]
FailedToParse(#[from] serde_json::Error),

#[error("execution failed with exit code {0}")]
FailedToRun(ExitStatus),
}

impl Pep508EnvMakers {
/// Try to determine the environment markers by executing python.
pub async fn from_env() -> Result<Self, FromPythonError> {
Self::from_python(Path::new("python")).await
}

/// Try to determine the environment markers from an existing python executable. The executable
/// is used to run a simple python program to extract the information.
pub async fn from_python(python: &Path) -> Result<Self, FromPythonError> {
let pep508_bytes = include_str!("pep508.py");

// Execute the python executable
let output = match tokio::process::Command::new(python)
.arg("-c")
.arg(pep508_bytes)
.output()
.await
{
Err(e) if e.kind() == ErrorKind::NotFound => {
return Err(FromPythonError::CouldNotFindPythonExecutable)
}
Err(e) => return Err(FromPythonError::FailedToExecute(e)),
Ok(output) => output,
};

// Ensure that we have a valid success code
if !output.status.success() {
return Err(FromPythonError::FailedToRun(output.status));
}

// Convert the JSON
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(serde_json::from_str(stdout.trim())?)
}
}

impl Env for Pep508EnvMakers {
fn get_marker_var(&self, var: &str) -> Option<&str> {
match var {
"os_name" => Some(&self.os_name),
"sys_platform" => Some(&self.sys_platform),
"platform_machine" => Some(&self.platform_machine),
"platform_python_implementation" => Some(&self.platform_python_implementation),
"platform_release" => Some(&self.platform_release),
"platform_system" => Some(&self.platform_system),
"platform_version" => Some(&self.platform_version),
"python_version" => Some(&self.python_version),
"python_full_version" => Some(&self.python_full_version),
"implementation_name" => Some(&self.implementation_name),
"implementation_version" => Some(&self.implementation_version),
_ => None,
}
}
}

#[cfg(test)]
mod test {
use super::*;

#[tokio::test]
pub async fn test_from_env() {
match Pep508EnvMakers::from_env().await {
Err(FromPythonError::CouldNotFindPythonExecutable) => {
// This is fine, the test machine does not include a python binary.
}
Err(e) => panic!("{e}"),
Ok(env) => {
println!(
"Found the following environment markers on the current system:\n\n{env:#?}"
)
}
}
}
}
2 changes: 1 addition & 1 deletion crates/rattler_installs_packages/src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fn into_artifact_info(base: &Url, tag: &HTMLTag) -> Option<ArtifactInfo> {
let requires_python = attributes
.get("data-requires-python")
.flatten()
.map(|a| a.as_utf8_str().to_string());
.map(|a| html_escape::decode_html_entities(&a.as_utf8_str()).into_owned());

let metadata_attr = attributes
.get("data-dist-info-metadata")
Expand Down
2 changes: 2 additions & 0 deletions crates/rattler_installs_packages/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod seek_slice;
mod specifier;
mod utils;

mod env_markers;
#[cfg(feature = "resolvo")]
mod resolve;

Expand All @@ -35,6 +36,7 @@ pub use artifact_name::{
ArtifactName, BuildTag, InnerAsArtifactName, ParseArtifactNameError, SDistFormat, SDistName,
WheelName,
};
pub use env_markers::Pep508EnvMakers;
pub use extra::Extra;
pub use package_name::{NormalizedPackageName, PackageName, ParsePackageNameError};
pub use pep440::Version;
Expand Down
40 changes: 40 additions & 0 deletions crates/rattler_installs_packages/src/pep508.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# A program that outputs PEP 508 environment markers in a JSON format. Most of the
# implementation has been taken from the example in the PEP.
#
# See: https://peps.python.org/pep-0508/

import os
import sys
import platform
import json


def format_full_version(info):
version = '{0.major}.{0.minor}.{0.micro}'.format(info)
kind = info.releaselevel
if kind != 'final':
version += kind[0] + str(info.serial)
return version


if hasattr(sys, 'implementation'):
implementation_version = format_full_version(sys.implementation.version)
implementation_name = sys.implementation.name
else:
implementation_version = '0'
implementation_name = ''
bindings = {
'implementation_name': implementation_name,
'implementation_version': implementation_version,
'os_name': os.name,
'platform_machine': platform.machine(),
'platform_python_implementation': platform.python_implementation(),
'platform_release': platform.release(),
'platform_system': platform.system(),
'platform_version': platform.version(),
'python_full_version': platform.python_version(),
'python_version': '.'.join(platform.python_version_tuple()[:2]),
'sys_platform': sys.platform,
}

json.dump(bindings, sys.stdout)
6 changes: 6 additions & 0 deletions crates/rattler_installs_packages/src/requirement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ pub mod marker {
fn get_marker_var(&self, var: &str) -> Option<&str>;
}

impl<E: Env> Env for &E {
fn get_marker_var(&self, var: &str) -> Option<&str> {
(*self).get_marker_var(var)
}
}

impl<T: Borrow<str> + Eq + Hash> Env for HashMap<T, T> {
fn get_marker_var(&self, var: &str) -> Option<&str> {
self.get(var).map(|s| s.borrow())
Expand Down
Loading

0 comments on commit ab1e94b

Please sign in to comment.