From d0764bdc23840d31eb8d515e42892f95cfaca512 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 10 Oct 2023 13:46:25 -0400 Subject: [PATCH] Add `puffin venv` command to create virtual environments (#83) Closes https://github.com/astral-sh/puffin/issues/58. --- Cargo.lock | 2 ++ crates/gourgeist/src/interpreter.rs | 7 ++++- crates/gourgeist/src/lib.rs | 11 +++++-- crates/gourgeist/src/main.rs | 2 +- crates/puffin-cli/Cargo.toml | 2 ++ crates/puffin-cli/src/commands/mod.rs | 2 ++ crates/puffin-cli/src/commands/venv.rs | 36 ++++++++++++++++++++++ crates/puffin-cli/src/main.rs | 42 +++++++++++--------------- 8 files changed, 76 insertions(+), 28 deletions(-) create mode 100644 crates/puffin-cli/src/commands/venv.rs diff --git a/Cargo.lock b/Cargo.lock index 33c189de6..ec27236b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1773,6 +1773,7 @@ dependencies = [ "colored", "directories", "futures", + "gourgeist", "indicatif", "install-wheel-rs", "itertools 0.11.0", @@ -1791,6 +1792,7 @@ dependencies = [ "tracing-subscriber", "tracing-tree", "url", + "which", ] [[package]] diff --git a/crates/gourgeist/src/interpreter.rs b/crates/gourgeist/src/interpreter.rs index fa9563934..8b9702199 100644 --- a/crates/gourgeist/src/interpreter.rs +++ b/crates/gourgeist/src/interpreter.rs @@ -23,7 +23,12 @@ pub struct InterpreterInfo { } /// Gets the interpreter.rs info, either cached or by running it. -pub fn get_interpreter_info(interpreter: &Utf8Path) -> Result { +pub fn get_interpreter_info( + interpreter: impl AsRef, +) -> Result { + let interpreter = Utf8Path::from_path(interpreter.as_ref()) + .ok_or_else(|| Error::NonUTF8Path(interpreter.as_ref().to_path_buf()))?; + let cache_dir = crate_cache_dir()?.join("interpreter_info"); let index = seahash::hash(interpreter.as_str().as_bytes()); diff --git a/crates/gourgeist/src/lib.rs b/crates/gourgeist/src/lib.rs index bf37f50df..0c9d23293 100644 --- a/crates/gourgeist/src/lib.rs +++ b/crates/gourgeist/src/lib.rs @@ -50,6 +50,8 @@ pub enum Error { #[source] err: install_wheel_rs::Error, }, + #[error("{0} is not a valid UTF-8 path")] + NonUTF8Path(std::path::PathBuf), } pub(crate) fn crate_cache_dir() -> io::Result { @@ -61,11 +63,16 @@ pub(crate) fn crate_cache_dir() -> io::Result { /// Create a virtualenv and if not bare, install `wheel`, `pip` and `setuptools`. pub fn create_venv( - location: &Utf8Path, - base_python: &Utf8Path, + location: impl AsRef, + base_python: impl AsRef, info: &InterpreterInfo, bare: bool, ) -> Result<(), Error> { + let location = Utf8Path::from_path(location.as_ref()) + .ok_or_else(|| Error::NonUTF8Path(location.as_ref().to_path_buf()))?; + let base_python = Utf8Path::from_path(base_python.as_ref()) + .ok_or_else(|| Error::NonUTF8Path(base_python.as_ref().to_path_buf()))?; + let paths = create_bare_venv(location, base_python, info)?; if !bare { diff --git a/crates/gourgeist/src/main.rs b/crates/gourgeist/src/main.rs index a6df9497b..ef4c6cbc1 100644 --- a/crates/gourgeist/src/main.rs +++ b/crates/gourgeist/src/main.rs @@ -25,7 +25,7 @@ fn run() -> Result<(), gourgeist::Error> { let location = cli.path.unwrap_or(Utf8PathBuf::from(".venv")); let python = parse_python_cli(cli.python)?; let data = get_interpreter_info(&python)?; - create_venv(&location, &python, &data, cli.bare)?; + create_venv(location, &python, &data, cli.bare)?; Ok(()) } diff --git a/crates/puffin-cli/Cargo.toml b/crates/puffin-cli/Cargo.toml index 897063289..7024fcc26 100644 --- a/crates/puffin-cli/Cargo.toml +++ b/crates/puffin-cli/Cargo.toml @@ -8,6 +8,7 @@ name = "puffin" path = "src/main.rs" [dependencies] +gourgeist = { path = "../gourgeist" } install-wheel-rs = { path = "../install-wheel-rs", default-features = false } pep440_rs = { path = "../pep440-rs" } pep508_rs = { path = "../pep508-rs" } @@ -34,3 +35,4 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } tracing-tree = { workspace = true } url = { workspace = true } +which = { workspace = true} diff --git a/crates/puffin-cli/src/commands/mod.rs b/crates/puffin-cli/src/commands/mod.rs index 2404fc5ed..fd39aa1b3 100644 --- a/crates/puffin-cli/src/commands/mod.rs +++ b/crates/puffin-cli/src/commands/mod.rs @@ -6,6 +6,7 @@ pub(crate) use compile::compile; pub(crate) use freeze::freeze; pub(crate) use sync::{sync, SyncFlags}; pub(crate) use uninstall::uninstall; +pub(crate) use venv::venv; mod clean; mod compile; @@ -13,6 +14,7 @@ mod freeze; mod reporters; mod sync; mod uninstall; +mod venv; #[derive(Copy, Clone)] pub(crate) enum ExitStatus { diff --git a/crates/puffin-cli/src/commands/venv.rs b/crates/puffin-cli/src/commands/venv.rs new file mode 100644 index 000000000..f05107d5c --- /dev/null +++ b/crates/puffin-cli/src/commands/venv.rs @@ -0,0 +1,36 @@ +use std::fmt::Write; +use std::path::Path; + +use anyhow::Result; +use colored::Colorize; + +use crate::commands::ExitStatus; +use crate::printer::Printer; + +/// Create a virtual environment. +pub(crate) async fn venv(path: &Path, mut printer: Printer) -> Result { + // Locate the Python interpreter. + // TODO(charlie): Look at how Maturin discovers and ranks all the available Python interpreters. + let executable = which::which("python3").or_else(|_| which::which("python"))?; + let interpreter_info = gourgeist::get_interpreter_info(&executable)?; + writeln!( + printer, + "Using Python interpreter: {}", + format!("{}", executable.display()).cyan() + )?; + + // If the path already exists, remove it. + tokio::fs::remove_file(path).await.ok(); + tokio::fs::remove_dir_all(path).await.ok(); + + writeln!( + printer, + "Creating virtual environment at: {}", + format!("{}", path.display()).cyan() + )?; + + // Create the virtual environment. + gourgeist::create_venv(path, &executable, &interpreter_info, true)?; + + Ok(ExitStatus::Success) +} diff --git a/crates/puffin-cli/src/main.rs b/crates/puffin-cli/src/main.rs index 1cd83e5f0..bc489b7b0 100644 --- a/crates/puffin-cli/src/main.rs +++ b/crates/puffin-cli/src/main.rs @@ -25,6 +25,10 @@ struct Cli { /// Use verbose output. #[arg(global = true, long, short, conflicts_with = "quiet")] verbose: bool, + + /// Avoid reading from or writing to the cache. + #[arg(long)] + no_cache: bool, } #[derive(Subcommand)] @@ -36,19 +40,17 @@ enum Commands { /// Clear the cache. Clean, /// Enumerate the installed packages in the current environment. - Freeze(FreezeArgs), + Freeze, /// Uninstall a package. Uninstall(UninstallArgs), + /// Create a virtual environment. + Venv(VenvArgs), } #[derive(Args)] struct CompileArgs { /// Path to the `requirements.txt` file to compile. src: PathBuf, - - /// Avoid reading from or writing to the cache. - #[arg(long)] - no_cache: bool, } #[derive(Args)] @@ -56,30 +58,21 @@ struct SyncArgs { /// Path to the `requirements.txt` file to install. src: PathBuf, - /// 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, } -#[derive(Args)] -struct FreezeArgs { - /// Avoid reading from or writing to the cache. - #[arg(long)] - no_cache: bool, -} - #[derive(Args)] struct UninstallArgs { /// The name of the package to uninstall. name: String, +} - /// Avoid reading from or writing to the cache. - #[arg(long)] - no_cache: bool, +#[derive(Args)] +struct VenvArgs { + /// The path to the virtual environment to create. + name: PathBuf, } #[tokio::main] @@ -108,7 +101,7 @@ async fn main() -> ExitCode { &args.src, dirs.as_ref() .map(ProjectDirs::cache_dir) - .filter(|_| !args.no_cache), + .filter(|_| !cli.no_cache), printer, ) .await @@ -118,7 +111,7 @@ async fn main() -> ExitCode { &args.src, dirs.as_ref() .map(ProjectDirs::cache_dir) - .filter(|_| !args.no_cache), + .filter(|_| !cli.no_cache), if args.ignore_installed { commands::SyncFlags::IGNORE_INSTALLED } else { @@ -131,11 +124,11 @@ async fn main() -> ExitCode { Commands::Clean => { commands::clean(dirs.as_ref().map(ProjectDirs::cache_dir), printer).await } - Commands::Freeze(args) => { + Commands::Freeze => { commands::freeze( dirs.as_ref() .map(ProjectDirs::cache_dir) - .filter(|_| !args.no_cache), + .filter(|_| !cli.no_cache), printer, ) .await @@ -145,11 +138,12 @@ async fn main() -> ExitCode { &args.name, dirs.as_ref() .map(ProjectDirs::cache_dir) - .filter(|_| !args.no_cache), + .filter(|_| !cli.no_cache), printer, ) .await } + Commands::Venv(args) => commands::venv(&args.name, printer).await, }; match result {