Add a freeze command to list installed dependencies (#42)

A pre-requisite for https://github.com/astral-sh/puffin/issues/35.
This commit is contained in:
Charlie Marsh 2023-10-07 14:46:09 -04:00 committed by GitHub
parent f3015ffc1f
commit bc1736feff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 155 additions and 10 deletions

2
Cargo.lock generated
View file

@ -1734,7 +1734,9 @@ dependencies = [
"pep440_rs", "pep440_rs",
"pep508_rs", "pep508_rs",
"platform-host", "platform-host",
"puffin-package",
"serde_json", "serde_json",
"tokio",
"tracing", "tracing",
] ]

View file

@ -21,7 +21,7 @@ pub(crate) async fn compile(src: &Path, cache: Option<&Path>) -> Result<ExitStat
// Detect the current Python interpreter. // Detect the current Python interpreter.
let platform = Platform::current()?; let platform = Platform::current()?;
let python = PythonExecutable::from_env(&platform)?; let python = PythonExecutable::from_env(platform)?;
debug!( debug!(
"Using Python interpreter: {}", "Using Python interpreter: {}",
python.executable().display() python.executable().display()
@ -31,7 +31,7 @@ pub(crate) async fn compile(src: &Path, cache: Option<&Path>) -> Result<ExitStat
let markers = python.markers(); let markers = python.markers();
// Determine the compatible platform tags. // 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. // Instantiate a client.
let client = { let client = {

View file

@ -0,0 +1,28 @@
use anyhow::Result;
use platform_host::Platform;
use puffin_interpreter::{PythonExecutable, SitePackages};
use tracing::debug;
use crate::commands::ExitStatus;
/// Enumerate the installed packages in the current environment.
pub(crate) async fn freeze() -> Result<ExitStatus> {
// 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)
}

View file

@ -2,10 +2,12 @@ use std::process::ExitCode;
pub(crate) use clean::clean; pub(crate) use clean::clean;
pub(crate) use compile::compile; pub(crate) use compile::compile;
pub(crate) use freeze::freeze;
pub(crate) use sync::sync; pub(crate) use sync::sync;
mod clean; mod clean;
mod compile; mod compile;
mod freeze;
mod sync; mod sync;
#[derive(Copy, Clone)] #[derive(Copy, Clone)]

View file

@ -21,7 +21,7 @@ pub(crate) async fn sync(src: &Path, cache: Option<&Path>) -> Result<ExitStatus>
// Detect the current Python interpreter. // Detect the current Python interpreter.
let platform = Platform::current()?; let platform = Platform::current()?;
let python = PythonExecutable::from_env(&platform)?; let python = PythonExecutable::from_env(platform)?;
debug!( debug!(
"Using Python interpreter: {}", "Using Python interpreter: {}",
python.executable().display() python.executable().display()
@ -31,7 +31,7 @@ pub(crate) async fn sync(src: &Path, cache: Option<&Path>) -> Result<ExitStatus>
let markers = python.markers(); let markers = python.markers();
// Determine the compatible platform tags. // 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. // Instantiate a client.
let client = { let client = {

View file

@ -26,6 +26,8 @@ enum Commands {
Sync(SyncArgs), Sync(SyncArgs),
/// Clear the cache. /// Clear the cache.
Clean, Clean,
/// Enumerate the installed packages in the current environment.
Freeze,
} }
#[derive(Args)] #[derive(Args)]
@ -76,6 +78,7 @@ async fn main() -> ExitCode {
.await .await
} }
Commands::Clean => commands::clean(dirs.as_ref().map(ProjectDirs::cache_dir)).await, Commands::Clean => commands::clean(dirs.as_ref().map(ProjectDirs::cache_dir)).await,
Commands::Freeze => commands::freeze().await,
}; };
match result { match result {

View file

@ -13,7 +13,9 @@ license.workspace = true
pep440_rs = { path = "../pep440-rs" } pep440_rs = { path = "../pep440-rs" }
pep508_rs = { path = "../pep508-rs" } pep508_rs = { path = "../pep508-rs" }
platform-host = { path = "../platform-host" } platform-host = { path = "../platform-host" }
puffin-package = { path = "../puffin-package" }
anyhow = { workspace = true } anyhow = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }

View file

@ -7,14 +7,17 @@ use pep508_rs::MarkerEnvironment;
use platform_host::Platform; use platform_host::Platform;
use crate::python_platform::PythonPlatform; use crate::python_platform::PythonPlatform;
pub use crate::site_packages::SitePackages;
mod markers; mod markers;
mod python_platform; mod python_platform;
mod site_packages;
mod virtual_env; mod virtual_env;
/// A Python executable and its associated platform markers. /// A Python executable and its associated platform markers.
#[derive(Debug)] #[derive(Debug)]
pub struct PythonExecutable { pub struct PythonExecutable {
platform: PythonPlatform,
venv: PathBuf, venv: PathBuf,
executable: PathBuf, executable: PathBuf,
markers: MarkerEnvironment, markers: MarkerEnvironment,
@ -22,19 +25,31 @@ pub struct PythonExecutable {
impl PythonExecutable { impl PythonExecutable {
/// Detect the current Python executable from the host environment. /// Detect the current Python executable from the host environment.
pub fn from_env(platform: &Platform) -> Result<Self> { pub fn from_env(platform: Platform) -> Result<Self> {
let platform = PythonPlatform::from(platform); let platform = PythonPlatform::from(platform);
let venv = virtual_env::detect_virtual_env(&platform)?; let venv = virtual_env::detect_virtual_env(&platform)?;
let executable = platform.venv_python(&venv); let executable = platform.venv_python(&venv);
let markers = markers::detect_markers(&executable)?; let markers = markers::detect_markers(&executable)?;
Ok(Self { Ok(Self {
platform,
venv, venv,
executable, executable,
markers, 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. /// Returns the path to the Python virtual environment.
pub fn venv(&self) -> &Path { pub fn venv(&self) -> &Path {
self.venv.as_path() self.venv.as_path()

View file

@ -1,3 +1,4 @@
use std::ops::Deref;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
@ -5,9 +6,9 @@ use platform_host::{Os, Platform};
/// A Python-aware wrapper around [`Platform`]. /// A Python-aware wrapper around [`Platform`].
#[derive(Debug, Clone, Eq, PartialEq)] #[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. /// Returns the path to the `python` executable inside a virtual environment.
pub(crate) fn venv_python(&self, venv_base: impl AsRef<Path>) -> PathBuf { pub(crate) fn venv_python(&self, venv_base: impl AsRef<Path>) -> PathBuf {
let python = if matches!(self.0.os(), Os::Windows) { let python = if matches!(self.0.os(), Os::Windows) {
@ -38,10 +39,34 @@ impl PythonPlatform<'_> {
venv.join("bin") 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<Path>,
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> { impl From<Platform> for PythonPlatform {
fn from(platform: &'a Platform) -> Self { fn from(platform: Platform) -> Self {
Self(platform) Self(platform)
} }
} }
impl Deref for PythonPlatform {
type Target = Platform;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View file

@ -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<PackageName, Version>);
impl SitePackages {
/// Build an index of installed packages from the given Python executable.
pub async fn from_executable(python: &PythonExecutable) -> Result<Self> {
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<Item = (&PackageName, &Version)> {
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: <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#recording-installed-packages>
fn try_from_path(path: &Path) -> Result<Option<Self>> {
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)
}
}

View file

@ -5,7 +5,7 @@ use std::ops::Deref;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PackageName(String); pub struct PackageName(String);
impl Display for PackageName { impl Display for PackageName {