diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index c0c71d4f582..7e8425022f1 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -7,7 +7,7 @@ use tracing::info_span; use uv_client::{DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, DEFAULT_READ_TIMEOUT_UPLOAD}; use uv_configuration::RequiredVersion; use uv_dirs::{system_config_file, user_config_dir}; -use uv_distribution_types::Origin; +use uv_distribution_types::{IndexUrlError, Origin}; use uv_flags::EnvironmentFlags; use uv_fs::Simplified; use uv_normalize::{GroupName, PackageName}; @@ -32,6 +32,11 @@ impl FilesystemOptions { pub fn into_options(self) -> Options { self.0 } + + /// Resolve the [`FilesystemOptions`] relative to the given root directory. + pub fn relative_to(self, root_dir: &Path) -> Result { + Ok(Self(self.0.relative_to(root_dir)?)) + } } impl Deref for FilesystemOptions { diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 23f4ca7f902..51a8c36bdbc 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -26,7 +26,7 @@ use uv_distribution_types::{ Identifier, Index, IndexLocations, IndexName, IndexUrl, NameRequirementSpecification, Requirement, RequirementSource, UnresolvedRequirement, }; -use uv_fs::{CWD, LockedFile, LockedFileError, Simplified}; +use uv_fs::{LockedFile, LockedFileError, Simplified}; use uv_git::store_credentials; use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, DefaultGroups, ExtraName, PackageName}; use uv_pep508::{MarkerTree, VersionOrUrl}; @@ -695,7 +695,9 @@ pub(crate) async fn add( // Add any indexes that were provided on the command-line, in priority order. if !raw { let root_dir = match &target { - AddTarget::Script(_, _) => CWD.as_path(), + AddTarget::Script(script, _) => { + script.path.parent().expect("script path has no parent") + } AddTarget::Project(project, _) => project.root(), }; let locations = IndexLocations::new(indexes, Vec::new(), false); diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index f03b7ddb404..91cc40485be 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -21,7 +21,7 @@ use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution::{DistributionDatabase, LoweredExtraBuildDependencies, LoweredRequirement}; use uv_distribution_types::{ ExtraBuildRequirement, ExtraBuildRequires, HashGeneration, Index, IndexCredentialsError, - Requirement, RequiresPython, Resolution, UnresolvedRequirement, + IndexUrlError, Requirement, RequiresPython, Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification, }; use uv_fs::{CWD, LockedFile, LockedFileError, LockedFileMode, Simplified, verbatim_path}; @@ -312,6 +312,9 @@ pub(crate) enum ProjectError { #[error(transparent)] IndexCredentials(#[from] IndexCredentialsError), + #[error(transparent)] + IndexUrl(#[from] IndexUrlError), + #[error(transparent)] Python(#[from] uv_python::Error), @@ -3319,7 +3322,12 @@ pub(crate) async fn script_specification( }; let script_dir = script.directory()?; - let script_indexes = script.indexes(&settings.sources); + let script_indexes = script + .indexes(&settings.sources) + .iter() + .cloned() + .map(|index| index.relative_to(&script_dir)) + .collect::, _>>()?; let script_sources = script.sources(&settings.sources); let mut requirements = Vec::new(); @@ -3329,7 +3337,7 @@ pub(crate) async fn script_specification( requirement, script_dir.as_ref(), script_sources.as_ref(), - script_indexes, + &script_indexes, &settings.index_locations, cache, workspace_cache, @@ -3356,7 +3364,7 @@ pub(crate) async fn script_specification( requirement, script_dir.as_ref(), script_sources.as_ref(), - script_indexes, + &script_indexes, &settings.index_locations, cache, workspace_cache, @@ -3386,7 +3394,7 @@ pub(crate) async fn script_specification( requirement, script_dir.as_ref(), script_sources.as_ref(), - script_indexes, + &script_indexes, &settings.index_locations, cache, workspace_cache, @@ -3406,7 +3414,7 @@ pub(crate) async fn script_specification( requirement, script_dir.as_ref(), script_sources.as_ref(), - script_indexes, + &script_indexes, &settings.index_locations, cache, workspace_cache, @@ -3453,7 +3461,12 @@ pub(crate) async fn script_extra_build_requires( credentials_cache: &CredentialsCache, ) -> Result { let script_dir = script.directory()?; - let script_indexes = script.indexes(&settings.sources); + let script_indexes = script + .indexes(&settings.sources) + .iter() + .cloned() + .map(|index| index.relative_to(&script_dir)) + .collect::, _>>()?; let script_sources = script.sources(&settings.sources); // Collect any `tool.uv.extra-build-dependencies` from the script. @@ -3480,7 +3493,7 @@ pub(crate) async fn script_extra_build_requires( requirement, script_dir.as_ref(), script_sources.as_ref(), - script_indexes, + &script_indexes, &settings.index_locations, cache, workspace_cache, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index c8d0056754d..427c202205b 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -487,14 +487,22 @@ async fn run_with_workspace_cache( }; // If the target is a PEP 723 script, merge the metadata into the filesystem metadata. - let filesystem = script + let script_filesystem = script .as_ref() .map(Pep723Item::metadata) .and_then(|metadata| metadata.tool.as_ref()) .and_then(|tool| tool.uv.as_ref()) .map(|uv| Options::simple(uv.globals.clone(), uv.top_level.clone())) - .map(FilesystemOptions::from) - .combine(filesystem); + .map(FilesystemOptions::from); + let script_filesystem = if let Some(Pep723Item::Script(script)) = script.as_ref() { + let script_dir = script.path.parent().expect("script path has no parent"); + script_filesystem + .map(|options| options.relative_to(script_dir)) + .transpose()? + } else { + script_filesystem + }; + let filesystem = script_filesystem.combine(filesystem); let custom_certificate_file = match &*cli.command { Commands::Pip(PipNamespace { cert, .. }) => cert.as_deref(), diff --git a/crates/uv/tests/project/edit.rs b/crates/uv/tests/project/edit.rs index 439763f7564..3081d083bdd 100644 --- a/crates/uv/tests/project/edit.rs +++ b/crates/uv/tests/project/edit.rs @@ -10841,7 +10841,7 @@ fn add_index_with_existing_relative_path_in_script() -> Result<()> { # # [[tool.uv.index]] # name = "local" - # url = "./links" + # url = "../links" # format = "flat" # /// "#})?; @@ -10872,7 +10872,7 @@ fn add_index_with_existing_relative_path_in_script() -> Result<()> { # # [[tool.uv.index]] # name = "local" - # url = "links" + # url = "../links" # format = "flat" # # [tool.uv.sources] diff --git a/crates/uv/tests/project/run.rs b/crates/uv/tests/project/run.rs index d95b179bffe..8457003a296 100644 --- a/crates/uv/tests/project/run.rs +++ b/crates/uv/tests/project/run.rs @@ -815,6 +815,63 @@ fn run_pep723_script_index() -> Result<()> { Ok(()) } +/// Run a PEP 723-compatible script with a relative index and pinned and unpinned dependencies. +#[test] +fn run_pep723_script_relative_index() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let scripts = context.temp_dir.child("scripts"); + let links = scripts.child("links"); + links.create_dir_all()?; + fs_err::copy( + context + .workspace_root + .join("test/links/ok-1.0.0-py3-none-any.whl"), + links.child("ok-1.0.0-py3-none-any.whl"), + )?; + fs_err::copy( + context + .workspace_root + .join("test/links/validation-1.0.0-py3-none-any.whl"), + links.child("validation-1.0.0-py3-none-any.whl"), + )?; + + let test_script = scripts.child("main.py"); + test_script.write_str(indoc! { r#" + # /// script + # requires-python = ">=3.11" + # dependencies = ["ok", "validation"] + # + # [[tool.uv.index]] + # name = "local" + # url = "./links" + # format = "flat" + # + # [tool.uv.sources] + # ok = { index = "local" } + # /// + + import ok + import validation + "# + })?; + + let elsewhere = context.temp_dir.child("elsewhere"); + elsewhere.create_dir_all()?; + + uv_snapshot!(context.filters(), context.run().current_dir(elsewhere).arg("--offline").arg(test_script.path()), @r" + exit_code: 0 (success) + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + ok==1.0.0 + + validation==1.0.0 + "); + + Ok(()) +} + /// Package-scoped source disabling must not discard unrelated script sources or indexes. #[test] fn run_pep723_script_no_sources_package() -> Result<()> {