mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Copy over install-wheel-rs
crate (#33)
This PR copies over the `install-wheel-rs` crate at commit `10730ea1a84c58af6b35fb74c89ed0578ab042b6` with no modifications. It won't pass CI, but modifications will intentionally be confined to later PRs.
This commit is contained in:
parent
c8477991a9
commit
e824fe6d2b
17 changed files with 3034 additions and 0 deletions
53
crates/install-wheel-rs/Cargo.toml
Normal file
53
crates/install-wheel-rs/Cargo.toml
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
[package]
|
||||||
|
name = "install-wheel-rs"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Takes a wheel and installs it, either in a venv or for monotrail"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
repository = "https://github.com/konstin/poc-monotrail"
|
||||||
|
keywords = ["wheel", "python"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "install_wheel_rs"
|
||||||
|
# https://github.com/PyO3/maturin/issues/1080 :((
|
||||||
|
#crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.4.6", optional = true, features = ["derive", "env"] }
|
||||||
|
configparser = "3.0.2"
|
||||||
|
csv = "1.2.2"
|
||||||
|
data-encoding = "2.4.0"
|
||||||
|
fs-err = { workspace = true }
|
||||||
|
fs2 = { workspace = true }
|
||||||
|
glibc_version = "0.1.2"
|
||||||
|
goblin = "0.7.1"
|
||||||
|
mailparse = "0.14.0"
|
||||||
|
once_cell = "1.18.0"
|
||||||
|
platform-info = "2.0.2"
|
||||||
|
plist = "1.5.0"
|
||||||
|
pyo3 = { workspace = true, features = ["extension-module", "abi3-py37"], optional = true }
|
||||||
|
rayon = { version = "1.8.0", optional = true }
|
||||||
|
regex = { workspace = true }
|
||||||
|
rfc2047-decoder = "1.0.1"
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
sha2 = { workspace = true }
|
||||||
|
target-lexicon = "0.12.11"
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true, optional = true }
|
||||||
|
walkdir = { workspace = true }
|
||||||
|
zip = { version = "0.6.6", default-features = false, features = ["deflate"] } # no default features for zstd
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["cli", "parallel"]
|
||||||
|
python_bindings = ["pyo3", "tracing-subscriber"]
|
||||||
|
cli = ["clap"]
|
||||||
|
parallel = ["rayon"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
indoc = { workspace = true }
|
||||||
|
|
||||||
|
[package.metadata.dist]
|
||||||
|
dist = false
|
14
crates/install-wheel-rs/Readme.md
Normal file
14
crates/install-wheel-rs/Readme.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
Reimplementation of wheel installing in rust. Supports both classical venvs and monotrail.
|
||||||
|
|
||||||
|
There are simple python bindings:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from install_wheel_rs import LockedVenv
|
||||||
|
|
||||||
|
locked_venv = LockedVenv("path/to/.venv")
|
||||||
|
locked_venv.install_wheel("path/to/some_tagged_wheel.whl")
|
||||||
|
```
|
||||||
|
|
||||||
|
and there's only one function: `install_wheels_venv(wheels: List[str], venv: str)`, where `wheels` is a list of paths to wheel files and `venv` is the location of the venv to install the packages in.
|
||||||
|
|
||||||
|
See monotrail for benchmarks.
|
3
crates/install-wheel-rs/install_wheel_rs.pyi
Normal file
3
crates/install-wheel-rs/install_wheel_rs.pyi
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
class LockedVenv:
|
||||||
|
def __init__(self, venv: str): ...
|
||||||
|
def install_wheel(self, wheel: str): ...
|
9
crates/install-wheel-rs/pyproject.toml
Normal file
9
crates/install-wheel-rs/pyproject.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[project]
|
||||||
|
name = "install-wheel-rs"
|
||||||
|
|
||||||
|
[tool.maturin]
|
||||||
|
features = ["python_bindings"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["maturin>=1.2,<2.0"]
|
||||||
|
build-backend = "maturin"
|
12
crates/install-wheel-rs/run_tests.sh
Normal file
12
crates/install-wheel-rs/run_tests.sh
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# cd to project root
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
rm -f target-maturin/wheels/install_wheel_rs-*.whl
|
||||||
|
CARGO_TARGET_DIR=target-maturin maturin build --release --strip --no-sdist -m install-wheel-rs/Cargo.toml
|
||||||
|
.venv/bin/pip uninstall -y install-wheel-rs
|
||||||
|
.venv/bin/pip install target-maturin/wheels/install_wheel_rs-*.whl
|
||||||
|
.venv/bin/pytest install-wheel-rs/test
|
||||||
|
cargo test
|
188
crates/install-wheel-rs/src/install_location.rs
Normal file
188
crates/install-wheel-rs/src/install_location.rs
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
//! Multiplexing between venv install and monotrail install
|
||||||
|
|
||||||
|
use fs2::FileExt;
|
||||||
|
use fs_err as fs;
|
||||||
|
use fs_err::File;
|
||||||
|
use std::io;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tracing::{error, warn};
|
||||||
|
|
||||||
|
const INSTALL_LOCKFILE: &str = "install-wheel-rs.lock";
|
||||||
|
|
||||||
|
/// I'm not sure that's the right way to normalize here, but it's a single place to change
|
||||||
|
/// everything.
|
||||||
|
///
|
||||||
|
/// For displaying to the user, `-` is better, and it's also what poetry lockfile 2.0 does
|
||||||
|
///
|
||||||
|
/// Keep in sync with `find_distributions`
|
||||||
|
pub fn normalize_name(dep_name: &str) -> String {
|
||||||
|
dep_name.to_lowercase().replace(['.', '_'], "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A directory for which we acquired a install-wheel-rs.lock lockfile
|
||||||
|
pub struct LockedDir {
|
||||||
|
/// The directory to lock
|
||||||
|
path: PathBuf,
|
||||||
|
/// handle on the install-wheel-rs.lock that drops the lock
|
||||||
|
lockfile: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LockedDir {
|
||||||
|
/// Tries to lock the directory, returns Ok(None) if it is already locked
|
||||||
|
pub fn try_acquire(path: &Path) -> io::Result<Option<Self>> {
|
||||||
|
let lockfile = File::create(path.join(INSTALL_LOCKFILE))?;
|
||||||
|
if lockfile.file().try_lock_exclusive().is_ok() {
|
||||||
|
Ok(Some(Self {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
lockfile,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Locks the directory, if necessary blocking until the lock becomes free
|
||||||
|
pub fn acquire(path: &Path) -> io::Result<Self> {
|
||||||
|
let lockfile = File::create(path.join(INSTALL_LOCKFILE))?;
|
||||||
|
lockfile.file().lock_exclusive()?;
|
||||||
|
Ok(Self {
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
lockfile,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for LockedDir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(err) = self.lockfile.file().unlock() {
|
||||||
|
error!(
|
||||||
|
"Failed to unlock {}: {}",
|
||||||
|
self.lockfile.path().display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for LockedDir {
|
||||||
|
type Target = Path;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multiplexing between venv install and monotrail install
|
||||||
|
///
|
||||||
|
/// For monotrail, we have a structure that is {monotrail}/{normalized(name)}/{version}/tag
|
||||||
|
///
|
||||||
|
/// We use a lockfile to prevent multiple instance writing stuff on the same time
|
||||||
|
/// As of pip 22.0, e.g. `pip install numpy; pip install numpy; pip install numpy` will
|
||||||
|
/// nondeterministically fail
|
||||||
|
///
|
||||||
|
/// I was also thinking about making a shared lock on the import side, but monotrail install
|
||||||
|
/// is supposedly atomic (by directory renaming), while for venv installation there can't be
|
||||||
|
/// atomicity (we need to add lots of different file without a top level directory / key-turn
|
||||||
|
/// file we could rename) and the locking would also need to happen in the import mechanism
|
||||||
|
/// itself to ensure
|
||||||
|
pub enum InstallLocation<T: Deref<Target = Path>> {
|
||||||
|
Venv {
|
||||||
|
/// absolute path
|
||||||
|
venv_base: T,
|
||||||
|
python_version: (u8, u8),
|
||||||
|
},
|
||||||
|
Monotrail {
|
||||||
|
monotrail_root: T,
|
||||||
|
python: PathBuf,
|
||||||
|
python_version: (u8, u8),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Deref<Target = Path>> InstallLocation<T> {
|
||||||
|
/// Returns the location of the python interpreter
|
||||||
|
pub fn get_python(&self) -> PathBuf {
|
||||||
|
match self {
|
||||||
|
InstallLocation::Venv { venv_base, .. } => {
|
||||||
|
if cfg!(windows) {
|
||||||
|
venv_base.join("Scripts").join("python.exe")
|
||||||
|
} else {
|
||||||
|
// canonicalize on python would resolve the symlink
|
||||||
|
venv_base.join("bin").join("python")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: For monotrail use the monotrail launcher
|
||||||
|
InstallLocation::Monotrail { python, .. } => python.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_python_version(&self) -> (u8, u8) {
|
||||||
|
match self {
|
||||||
|
InstallLocation::Venv { python_version, .. } => *python_version,
|
||||||
|
InstallLocation::Monotrail { python_version, .. } => *python_version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TODO: This function is unused?
|
||||||
|
pub fn is_installed(&self, normalized_name: &str, version: &str) -> bool {
|
||||||
|
match self {
|
||||||
|
InstallLocation::Venv {
|
||||||
|
venv_base,
|
||||||
|
python_version,
|
||||||
|
} => {
|
||||||
|
let site_packages = if cfg!(target_os = "windows") {
|
||||||
|
venv_base.join("Lib").join("site-packages")
|
||||||
|
} else {
|
||||||
|
venv_base
|
||||||
|
.join("lib")
|
||||||
|
.join(format!("python{}.{}", python_version.0, python_version.1))
|
||||||
|
.join("site-packages")
|
||||||
|
};
|
||||||
|
site_packages
|
||||||
|
.join(format!("{}-{}.dist-info", normalized_name, version))
|
||||||
|
.is_dir()
|
||||||
|
}
|
||||||
|
InstallLocation::Monotrail { monotrail_root, .. } => monotrail_root
|
||||||
|
.join(format!("{}-{}", normalized_name, version))
|
||||||
|
.is_dir(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstallLocation<PathBuf> {
|
||||||
|
pub fn acquire_lock(&self) -> io::Result<InstallLocation<LockedDir>> {
|
||||||
|
let root = match self {
|
||||||
|
Self::Venv { venv_base, .. } => venv_base,
|
||||||
|
Self::Monotrail { monotrail_root, .. } => monotrail_root,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If necessary, create monotrail dir
|
||||||
|
fs::create_dir_all(root)?;
|
||||||
|
|
||||||
|
let locked_dir = if let Some(locked_dir) = LockedDir::try_acquire(root)? {
|
||||||
|
locked_dir
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Could not acquire exclusive lock for installing, is another installation process \
|
||||||
|
running? Sleeping until lock becomes free"
|
||||||
|
);
|
||||||
|
LockedDir::acquire(root)?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(match self {
|
||||||
|
Self::Venv { python_version, .. } => InstallLocation::Venv {
|
||||||
|
venv_base: locked_dir,
|
||||||
|
python_version: *python_version,
|
||||||
|
},
|
||||||
|
Self::Monotrail {
|
||||||
|
python_version,
|
||||||
|
python,
|
||||||
|
..
|
||||||
|
} => InstallLocation::Monotrail {
|
||||||
|
monotrail_root: locked_dir,
|
||||||
|
python: python.clone(),
|
||||||
|
python_version: *python_version,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
123
crates/install-wheel-rs/src/lib.rs
Normal file
123
crates/install-wheel-rs/src/lib.rs
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
//! Takes a wheel and installs it, either in a venv or for monotrail
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use std::path::Path;
|
||||||
|
//! use install_wheel_rs::install_wheel_in_venv;
|
||||||
|
//!
|
||||||
|
//! install_wheel_in_venv(
|
||||||
|
//! "Django-4.2.6-py3-none-any.whl",
|
||||||
|
//! ".venv",
|
||||||
|
//! ".venv/bin/python",
|
||||||
|
//! (3, 8),
|
||||||
|
//! ).unwrap();
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use platform_info::PlatformInfoError;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use thiserror::Error;
|
||||||
|
use zip::result::ZipError;
|
||||||
|
|
||||||
|
pub use install_location::{normalize_name, InstallLocation, LockedDir};
|
||||||
|
pub use wheel::{
|
||||||
|
get_script_launcher, install_wheel, parse_key_value_file, read_record_file, relative_to,
|
||||||
|
Script, SHEBANG_PYTHON,
|
||||||
|
};
|
||||||
|
pub use wheel_tags::{Arch, CompatibleTags, Os, WheelFilename};
|
||||||
|
|
||||||
|
mod install_location;
|
||||||
|
#[cfg(feature = "python_bindings")]
|
||||||
|
mod python_bindings;
|
||||||
|
mod wheel;
|
||||||
|
mod wheel_tags;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
IO(#[from] io::Error),
|
||||||
|
/// This shouldn't actually be possible to occur
|
||||||
|
#[error("Failed to serialize direct_url.json ಠ_ಠ")]
|
||||||
|
DirectUrlSerdeJson(#[source] serde_json::Error),
|
||||||
|
/// Tags/metadata didn't match platform
|
||||||
|
#[error("The wheel is incompatible with the current platform {os} {arch}")]
|
||||||
|
IncompatibleWheel { os: Os, arch: Arch },
|
||||||
|
/// The wheel is broken
|
||||||
|
#[error("The wheel is invalid: {0}")]
|
||||||
|
InvalidWheel(String),
|
||||||
|
/// pyproject.toml or poetry.lock are broken
|
||||||
|
#[error("The poetry dependency specification (pyproject.toml or poetry.lock) is broken (try `poetry update`?): {0}")]
|
||||||
|
InvalidPoetry(String),
|
||||||
|
/// Doesn't follow file name schema
|
||||||
|
#[error("The wheel filename \"{0}\" is invalid: {1}")]
|
||||||
|
InvalidWheelFileName(String, String),
|
||||||
|
#[error("Failed to read the wheel file {0}")]
|
||||||
|
Zip(String, #[source] ZipError),
|
||||||
|
#[error("Failed to run python subcommand")]
|
||||||
|
PythonSubcommand(#[source] io::Error),
|
||||||
|
#[error("Failed to move data files")]
|
||||||
|
WalkDir(#[from] walkdir::Error),
|
||||||
|
#[error("RECORD file doesn't match wheel contents: {0}")]
|
||||||
|
RecordFile(String),
|
||||||
|
#[error("RECORD file is invalid")]
|
||||||
|
RecordCsv(#[from] csv::Error),
|
||||||
|
#[error("Broken virtualenv: {0}")]
|
||||||
|
BrokenVenv(String),
|
||||||
|
#[error("Failed to detect the operating system version: {0}")]
|
||||||
|
OsVersionDetection(String),
|
||||||
|
#[error("Failed to detect the current platform")]
|
||||||
|
PlatformInfo(#[source] PlatformInfoError),
|
||||||
|
#[error("Invalid version specification, only none or == is supported")]
|
||||||
|
Pep440,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub(crate) fn from_zip_error(file: String, value: ZipError) -> Self {
|
||||||
|
match value {
|
||||||
|
ZipError::Io(io_error) => Self::IO(io_error),
|
||||||
|
_ => Self::Zip(file, value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// High level API: Install a wheel in a virtualenv
|
||||||
|
///
|
||||||
|
/// The python interpreter is used for compiling to byte code, the python version for computing
|
||||||
|
/// the site packages path on unix.
|
||||||
|
///
|
||||||
|
/// Returns the tag of the wheel
|
||||||
|
pub fn install_wheel_in_venv(
|
||||||
|
wheel: impl AsRef<Path>,
|
||||||
|
venv: impl AsRef<Path>,
|
||||||
|
interpreter: impl AsRef<Path>,
|
||||||
|
major_minor: (u8, u8),
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
let venv_base = venv.as_ref().canonicalize()?;
|
||||||
|
let location = InstallLocation::Venv {
|
||||||
|
venv_base,
|
||||||
|
python_version: major_minor,
|
||||||
|
};
|
||||||
|
let locked_dir = location.acquire_lock()?;
|
||||||
|
|
||||||
|
let filename = wheel
|
||||||
|
.as_ref()
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| Error::InvalidWheel("Expected a file".to_string()))?
|
||||||
|
.to_string_lossy();
|
||||||
|
let filename = WheelFilename::from_str(&filename)?;
|
||||||
|
let compatible_tags = CompatibleTags::current(location.get_python_version())?;
|
||||||
|
filename.compatibility(&compatible_tags)?;
|
||||||
|
|
||||||
|
install_wheel(
|
||||||
|
&locked_dir,
|
||||||
|
File::open(wheel)?,
|
||||||
|
filename,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
&[],
|
||||||
|
// Only relevant for monotrail style installation
|
||||||
|
"",
|
||||||
|
interpreter,
|
||||||
|
)
|
||||||
|
}
|
81
crates/install-wheel-rs/src/main.rs
Normal file
81
crates/install-wheel-rs/src/main.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use fs_err::File;
|
||||||
|
use install_wheel_rs::{install_wheel, CompatibleTags, Error, InstallLocation, WheelFilename};
|
||||||
|
#[cfg(feature = "rayon")]
|
||||||
|
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// Low level install CLI, mainly used for testing
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Args {
|
||||||
|
wheels: Vec<PathBuf>,
|
||||||
|
/// The root of the venv to install in
|
||||||
|
#[clap(long, env = "VIRTUAL_ENV")]
|
||||||
|
venv: PathBuf,
|
||||||
|
/// The major version of the current python interpreter
|
||||||
|
#[clap(long)]
|
||||||
|
major: u8,
|
||||||
|
/// The minor version of the current python interpreter
|
||||||
|
#[clap(long)]
|
||||||
|
minor: u8,
|
||||||
|
/// Compile .py files to .pyc (errors are ignored)
|
||||||
|
#[clap(long)]
|
||||||
|
compile: bool,
|
||||||
|
/// Don't check the hashes in RECORD
|
||||||
|
#[clap(long)]
|
||||||
|
skip_hashes: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Error> {
|
||||||
|
let args = Args::parse();
|
||||||
|
let venv_base = args.venv.canonicalize()?;
|
||||||
|
let location = InstallLocation::Venv {
|
||||||
|
venv_base,
|
||||||
|
python_version: (args.major, args.minor),
|
||||||
|
};
|
||||||
|
let locked_dir = location.acquire_lock()?;
|
||||||
|
|
||||||
|
let wheels: Vec<(PathBuf, WheelFilename)> = args
|
||||||
|
.wheels
|
||||||
|
.into_iter()
|
||||||
|
.map(|wheel| {
|
||||||
|
let filename = wheel
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| Error::InvalidWheel("Expected a file".to_string()))?
|
||||||
|
.to_string_lossy();
|
||||||
|
let filename = WheelFilename::from_str(&filename)?;
|
||||||
|
let compatible_tags = CompatibleTags::current(location.get_python_version())?;
|
||||||
|
filename.compatibility(&compatible_tags)?;
|
||||||
|
Ok((wheel, filename))
|
||||||
|
})
|
||||||
|
.collect::<Result<_, Error>>()?;
|
||||||
|
|
||||||
|
let wheels = {
|
||||||
|
#[cfg(feature = "rayon")]
|
||||||
|
{
|
||||||
|
wheels.into_par_iter()
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "rayon"))]
|
||||||
|
{
|
||||||
|
wheels.into_iter()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
wheels
|
||||||
|
.map(|(wheel, filename)| {
|
||||||
|
install_wheel(
|
||||||
|
&locked_dir,
|
||||||
|
File::open(wheel)?,
|
||||||
|
filename,
|
||||||
|
args.compile,
|
||||||
|
!args.skip_hashes,
|
||||||
|
&[],
|
||||||
|
// Only relevant for monotrail style installation
|
||||||
|
"",
|
||||||
|
location.get_python(),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.collect::<Result<_, Error>>()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
31
crates/install-wheel-rs/src/pip_compileall.py
Normal file
31
crates/install-wheel-rs/src/pip_compileall.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
"""
|
||||||
|
Based on
|
||||||
|
https://github.com/pypa/pip/blob/3820b0e52c7fed2b2c43ba731b718f316e6816d1/src/pip/_internal/operations/install/wheel.py#L612-L623
|
||||||
|
|
||||||
|
pip silently just swallows all pyc compilation errors, but `python -m compileall` does
|
||||||
|
not have such a flag, so we adapt the pip code. This is relevant e.g. for
|
||||||
|
`debugpy-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64`,
|
||||||
|
which contains some vendored python 2 code which fails to compile
|
||||||
|
"""
|
||||||
|
|
||||||
|
import compileall
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
# in rust, we give one line per file to compile
|
||||||
|
# we also have to read it before printing to stdout or we risk pipes running full
|
||||||
|
paths = sys.stdin.readlines()
|
||||||
|
for path in paths:
|
||||||
|
# just to be sure
|
||||||
|
path = path.strip()
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
|
# Unlike pip, we set quiet=2, so we don't have to capture stdout
|
||||||
|
# I'd like to show those errors, but given that pip thinks that's totally fine
|
||||||
|
# we can't really change that
|
||||||
|
success = compileall.compile_file(path, force=True, quiet=2)
|
||||||
|
if success:
|
||||||
|
# return successfully compiled files so we can update RECORD accordingly
|
||||||
|
print(path)
|
93
crates/install-wheel-rs/src/python_bindings.rs
Normal file
93
crates/install-wheel-rs/src/python_bindings.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
#![allow(clippy::format_push_string)] // I will not replace clear and infallible with fallible, io looking code
|
||||||
|
|
||||||
|
use crate::{install_wheel, CompatibleTags, Error, InstallLocation, LockedDir, WheelFilename};
|
||||||
|
use pyo3::create_exception;
|
||||||
|
use pyo3::types::PyModule;
|
||||||
|
use pyo3::{pyclass, pymethods, pymodule, PyErr, PyResult, Python};
|
||||||
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
create_exception!(
|
||||||
|
install_wheel_rs,
|
||||||
|
PyWheelInstallerError,
|
||||||
|
pyo3::exceptions::PyException
|
||||||
|
);
|
||||||
|
|
||||||
|
impl From<Error> for PyErr {
|
||||||
|
fn from(err: Error) -> Self {
|
||||||
|
let mut accumulator = format!("Failed to install wheels: {}", err);
|
||||||
|
|
||||||
|
let mut current_err: &dyn std::error::Error = &err;
|
||||||
|
while let Some(cause) = current_err.source() {
|
||||||
|
accumulator.push_str(&format!("\n Caused by: {}", cause));
|
||||||
|
current_err = cause;
|
||||||
|
}
|
||||||
|
PyWheelInstallerError::new_err(accumulator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyclass]
|
||||||
|
struct LockedVenv {
|
||||||
|
location: InstallLocation<LockedDir>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl LockedVenv {
|
||||||
|
#[new]
|
||||||
|
pub fn new(py: Python, venv: PathBuf) -> PyResult<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
location: InstallLocation::Venv {
|
||||||
|
venv_base: LockedDir::acquire(&venv)?,
|
||||||
|
python_version: (py.version_info().major, py.version_info().minor),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_wheel(&self, py: Python, wheel: PathBuf) -> PyResult<()> {
|
||||||
|
// Would be nicer through https://docs.python.org/3/c-api/init.html#c.Py_GetProgramFullPath
|
||||||
|
let sys_executable: String = py.import("sys")?.getattr("executable")?.extract()?;
|
||||||
|
|
||||||
|
// TODO: Pass those options on to the user
|
||||||
|
py.allow_threads(|| {
|
||||||
|
let filename = wheel
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| Error::InvalidWheel("Expected a file".to_string()))?
|
||||||
|
.to_string_lossy();
|
||||||
|
let filename = WheelFilename::from_str(&filename)?;
|
||||||
|
let compatible_tags = CompatibleTags::current(self.location.get_python_version())?;
|
||||||
|
filename.compatibility(&compatible_tags)?;
|
||||||
|
|
||||||
|
install_wheel(
|
||||||
|
&self.location,
|
||||||
|
File::open(wheel)?,
|
||||||
|
filename,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
&[],
|
||||||
|
// unique_version can be anything since it's only used to monotrail
|
||||||
|
"",
|
||||||
|
Path::new(&sys_executable),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymodule]
|
||||||
|
pub fn install_wheel_rs(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||||
|
// Good enough for now
|
||||||
|
if env::var_os("RUST_LOG").is_some() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
} else {
|
||||||
|
let format = tracing_subscriber::fmt::format()
|
||||||
|
.with_level(false)
|
||||||
|
.with_target(false)
|
||||||
|
.without_time()
|
||||||
|
.compact();
|
||||||
|
tracing_subscriber::fmt().event_format(format).init();
|
||||||
|
}
|
||||||
|
m.add_class::<LockedVenv>()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
1455
crates/install-wheel-rs/src/wheel.rs
Normal file
1455
crates/install-wheel-rs/src/wheel.rs
Normal file
File diff suppressed because it is too large
Load diff
937
crates/install-wheel-rs/src/wheel_tags.rs
Normal file
937
crates/install-wheel-rs/src/wheel_tags.rs
Normal file
|
@ -0,0 +1,937 @@
|
||||||
|
//! Parses the wheel filename, the current host os/arch and checks wheels for compatibility
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
use fs_err as fs;
|
||||||
|
use goblin::elf::Elf;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use platform_info::{PlatformInfo, PlatformInfoAPI, UNameAPI};
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
/// The name of a wheel split into its parts ([PEP 491](https://peps.python.org/pep-0491/))
|
||||||
|
///
|
||||||
|
/// Ignores the build tag atm.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use std::str::FromStr;
|
||||||
|
/// use install_wheel_rs::WheelFilename;
|
||||||
|
///
|
||||||
|
/// let filename = WheelFilename::from_str("foo-1.0-py32-none-any.whl").unwrap();
|
||||||
|
/// assert_eq!(filename, WheelFilename {
|
||||||
|
/// distribution: "foo".to_string(),
|
||||||
|
/// version: "1.0".to_string(),
|
||||||
|
/// python_tag: vec!["py32".to_string()],
|
||||||
|
/// abi_tag: vec!["none".to_string()],
|
||||||
|
/// platform_tag: vec!["any".to_string()]
|
||||||
|
/// });
|
||||||
|
/// let filename = WheelFilename::from_str(
|
||||||
|
/// "numpy-1.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
|
||||||
|
/// ).unwrap();
|
||||||
|
/// assert_eq!(filename, WheelFilename {
|
||||||
|
/// distribution: "numpy".to_string(),
|
||||||
|
/// version: "1.26.0".to_string(),
|
||||||
|
/// python_tag: vec!["cp312".to_string()],
|
||||||
|
/// abi_tag: vec!["cp312".to_string()],
|
||||||
|
/// platform_tag: vec![
|
||||||
|
/// "manylinux_2_17_aarch64".to_string(),
|
||||||
|
/// "manylinux2014_aarch64".to_string()
|
||||||
|
/// ]
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct WheelFilename {
|
||||||
|
pub distribution: String,
|
||||||
|
pub version: String,
|
||||||
|
pub python_tag: Vec<String>,
|
||||||
|
pub abi_tag: Vec<String>,
|
||||||
|
pub platform_tag: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for WheelFilename {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(filename: &str) -> Result<Self, Self::Err> {
|
||||||
|
let basename = filename.strip_suffix(".whl").ok_or_else(|| {
|
||||||
|
Error::InvalidWheelFileName(filename.to_string(), "Must end with .whl".to_string())
|
||||||
|
})?;
|
||||||
|
// https://www.python.org/dev/peps/pep-0427/#file-name-convention
|
||||||
|
match basename.split('-').collect::<Vec<_>>().as_slice() {
|
||||||
|
// TODO: Build tag precedence
|
||||||
|
&[distribution, version, _, python_tag, abi_tag, platform_tag]
|
||||||
|
| &[distribution, version, python_tag, abi_tag, platform_tag] => Ok(WheelFilename {
|
||||||
|
distribution: distribution.to_string(),
|
||||||
|
version: version.to_string(),
|
||||||
|
python_tag: python_tag.split('.').map(String::from).collect(),
|
||||||
|
abi_tag: abi_tag.split('.').map(String::from).collect(),
|
||||||
|
platform_tag: platform_tag.split('.').map(String::from).collect(),
|
||||||
|
}),
|
||||||
|
_ => Err(Error::InvalidWheelFileName(
|
||||||
|
filename.to_string(),
|
||||||
|
"Expected four or five dashes (\"-\") in the filename".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WheelFilename {
|
||||||
|
/// Returns Some(precedence) is the wheels are compatible, otherwise none
|
||||||
|
///
|
||||||
|
/// Precedence is e.g. used to install newer manylinux wheels over older manylinux wheels
|
||||||
|
pub fn compatibility(&self, compatible_tags: &CompatibleTags) -> Result<usize, Error> {
|
||||||
|
compatible_tags
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(precedence, tag)| {
|
||||||
|
if self.python_tag.contains(&tag.0)
|
||||||
|
&& self.abi_tag.contains(&tag.1)
|
||||||
|
&& self.platform_tag.contains(&tag.2)
|
||||||
|
{
|
||||||
|
Some(precedence)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error::IncompatibleWheel {
|
||||||
|
os: compatible_tags.os.clone(),
|
||||||
|
arch: compatible_tags.arch,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Effectively undoes the wheel filename parsing step
|
||||||
|
pub fn get_tag(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"{}-{}-{}",
|
||||||
|
self.python_tag.join("."),
|
||||||
|
self.abi_tag.join("."),
|
||||||
|
self.platform_tag.join(".")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A platform, defined by the list of compatible wheel tags in order
|
||||||
|
pub struct CompatibleTags {
|
||||||
|
pub os: Os,
|
||||||
|
pub arch: Arch,
|
||||||
|
pub tags: Vec<(String, String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for CompatibleTags {
|
||||||
|
type Target = [(String, String, String)];
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the compatible tags in a (python_tag, abi_tag, platform_tag) format, ordered from
|
||||||
|
/// highest precedence to lowest precedence
|
||||||
|
impl CompatibleTags {
|
||||||
|
/// Compatible tags for the current operating system and architecture
|
||||||
|
pub fn current(python_version: (u8, u8)) -> Result<CompatibleTags, Error> {
|
||||||
|
Self::new(python_version, Os::current()?, Arch::current()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(python_version: (u8, u8), os: Os, arch: Arch) -> Result<CompatibleTags, Error> {
|
||||||
|
assert_eq!(python_version.0, 3);
|
||||||
|
let mut tags = Vec::new();
|
||||||
|
let platform_tags = compatible_platform_tags(&os, &arch)?;
|
||||||
|
// 1. This exact c api version
|
||||||
|
for platform_tag in &platform_tags {
|
||||||
|
tags.push((
|
||||||
|
format!("cp{}{}", python_version.0, python_version.1),
|
||||||
|
format!(
|
||||||
|
"cp{}{}{}",
|
||||||
|
python_version.0,
|
||||||
|
python_version.1,
|
||||||
|
// hacky but that's legacy anyways
|
||||||
|
if python_version.1 <= 7 { "m" } else { "" }
|
||||||
|
),
|
||||||
|
platform_tag.clone(),
|
||||||
|
));
|
||||||
|
tags.push((
|
||||||
|
format!("cp{}{}", python_version.0, python_version.1),
|
||||||
|
"none".to_string(),
|
||||||
|
platform_tag.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// 2. abi3 and no abi (e.g. executable binary)
|
||||||
|
// For some reason 3.2 is the minimum python for the cp abi
|
||||||
|
for minor in 2..=python_version.1 {
|
||||||
|
for platform_tag in &platform_tags {
|
||||||
|
tags.push((
|
||||||
|
format!("cp{}{}", python_version.0, minor),
|
||||||
|
"abi3".to_string(),
|
||||||
|
platform_tag.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3. no abi (e.g. executable binary)
|
||||||
|
for minor in 0..=python_version.1 {
|
||||||
|
for platform_tag in &platform_tags {
|
||||||
|
tags.push((
|
||||||
|
format!("py{}{}", python_version.0, minor),
|
||||||
|
"none".to_string(),
|
||||||
|
platform_tag.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 4. major only
|
||||||
|
for platform_tag in platform_tags {
|
||||||
|
tags.push((
|
||||||
|
format!("py{}", python_version.0),
|
||||||
|
"none".to_string(),
|
||||||
|
platform_tag,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// 5. no binary
|
||||||
|
for minor in 0..=python_version.1 {
|
||||||
|
tags.push((
|
||||||
|
format!("py{}{}", python_version.0, minor),
|
||||||
|
"none".to_string(),
|
||||||
|
"any".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
tags.push((
|
||||||
|
format!("py{}", python_version.0),
|
||||||
|
"none".to_string(),
|
||||||
|
"any".to_string(),
|
||||||
|
));
|
||||||
|
Ok(CompatibleTags { os, arch, tags })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All supported operating system
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub enum Os {
|
||||||
|
Manylinux { major: u16, minor: u16 },
|
||||||
|
Musllinux { major: u16, minor: u16 },
|
||||||
|
Windows,
|
||||||
|
Macos { major: u16, minor: u16 },
|
||||||
|
FreeBsd { release: String },
|
||||||
|
NetBsd { release: String },
|
||||||
|
OpenBsd { release: String },
|
||||||
|
Dragonfly { release: String },
|
||||||
|
Illumos { release: String, arch: String },
|
||||||
|
Haiku { release: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Os {
|
||||||
|
fn detect_linux_libc() -> Result<Self, Error> {
|
||||||
|
let libc = find_libc()?;
|
||||||
|
let linux = if let Ok(Some((major, minor))) = get_musl_version(&libc) {
|
||||||
|
Os::Musllinux { major, minor }
|
||||||
|
} else if let Ok(glibc_ld) = fs::read_link(&libc) {
|
||||||
|
// Try reading the link first as it's faster
|
||||||
|
let filename = glibc_ld
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::OsVersionDetection("Expected the glibc ld to be a file".to_string())
|
||||||
|
})?
|
||||||
|
.to_string_lossy();
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
static expr: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new(r"ld-(\d{1,3})\.(\d{1,3})\.so").unwrap());
|
||||||
|
|
||||||
|
if let Some(capture) = expr.captures(&filename) {
|
||||||
|
let major = capture.get(1).unwrap().as_str().parse::<u16>().unwrap();
|
||||||
|
let minor = capture.get(2).unwrap().as_str().parse::<u16>().unwrap();
|
||||||
|
Os::Manylinux { major, minor }
|
||||||
|
} else {
|
||||||
|
trace!("Couldn't use ld filename, using `ldd --version`");
|
||||||
|
// runs `ldd --version`
|
||||||
|
let version = glibc_version::get_version().map_err(|err| {
|
||||||
|
Error::OsVersionDetection(format!(
|
||||||
|
"Failed to determine glibc version with `ldd --version`: {}",
|
||||||
|
err
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Os::Manylinux {
|
||||||
|
major: version.major as u16,
|
||||||
|
minor: version.minor as u16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::OsVersionDetection("Couldn't detect neither glibc version nor musl libc version, at least one of which is required".to_string()));
|
||||||
|
};
|
||||||
|
trace!("libc: {}", linux);
|
||||||
|
Ok(linux)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current() -> Result<Self, Error> {
|
||||||
|
let target_triple = target_lexicon::HOST;
|
||||||
|
|
||||||
|
let os = match target_triple.operating_system {
|
||||||
|
target_lexicon::OperatingSystem::Linux => Self::detect_linux_libc()?,
|
||||||
|
target_lexicon::OperatingSystem::Windows => Os::Windows,
|
||||||
|
target_lexicon::OperatingSystem::MacOSX { major, minor, .. } => {
|
||||||
|
Os::Macos { major, minor }
|
||||||
|
}
|
||||||
|
target_lexicon::OperatingSystem::Darwin => {
|
||||||
|
let (major, minor) = get_mac_os_version()?;
|
||||||
|
Os::Macos { major, minor }
|
||||||
|
}
|
||||||
|
target_lexicon::OperatingSystem::Netbsd => Os::NetBsd {
|
||||||
|
release: PlatformInfo::new()
|
||||||
|
.map_err(Error::PlatformInfo)?
|
||||||
|
.release()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
target_lexicon::OperatingSystem::Freebsd => Os::FreeBsd {
|
||||||
|
release: PlatformInfo::new()
|
||||||
|
.map_err(Error::PlatformInfo)?
|
||||||
|
.release()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
target_lexicon::OperatingSystem::Openbsd => Os::OpenBsd {
|
||||||
|
release: PlatformInfo::new()
|
||||||
|
.map_err(Error::PlatformInfo)?
|
||||||
|
.release()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
target_lexicon::OperatingSystem::Dragonfly => Os::Dragonfly {
|
||||||
|
release: PlatformInfo::new()
|
||||||
|
.map_err(Error::PlatformInfo)?
|
||||||
|
.release()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
target_lexicon::OperatingSystem::Illumos => {
|
||||||
|
let platform_info = PlatformInfo::new().map_err(Error::PlatformInfo)?;
|
||||||
|
Os::Illumos {
|
||||||
|
release: platform_info.release().to_string_lossy().to_string(),
|
||||||
|
arch: platform_info.machine().to_string_lossy().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target_lexicon::OperatingSystem::Haiku => Os::Haiku {
|
||||||
|
release: PlatformInfo::new()
|
||||||
|
.map_err(Error::PlatformInfo)?
|
||||||
|
.release()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
},
|
||||||
|
unsupported => {
|
||||||
|
return Err(Error::OsVersionDetection(format!(
|
||||||
|
"The operating system {:?} is not supported",
|
||||||
|
unsupported
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(os)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Os {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Os::Manylinux { .. } => write!(f, "Manylinux"),
|
||||||
|
Os::Musllinux { .. } => write!(f, "Musllinux"),
|
||||||
|
Os::Windows => write!(f, "Windows"),
|
||||||
|
Os::Macos { .. } => write!(f, "MacOS"),
|
||||||
|
Os::FreeBsd { .. } => write!(f, "FreeBSD"),
|
||||||
|
Os::NetBsd { .. } => write!(f, "NetBSD"),
|
||||||
|
Os::OpenBsd { .. } => write!(f, "OpenBSD"),
|
||||||
|
Os::Dragonfly { .. } => write!(f, "DragonFly"),
|
||||||
|
Os::Illumos { .. } => write!(f, "Illumos"),
|
||||||
|
Os::Haiku { .. } => write!(f, "Haiku"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All supported CPU architectures
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
pub enum Arch {
|
||||||
|
Aarch64,
|
||||||
|
Armv7L,
|
||||||
|
Powerpc64Le,
|
||||||
|
Powerpc64,
|
||||||
|
X86,
|
||||||
|
X86_64,
|
||||||
|
S390X,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Arch {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Arch::Aarch64 => write!(f, "aarch64"),
|
||||||
|
Arch::Armv7L => write!(f, "armv7l"),
|
||||||
|
Arch::Powerpc64Le => write!(f, "ppc64le"),
|
||||||
|
Arch::Powerpc64 => write!(f, "ppc64"),
|
||||||
|
Arch::X86 => write!(f, "i686"),
|
||||||
|
Arch::X86_64 => write!(f, "x86_64"),
|
||||||
|
Arch::S390X => write!(f, "s390x"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Arch {
|
||||||
|
pub fn current() -> Result<Arch, Error> {
|
||||||
|
let target_triple = target_lexicon::HOST;
|
||||||
|
let arch = match target_triple.architecture {
|
||||||
|
target_lexicon::Architecture::X86_64 => Arch::X86_64,
|
||||||
|
target_lexicon::Architecture::X86_32(_) => Arch::X86,
|
||||||
|
target_lexicon::Architecture::Arm(_) => Arch::Armv7L,
|
||||||
|
target_lexicon::Architecture::Aarch64(_) => Arch::Aarch64,
|
||||||
|
target_lexicon::Architecture::Powerpc64 => Arch::Powerpc64,
|
||||||
|
target_lexicon::Architecture::Powerpc64le => Arch::Powerpc64Le,
|
||||||
|
target_lexicon::Architecture::S390x => Arch::S390X,
|
||||||
|
unsupported => {
|
||||||
|
return Err(Error::OsVersionDetection(format!(
|
||||||
|
"The architecture {} is not supported",
|
||||||
|
unsupported
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the oldest possible Manylinux tag for this architecture
|
||||||
|
pub fn get_minimum_manylinux_minor(&self) -> u16 {
|
||||||
|
match self {
|
||||||
|
// manylinux 2014
|
||||||
|
Arch::Aarch64 | Arch::Armv7L | Arch::Powerpc64 | Arch::Powerpc64Le | Arch::S390X => 17,
|
||||||
|
// manylinux 1
|
||||||
|
Arch::X86 | Arch::X86_64 => 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mac_os_version() -> Result<(u16, u16), Error> {
|
||||||
|
// This is actually what python does
|
||||||
|
// https://github.com/python/cpython/blob/cb2b3c8d3566ae46b3b8d0718019e1c98484589e/Lib/platform.py#L409-L428
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
struct SystemVersion {
|
||||||
|
product_version: String,
|
||||||
|
}
|
||||||
|
let system_version: SystemVersion =
|
||||||
|
plist::from_file("/System/Library/CoreServices/SystemVersion.plist")
|
||||||
|
.map_err(|err| Error::OsVersionDetection(err.to_string()))?;
|
||||||
|
|
||||||
|
let invalid_mac_os_version = || {
|
||||||
|
Error::OsVersionDetection(format!(
|
||||||
|
"Invalid mac os version {}",
|
||||||
|
system_version.product_version
|
||||||
|
))
|
||||||
|
};
|
||||||
|
match system_version
|
||||||
|
.product_version
|
||||||
|
.split('.')
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.as_slice()
|
||||||
|
{
|
||||||
|
[major, minor] | [major, minor, _] => {
|
||||||
|
let major = major.parse::<u16>().map_err(|_| invalid_mac_os_version())?;
|
||||||
|
let minor = minor.parse::<u16>().map_err(|_| invalid_mac_os_version())?;
|
||||||
|
Ok((major, minor))
|
||||||
|
}
|
||||||
|
_ => Err(invalid_mac_os_version()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine the appropriate binary formats for a mac os version.
|
||||||
|
/// Source: https://github.com/pypa/packaging/blob/fd4f11139d1c884a637be8aa26bb60a31fbc9411/packaging/tags.py#L314
|
||||||
|
fn get_mac_binary_formats(major: u16, minor: u16, arch: &Arch) -> Vec<String> {
|
||||||
|
let mut formats = vec![match arch {
|
||||||
|
Arch::Aarch64 => "arm64".to_string(),
|
||||||
|
_ => arch.to_string(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
if matches!(arch, Arch::X86_64) {
|
||||||
|
if (major, minor) < (10, 4) {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
formats.extend([
|
||||||
|
"intel".to_string(),
|
||||||
|
"fat64".to_string(),
|
||||||
|
"fat32".to_string(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(arch, Arch::X86_64 | Arch::Aarch64) {
|
||||||
|
formats.push("universal2".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(arch, Arch::X86_64) {
|
||||||
|
formats.push("universal".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
formats
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find musl libc path from executable's ELF header
|
||||||
|
pub fn find_libc() -> Result<PathBuf, Error> {
|
||||||
|
let buffer = fs::read("/bin/ls")?;
|
||||||
|
let error_str = "Couldn't parse /bin/ls for detecting the ld version";
|
||||||
|
let elf = Elf::parse(&buffer)
|
||||||
|
.map_err(|err| Error::OsVersionDetection(format!("{}: {}", error_str, err)))?;
|
||||||
|
if let Some(elf_interpreter) = elf.interpreter {
|
||||||
|
Ok(PathBuf::from(elf_interpreter))
|
||||||
|
} else {
|
||||||
|
Err(Error::OsVersionDetection(error_str.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the musl version from libc library's output. Taken from maturin
|
||||||
|
///
|
||||||
|
/// The libc library should output something like this to stderr::
|
||||||
|
///
|
||||||
|
/// musl libc (x86_64)
|
||||||
|
/// Version 1.2.2
|
||||||
|
/// Dynamic Program Loader
|
||||||
|
pub fn get_musl_version(ld_path: impl AsRef<Path>) -> std::io::Result<Option<(u16, u16)>> {
|
||||||
|
let output = Command::new(ld_path.as_ref())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.output()?;
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
static expr: Lazy<Regex> = Lazy::new(|| Regex::new(r"Version (\d{2,4})\.(\d{2,4})").unwrap());
|
||||||
|
if let Some(capture) = expr.captures(&stderr) {
|
||||||
|
let major = capture.get(1).unwrap().as_str().parse::<u16>().unwrap();
|
||||||
|
let minor = capture.get(2).unwrap().as_str().parse::<u16>().unwrap();
|
||||||
|
return Ok(Some((major, minor)));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the compatible platform tags from highest precedence to lowest precedence
|
||||||
|
///
|
||||||
|
/// Examples: manylinux_2_17, macosx_11_0_arm64, win_amd64
|
||||||
|
///
|
||||||
|
/// We have two cases: Actual platform specific tags (including "merged" tags such as universal2)
|
||||||
|
/// and "any".
|
||||||
|
///
|
||||||
|
/// Bit of a mess, needs to be cleaned up. The order also isn't exactly matching that of pip yet,
|
||||||
|
/// but works good enough in practice
|
||||||
|
pub fn compatible_platform_tags(os: &Os, arch: &Arch) -> Result<Vec<String>, Error> {
|
||||||
|
let platform_tags = match (os.clone(), *arch) {
|
||||||
|
(Os::Manylinux { major, minor }, _) => {
|
||||||
|
let mut platform_tags = vec![format!("linux_{}", arch)];
|
||||||
|
// Use newer manylinux first like pip does
|
||||||
|
platform_tags.extend(
|
||||||
|
(arch.get_minimum_manylinux_minor()..=minor)
|
||||||
|
.rev()
|
||||||
|
.map(|minor| format!("manylinux_{}_{}_{}", major, minor, arch)),
|
||||||
|
);
|
||||||
|
if (arch.get_minimum_manylinux_minor()..=minor).contains(&17) {
|
||||||
|
platform_tags.push(format!("manylinux2014_{}", arch))
|
||||||
|
}
|
||||||
|
if (arch.get_minimum_manylinux_minor()..=minor).contains(&12) {
|
||||||
|
platform_tags.push(format!("manylinux2010_{}", arch))
|
||||||
|
}
|
||||||
|
if (arch.get_minimum_manylinux_minor()..=minor).contains(&5) {
|
||||||
|
platform_tags.push(format!("manylinux1_{}", arch))
|
||||||
|
}
|
||||||
|
platform_tags
|
||||||
|
}
|
||||||
|
(Os::Musllinux { major, minor }, _) => {
|
||||||
|
let mut platform_tags = vec![format!("linux_{}", arch)];
|
||||||
|
// musl 1.1 is the lowest supported version in musllinux
|
||||||
|
platform_tags.extend(
|
||||||
|
(1..=minor)
|
||||||
|
.rev()
|
||||||
|
.map(|minor| format!("musllinux_{}_{}_{}", major, minor, arch)),
|
||||||
|
);
|
||||||
|
platform_tags
|
||||||
|
}
|
||||||
|
(Os::Macos { major, minor }, Arch::X86_64) => {
|
||||||
|
// Source: https://github.com/pypa/packaging/blob/fd4f11139d1c884a637be8aa26bb60a31fbc9411/packaging/tags.py#L346
|
||||||
|
let mut platform_tags = vec![];
|
||||||
|
match major {
|
||||||
|
10 => {
|
||||||
|
// Prior to Mac OS 11, each yearly release of Mac OS bumped the "minor" version
|
||||||
|
// number. The major version was always 10.
|
||||||
|
for minor in (0..=minor).rev() {
|
||||||
|
for binary_format in get_mac_binary_formats(major, minor, arch) {
|
||||||
|
platform_tags
|
||||||
|
.push(format!("macosx_{}_{}_{}", major, minor, binary_format));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x if x >= 11 => {
|
||||||
|
// Starting with Mac OS 11, each yearly release bumps the major version number.
|
||||||
|
// The minor versions are now the midyear updates.
|
||||||
|
for major in (10..=major).rev() {
|
||||||
|
for binary_format in get_mac_binary_formats(major, 0, arch) {
|
||||||
|
platform_tags.push(format!("macosx_{}_{}_{}", major, 0, binary_format));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The "universal2" binary format can have a macOS version earlier than 11.0
|
||||||
|
// when the x86_64 part of the binary supports that version of macOS.
|
||||||
|
for minor in (4..=16).rev() {
|
||||||
|
for binary_format in get_mac_binary_formats(10, minor, arch) {
|
||||||
|
platform_tags
|
||||||
|
.push(format!("macosx_{}_{}_{}", 10, minor, binary_format));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(Error::OsVersionDetection(format!(
|
||||||
|
"Unsupported mac os version: {}",
|
||||||
|
major,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
platform_tags
|
||||||
|
}
|
||||||
|
(Os::Macos { major, .. }, Arch::Aarch64) => {
|
||||||
|
// Source: https://github.com/pypa/packaging/blob/fd4f11139d1c884a637be8aa26bb60a31fbc9411/packaging/tags.py#L346
|
||||||
|
let mut platform_tags = vec![];
|
||||||
|
// Starting with Mac OS 11, each yearly release bumps the major version number.
|
||||||
|
// The minor versions are now the midyear updates.
|
||||||
|
for major in (10..=major).rev() {
|
||||||
|
for binary_format in get_mac_binary_formats(major, 0, arch) {
|
||||||
|
platform_tags.push(format!("macosx_{}_{}_{}", major, 0, binary_format));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The "universal2" binary format can have a macOS version earlier than 11.0
|
||||||
|
// when the x86_64 part of the binary supports that version of macOS.
|
||||||
|
platform_tags.extend(
|
||||||
|
(4..=16)
|
||||||
|
.rev()
|
||||||
|
.map(|minor| format!("macosx_{}_{}_universal2", 10, minor)),
|
||||||
|
);
|
||||||
|
platform_tags
|
||||||
|
}
|
||||||
|
(Os::Windows, Arch::X86) => {
|
||||||
|
vec!["win32".to_string()]
|
||||||
|
}
|
||||||
|
(Os::Windows, Arch::X86_64) => {
|
||||||
|
vec!["win_amd64".to_string()]
|
||||||
|
}
|
||||||
|
(Os::Windows, Arch::Aarch64) => vec!["win_arm64".to_string()],
|
||||||
|
(
|
||||||
|
Os::FreeBsd { release: _ }
|
||||||
|
| Os::NetBsd { release: _ }
|
||||||
|
| Os::OpenBsd { release: _ }
|
||||||
|
| Os::Dragonfly { release: _ }
|
||||||
|
| Os::Haiku { release: _ },
|
||||||
|
_,
|
||||||
|
) => {
|
||||||
|
let info = PlatformInfo::new().map_err(Error::PlatformInfo)?;
|
||||||
|
let release = info.release().to_string_lossy().replace(['.', '-'], "_");
|
||||||
|
vec![format!(
|
||||||
|
"{}_{}_{}",
|
||||||
|
os.to_string().to_lowercase(),
|
||||||
|
release,
|
||||||
|
arch
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
(
|
||||||
|
Os::Illumos {
|
||||||
|
mut release,
|
||||||
|
mut arch,
|
||||||
|
},
|
||||||
|
_,
|
||||||
|
) => {
|
||||||
|
let mut os = os.to_string().to_lowercase();
|
||||||
|
// See https://github.com/python/cpython/blob/46c8d915715aa2bd4d697482aa051fe974d440e1/Lib/sysconfig.py#L722-L730
|
||||||
|
if let Some((major, other)) = release.split_once('_') {
|
||||||
|
let major_ver: u64 = major.parse().map_err(|err| {
|
||||||
|
Error::OsVersionDetection(format!(
|
||||||
|
"illumos major version is not a number: {}",
|
||||||
|
err
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if major_ver >= 5 {
|
||||||
|
// SunOS 5 == Solaris 2
|
||||||
|
os = "solaris".to_string();
|
||||||
|
release = format!("{}_{}", major_ver - 3, other);
|
||||||
|
arch = format!("{}_64bit", arch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vec![format!("{}_{}_{}", os, release, arch)]
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(Error::OsVersionDetection(format!(
|
||||||
|
"Unsupported operating system and architecture combination: {} {}",
|
||||||
|
os, arch
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(platform_tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::{compatible_platform_tags, WheelFilename};
|
||||||
|
use crate::{Arch, CompatibleTags, Error, Os};
|
||||||
|
use fs_err::File;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
const FILENAMES: &[&str] = &[
|
||||||
|
"numpy-1.22.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"numpy-1.22.2-cp310-cp310-win_amd64.whl",
|
||||||
|
"numpy-1.22.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"numpy-1.22.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
|
||||||
|
"numpy-1.22.2-cp310-cp310-macosx_11_0_arm64.whl",
|
||||||
|
"numpy-1.22.2-cp310-cp310-macosx_10_14_x86_64.whl",
|
||||||
|
"numpy-1.22.2-cp39-cp39-win_amd64.whl",
|
||||||
|
"numpy-1.22.2-cp39-cp39-win32.whl",
|
||||||
|
"numpy-1.22.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"numpy-1.22.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
|
||||||
|
"numpy-1.22.2-cp39-cp39-macosx_11_0_arm64.whl",
|
||||||
|
"numpy-1.22.2-cp39-cp39-macosx_10_14_x86_64.whl",
|
||||||
|
"numpy-1.22.2-cp38-cp38-win_amd64.whl",
|
||||||
|
"numpy-1.22.2-cp38-cp38-win32.whl",
|
||||||
|
"numpy-1.22.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"numpy-1.22.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
|
||||||
|
"numpy-1.22.2-cp38-cp38-macosx_11_0_arm64.whl",
|
||||||
|
"numpy-1.22.2-cp38-cp38-macosx_10_14_x86_64.whl",
|
||||||
|
"tqdm-4.62.3-py2.py3-none-any.whl",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Test that we can parse the filenames
|
||||||
|
#[test]
|
||||||
|
fn test_wheel_filename_parsing() -> Result<(), Error> {
|
||||||
|
for filename in FILENAMES {
|
||||||
|
WheelFilename::from_str(filename)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that we correctly identify compatible pairs
|
||||||
|
#[test]
|
||||||
|
fn test_compatibility() -> Result<(), Error> {
|
||||||
|
let filenames = [
|
||||||
|
(
|
||||||
|
"numpy-1.22.2-cp38-cp38-win_amd64.whl",
|
||||||
|
((3, 8), Os::Windows, Arch::X86_64),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"numpy-1.22.2-cp38-cp38-win32.whl",
|
||||||
|
((3, 8), Os::Windows, Arch::X86),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"numpy-1.22.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
(
|
||||||
|
(3, 8),
|
||||||
|
Os::Manylinux {
|
||||||
|
major: 2,
|
||||||
|
minor: 31,
|
||||||
|
},
|
||||||
|
Arch::X86_64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"numpy-1.22.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
|
||||||
|
(
|
||||||
|
(3, 8),
|
||||||
|
Os::Manylinux {
|
||||||
|
major: 2,
|
||||||
|
minor: 31,
|
||||||
|
},
|
||||||
|
Arch::Aarch64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"numpy-1.22.2-cp38-cp38-macosx_11_0_arm64.whl",
|
||||||
|
(
|
||||||
|
(3, 8),
|
||||||
|
Os::Macos {
|
||||||
|
major: 11,
|
||||||
|
minor: 0,
|
||||||
|
},
|
||||||
|
Arch::Aarch64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"numpy-1.22.2-cp38-cp38-macosx_10_14_x86_64.whl",
|
||||||
|
(
|
||||||
|
(3, 8),
|
||||||
|
// Test backwards compatibility here
|
||||||
|
Os::Macos {
|
||||||
|
major: 11,
|
||||||
|
minor: 0,
|
||||||
|
},
|
||||||
|
Arch::X86_64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ruff-0.0.63-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl",
|
||||||
|
(
|
||||||
|
(3, 8),
|
||||||
|
Os::Macos {
|
||||||
|
major: 12,
|
||||||
|
minor: 0,
|
||||||
|
},
|
||||||
|
Arch::X86_64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"ruff-0.0.63-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl",
|
||||||
|
(
|
||||||
|
(3, 8),
|
||||||
|
Os::Macos {
|
||||||
|
major: 12,
|
||||||
|
minor: 0,
|
||||||
|
},
|
||||||
|
Arch::Aarch64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tqdm-4.62.3-py2.py3-none-any.whl",
|
||||||
|
(
|
||||||
|
(3, 8),
|
||||||
|
Os::Manylinux {
|
||||||
|
major: 2,
|
||||||
|
minor: 31,
|
||||||
|
},
|
||||||
|
Arch::X86_64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (filename, (python_version, os, arch)) in filenames {
|
||||||
|
let compatible_tags = CompatibleTags::new(python_version, os, arch)?;
|
||||||
|
assert!(
|
||||||
|
WheelFilename::from_str(filename)?
|
||||||
|
.compatibility(&compatible_tags)
|
||||||
|
.is_ok(),
|
||||||
|
"{}",
|
||||||
|
filename
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that incompatible pairs don't pass is_compatible
|
||||||
|
#[test]
|
||||||
|
fn test_compatibility_filter() -> Result<(), Error> {
|
||||||
|
let compatible_tags = CompatibleTags::new(
|
||||||
|
(3, 8),
|
||||||
|
Os::Manylinux {
|
||||||
|
major: 2,
|
||||||
|
minor: 31,
|
||||||
|
},
|
||||||
|
Arch::X86_64,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let compatible: Vec<&str> = FILENAMES
|
||||||
|
.iter()
|
||||||
|
.filter(|filename| {
|
||||||
|
WheelFilename::from_str(filename)
|
||||||
|
.unwrap()
|
||||||
|
.compatibility(&compatible_tags)
|
||||||
|
.is_ok()
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
vec![
|
||||||
|
"numpy-1.22.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"tqdm-4.62.3-py2.py3-none-any.whl"
|
||||||
|
],
|
||||||
|
compatible
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ubuntu_20_04_tags() -> Vec<String> {
|
||||||
|
serde_json::from_reader(File::open("../../test-data/tags/cp38-ubuntu-20-04.json").unwrap())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check against the tags that packaging.tags reports as compatible
|
||||||
|
#[test]
|
||||||
|
fn ubuntu_20_04_compatible() -> Result<(), Error> {
|
||||||
|
let tags = get_ubuntu_20_04_tags();
|
||||||
|
for tag in tags {
|
||||||
|
let compatible_tags = CompatibleTags::new(
|
||||||
|
(3, 8),
|
||||||
|
Os::Manylinux {
|
||||||
|
major: 2,
|
||||||
|
minor: 31,
|
||||||
|
},
|
||||||
|
Arch::X86_64,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
WheelFilename::from_str(&format!("foo-1.0-{}.whl", tag))?
|
||||||
|
.compatibility(&compatible_tags)
|
||||||
|
.is_ok(),
|
||||||
|
"{}",
|
||||||
|
tag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check against the tags that packaging.tags reports as compatible
|
||||||
|
#[test]
|
||||||
|
fn ubuntu_20_04_list() -> Result<(), Error> {
|
||||||
|
let expected_tags = get_ubuntu_20_04_tags();
|
||||||
|
let actual_tags: Vec<String> = CompatibleTags::new(
|
||||||
|
(3, 8),
|
||||||
|
Os::Manylinux {
|
||||||
|
major: 2,
|
||||||
|
minor: 31,
|
||||||
|
},
|
||||||
|
Arch::X86_64,
|
||||||
|
)?
|
||||||
|
.iter()
|
||||||
|
.map(|(python_tag, abi_tag, platform_tag)| {
|
||||||
|
format!("{}-{}-{}", python_tag, abi_tag, platform_tag)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(expected_tags, actual_tags);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_precedence() {
|
||||||
|
let tags = CompatibleTags::new(
|
||||||
|
(3, 8),
|
||||||
|
Os::Manylinux {
|
||||||
|
major: 2,
|
||||||
|
minor: 31,
|
||||||
|
},
|
||||||
|
Arch::X86_64,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let pairs = [
|
||||||
|
(
|
||||||
|
"greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl"
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl"
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (higher_str, lower_str) in pairs {
|
||||||
|
let higher = WheelFilename::from_str(higher_str).unwrap();
|
||||||
|
let lower = WheelFilename::from_str(lower_str).unwrap();
|
||||||
|
let higher_precedence = higher.compatibility(&tags).unwrap();
|
||||||
|
let lower_precedence = lower.compatibility(&tags).unwrap();
|
||||||
|
assert!(
|
||||||
|
higher_precedence < lower_precedence,
|
||||||
|
"{} {} {} {}",
|
||||||
|
higher_str,
|
||||||
|
higher_precedence,
|
||||||
|
lower_str,
|
||||||
|
lower_precedence
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Basic does-it-work test
|
||||||
|
#[test]
|
||||||
|
fn host_arch() -> Result<(), Error> {
|
||||||
|
let os = Os::current()?;
|
||||||
|
let arch = Arch::current()?;
|
||||||
|
compatible_platform_tags(&os, &arch)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
35
crates/install-wheel-rs/test/test_install_wheel_rs.py
Normal file
35
crates/install-wheel-rs/test/test_install_wheel_rs.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import platform
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import check_call, check_output
|
||||||
|
|
||||||
|
|
||||||
|
def check_installed(venv: Path) -> bool:
|
||||||
|
"""true: installed and working, false: not installed, borked: exception"""
|
||||||
|
try:
|
||||||
|
output = check_output(
|
||||||
|
[
|
||||||
|
venv.joinpath(
|
||||||
|
"Scripts" if platform.system() == "Windows" else "bin"
|
||||||
|
).joinpath("upsidedown")
|
||||||
|
],
|
||||||
|
input="hello world!",
|
||||||
|
text=True,
|
||||||
|
).strip()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
assert output == "¡pꞁɹoʍ oꞁꞁǝɥ"
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_wheel_rs(pytestconfig, tmp_path):
|
||||||
|
from install_wheel_rs import LockedVenv
|
||||||
|
|
||||||
|
venv = tmp_path.joinpath("venv_test_install_wheel_rs")
|
||||||
|
check_call(["virtualenv", venv])
|
||||||
|
assert not check_installed(venv)
|
||||||
|
locked_venv = LockedVenv(venv)
|
||||||
|
wheel = pytestconfig.rootpath.joinpath(
|
||||||
|
"install-wheel-rs/test-data/upsidedown-0.4-py2.py3-none-any.whl"
|
||||||
|
)
|
||||||
|
locked_venv.install_wheel(wheel)
|
||||||
|
assert check_installed(venv)
|
BIN
crates/install-wheel-rs/windows-launcher/t32.exe
Normal file
BIN
crates/install-wheel-rs/windows-launcher/t32.exe
Normal file
Binary file not shown.
BIN
crates/install-wheel-rs/windows-launcher/t64-arm.exe
Normal file
BIN
crates/install-wheel-rs/windows-launcher/t64-arm.exe
Normal file
Binary file not shown.
BIN
crates/install-wheel-rs/windows-launcher/t64.exe
Normal file
BIN
crates/install-wheel-rs/windows-launcher/t64.exe
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue