diff --git a/Cargo.lock b/Cargo.lock index 926f97b7e..ad5b44a07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1734,7 +1734,9 @@ dependencies = [ "pep440_rs", "pep508_rs", "platform-host", + "puffin-package", "serde_json", + "tokio", "tracing", ] diff --git a/crates/puffin-cli/src/commands/compile.rs b/crates/puffin-cli/src/commands/compile.rs index bf201db02..bc9e85553 100644 --- a/crates/puffin-cli/src/commands/compile.rs +++ b/crates/puffin-cli/src/commands/compile.rs @@ -21,7 +21,7 @@ pub(crate) async fn compile(src: &Path, cache: Option<&Path>) -> Result) -> Result Result { + // Detect the current Python interpreter. + let platform = Platform::current()?; + let python = PythonExecutable::from_env(platform)?; + debug!( + "Using Python interpreter: {}", + python.executable().display() + ); + + // Build the installed index. + let site_packages = SitePackages::from_executable(&python).await?; + for (name, version) in site_packages.iter() { + #[allow(clippy::print_stdout)] + { + println!("{name}=={version}"); + } + } + + Ok(ExitStatus::Success) +} diff --git a/crates/puffin-cli/src/commands/mod.rs b/crates/puffin-cli/src/commands/mod.rs index f0383fa06..70735beb9 100644 --- a/crates/puffin-cli/src/commands/mod.rs +++ b/crates/puffin-cli/src/commands/mod.rs @@ -2,10 +2,12 @@ use std::process::ExitCode; pub(crate) use clean::clean; pub(crate) use compile::compile; +pub(crate) use freeze::freeze; pub(crate) use sync::sync; mod clean; mod compile; +mod freeze; mod sync; #[derive(Copy, Clone)] diff --git a/crates/puffin-cli/src/commands/sync.rs b/crates/puffin-cli/src/commands/sync.rs index 7238db719..ceb2e36ae 100644 --- a/crates/puffin-cli/src/commands/sync.rs +++ b/crates/puffin-cli/src/commands/sync.rs @@ -21,7 +21,7 @@ pub(crate) async fn sync(src: &Path, cache: Option<&Path>) -> Result // Detect the current Python interpreter. let platform = Platform::current()?; - let python = PythonExecutable::from_env(&platform)?; + let python = PythonExecutable::from_env(platform)?; debug!( "Using Python interpreter: {}", python.executable().display() @@ -31,7 +31,7 @@ pub(crate) async fn sync(src: &Path, cache: Option<&Path>) -> Result let markers = python.markers(); // Determine the compatible platform tags. - let tags = Tags::from_env(&platform, python.simple_version())?; + let tags = Tags::from_env(python.platform(), python.simple_version())?; // Instantiate a client. let client = { diff --git a/crates/puffin-cli/src/main.rs b/crates/puffin-cli/src/main.rs index a72954b6f..f147e6787 100644 --- a/crates/puffin-cli/src/main.rs +++ b/crates/puffin-cli/src/main.rs @@ -26,6 +26,8 @@ enum Commands { Sync(SyncArgs), /// Clear the cache. Clean, + /// Enumerate the installed packages in the current environment. + Freeze, } #[derive(Args)] @@ -76,6 +78,7 @@ async fn main() -> ExitCode { .await } Commands::Clean => commands::clean(dirs.as_ref().map(ProjectDirs::cache_dir)).await, + Commands::Freeze => commands::freeze().await, }; match result { diff --git a/crates/puffin-interpreter/Cargo.toml b/crates/puffin-interpreter/Cargo.toml index 1f977e227..1e563e124 100644 --- a/crates/puffin-interpreter/Cargo.toml +++ b/crates/puffin-interpreter/Cargo.toml @@ -13,7 +13,9 @@ license.workspace = true pep440_rs = { path = "../pep440-rs" } pep508_rs = { path = "../pep508-rs" } platform-host = { path = "../platform-host" } +puffin-package = { path = "../puffin-package" } anyhow = { workspace = true } serde_json = { workspace = true } +tokio = { workspace = true } tracing = { workspace = true } diff --git a/crates/puffin-interpreter/src/lib.rs b/crates/puffin-interpreter/src/lib.rs index 2d1147c7a..2f85b6b7f 100644 --- a/crates/puffin-interpreter/src/lib.rs +++ b/crates/puffin-interpreter/src/lib.rs @@ -7,14 +7,17 @@ use pep508_rs::MarkerEnvironment; use platform_host::Platform; use crate::python_platform::PythonPlatform; +pub use crate::site_packages::SitePackages; mod markers; mod python_platform; +mod site_packages; mod virtual_env; /// A Python executable and its associated platform markers. #[derive(Debug)] pub struct PythonExecutable { + platform: PythonPlatform, venv: PathBuf, executable: PathBuf, markers: MarkerEnvironment, @@ -22,19 +25,31 @@ pub struct PythonExecutable { impl PythonExecutable { /// Detect the current Python executable from the host environment. - pub fn from_env(platform: &Platform) -> Result { + pub fn from_env(platform: Platform) -> Result { let platform = PythonPlatform::from(platform); let venv = virtual_env::detect_virtual_env(&platform)?; let executable = platform.venv_python(&venv); let markers = markers::detect_markers(&executable)?; Ok(Self { + platform, venv, executable, markers, }) } + /// Returns the path to the Python virtual environment. + pub fn platform(&self) -> &Platform { + &self.platform + } + + /// Returns the path to the `site-packages` directory inside a virtual environment. + pub fn site_packages(&self) -> PathBuf { + self.platform + .venv_site_packages(self.venv(), self.simple_version()) + } + /// Returns the path to the Python virtual environment. pub fn venv(&self) -> &Path { self.venv.as_path() diff --git a/crates/puffin-interpreter/src/python_platform.rs b/crates/puffin-interpreter/src/python_platform.rs index 774364b0c..c6245ac66 100644 --- a/crates/puffin-interpreter/src/python_platform.rs +++ b/crates/puffin-interpreter/src/python_platform.rs @@ -1,3 +1,4 @@ +use std::ops::Deref; use std::path::Path; use std::path::PathBuf; @@ -5,9 +6,9 @@ use platform_host::{Os, Platform}; /// A Python-aware wrapper around [`Platform`]. #[derive(Debug, Clone, Eq, PartialEq)] -pub(crate) struct PythonPlatform<'a>(&'a Platform); +pub(crate) struct PythonPlatform(Platform); -impl PythonPlatform<'_> { +impl PythonPlatform { /// Returns the path to the `python` executable inside a virtual environment. pub(crate) fn venv_python(&self, venv_base: impl AsRef) -> PathBuf { let python = if matches!(self.0.os(), Os::Windows) { @@ -38,10 +39,34 @@ impl PythonPlatform<'_> { venv.join("bin") } } + + /// Returns the path to the `site-packages` directory inside a virtual environment. + pub(crate) fn venv_site_packages( + &self, + venv_base: impl AsRef, + version: (u8, u8), + ) -> PathBuf { + let venv = venv_base.as_ref(); + if matches!(self.0.os(), Os::Windows) { + venv.join("Lib").join("site-packages") + } else { + venv.join("lib") + .join(format!("python{}.{}", version.0, version.1)) + .join("site-packages") + } + } } -impl<'a> From<&'a Platform> for PythonPlatform<'a> { - fn from(platform: &'a Platform) -> Self { +impl From for PythonPlatform { + fn from(platform: Platform) -> Self { Self(platform) } } + +impl Deref for PythonPlatform { + type Target = Platform; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/crates/puffin-interpreter/src/site_packages.rs b/crates/puffin-interpreter/src/site_packages.rs new file mode 100644 index 000000000..c7f5fe338 --- /dev/null +++ b/crates/puffin-interpreter/src/site_packages.rs @@ -0,0 +1,68 @@ +use std::collections::BTreeMap; +use std::path::Path; +use std::str::FromStr; + +use anyhow::{anyhow, Result}; + +use pep440_rs::Version; +use puffin_package::package_name::PackageName; + +use crate::PythonExecutable; + +#[derive(Debug)] +pub struct SitePackages(BTreeMap); + +impl SitePackages { + /// Build an index of installed packages from the given Python executable. + pub async fn from_executable(python: &PythonExecutable) -> Result { + let mut index = BTreeMap::new(); + + let mut dir = tokio::fs::read_dir(python.site_packages()).await?; + while let Some(entry) = dir.next_entry().await? { + if entry.file_type().await?.is_dir() { + if let Some(dist_info) = DistInfo::try_from_path(&entry.path())? { + index.insert(dist_info.name, dist_info.version); + } + } + } + + Ok(Self(index)) + } + + /// Returns an iterator over the installed packages. + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } +} + +#[derive(Debug)] +struct DistInfo { + name: PackageName, + version: Version, +} + +impl DistInfo { + /// Try to parse a (potential) `dist-info` directory into a package name and version. + /// + /// See: + fn try_from_path(path: &Path) -> Result> { + if path.extension().is_some_and(|ext| ext == "dist-info") { + let Some(file_stem) = path.file_stem() else { + return Ok(None); + }; + let Some(file_stem) = file_stem.to_str() else { + return Ok(None); + }; + let Some((name, version)) = file_stem.split_once('-') else { + return Ok(None); + }; + + let name = PackageName::normalize(name); + let version = Version::from_str(version).map_err(|err| anyhow!(err))?; + + return Ok(Some(DistInfo { name, version })); + } + + Ok(None) + } +} diff --git a/crates/puffin-package/src/package_name.rs b/crates/puffin-package/src/package_name.rs index 91687177b..16aa375aa 100644 --- a/crates/puffin-package/src/package_name.rs +++ b/crates/puffin-package/src/package_name.rs @@ -5,7 +5,7 @@ use std::ops::Deref; use once_cell::sync::Lazy; use regex::Regex; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct PackageName(String); impl Display for PackageName {