diff --git a/Cargo.lock b/Cargo.lock index ad5b44a07..b5ef13007 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1670,6 +1670,7 @@ name = "puffin-cli" version = "0.1.0" dependencies = [ "anyhow", + "bitflags 2.4.0", "cacache", "clap", "colored", diff --git a/README.md b/README.md index fc56ad4a3..ab1c7a598 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ To compare a warm run of `puffin` to `pip`: ```shell hyperfine --runs 10 --warmup 3 \ - "./target/release/puffin-cli sync requirements.txt" \ + "./target/release/puffin-cli sync requirements.txt --ignore-installed" \ "pip install -r requirements.txt --ignore-installed --no-deps" ``` @@ -50,10 +50,18 @@ To compare a cold run of `puffin` to `pip`: ```shell hyperfine --runs 10 --warmup 3 \ - "./target/release/puffin-cli sync requirements.txt --no-cache" \ + "./target/release/puffin-cli sync requirements.txt --ignore-installed --no-cache" \ "pip install -r requirements.txt --ignore-installed --no-cache-dir --no-deps" ``` +To compare a run in which all requirements are already installed: + +```shell +hyperfine --runs 10 --warmup 3 \ + "./target/release/puffin-cli sync requirements.txt" \ + "pip install -r requirements.txt --no-deps" +``` + ## License Puffin is licensed under either of diff --git a/crates/puffin-cli/Cargo.toml b/crates/puffin-cli/Cargo.toml index ca4425278..198e29cc8 100644 --- a/crates/puffin-cli/Cargo.toml +++ b/crates/puffin-cli/Cargo.toml @@ -15,14 +15,15 @@ platform-host = { path = "../platform-host" } puffin-resolver = { path = "../puffin-resolver" } anyhow = { workspace = true } +bitflags = { workspace = true } cacache = { workspace = true } clap = { workspace = true, features = ["derive"] } colored = { workspace = true } directories = { workspace = true } futures = { workspace = true } -tracing = { workspace = true } -tracing-tree = { workspace = true } -tracing-subscriber = { workspace = true } -url = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +tracing-tree = { workspace = true } +url = { workspace = true } diff --git a/crates/puffin-cli/src/commands/compile.rs b/crates/puffin-cli/src/commands/compile.rs index bc9e85553..97d731749 100644 --- a/crates/puffin-cli/src/commands/compile.rs +++ b/crates/puffin-cli/src/commands/compile.rs @@ -8,6 +8,7 @@ use platform_host::Platform; use platform_tags::Tags; use puffin_client::PypiClientBuilder; use puffin_interpreter::PythonExecutable; +use puffin_package::requirements::Requirements; use crate::commands::ExitStatus; @@ -17,7 +18,7 @@ pub(crate) async fn compile(src: &Path, cache: Option<&Path>) -> Result) -> Result { +pub(crate) async fn sync(src: &Path, cache: Option<&Path>, flags: SyncFlags) -> Result { // Read the `requirements.txt` from disk. let requirements_txt = std::fs::read_to_string(src)?; // Parse the `requirements.txt` into a list of requirements. - let requirements = puffin_package::requirements::Requirements::from_str(&requirements_txt)?; + let requirements = Requirements::from_str(&requirements_txt)?; // Detect the current Python interpreter. let platform = Platform::current()?; @@ -27,6 +38,29 @@ pub(crate) async fn sync(src: &Path, cache: Option<&Path>) -> Result python.executable().display() ); + // Remove any already-installed packages. + let requirements = if flags.intersects(SyncFlags::IGNORE_INSTALLED) { + requirements + } else { + let site_packages = SitePackages::from_executable(&python).await?; + requirements.filter(|requirement| { + let package = PackageName::normalize(&requirement.name); + if let Some(version) = site_packages.get(&package) { + #[allow(clippy::print_stdout)] + { + println!("Requirement already satisfied: {package} ({version})"); + } + false + } else { + true + } + }) + }; + + if requirements.is_empty() { + return Ok(ExitStatus::Success); + } + // Determine the current environment markers. let markers = python.markers(); diff --git a/crates/puffin-cli/src/main.rs b/crates/puffin-cli/src/main.rs index f147e6787..022d6c6fa 100644 --- a/crates/puffin-cli/src/main.rs +++ b/crates/puffin-cli/src/main.rs @@ -48,6 +48,10 @@ struct SyncArgs { /// Avoid reading from or writing to the cache. #[arg(long)] no_cache: bool, + + /// Ignore any installed packages, forcing a re-installation. + #[arg(long)] + ignore_installed: bool, } #[tokio::main] @@ -74,6 +78,11 @@ async fn main() -> ExitCode { dirs.as_ref() .map(ProjectDirs::cache_dir) .filter(|_| !args.no_cache), + if args.ignore_installed { + commands::SyncFlags::IGNORE_INSTALLED + } else { + commands::SyncFlags::empty() + }, ) .await } diff --git a/crates/puffin-interpreter/src/site_packages.rs b/crates/puffin-interpreter/src/site_packages.rs index c7f5fe338..76cdc8904 100644 --- a/crates/puffin-interpreter/src/site_packages.rs +++ b/crates/puffin-interpreter/src/site_packages.rs @@ -33,6 +33,11 @@ impl SitePackages { pub fn iter(&self) -> impl Iterator { self.0.iter() } + + /// Returns the version of the given package, if it is installed. + pub fn get(&self, name: &PackageName) -> Option<&Version> { + self.0.get(name) + } } #[derive(Debug)] diff --git a/crates/puffin-package/src/requirements.rs b/crates/puffin-package/src/requirements.rs index 89089c24f..28f2c9fc2 100644 --- a/crates/puffin-package/src/requirements.rs +++ b/crates/puffin-package/src/requirements.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::ops::Deref; use std::str::FromStr; use anyhow::Result; @@ -10,11 +9,46 @@ use pep508_rs::{Pep508Error, Requirement}; #[derive(Debug)] pub struct Requirements(Vec); +impl Requirements { + pub fn new(requirements: Vec) -> Self { + Self(requirements) + } + + /// Filter the requirements. + #[must_use] + pub fn filter(self, mut f: F) -> Self + where + F: FnMut(&Requirement) -> bool, + { + Self( + self.0 + .into_iter() + .filter(|requirement| f(requirement)) + .collect(), + ) + } + + /// Return the number of requirements. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Return `true` if there are no requirements. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Return an iterator over the requirements. + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } +} + impl FromStr for Requirements { type Err = Pep508Error; fn from_str(s: &str) -> Result { - Ok(Self( + Ok(Self::new( RequirementsIterator::new(s) .map(|requirement| Requirement::from_str(requirement.as_str())) .collect::, Pep508Error>>()?, @@ -22,14 +56,6 @@ impl FromStr for Requirements { } } -impl Deref for Requirements { - type Target = [Requirement]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - #[derive(Debug)] struct RequirementsIterator<'a> { text: &'a str,