diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index 9604fbd30..a523b4811 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -511,30 +511,23 @@ impl<'a> IndexUrls { /// iterator. pub fn defined_indexes(&'a self) -> impl Iterator + 'a { if self.no_index { - Either::Left(std::iter::empty()) - } else { - Either::Right( - { - let mut seen = FxHashSet::default(); - self.indexes - .iter() - .filter(move |index| { - index.name.as_ref().is_none_or(|name| seen.insert(name)) - }) - .filter(|index| !index.default) - } - .chain({ - let mut seen = FxHashSet::default(); - self.indexes - .iter() - .filter(move |index| { - index.name.as_ref().is_none_or(|name| seen.insert(name)) - }) - .find(|index| index.default) - .into_iter() - }), - ) + return Either::Left(std::iter::empty()); } + + let mut seen = FxHashSet::default(); + let (non_default, default) = self + .indexes + .iter() + .filter(move |index| { + if let Some(name) = &index.name { + seen.insert(name) + } else { + true + } + }) + .partition::, _>(|index| !index.default); + + Either::Right(non_default.into_iter().chain(default)) } /// Return the `--no-index` flag. diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 8e3c4a03a..1c5297f90 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -512,8 +512,9 @@ pub(crate) async fn add( )?; // Validate any indexes that were provided on the command-line to ensure - // they point to existing directories when using path URLs. - for index in &indexes { + // they point to existing non-empty directories when using path URLs. + let mut valid_indexes = Vec::with_capacity(indexes.len()); + for index in indexes { if let IndexUrl::Path(url) = &index.url { let path = url .to_file_path() @@ -521,8 +522,14 @@ pub(crate) async fn add( if !path.is_dir() { bail!("Directory not found for index: {url}"); } + if fs_err::read_dir(&path)?.next().is_none() { + warn_user_once!("Index directory `{url}` is empty, skipping"); + continue; + } } + valid_indexes.push(index); } + let indexes = valid_indexes; // Add any indexes that were provided on the command-line, in priority order. if !raw { diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index a66cd2ed5..68eae5110 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -9434,6 +9434,38 @@ fn add_index_with_non_existent_relative_path_with_same_name_as_index() -> Result Ok(()) } +#[test] +fn add_index_empty_directory() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + let packages = context.temp_dir.child("test-index"); + packages.create_dir_all()?; + + uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: Index directory `file://[TEMP_DIR]/test-index` is empty, skipping + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "); + + Ok(()) +} + /// Add a PyPI requirement. #[test] fn add_group_comment() -> Result<()> {