mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 02:48:17 +00:00
Use a stable directory for (local) script virtual environments (#11347)
## Summary Today, scripts use `CachedEnvironment`, which results in a different virtual environment path every time the interpreter changes _or_ the project requirements change. This makes it impossible to provide users with a stable path to the script that they can use for (e.g.) directing their editor. This PR modifies `uv run` to use a stable path for local scripts (we continue to use `CachedEnvironment` for remote scripts and scripts from `stdin`). The logic now looks a lot more like it does for projects: we `get_or_init` an environment, etc. For now, the path to the script is like: `environments-v1/4485801245a4732f`, where `4485801245a4732f` is a SHA of the absolute path to the script. But I'm not picky on that :)
This commit is contained in:
parent
ba5efa8aa4
commit
79ad7a1ab9
7 changed files with 430 additions and 196 deletions
|
@ -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 {
|
||||
|
|
|
@ -16,7 +16,7 @@ pub(crate) fn url_to_precise(url: VerbatimParsedUrl, git: &GitResolver) -> Verba
|
|||
|
||||
let Some(new_git_url) = git.precise(git_url.clone()) else {
|
||||
if cfg!(debug_assertions) {
|
||||
panic!("Unresolved Git URL: {}, {git_url:?}", url.verbatim,);
|
||||
panic!("Unresolved Git URL: {}, {git_url:?}", url.verbatim);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
@ -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.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write;
|
||||
use std::ops::Deref;
|
||||
|
@ -6,9 +7,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::{
|
||||
|
@ -38,7 +39,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};
|
||||
|
@ -519,9 +520,33 @@ 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(name) = script
|
||||
.path
|
||||
.file_stem()
|
||||
.and_then(|name| name.to_str())
|
||||
.and_then(cache_name)
|
||||
{
|
||||
format!("{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<'_>,
|
||||
|
@ -545,6 +570,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)
|
||||
|
@ -578,12 +640,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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1140,6 +1214,99 @@ impl Deref for ProjectEnvironment {
|
|||
}
|
||||
}
|
||||
|
||||
/// The Python environment for a script.
|
||||
#[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>,
|
||||
|
@ -2094,3 +2261,56 @@ fn warn_on_requirements_txt_setting(
|
|||
warn_user_once!("Ignoring `--no-binary` setting from requirements file. Instead, use the `--no-build` command-line argument, or set `no-build` in a `uv.toml` or `pyproject.toml` file.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize a filename for use in a cache entry.
|
||||
///
|
||||
/// Replaces non-alphanumeric characters with dashes, and lowercases the filename.
|
||||
fn cache_name(name: &str) -> Option<Cow<'_, str>> {
|
||||
if name.bytes().all(|c| matches!(c, b'0'..=b'9' | b'a'..=b'f')) {
|
||||
return if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Cow::Borrowed(name))
|
||||
};
|
||||
}
|
||||
let mut normalized = String::with_capacity(name.len());
|
||||
let mut dash = false;
|
||||
for char in name.bytes() {
|
||||
match char {
|
||||
b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' => {
|
||||
dash = false;
|
||||
normalized.push(char.to_ascii_lowercase() as char);
|
||||
}
|
||||
_ => {
|
||||
if !dash {
|
||||
normalized.push('-');
|
||||
dash = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if normalized.ends_with('-') {
|
||||
normalized.pop();
|
||||
}
|
||||
if normalized.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Cow::Owned(normalized))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cache_name() {
|
||||
assert_eq!(cache_name("foo"), Some("foo".into()));
|
||||
assert_eq!(cache_name("foo-bar"), Some("foo-bar".into()));
|
||||
assert_eq!(cache_name("foo_bar"), Some("foo-bar".into()));
|
||||
assert_eq!(cache_name("foo-bar_baz"), Some("foo-bar-baz".into()));
|
||||
assert_eq!(cache_name("foo-bar_baz_"), Some("foo-bar-baz".into()));
|
||||
assert_eq!(cache_name("foo-_bar_baz"), Some("foo-bar-baz".into()));
|
||||
assert_eq!(cache_name("_+-_"), None);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,9 +46,9 @@ use crate::commands::project::install_target::InstallTarget;
|
|||
use crate::commands::project::lock::LockMode;
|
||||
use crate::commands::project::lock_target::LockTarget;
|
||||
use crate::commands::project::{
|
||||
default_dependency_groups, validate_project_requires_python, DependencyGroupsTarget,
|
||||
EnvironmentSpecification, ProjectEnvironment, ProjectError, ScriptInterpreter, UniversalState,
|
||||
WorkspacePython,
|
||||
default_dependency_groups, update_environment, validate_project_requires_python,
|
||||
DependencyGroupsTarget, EnvironmentSpecification, ProjectEnvironment, ProjectError,
|
||||
ScriptEnvironment, ScriptInterpreter, UniversalState, WorkspacePython,
|
||||
};
|
||||
use crate::commands::reporters::PythonDownloadReporter;
|
||||
use crate::commands::run::run_to_completion;
|
||||
|
@ -191,23 +191,6 @@ pub(crate) async fn run(
|
|||
}
|
||||
}
|
||||
|
||||
// Discover the interpreter for the script.
|
||||
let interpreter = ScriptInterpreter::discover(
|
||||
(&script).into(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
python_preference,
|
||||
python_downloads,
|
||||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
&install_mirrors,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_interpreter();
|
||||
|
||||
// If a lockfile already exists, lock the script.
|
||||
if let Some(target) = script
|
||||
.as_script()
|
||||
|
@ -216,17 +199,34 @@ pub(crate) async fn run(
|
|||
{
|
||||
debug!("Found existing lockfile for script");
|
||||
|
||||
// Discover the interpreter for the script.
|
||||
let environment = ScriptEnvironment::get_or_init(
|
||||
script.as_script().unwrap(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
python_preference,
|
||||
python_downloads,
|
||||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
&install_mirrors,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_environment();
|
||||
|
||||
// Determine the lock mode.
|
||||
let mode = if frozen {
|
||||
LockMode::Frozen
|
||||
} else if locked {
|
||||
LockMode::Locked(&interpreter)
|
||||
LockMode::Locked(environment.interpreter())
|
||||
} else {
|
||||
LockMode::Write(&interpreter)
|
||||
LockMode::Write(environment.interpreter())
|
||||
};
|
||||
|
||||
// Generate a lockfile.
|
||||
let lock = project::lock::do_safe_lock(
|
||||
let lock = match project::lock::do_safe_lock(
|
||||
mode,
|
||||
target,
|
||||
settings.as_ref().into(),
|
||||
|
@ -244,19 +244,35 @@ pub(crate) async fn run(
|
|||
printer,
|
||||
preview,
|
||||
)
|
||||
.await?
|
||||
.into_lock();
|
||||
.await
|
||||
{
|
||||
Ok(result) => result.into_lock(),
|
||||
Err(ProjectError::Operation(err)) => {
|
||||
return diagnostics::OperationDiagnostic::native_tls(native_tls)
|
||||
.with_context("script")
|
||||
.report(err)
|
||||
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
let result = CachedEnvironment::from_lock(
|
||||
InstallTarget::Script {
|
||||
script: script.as_script().unwrap(),
|
||||
lock: &lock,
|
||||
},
|
||||
&ExtrasSpecification::default(),
|
||||
&DevGroupsSpecification::default().with_defaults(Vec::new()),
|
||||
InstallOptions::default(),
|
||||
&settings,
|
||||
&interpreter,
|
||||
// Sync the environment.
|
||||
let target = InstallTarget::Script {
|
||||
script: script.as_script().unwrap(),
|
||||
lock: &lock,
|
||||
};
|
||||
|
||||
let install_options = InstallOptions::default();
|
||||
|
||||
match project::sync::do_sync(
|
||||
target,
|
||||
&environment,
|
||||
&extras,
|
||||
&dev.with_defaults(Vec::new()),
|
||||
editable,
|
||||
install_options,
|
||||
modifications,
|
||||
settings.as_ref().into(),
|
||||
&sync_state,
|
||||
if show_resolution {
|
||||
Box::new(DefaultInstallLogger)
|
||||
|
@ -269,13 +285,13 @@ pub(crate) async fn run(
|
|||
native_tls,
|
||||
allow_insecure_host,
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
preview,
|
||||
)
|
||||
.await;
|
||||
|
||||
let environment = match result {
|
||||
Ok(resolution) => resolution,
|
||||
.await
|
||||
{
|
||||
Ok(()) => {}
|
||||
Err(ProjectError::Operation(err)) => {
|
||||
return diagnostics::OperationDiagnostic::native_tls(native_tls)
|
||||
.with_context("script")
|
||||
|
@ -283,10 +299,7 @@ pub(crate) async fn run(
|
|||
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
// Clear any existing overlay.
|
||||
environment.clear_overlay()?;
|
||||
}
|
||||
|
||||
Some(environment.into_interpreter())
|
||||
} else {
|
||||
|
@ -312,14 +325,14 @@ pub(crate) async fn run(
|
|||
.to_owned(),
|
||||
Pep723Item::Stdin(..) | Pep723Item::Remote(..) => std::env::current_dir()?,
|
||||
};
|
||||
let script = script.into_metadata();
|
||||
let metadata = script.metadata();
|
||||
|
||||
// Install the script requirements, if necessary. Otherwise, use an isolated environment.
|
||||
if let Some(dependencies) = script.dependencies {
|
||||
if let Some(dependencies) = metadata.dependencies.as_ref() {
|
||||
// Collect any `tool.uv.index` from the script.
|
||||
let empty = Vec::default();
|
||||
let script_indexes = match settings.sources {
|
||||
SourceStrategy::Enabled => script
|
||||
SourceStrategy::Enabled => metadata
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
|
@ -331,7 +344,7 @@ pub(crate) async fn run(
|
|||
// Collect any `tool.uv.sources` from the script.
|
||||
let empty = BTreeMap::default();
|
||||
let script_sources = match settings.sources {
|
||||
SourceStrategy::Enabled => script
|
||||
SourceStrategy::Enabled => metadata
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
|
@ -341,7 +354,8 @@ pub(crate) async fn run(
|
|||
};
|
||||
|
||||
let requirements = dependencies
|
||||
.into_iter()
|
||||
.iter()
|
||||
.cloned()
|
||||
.flat_map(|requirement| {
|
||||
LoweredRequirement::from_non_workspace_requirement(
|
||||
requirement,
|
||||
|
@ -353,7 +367,7 @@ pub(crate) async fn run(
|
|||
.map_ok(LoweredRequirement::into_inner)
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
let constraints = script
|
||||
let constraints = metadata
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
|
@ -372,7 +386,7 @@ pub(crate) async fn run(
|
|||
.map_ok(LoweredRequirement::into_inner)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let overrides = script
|
||||
let overrides = metadata
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
|
@ -394,49 +408,139 @@ pub(crate) async fn run(
|
|||
|
||||
let spec =
|
||||
RequirementsSpecification::from_overrides(requirements, constraints, overrides);
|
||||
let result = CachedEnvironment::from_spec(
|
||||
EnvironmentSpecification::from(spec),
|
||||
&interpreter,
|
||||
&settings,
|
||||
&sync_state,
|
||||
if show_resolution {
|
||||
Box::new(DefaultResolveLogger)
|
||||
} else {
|
||||
Box::new(SummaryResolveLogger)
|
||||
},
|
||||
if show_resolution {
|
||||
Box::new(DefaultInstallLogger)
|
||||
} else {
|
||||
Box::new(SummaryInstallLogger)
|
||||
},
|
||||
installer_metadata,
|
||||
connectivity,
|
||||
concurrency,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
cache,
|
||||
printer,
|
||||
preview,
|
||||
)
|
||||
.await;
|
||||
|
||||
let environment = match result {
|
||||
Ok(resolution) => resolution,
|
||||
Err(ProjectError::Operation(err)) => {
|
||||
return diagnostics::OperationDiagnostic::native_tls(native_tls)
|
||||
.with_context("script")
|
||||
.report(err)
|
||||
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
|
||||
if let Some(script) = script.as_script() {
|
||||
// If the script is a local file, use a persistent environment.
|
||||
let environment = ScriptEnvironment::get_or_init(
|
||||
script,
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
python_preference,
|
||||
python_downloads,
|
||||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
&install_mirrors,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_environment();
|
||||
|
||||
match update_environment(
|
||||
environment,
|
||||
spec,
|
||||
modifications,
|
||||
&settings,
|
||||
&sync_state,
|
||||
if show_resolution {
|
||||
Box::new(DefaultResolveLogger)
|
||||
} else {
|
||||
Box::new(SummaryResolveLogger)
|
||||
},
|
||||
if show_resolution {
|
||||
Box::new(DefaultInstallLogger)
|
||||
} else {
|
||||
Box::new(SummaryInstallLogger)
|
||||
},
|
||||
installer_metadata,
|
||||
connectivity,
|
||||
concurrency,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
cache,
|
||||
printer,
|
||||
preview,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(update) => Some(update.into_environment().into_interpreter()),
|
||||
Err(ProjectError::Operation(err)) => {
|
||||
return diagnostics::OperationDiagnostic::native_tls(native_tls)
|
||||
.with_context("script")
|
||||
.report(err)
|
||||
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
} else {
|
||||
// Otherwise, use an ephemeral environment.
|
||||
let interpreter = ScriptInterpreter::discover(
|
||||
(&script).into(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
python_preference,
|
||||
python_downloads,
|
||||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
&install_mirrors,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_interpreter();
|
||||
|
||||
// Clear any existing overlay.
|
||||
environment.clear_overlay()?;
|
||||
let result = CachedEnvironment::from_spec(
|
||||
EnvironmentSpecification::from(spec),
|
||||
&interpreter,
|
||||
&settings,
|
||||
&sync_state,
|
||||
if show_resolution {
|
||||
Box::new(DefaultResolveLogger)
|
||||
} else {
|
||||
Box::new(SummaryResolveLogger)
|
||||
},
|
||||
if show_resolution {
|
||||
Box::new(DefaultInstallLogger)
|
||||
} else {
|
||||
Box::new(SummaryInstallLogger)
|
||||
},
|
||||
installer_metadata,
|
||||
connectivity,
|
||||
concurrency,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
cache,
|
||||
printer,
|
||||
preview,
|
||||
)
|
||||
.await;
|
||||
|
||||
Some(environment.into_interpreter())
|
||||
let environment = match result {
|
||||
Ok(resolution) => resolution,
|
||||
Err(ProjectError::Operation(err)) => {
|
||||
return diagnostics::OperationDiagnostic::native_tls(native_tls)
|
||||
.with_context("script")
|
||||
.report(err)
|
||||
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
// Clear any existing overlay.
|
||||
environment.clear_overlay()?;
|
||||
|
||||
Some(environment.into_interpreter())
|
||||
}
|
||||
} else {
|
||||
// Create a virtual environment.
|
||||
let interpreter = ScriptInterpreter::discover(
|
||||
(&script).into(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
python_preference,
|
||||
python_downloads,
|
||||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
&install_mirrors,
|
||||
no_config,
|
||||
cache,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_interpreter();
|
||||
|
||||
temp_dir = cache.venv_dir()?;
|
||||
let environment = uv_virtualenv::create_venv(
|
||||
temp_dir.path(),
|
||||
|
@ -471,7 +575,7 @@ pub(crate) async fn run(
|
|||
warn_user!("Extras are not supported for Python scripts with inline metadata");
|
||||
}
|
||||
for flag in dev.history().as_flags_pretty() {
|
||||
warn_user!("`{flag}` is not supported for Python scripts with inline metadata",);
|
||||
warn_user!("`{flag}` is not supported for Python scripts with inline metadata");
|
||||
}
|
||||
if all_packages {
|
||||
warn_user!(
|
||||
|
|
|
@ -319,7 +319,6 @@ fn run_pep723_script() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
// But neither invocation should create a lockfile.
|
||||
|
@ -847,8 +846,7 @@ fn run_pep723_script_lock() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
Audited 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
// With a lockfile, running with `--locked` should not warn.
|
||||
|
@ -860,6 +858,7 @@ fn run_pep723_script_lock() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
Audited 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
// Modify the metadata.
|
||||
|
@ -895,6 +894,7 @@ fn run_pep723_script_lock() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Audited 1 package in [TIME]
|
||||
Traceback (most recent call last):
|
||||
File "[TEMP_DIR]/main.py", line 8, in <module>
|
||||
import anyio
|
||||
|
@ -3276,8 +3276,6 @@ fn run_gui_script_explicit_windows() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved in [TIME]
|
||||
Audited in [TIME]
|
||||
Using executable: pythonw.exe
|
||||
"###);
|
||||
|
||||
|
@ -3341,9 +3339,7 @@ fn run_gui_script_explicit_unix() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved in [TIME]
|
||||
Audited in [TIME]
|
||||
Using executable: python3
|
||||
Using executable: python
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue