diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index 6569b1a3214..cab7f861d75 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -1,3 +1,4 @@ +use std::env; use std::error::Error; use std::fmt; @@ -231,7 +232,7 @@ impl TryFrom for Refresh { } /// Extract the `--index` and `--default-index` values from [`IndexArgs`]. -pub fn indexes_from_args( +fn indexes_from_args( default_index: Option<&Maybe>, index: Option<&[Vec>]>, ) -> Option> { @@ -319,7 +320,7 @@ impl TryFrom for PipOptions { } else { Some(no_sources_package) }, - ..Self::from(index_args) + ..Self::try_from(index_args)? }) } } @@ -380,7 +381,7 @@ impl TryFrom for PipOptions { } else { Some(no_sources_package) }, - ..Self::from(index_args) + ..Self::try_from(index_args)? }) } } @@ -467,13 +468,15 @@ impl TryFrom for PipOptions { } else { Some(no_sources_package) }, - ..Self::from(index_args) + ..Self::try_from(index_args)? }) } } -impl From for PipOptions { - fn from(args: FetchArgs) -> Self { +impl TryFrom for PipOptions { + type Error = anyhow::Error; + + fn try_from(args: FetchArgs) -> anyhow::Result { let FetchArgs { index_args, registry_client: @@ -484,17 +487,19 @@ impl From for PipOptions { exclude_newer, } = args; - Self { + Ok(Self { index_strategy, keyring_provider, exclude_newer, - ..Self::from(index_args) - } + ..Self::try_from(index_args)? + }) } } -impl From for PipOptions { - fn from(args: IndexArgs) -> Self { +impl TryFrom for PipOptions { + type Error = anyhow::Error; + + fn try_from(args: IndexArgs) -> anyhow::Result { let IndexArgs { default_index, index, @@ -522,6 +527,8 @@ impl From for PipOptions { }), ..Self::default() } + .relative_to(&env::current_dir()?) + .map_err(Into::into) } } @@ -566,7 +573,7 @@ pub fn resolver_options( no_binary_package, } = build_args; - Ok(ResolverOptions { + ResolverOptions { index: indexes_from_args( index_args.default_index.as_ref(), index_args.index.as_deref(), @@ -639,26 +646,15 @@ pub fn resolver_options( } else { Some(no_sources_package) }, - }) + } + .relative_to(&env::current_dir()?) + .map_err(Into::into) } /// Construct the [`ResolverInstallerOptions`] from the [`ResolverInstallerArgs`] and [`BuildOptionsArgs`]. pub fn resolver_installer_options( resolver_installer_args: ResolverInstallerArgs, build_args: BuildOptionsArgs, -) -> anyhow::Result { - let index = indexes_from_args( - resolver_installer_args.index_args.default_index.as_ref(), - resolver_installer_args.index_args.index.as_deref(), - ); - resolver_installer_options_with_indexes(resolver_installer_args, build_args, index) -} - -/// Construct the [`ResolverInstallerOptions`] with a precomputed list of indexes. -pub fn resolver_installer_options_with_indexes( - resolver_installer_args: ResolverInstallerArgs, - build_args: BuildOptionsArgs, - index: Option>, ) -> anyhow::Result { let ResolverInstallerArgs { index_args, @@ -707,8 +703,11 @@ pub fn resolver_installer_options_with_indexes( no_binary_package, } = build_args; - Ok(ResolverInstallerOptions { - index, + ResolverInstallerOptions { + index: indexes_from_args( + index_args.default_index.as_ref(), + index_args.index.as_deref(), + ), index_url: index_args.index_url.and_then(Maybe::into_option), extra_index_url: index_args.extra_index_url.map(|extra_index_url| { extra_index_url @@ -782,5 +781,7 @@ pub fn resolver_installer_options_with_indexes( Some(no_sources_package) }, torch_backend: None, - }) + } + .relative_to(&env::current_dir()?) + .map_err(Into::into) } diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 01276419d6c..bd00fc00d5c 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -510,6 +510,49 @@ impl TryFrom for GlobalOptions { } } +/// Resolve registry indexes and find-links relative to the given root directory. +fn rebase_indexes( + root_dir: &Path, + indexes: &mut Option>, + index_url: &mut Option, + extra_index_urls: &mut Option>, + find_links: &mut Option>, +) -> Result<(), IndexUrlError> { + *indexes = indexes + .take() + .map(|indexes| { + indexes + .into_iter() + .map(|index| index.relative_to(root_dir)) + .collect::, _>>() + }) + .transpose()?; + *index_url = index_url + .take() + .map(|index| index.relative_to(root_dir)) + .transpose()?; + *extra_index_urls = extra_index_urls + .take() + .map(|indexes| { + indexes + .into_iter() + .map(|index| index.relative_to(root_dir)) + .collect::, _>>() + }) + .transpose()?; + *find_links = find_links + .take() + .map(|find_links| { + find_links + .into_iter() + .map(|find_link| find_link.relative_to(root_dir)) + .collect::, _>>() + }) + .transpose()?; + + Ok(()) +} + /// Settings relevant to all installer operations. #[derive(Debug, Clone, Default, CombineOptions)] pub struct InstallerOptions { @@ -566,6 +609,21 @@ pub struct ResolverOptions { pub no_sources_package: Option>, } +impl ResolverOptions { + /// Resolve the [`ResolverOptions`] relative to the given root directory. + pub fn relative_to(mut self, root_dir: &Path) -> Result { + rebase_indexes( + root_dir, + &mut self.index, + &mut self.index_url, + &mut self.extra_index_url, + &mut self.find_links, + )?; + + Ok(self) + } +} + /// Shared settings, relevant to all operations that must resolve and install dependencies. The /// union of [`InstallerOptions`] and [`ResolverOptions`]. #[derive(Debug, Clone, Default, CombineOptions)] @@ -601,6 +659,21 @@ pub struct ResolverInstallerOptions { pub no_binary_package: Option>, } +impl ResolverInstallerOptions { + /// Resolve the [`ResolverInstallerOptions`] relative to the given root directory. + pub fn relative_to(mut self, root_dir: &Path) -> Result { + rebase_indexes( + root_dir, + &mut self.index, + &mut self.index_url, + &mut self.extra_index_url, + &mut self.find_links, + )?; + + Ok(self) + } +} + impl From for ResolverInstallerOptions { fn from(value: ResolverInstallerSchema) -> Self { let ResolverInstallerSchema { @@ -684,41 +757,16 @@ impl From for ResolverInstallerOptions { impl ResolverInstallerSchema { /// Resolve the [`ResolverInstallerSchema`] relative to the given root directory. - fn relative_to(self, root_dir: &Path) -> Result { - Ok(Self { - index: self - .index - .map(|index| { - index - .into_iter() - .map(|index| index.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - index_url: self - .index_url - .map(|index_url| index_url.relative_to(root_dir)) - .transpose()?, - extra_index_url: self - .extra_index_url - .map(|extra_index_url| { - extra_index_url - .into_iter() - .map(|extra_index_url| extra_index_url.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - find_links: self - .find_links - .map(|find_links| { - find_links - .into_iter() - .map(|find_link| find_link.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - ..self - }) + fn relative_to(mut self, root_dir: &Path) -> Result { + rebase_indexes( + root_dir, + &mut self.index, + &mut self.index_url, + &mut self.extra_index_url, + &mut self.find_links, + )?; + + Ok(self) } } @@ -2079,41 +2127,16 @@ pub struct PipOptions { impl PipOptions { /// Resolve the [`PipOptions`] relative to the given root directory. - fn relative_to(self, root_dir: &Path) -> Result { - Ok(Self { - index: self - .index - .map(|index| { - index - .into_iter() - .map(|index| index.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - index_url: self - .index_url - .map(|index_url| index_url.relative_to(root_dir)) - .transpose()?, - extra_index_url: self - .extra_index_url - .map(|extra_index_url| { - extra_index_url - .into_iter() - .map(|extra_index_url| extra_index_url.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - find_links: self - .find_links - .map(|find_links| { - find_links - .into_iter() - .map(|find_link| find_link.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - ..self - }) + pub fn relative_to(mut self, root_dir: &Path) -> Result { + rebase_indexes( + root_dir, + &mut self.index, + &mut self.index_url, + &mut self.extra_index_url, + &mut self.find_links, + )?; + + Ok(self) } } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index da6ef30d258..cf5496dffa0 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -27,9 +27,8 @@ use uv_cli::{ AuthorFrom, BuildArgs, CheckArgs, ExportArgs, FormatArgs, PublishArgs, PythonDirArgs, RegistryClientArgs, ResolverInstallerArgs, ToolUpgradeArgs, options::{ - Flag, FlagSource, check_conflicts, flag, indexes_from_args, resolve_flag, - resolve_flag_pair, resolver_installer_options, resolver_installer_options_with_indexes, - resolver_options, + Flag, FlagSource, check_conflicts, flag, resolve_flag, resolve_flag_pair, + resolver_installer_options, resolver_options, }, }; use uv_client::Connectivity; @@ -2268,17 +2267,17 @@ impl AddSettings { DependencyType::Production }; - // Track the `--index` and `--default-index` arguments from the command-line. - let index = indexes_from_args( - installer.index_args.default_index.as_ref(), - installer.index_args.index.as_deref(), - ); - let indexes = index.clone().unwrap_or_default(); - // Warn user if an ambiguous relative path was passed as a value for // `--index` or `--default-index`. - for index in &indexes { - index.url().warn_on_disambiguated_relative_path(); + for index in installer + .index_args + .default_index + .iter() + .chain(installer.index_args.index.iter().flatten().flatten()) + { + if let Maybe::Some(index) = index { + index.url().warn_on_disambiguated_relative_path(); + } } // If the user passed an `--index-url` or `--extra-index-url`, warn. @@ -2368,11 +2367,20 @@ impl AddSettings { let only_install_local = only_install_local.is_enabled(); let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment); + let active = flag(active, no_active, "active")?; + let workspace = flag(workspace, no_workspace, "workspace")?; + let editable = EditableMode::from_args( + flag(editable.into(), no_editable.into(), "editable")?, + no_editable_package, + ); + let refresh = Refresh::try_from(refresh)?; + let options = resolver_installer_options(installer, build)?; + let indexes = options.index.clone().unwrap_or_default(); Ok(Self { lock_check: resolve_lock_check(locked), frozen: resolve_frozen(frozen), - active: flag(active, no_active, "active")?, + active, no_sync: no_sync.is_enabled(), packages, requirements, @@ -2391,7 +2399,7 @@ impl AddSettings { package, script, python: python.and_then(Maybe::into_option), - workspace: flag(workspace, no_workspace, "workspace")?, + workspace, no_install_project, only_install_project, no_install_workspace, @@ -2400,18 +2408,11 @@ impl AddSettings { only_install_local, no_install_package, only_install_package, - editable: EditableMode::from_args( - flag(editable.into(), no_editable.into(), "editable")?, - no_editable_package, - ), + editable, extras: extra.unwrap_or_default(), - refresh: Refresh::try_from(refresh)?, + refresh, indexes, - settings: ResolverInstallerSettings::combine( - resolver_installer_options_with_indexes(installer, build, index)?, - filesystem, - &environment, - ), + settings: ResolverInstallerSettings::combine(options, filesystem, &environment), install_mirrors: environment .install_mirrors .combine(filesystem_install_mirrors), @@ -3875,7 +3876,7 @@ impl PipListSettings { strict: flag(strict, no_strict, "strict")?, target, prefix, - ..PipOptions::from(fetch) + ..PipOptions::try_from(fetch)? }, filesystem, environment, @@ -3976,7 +3977,7 @@ impl PipTreeSettings { python: python.and_then(Maybe::into_option), system: flag(system, no_system, "system")?, strict: flag(strict, no_strict, "strict")?, - ..PipOptions::from(fetch) + ..PipOptions::try_from(fetch)? }, filesystem, environment, @@ -4225,7 +4226,7 @@ impl VenvSettings { exclude_newer_package: exclude_newer_package .map(ExcludeNewerPackage::from_iter), link_mode, - ..PipOptions::from(index_args) + ..PipOptions::try_from(index_args)? }, filesystem, environment, diff --git a/crates/uv/tests/sync/show_settings.rs b/crates/uv/tests/sync/show_settings.rs index aab720e014b..d7a210a6d5e 100644 --- a/crates/uv/tests/sync/show_settings.rs +++ b/crates/uv/tests/sync/show_settings.rs @@ -1,6 +1,7 @@ use std::process::Command; use assert_fs::prelude::*; +use url::Url; use uv_static::EnvVars; use uv_test::{capture_uv_snapshot, diff_uv_snapshot, uv_snapshot}; @@ -2830,6 +2831,160 @@ fn allow_insecure_host() -> anyhow::Result<()> { Ok(()) } +/// Resolve relative CLI indexes and find-links against the directory selected by `--directory`. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn resolve_relative_indexes_with_directory() -> anyhow::Result<()> { + let context = uv_test::test_context!("3.12"); + context.temp_dir.child("project").create_dir_all()?; + + // Add a configured index to ensure `--directory` does not rebase it. + let config_directory = context.temp_dir.child("configuration"); + config_directory.create_dir_all()?; + let config_file = config_directory.child("uv.toml"); + config_file.write_str(indoc::indoc! {r#" + [[index]] + name = "configured" + url = "./configured-index" + "#})?; + + let absolute_index = context.temp_dir.child("absolute-index"); + let absolute_find_links = context.temp_dir.child("absolute-find-links"); + let file_url = Url::from_directory_path(context.temp_dir.child("file-index").path()).unwrap(); + + let show_settings = || { + let mut command = add_shared_args(context.pip_install()); + command + .arg("tqdm") + .arg("--offline") + .arg("--config-file") + .arg(config_file.path()) + .arg("--show-settings") + .arg("--index=index") + .arg("--index=local=./named-index") + .arg("--default-index=./default-index") + .arg("--index-url=./legacy-index") + .arg("--extra-index-url=./extra-index") + .arg("--find-links=./find-links") + .arg("--find-links") + .arg(absolute_find_links.path()) + .arg("--index=https://test.pypi.org/simple") + .arg("--index") + .arg(absolute_index.path()) + .arg("--index") + .arg(file_url.as_str()); + command + }; + + // Capture CLI index paths before changing directories. + let original_directory = capture_uv_snapshot!(context.filters(), show_settings()); + + // Only relative CLI indexes and find-links should move into `project`. + diff_uv_snapshot!(context.filters(), &original_directory, show_settings() + .arg("--directory") + .arg("project"), @r#" + ... + password: None, + host: None, + port: None, + - path: "[TEMP_DIR]/default-index", + + path: "[TEMP_DIR]/project/default-index", + query: None, + fragment: None, + }, + ... + password: None, + host: None, + port: None, + - path: "[TEMP_DIR]/index", + + path: "[TEMP_DIR]/project/index", + query: None, + fragment: None, + }, + ... + password: None, + host: None, + port: None, + - path: "[TEMP_DIR]/named-index", + + path: "[TEMP_DIR]/project/named-index", + query: None, + fragment: None, + }, + ... + password: None, + host: None, + port: None, + - path: "[TEMP_DIR]/extra-index", + + path: "[TEMP_DIR]/project/extra-index", + query: None, + fragment: None, + }, + ... + password: None, + host: None, + port: None, + - path: "[TEMP_DIR]/legacy-index", + + path: "[TEMP_DIR]/project/legacy-index", + query: None, + fragment: None, + }, + ... + password: None, + host: None, + port: None, + - path: "[TEMP_DIR]/find-links", + + path: "[TEMP_DIR]/project/find-links", + query: None, + fragment: None, + }, + ... + "#); + + let show_project_settings = || { + let mut command = add_shared_args(context.add()); + command + .arg("tqdm") + .arg("--offline") + .arg("--config-file") + .arg(config_file.path()) + .arg("--show-settings") + .arg("--index=local=./project-index"); + command + }; + + let original_directory = capture_uv_snapshot!(context.filters(), show_project_settings()); + + // Project commands rebase indexes through a separate settings path. + diff_uv_snapshot!(context.filters(), &original_directory, show_project_settings() + .arg("--directory") + .arg("project"), @r#" + ... + password: None, + host: None, + port: None, + - path: "[TEMP_DIR]/project-index", + + path: "[TEMP_DIR]/project/project-index", + query: None, + fragment: None, + }, + ... + password: None, + host: None, + port: None, + - path: "[TEMP_DIR]/project-index", + + path: "[TEMP_DIR]/project/project-index", + query: None, + fragment: None, + }, + ... + "#); + + Ok(()) +} + /// Prioritize indexes defined across multiple configuration sources. #[test] #[cfg_attr(