Add puffin venv command to create virtual environments (#83)

Closes https://github.com/astral-sh/puffin/issues/58.
This commit is contained in:
Charlie Marsh 2023-10-10 13:46:25 -04:00 committed by GitHub
parent a0294a510c
commit d0764bdc23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 76 additions and 28 deletions

2
Cargo.lock generated
View file

@ -1773,6 +1773,7 @@ dependencies = [
"colored", "colored",
"directories", "directories",
"futures", "futures",
"gourgeist",
"indicatif", "indicatif",
"install-wheel-rs", "install-wheel-rs",
"itertools 0.11.0", "itertools 0.11.0",
@ -1791,6 +1792,7 @@ dependencies = [
"tracing-subscriber", "tracing-subscriber",
"tracing-tree", "tracing-tree",
"url", "url",
"which",
] ]
[[package]] [[package]]

View file

@ -23,7 +23,12 @@ pub struct InterpreterInfo {
} }
/// Gets the interpreter.rs info, either cached or by running it. /// Gets the interpreter.rs info, either cached or by running it.
pub fn get_interpreter_info(interpreter: &Utf8Path) -> Result<InterpreterInfo, Error> { pub fn get_interpreter_info(
interpreter: impl AsRef<std::path::Path>,
) -> Result<InterpreterInfo, Error> {
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 cache_dir = crate_cache_dir()?.join("interpreter_info");
let index = seahash::hash(interpreter.as_str().as_bytes()); let index = seahash::hash(interpreter.as_str().as_bytes());

View file

@ -50,6 +50,8 @@ pub enum Error {
#[source] #[source]
err: install_wheel_rs::Error, 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<Utf8PathBuf> { pub(crate) fn crate_cache_dir() -> io::Result<Utf8PathBuf> {
@ -61,11 +63,16 @@ pub(crate) fn crate_cache_dir() -> io::Result<Utf8PathBuf> {
/// Create a virtualenv and if not bare, install `wheel`, `pip` and `setuptools`. /// Create a virtualenv and if not bare, install `wheel`, `pip` and `setuptools`.
pub fn create_venv( pub fn create_venv(
location: &Utf8Path, location: impl AsRef<std::path::Path>,
base_python: &Utf8Path, base_python: impl AsRef<std::path::Path>,
info: &InterpreterInfo, info: &InterpreterInfo,
bare: bool, bare: bool,
) -> Result<(), Error> { ) -> 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)?; let paths = create_bare_venv(location, base_python, info)?;
if !bare { if !bare {

View file

@ -25,7 +25,7 @@ fn run() -> Result<(), gourgeist::Error> {
let location = cli.path.unwrap_or(Utf8PathBuf::from(".venv")); let location = cli.path.unwrap_or(Utf8PathBuf::from(".venv"));
let python = parse_python_cli(cli.python)?; let python = parse_python_cli(cli.python)?;
let data = get_interpreter_info(&python)?; let data = get_interpreter_info(&python)?;
create_venv(&location, &python, &data, cli.bare)?; create_venv(location, &python, &data, cli.bare)?;
Ok(()) Ok(())
} }

View file

@ -8,6 +8,7 @@ name = "puffin"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
gourgeist = { path = "../gourgeist" }
install-wheel-rs = { path = "../install-wheel-rs", default-features = false } install-wheel-rs = { path = "../install-wheel-rs", default-features = false }
pep440_rs = { path = "../pep440-rs" } pep440_rs = { path = "../pep440-rs" }
pep508_rs = { path = "../pep508-rs" } pep508_rs = { path = "../pep508-rs" }
@ -34,3 +35,4 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
tracing-tree = { workspace = true } tracing-tree = { workspace = true }
url = { workspace = true } url = { workspace = true }
which = { workspace = true}

View file

@ -6,6 +6,7 @@ pub(crate) use compile::compile;
pub(crate) use freeze::freeze; pub(crate) use freeze::freeze;
pub(crate) use sync::{sync, SyncFlags}; pub(crate) use sync::{sync, SyncFlags};
pub(crate) use uninstall::uninstall; pub(crate) use uninstall::uninstall;
pub(crate) use venv::venv;
mod clean; mod clean;
mod compile; mod compile;
@ -13,6 +14,7 @@ mod freeze;
mod reporters; mod reporters;
mod sync; mod sync;
mod uninstall; mod uninstall;
mod venv;
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub(crate) enum ExitStatus { pub(crate) enum ExitStatus {

View file

@ -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<ExitStatus> {
// 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)
}

View file

@ -25,6 +25,10 @@ struct Cli {
/// Use verbose output. /// Use verbose output.
#[arg(global = true, long, short, conflicts_with = "quiet")] #[arg(global = true, long, short, conflicts_with = "quiet")]
verbose: bool, verbose: bool,
/// Avoid reading from or writing to the cache.
#[arg(long)]
no_cache: bool,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@ -36,19 +40,17 @@ enum Commands {
/// Clear the cache. /// Clear the cache.
Clean, Clean,
/// Enumerate the installed packages in the current environment. /// Enumerate the installed packages in the current environment.
Freeze(FreezeArgs), Freeze,
/// Uninstall a package. /// Uninstall a package.
Uninstall(UninstallArgs), Uninstall(UninstallArgs),
/// Create a virtual environment.
Venv(VenvArgs),
} }
#[derive(Args)] #[derive(Args)]
struct CompileArgs { struct CompileArgs {
/// Path to the `requirements.txt` file to compile. /// Path to the `requirements.txt` file to compile.
src: PathBuf, src: PathBuf,
/// Avoid reading from or writing to the cache.
#[arg(long)]
no_cache: bool,
} }
#[derive(Args)] #[derive(Args)]
@ -56,30 +58,21 @@ struct SyncArgs {
/// Path to the `requirements.txt` file to install. /// Path to the `requirements.txt` file to install.
src: PathBuf, src: PathBuf,
/// Avoid reading from or writing to the cache.
#[arg(long)]
no_cache: bool,
/// Ignore any installed packages, forcing a re-installation. /// Ignore any installed packages, forcing a re-installation.
#[arg(long)] #[arg(long)]
ignore_installed: bool, ignore_installed: bool,
} }
#[derive(Args)]
struct FreezeArgs {
/// Avoid reading from or writing to the cache.
#[arg(long)]
no_cache: bool,
}
#[derive(Args)] #[derive(Args)]
struct UninstallArgs { struct UninstallArgs {
/// The name of the package to uninstall. /// The name of the package to uninstall.
name: String, name: String,
}
/// Avoid reading from or writing to the cache. #[derive(Args)]
#[arg(long)] struct VenvArgs {
no_cache: bool, /// The path to the virtual environment to create.
name: PathBuf,
} }
#[tokio::main] #[tokio::main]
@ -108,7 +101,7 @@ async fn main() -> ExitCode {
&args.src, &args.src,
dirs.as_ref() dirs.as_ref()
.map(ProjectDirs::cache_dir) .map(ProjectDirs::cache_dir)
.filter(|_| !args.no_cache), .filter(|_| !cli.no_cache),
printer, printer,
) )
.await .await
@ -118,7 +111,7 @@ async fn main() -> ExitCode {
&args.src, &args.src,
dirs.as_ref() dirs.as_ref()
.map(ProjectDirs::cache_dir) .map(ProjectDirs::cache_dir)
.filter(|_| !args.no_cache), .filter(|_| !cli.no_cache),
if args.ignore_installed { if args.ignore_installed {
commands::SyncFlags::IGNORE_INSTALLED commands::SyncFlags::IGNORE_INSTALLED
} else { } else {
@ -131,11 +124,11 @@ async fn main() -> ExitCode {
Commands::Clean => { Commands::Clean => {
commands::clean(dirs.as_ref().map(ProjectDirs::cache_dir), printer).await commands::clean(dirs.as_ref().map(ProjectDirs::cache_dir), printer).await
} }
Commands::Freeze(args) => { Commands::Freeze => {
commands::freeze( commands::freeze(
dirs.as_ref() dirs.as_ref()
.map(ProjectDirs::cache_dir) .map(ProjectDirs::cache_dir)
.filter(|_| !args.no_cache), .filter(|_| !cli.no_cache),
printer, printer,
) )
.await .await
@ -145,11 +138,12 @@ async fn main() -> ExitCode {
&args.name, &args.name,
dirs.as_ref() dirs.as_ref()
.map(ProjectDirs::cache_dir) .map(ProjectDirs::cache_dir)
.filter(|_| !args.no_cache), .filter(|_| !cli.no_cache),
printer, printer,
) )
.await .await
} }
Commands::Venv(args) => commands::venv(&args.name, printer).await,
}; };
match result { match result {