mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
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:
parent
f3015ffc1f
commit
bc1736feff
11 changed files with 155 additions and 10 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
28
crates/puffin-cli/src/commands/freeze.rs
Normal file
28
crates/puffin-cli/src/commands/freeze.rs
Normal 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)
|
||||||
|
}
|
|
@ -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)]
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
68
crates/puffin-interpreter/src/site_packages.rs
Normal file
68
crates/puffin-interpreter/src/site_packages.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue