Skip to content

Commit

Permalink
Add stable dir for scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Feb 9, 2025
1 parent 1b21257 commit 610e1a4
Show file tree
Hide file tree
Showing 6 changed files with 370 additions and 193 deletions.
5 changes: 5 additions & 0 deletions crates/uv-cache/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ impl CacheShard {
fs_err::create_dir_all(self.as_ref())?;
LockedFile::acquire(self.join(".lock"), self.display()).await
}

/// Return the [`CacheShard`] as a [`PathBuf`].
pub fn into_path_buf(self) -> PathBuf {
self.0
}
}

impl AsRef<Path> for CacheShard {
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-scripts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ impl Pep723Item {
}

/// A reference to a PEP 723 item.
#[derive(Debug)]
#[derive(Debug, Copy, Clone)]
pub enum Pep723ItemRef<'item> {
/// A PEP 723 script read from disk.
Script(&'item Pep723Script),
Expand Down
93 changes: 1 addition & 92 deletions crates/uv/src/commands/project/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,12 @@ use tracing::debug;
use uv_cache::{Cache, CacheBucket};
use uv_cache_key::{cache_digest, hash_digest};
use uv_client::Connectivity;
use uv_configuration::{
Concurrency, DevGroupsManifest, ExtrasSpecification, InstallOptions, PreviewMode, TrustedHost,
};
use uv_configuration::{Concurrency, PreviewMode, TrustedHost};
use uv_distribution_types::{Name, Resolution};
use uv_python::{Interpreter, PythonEnvironment};
use uv_resolver::Installable;

use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
use crate::commands::pip::operations::Modifications;
use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::{
resolve_environment, sync_environment, EnvironmentSpecification, PlatformState, ProjectError,
};
Expand Down Expand Up @@ -68,93 +64,6 @@ impl CachedEnvironment {
.await?,
);

Self::from_resolution(
resolution,
interpreter,
settings,
state,
install,
installer_metadata,
connectivity,
concurrency,
native_tls,
allow_insecure_host,
cache,
printer,
preview,
)
.await
}

/// Get or create an [`CachedEnvironment`] based on a given [`InstallTarget`].
pub(crate) async fn from_lock(
target: InstallTarget<'_>,
extras: &ExtrasSpecification,
dev: &DevGroupsManifest,
install_options: InstallOptions,
settings: &ResolverInstallerSettings,
interpreter: &Interpreter,
state: &PlatformState,
install: Box<dyn InstallLogger>,
installer_metadata: bool,
connectivity: Connectivity,
concurrency: Concurrency,
native_tls: bool,
allow_insecure_host: &[TrustedHost],
cache: &Cache,
printer: Printer,
preview: PreviewMode,
) -> Result<Self, ProjectError> {
let interpreter = Self::base_interpreter(interpreter, cache)?;

// Determine the tags, markers, and interpreter to use for resolution.
let tags = interpreter.tags()?;
let marker_env = interpreter.resolver_marker_environment();

// Read the lockfile.
let resolution = target.to_resolution(
&marker_env,
tags,
extras,
dev,
&settings.build_options,
&install_options,
)?;

Self::from_resolution(
resolution,
interpreter,
settings,
state,
install,
installer_metadata,
connectivity,
concurrency,
native_tls,
allow_insecure_host,
cache,
printer,
preview,
)
.await
}

/// Get or create an [`CachedEnvironment`] based on a given [`Resolution`].
pub(crate) async fn from_resolution(
resolution: Resolution,
interpreter: Interpreter,
settings: &ResolverInstallerSettings,
state: &PlatformState,
install: Box<dyn InstallLogger>,
installer_metadata: bool,
connectivity: Connectivity,
concurrency: Concurrency,
native_tls: bool,
allow_insecure_host: &[TrustedHost],
cache: &Cache,
printer: Printer,
preview: PreviewMode,
) -> Result<Self, ProjectError> {
// Hash the resolution by hashing the generated lockfile.
// TODO(charlie): If the resolution contains any mutable metadata (like a path or URL
// dependency), skip this step.
Expand Down
175 changes: 169 additions & 6 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use std::sync::Arc;

use itertools::Itertools;
use owo_colors::OwoColorize;
use tracing::debug;
use tracing::{debug, warn};

use uv_cache::Cache;
use uv_cache::{Cache, CacheBucket};
use uv_cache_key::cache_digest;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Expand Down Expand Up @@ -37,7 +37,7 @@ use uv_resolver::{
FlatIndex, Lock, OptionsBuilder, PythonRequirement, RequiresPython, ResolverEnvironment,
ResolverOutput,
};
use uv_scripts::Pep723ItemRef;
use uv_scripts::{Pep723ItemRef, Pep723Script};
use uv_settings::PythonInstallMirrors;
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once};
Expand Down Expand Up @@ -515,9 +515,30 @@ fn validate_script_requires_python(

/// An interpreter suitable for a PEP 723 script.
#[derive(Debug, Clone)]
pub(crate) struct ScriptInterpreter(Interpreter);
#[allow(clippy::large_enum_variant)]
pub(crate) enum ScriptInterpreter {
/// An interpreter to use to create a new script environment.
Interpreter(Interpreter),
/// An interpreter from an existing script environment.
Environment(PythonEnvironment),
}

impl ScriptInterpreter {
/// Return the expected virtual environment path for the [`Pep723Script`].
pub(crate) fn root(script: &Pep723Script, cache: &Cache) -> PathBuf {
let digest = cache_digest(&script.path);
let entry = if let Some(file_name) = script.path.file_stem().and_then(|name| name.to_str())
{
// Replace any non-ASCII characters with underscores.
format!("{file_name}-{digest}",)
} else {
digest
};
cache
.shard(CacheBucket::Environments, entry)
.into_path_buf()
}

/// Discover the interpreter to use for the current [`Pep723Item`].
pub(crate) async fn discover(
script: Pep723ItemRef<'_>,
Expand All @@ -541,6 +562,43 @@ impl ScriptInterpreter {
requires_python,
} = ScriptPython::from_request(python_request, workspace, script, no_config).await?;

// If this is a local script, use a stable virtual environment.
if let Pep723ItemRef::Script(script) = script {
let root = Self::root(script, cache);
match PythonEnvironment::from_root(&root, cache) {
Ok(venv) => {
if python_request.as_ref().map_or(true, |request| {
if request.satisfied(venv.interpreter(), cache) {
debug!(
"The script environment's Python version satisfies `{}`",
request.to_canonical_string()
);
true
} else {
debug!(
"The script environment's Python version does not satisfy `{}`",
request.to_canonical_string()
);
false
}
}) {
if let Some((requires_python, ..)) = requires_python.as_ref() {
if requires_python.contains(venv.interpreter().python_version()) {
return Ok(Self::Environment(venv));
}
debug!(
"The script environment's Python version does not meet the script's Python requirement: `{requires_python}`"
);
} else {
return Ok(Self::Environment(venv));
}
}
}
Err(uv_python::Error::MissingEnvironment(_)) => {}
Err(err) => warn!("Ignoring existing script environment: {err}"),
};
}

let client_builder = BaseClientBuilder::new()
.connectivity(connectivity)
.native_tls(native_tls)
Expand Down Expand Up @@ -574,12 +632,24 @@ impl ScriptInterpreter {
warn_user!("{err}");
}

Ok(Self(interpreter))
Ok(Self::Interpreter(interpreter))
}

/// Consume the [`PythonInstallation`] and return the [`Interpreter`].
pub(crate) fn into_interpreter(self) -> Interpreter {
self.0
match self {
ScriptInterpreter::Interpreter(interpreter) => interpreter,
ScriptInterpreter::Environment(venv) => venv.into_interpreter(),
}
}

/// Grab a file lock for the script to prevent concurrent writes across processes.
pub(crate) async fn lock(script: &Pep723Script) -> Result<LockedFile, std::io::Error> {
LockedFile::acquire(
std::env::temp_dir().join(format!("uv-{}.lock", cache_digest(&script.path))),
script.path.simplified_display(),
)
.await
}
}

Expand Down Expand Up @@ -1098,6 +1168,99 @@ impl ProjectEnvironment {
}
}

/// The Python environment for a project.
#[derive(Debug)]
struct ScriptEnvironment(PythonEnvironment);

impl ScriptEnvironment {
/// Initialize a virtual environment for a PEP 723 script.
pub(crate) async fn get_or_init(
script: &Pep723Script,
python_request: Option<PythonRequest>,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
connectivity: Connectivity,
native_tls: bool,
allow_insecure_host: &[TrustedHost],
install_mirrors: &PythonInstallMirrors,
no_config: bool,
cache: &Cache,
printer: Printer,
) -> Result<Self, ProjectError> {
// Lock the script environment to avoid synchronization issues.
let _lock = ScriptInterpreter::lock(script).await?;

match ScriptInterpreter::discover(
Pep723ItemRef::Script(script),
python_request,
python_preference,
python_downloads,
connectivity,
native_tls,
allow_insecure_host,
install_mirrors,
no_config,
cache,
printer,
)
.await?
{
// If we found an existing, compatible environment, use it.
ScriptInterpreter::Environment(environment) => Ok(Self(environment)),

// Otherwise, create a virtual environment with the discovered interpreter.
ScriptInterpreter::Interpreter(interpreter) => {
let root = ScriptInterpreter::root(script, cache);

// Remove the existing virtual environment.
match fs_err::remove_dir_all(&root) {
Ok(()) => {
debug!(
"Removed virtual environment at: {}",
root.user_display().cyan()
);
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
};

debug!(
"Creating script environment at: {}",
root.user_display().cyan()
);

// Determine a prompt for the environment, in order of preference:
//
// 1) The name of the script
// 2) No prompt
let prompt = script
.path
.file_name()
.map(|f| f.to_string_lossy().to_string())
.map(uv_virtualenv::Prompt::Static)
.unwrap_or(uv_virtualenv::Prompt::None);

let environment = uv_virtualenv::create_venv(
&root,
interpreter,
prompt,
false,
false,
false,
false,
)?;

Ok(Self(environment))
}
}
}

/// Convert the [`ScriptEnvironment`] into a [`PythonEnvironment`].
pub(crate) fn into_environment(self) -> PythonEnvironment {
self.0
}
}

/// Resolve any [`UnresolvedRequirementSpecification`] into a fully-qualified [`Requirement`].
pub(crate) async fn resolve_names(
requirements: Vec<UnresolvedRequirementSpecification>,
Expand Down
Loading

0 comments on commit 610e1a4

Please sign in to comment.