1use std::collections::BTreeMap;
2use std::io;
3use std::path::{Path, PathBuf};
4
5use either::Either;
6
7use thiserror::Error;
8use uv_auth::CredentialsCache;
9use uv_distribution_filename::DistExtension;
10use uv_distribution_types::{
11 Index, IndexCredentialsError, IndexLocations, IndexMetadata, IndexName, Origin, Requirement,
12 RequirementSource,
13};
14use uv_fs::{Simplified, normalize_absolute_path, normalize_path};
15use uv_git_types::{GitLfs, GitReference, GitUrl, GitUrlParseError};
16use uv_normalize::{ExtraName, GroupName, PackageName};
17use uv_pep440::VersionSpecifiers;
18use uv_pep508::{MarkerTree, VerbatimUrl, VersionOrUrl, looks_like_git_repository};
19use uv_pypi_types::{
20 ConflictItem, ParsedGitDirectoryUrl, ParsedGitPathUrl, ParsedUrl, ParsedUrlError,
21 VerbatimParsedUrl,
22};
23use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
24use uv_workspace::Workspace;
25use uv_workspace::pyproject::{PyProjectToml, Source, Sources};
26
27use crate::metadata::GitWorkspaceMember;
28
29#[derive(Debug, Clone)]
30pub struct LoweredRequirement(Requirement);
31
32#[derive(Debug, Clone, Copy)]
33enum RequirementOrigin {
34 Project,
36 Workspace,
38}
39
40impl LoweredRequirement {
41 pub(crate) fn from_requirement<'data>(
43 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
44 project_name: Option<&'data PackageName>,
45 project_dir: &'data Path,
46 project_sources: &'data BTreeMap<PackageName, Sources>,
47 project_indexes: &'data [Index],
48 extra: Option<&ExtraName>,
49 group: Option<&GroupName>,
50 locations: &'data IndexLocations,
51 workspace: &'data Workspace,
52 git_member: Option<&'data GitWorkspaceMember<'data>>,
53 editable: bool,
54 credentials_cache: &'data CredentialsCache,
55 ) -> impl Iterator<Item = Result<Self, LoweringError>> + use<'data> + 'data {
56 let (sources, origin) = if let Some(source) = project_sources.get(&requirement.name) {
58 (Some(source), RequirementOrigin::Project)
59 } else if let Some(source) = workspace.sources().get(&requirement.name) {
60 (Some(source), RequirementOrigin::Workspace)
61 } else {
62 (None, RequirementOrigin::Project)
63 };
64
65 let sources = sources.map(|sources| {
67 sources
68 .iter()
69 .filter(|source| {
70 if let Some(target) = source.extra()
71 && extra != Some(target)
72 {
73 return false;
74 }
75
76 if let Some(target) = source.group()
77 && group != Some(target)
78 {
79 return false;
80 }
81
82 true
83 })
84 .cloned()
85 .collect::<Sources>()
86 });
87
88 if workspace.packages().contains_key(&requirement.name) {
90 if project_name.is_none_or(|project_name| *project_name != requirement.name) {
93 let Some(sources) = sources.as_ref() else {
95 return Either::Left(std::iter::once(Err(
97 LoweringError::MissingWorkspaceSource(requirement.name.clone()),
98 )));
99 };
100
101 for source in sources.iter() {
102 match source {
103 Source::Git { .. } => {
104 return Either::Left(std::iter::once(Err(
105 LoweringError::NonWorkspaceSource(
106 requirement.name.clone(),
107 SourceKind::Git,
108 ),
109 )));
110 }
111 Source::Url { .. } => {
112 return Either::Left(std::iter::once(Err(
113 LoweringError::NonWorkspaceSource(
114 requirement.name.clone(),
115 SourceKind::Url,
116 ),
117 )));
118 }
119 Source::Path { .. } => {
120 return Either::Left(std::iter::once(Err(
121 LoweringError::NonWorkspaceSource(
122 requirement.name.clone(),
123 SourceKind::Path,
124 ),
125 )));
126 }
127 Source::Registry { .. } => {
128 return Either::Left(std::iter::once(Err(
129 LoweringError::NonWorkspaceSource(
130 requirement.name.clone(),
131 SourceKind::Registry,
132 ),
133 )));
134 }
135 Source::Workspace { .. } => {
136 }
138 }
139 }
140 }
141 }
142
143 let Some(sources) = sources else {
144 return Either::Left(std::iter::once(Self::preserve_git_source(
145 requirement,
146 git_member,
147 )));
148 };
149
150 let remaining = {
153 let mut total = MarkerTree::FALSE;
155 for source in sources.iter() {
156 total.or(source.marker());
157 }
158
159 let mut remaining = total.negate();
161 remaining.and(requirement.marker);
162
163 Self(Requirement {
164 marker: remaining,
165 ..Requirement::from(requirement.clone())
166 })
167 };
168
169 Either::Right(
170 sources
171 .into_iter()
172 .map(move |source| {
173 let (source, mut marker) = match source {
174 Source::Git {
175 git,
176 subdirectory,
177 path,
178 rev,
179 tag,
180 branch,
181 lfs,
182 marker,
183 ..
184 } => {
185 let source = git_source(
186 &git,
187 subdirectory.map(Box::<Path>::from),
188 path.map(Box::<Path>::from).map(PathBuf::from),
189 rev,
190 tag,
191 branch,
192 lfs,
193 )?;
194 (source, marker)
195 }
196 Source::Url {
197 url,
198 subdirectory,
199 marker,
200 ..
201 } => {
202 let source =
203 url_source(&requirement, url, subdirectory.map(Box::<Path>::from))?;
204 (source, marker)
205 }
206 Source::Path {
207 path,
208 editable,
209 package,
210 marker,
211 ..
212 } => {
213 let source = path_source(
214 path,
215 git_member,
216 origin,
217 project_dir,
218 workspace.install_path(),
219 editable,
220 package,
221 )?;
222 (source, marker)
223 }
224 Source::Registry {
225 index,
226 marker,
227 extra,
228 group,
229 } => {
230 let Some(index) = locations
233 .indexes()
234 .filter(|index| matches!(index.origin, Some(Origin::Cli)))
235 .chain(project_indexes.iter())
236 .chain(workspace.indexes().iter())
237 .find(|Index { name, .. }| {
238 name.as_ref().is_some_and(|name| *name == index)
239 })
240 else {
241 let hint = missing_index_hint(locations, &index);
242 return Err(LoweringError::MissingIndex {
243 package: requirement.name.clone(),
244 index,
245 hint,
246 });
247 };
248 if let Some(credentials) = index.credentials()? {
249 credentials_cache.store_credentials(index.raw_url(), credentials);
250 }
251 let index = IndexMetadata {
252 url: index.url.clone(),
253 format: index.format,
254 };
255 let conflict = project_name.and_then(|project_name| {
256 if let Some(extra) = extra {
257 Some(ConflictItem::from((project_name.clone(), extra)))
258 } else {
259 group.map(|group| {
260 ConflictItem::from((project_name.clone(), group))
261 })
262 }
263 });
264 let source = registry_source(&requirement, index, conflict);
265 (source, marker)
266 }
267 Source::Workspace {
268 workspace: is_workspace,
269 marker,
270 ..
271 } => {
272 if !is_workspace {
273 return Err(LoweringError::WorkspaceFalse);
274 }
275 let member = workspace
276 .packages()
277 .get(&requirement.name)
278 .ok_or_else(|| {
279 LoweringError::UndeclaredWorkspacePackage(
280 requirement.name.clone(),
281 )
282 })?
283 .clone();
284
285 let url = VerbatimUrl::from_absolute_path(member.root())?;
303 let install_path = url.to_file_path().map_err(|()| {
304 LoweringError::RelativeTo(io::Error::other(
305 "Invalid path in file URL",
306 ))
307 })?;
308
309 let source = if let Some(git_member) = &git_member {
310 let subdirectory =
313 uv_fs::relative_to(member.root(), git_member.fetch_root)
314 .expect("Workspace member must be relative");
315 let subdirectory = normalize_path(subdirectory);
316 RequirementSource::GitDirectory {
317 git: git_member.git_source.git.clone(),
318 subdirectory: if subdirectory == PathBuf::new() {
319 None
320 } else {
321 Some(subdirectory.into_owned().into_boxed_path())
322 },
323 url,
324 }
325 } else {
326 let value = workspace.required_members().get(&requirement.name);
327 let is_required_member = value.is_some();
328 let editability = value.copied().flatten();
329 if member.pyproject_toml().is_package(!is_required_member) {
330 RequirementSource::Directory {
331 install_path: install_path.into_boxed_path(),
332 url,
333 editable: Some(editability.unwrap_or(editable)),
334 r#virtual: Some(false),
335 }
336 } else {
337 RequirementSource::Directory {
338 install_path: install_path.into_boxed_path(),
339 url,
340 editable: Some(false),
341 r#virtual: Some(true),
342 }
343 }
344 };
345 (source, marker)
346 }
347 };
348
349 marker.and(requirement.marker);
350
351 Ok(Self(Requirement {
352 name: requirement.name.clone(),
353 extras: requirement.extras.clone(),
354 groups: Box::new([]),
355 marker,
356 source,
357 origin: requirement.origin.clone(),
358 }))
359 })
360 .chain(std::iter::once(Ok(remaining)))
361 .filter(|requirement| match requirement {
362 Ok(requirement) => !requirement.0.marker.is_false(),
363 Err(_) => true,
364 }),
365 )
366 }
367
368 pub fn from_non_workspace_requirement<'data>(
371 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
372 dir: &'data Path,
373 sources: &'data BTreeMap<PackageName, Sources>,
374 indexes: &'data [Index],
375 locations: &'data IndexLocations,
376 credentials_cache: &'data CredentialsCache,
377 ) -> impl Iterator<Item = Result<Self, LoweringError>> + 'data {
378 let source = sources.get(&requirement.name).cloned();
379
380 let Some(source) = source else {
381 return Either::Left(std::iter::once(Ok(Self(Requirement::from(requirement)))));
382 };
383
384 let source = source
386 .iter()
387 .filter(|source| {
388 source.extra().is_none_or(|target| {
389 requirement
390 .marker
391 .top_level_extra_name()
392 .is_some_and(|extra| &*extra == target)
393 })
394 })
395 .cloned()
396 .collect::<Sources>();
397
398 let remaining = {
401 let mut total = MarkerTree::FALSE;
403 for source in source.iter() {
404 total.or(source.marker());
405 }
406
407 let mut remaining = total.negate();
409 remaining.and(requirement.marker);
410
411 Self(Requirement {
412 marker: remaining,
413 ..Requirement::from(requirement.clone())
414 })
415 };
416
417 Either::Right(
418 source
419 .into_iter()
420 .map(move |source| {
421 let (source, mut marker) = match source {
422 Source::Git {
423 git,
424 subdirectory,
425 path,
426 rev,
427 tag,
428 branch,
429 lfs,
430 marker,
431 ..
432 } => {
433 let source = git_source(
434 &git,
435 subdirectory.map(Box::<Path>::from),
436 path.map(Box::<Path>::from).map(PathBuf::from),
437 rev,
438 tag,
439 branch,
440 lfs,
441 )?;
442 (source, marker)
443 }
444 Source::Url {
445 url,
446 subdirectory,
447 marker,
448 ..
449 } => {
450 let source =
451 url_source(&requirement, url, subdirectory.map(Box::<Path>::from))?;
452 (source, marker)
453 }
454 Source::Path {
455 path,
456 editable,
457 package,
458 marker,
459 ..
460 } => {
461 let source = path_source(
462 path,
463 None,
464 RequirementOrigin::Project,
465 dir,
466 dir,
467 editable,
468 package,
469 )?;
470 (source, marker)
471 }
472 Source::Registry { index, marker, .. } => {
473 let Some(index) = locations
474 .indexes()
475 .filter(|index| matches!(index.origin, Some(Origin::Cli)))
476 .chain(indexes.iter())
477 .find(|Index { name, .. }| {
478 name.as_ref().is_some_and(|name| *name == index)
479 })
480 else {
481 let hint = missing_index_hint(locations, &index);
482 return Err(LoweringError::MissingIndex {
483 package: requirement.name.clone(),
484 index,
485 hint,
486 });
487 };
488 if let Some(credentials) = index.credentials()? {
489 credentials_cache.store_credentials(index.raw_url(), credentials);
490 }
491 let index = IndexMetadata {
492 url: index.url.clone(),
493 format: index.format,
494 };
495 let conflict = None;
496 let source = registry_source(&requirement, index, conflict);
497 (source, marker)
498 }
499 Source::Workspace { .. } => {
500 return Err(LoweringError::WorkspaceMember);
501 }
502 };
503
504 marker.and(requirement.marker);
505
506 Ok(Self(Requirement {
507 name: requirement.name.clone(),
508 extras: requirement.extras.clone(),
509 groups: Box::new([]),
510 marker,
511 source,
512 origin: requirement.origin.clone(),
513 }))
514 })
515 .chain(std::iter::once(Ok(remaining)))
516 .filter(|requirement| match requirement {
517 Ok(requirement) => !requirement.0.marker.is_false(),
518 Err(_) => true,
519 }),
520 )
521 }
522
523 pub(crate) fn preserve_git_source(
526 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
527 git_member: Option<&GitWorkspaceMember>,
528 ) -> Result<Self, LoweringError> {
529 let Some(git_member) = git_member else {
530 return Ok(Self(Requirement::from(requirement)));
531 };
532
533 let Some(VersionOrUrl::Url(url)) = &requirement.version_or_url else {
534 return Ok(Self(Requirement::from(requirement)));
535 };
536
537 let (install_path, is_archive) = match &url.parsed_url {
538 ParsedUrl::Directory(directory) => (directory.install_path.as_ref(), false),
539 ParsedUrl::Path(path) => (path.install_path.as_ref(), true),
540 _ => return Ok(Self(Requirement::from(requirement))),
541 };
542
543 let install_path = git_path(install_path)?;
544 let fetch_root = git_path(git_member.fetch_root)?;
545 if !install_path.starts_with(&fetch_root) {
546 return Ok(Self(Requirement::from(requirement)));
547 }
548
549 Ok(Self(Requirement {
550 name: requirement.name,
551 groups: Box::new([]),
552 extras: requirement.extras,
553 marker: requirement.marker,
554 source: if is_archive {
555 git_archive_source_from_path(&install_path, git_member)?
556 } else {
557 git_directory_source_from_path(&install_path, git_member)?
558 },
559 origin: requirement.origin,
560 }))
561 }
562
563 pub fn into_inner(self) -> Requirement {
565 self.0
566 }
567}
568
569#[derive(Debug, Error)]
572pub enum LoweringError {
573 #[error(
574 "`{0}` is included as a workspace member, but is missing an entry in `tool.uv.sources` (e.g., `{0} = {{ workspace = true }}`)"
575 )]
576 MissingWorkspaceSource(PackageName),
577 #[error(
578 "`{0}` is included as a workspace member, but references a {1} in `tool.uv.sources`. Workspace members must be declared as workspace sources (e.g., `{0} = {{ workspace = true }}`)."
579 )]
580 NonWorkspaceSource(PackageName, SourceKind),
581 #[error(
582 "`{0}` references a workspace in `tool.uv.sources` (e.g., `{0} = {{ workspace = true }}`), but is not a workspace member"
583 )]
584 UndeclaredWorkspacePackage(PackageName),
585 #[error("Can only specify one of: `rev`, `tag`, or `branch`")]
586 MoreThanOneGitRef,
587 #[error(transparent)]
588 GitUrlParse(#[from] GitUrlParseError),
589 #[error("Package `{package}` references an undeclared index: `{index}`")]
590 MissingIndex {
591 package: PackageName,
592 index: IndexName,
593 hint: Option<String>,
594 },
595 #[error("Workspace members are not allowed in non-workspace contexts")]
596 WorkspaceMember,
597 #[error(transparent)]
598 InvalidUrl(#[from] DisplaySafeUrlError),
599 #[error(transparent)]
600 IndexCredentials(#[from] IndexCredentialsError),
601 #[error(transparent)]
602 InvalidVerbatimUrl(#[from] uv_pep508::VerbatimUrlError),
603 #[error("Fragments are not allowed in URLs: `{0}`")]
604 ForbiddenFragment(DisplaySafeUrl),
605 #[error(
606 "`{0}` is associated with a URL source, but references a Git repository. Consider using a Git source instead (e.g., `{0} = {{ git = \"{1}\" }}`)"
607 )]
608 MissingGitSource(PackageName, DisplaySafeUrl),
609 #[error("`workspace = false` is not yet supported")]
610 WorkspaceFalse,
611 #[error("Source with `editable = true` must refer to a local directory, not a file: `{0}`")]
612 EditableFile(String),
613 #[error("Source with `package = true` must refer to a local directory, not a file: `{0}`")]
614 PackagedFile(String),
615 #[error(
616 "Git repository references local file source, but only directories are supported as transitive Git dependencies: `{0}`"
617 )]
618 GitFile(String),
619 #[error(transparent)]
620 ParsedUrl(#[from] ParsedUrlError),
621 #[error("Path must be UTF-8: `{0}`")]
622 NonUtf8Path(PathBuf),
623 #[error(transparent)] RelativeTo(io::Error),
625}
626
627impl uv_errors::Hint for LoweringError {
628 fn hints(&self) -> uv_errors::Hints<'_> {
629 match self {
630 Self::MissingIndex {
631 hint: Some(hint), ..
632 } => uv_errors::Hints::from(hint.clone()),
633 _ => uv_errors::Hints::none(),
634 }
635 }
636}
637
638#[derive(Debug, Copy, Clone)]
639pub enum SourceKind {
640 Path,
641 Url,
642 Git,
643 Registry,
644}
645
646impl std::fmt::Display for SourceKind {
647 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
648 match self {
649 Self::Path => write!(f, "path"),
650 Self::Url => write!(f, "URL"),
651 Self::Git => write!(f, "Git"),
652 Self::Registry => write!(f, "registry"),
653 }
654 }
655}
656
657fn missing_index_hint(locations: &IndexLocations, index: &IndexName) -> Option<String> {
660 let config_index = locations
661 .simple_indexes()
662 .filter(|idx| !matches!(idx.origin, Some(Origin::Cli)))
663 .find(|idx| idx.name.as_ref().is_some_and(|name| *name == *index));
664
665 config_index.and_then(|idx| {
666 let source = match idx.origin {
667 Some(Origin::User) => "a user-level `uv.toml`",
668 Some(Origin::System) => "a system-level `uv.toml`",
669 Some(Origin::Project) => "a project-level `uv.toml`",
670 Some(Origin::Cli | Origin::RequirementsTxt) | None => return None,
671 };
672 Some(format!(
673 "Index `{index}` was found in {source}, but indexes \
674 referenced via `tool.uv.sources` must be defined in the project's \
675 `pyproject.toml`"
676 ))
677 })
678}
679
680fn git_source(
682 git: &DisplaySafeUrl,
683 subdirectory: Option<Box<Path>>,
684 path: Option<PathBuf>,
685 rev: Option<String>,
686 tag: Option<String>,
687 branch: Option<String>,
688 lfs: Option<bool>,
689) -> Result<RequirementSource, LoweringError> {
690 let reference = match (rev, tag, branch) {
691 (None, None, None) => GitReference::DefaultBranch,
692 (Some(rev), None, None) => GitReference::from_rev(rev),
693 (None, Some(tag), None) => GitReference::Tag(tag),
694 (None, None, Some(branch)) => GitReference::Branch(branch),
695 _ => return Err(LoweringError::MoreThanOneGitRef),
696 };
697
698 let mut url = DisplaySafeUrl::parse(&format!("git+{git}"))?;
700 if let Some(rev) = reference.as_str() {
701 let path = format!("{}@{}", url.path(), rev);
702 url.set_path(&path);
703 }
704 let mut frags: Vec<String> = Vec::new();
705 if let Some(subdirectory) = subdirectory.as_ref() {
706 let subdirectory = subdirectory
707 .to_str()
708 .ok_or_else(|| LoweringError::NonUtf8Path(subdirectory.to_path_buf()))?;
709 frags.push(format!("subdirectory={subdirectory}"));
710 }
711 let lfs = GitLfs::from(lfs);
715 if lfs.enabled() {
717 frags.push("lfs=true".to_string());
718 }
719 if let Some(path) = path.as_ref() {
720 let path = path
721 .to_str()
722 .ok_or_else(|| LoweringError::NonUtf8Path(path.clone()))?;
723 frags.push(format!("path={path}"));
724 }
725 if !frags.is_empty() {
726 url.set_fragment(Some(&frags.join("&")));
727 }
728 let url = VerbatimUrl::from_url(url);
729
730 let repository = git.clone();
731 let git = GitUrl::from_fields(repository, reference, None, lfs)?;
732
733 if let Some(path) = path {
734 let ext = match DistExtension::from_path(&path) {
735 Ok(ext) => ext,
736 Err(err) => {
737 return Err(ParsedUrlError::MissingExtensionPath(path, err).into());
738 }
739 };
740 Ok(RequirementSource::GitPath {
741 url,
742 git,
743 install_path: path,
744 ext,
745 })
746 } else {
747 Ok(RequirementSource::GitDirectory {
748 url,
749 git,
750 subdirectory,
751 })
752 }
753}
754
755fn url_source(
757 requirement: &uv_pep508::Requirement<VerbatimParsedUrl>,
758 url: DisplaySafeUrl,
759 subdirectory: Option<Box<Path>>,
760) -> Result<RequirementSource, LoweringError> {
761 let mut verbatim_url = url.clone();
762 if verbatim_url.fragment().is_some() {
763 return Err(LoweringError::ForbiddenFragment(url));
764 }
765 if let Some(subdirectory) = subdirectory.as_ref() {
766 let subdirectory = subdirectory
767 .to_str()
768 .ok_or_else(|| LoweringError::NonUtf8Path(subdirectory.to_path_buf()))?;
769 verbatim_url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
770 }
771
772 let ext = match DistExtension::from_path(url.path()) {
773 Ok(ext) => ext,
774 Err(..) if looks_like_git_repository(&url) => {
775 return Err(LoweringError::MissingGitSource(
776 requirement.name.clone(),
777 url.clone(),
778 ));
779 }
780 Err(err) => {
781 return Err(ParsedUrlError::MissingExtensionUrl(url.to_string(), err).into());
782 }
783 };
784
785 let verbatim_url = VerbatimUrl::from_url(verbatim_url);
786 Ok(RequirementSource::Url {
787 location: url,
788 subdirectory,
789 ext,
790 url: verbatim_url,
791 })
792}
793
794fn registry_source(
796 requirement: &uv_pep508::Requirement<VerbatimParsedUrl>,
797 index: IndexMetadata,
798 conflict: Option<ConflictItem>,
799) -> RequirementSource {
800 match &requirement.version_or_url {
801 None => RequirementSource::Registry {
802 specifier: VersionSpecifiers::empty(),
803 index: Some(index),
804 conflict,
805 },
806 Some(VersionOrUrl::VersionSpecifier(version)) => RequirementSource::Registry {
807 specifier: version.clone(),
808 index: Some(index),
809 conflict,
810 },
811 Some(VersionOrUrl::Url(_)) => RequirementSource::Registry {
812 specifier: VersionSpecifiers::empty(),
813 index: Some(index),
814 conflict,
815 },
816 }
817}
818
819fn path_source(
821 path: impl AsRef<Path>,
822 git_member: Option<&GitWorkspaceMember>,
823 origin: RequirementOrigin,
824 project_dir: &Path,
825 workspace_root: &Path,
826 editable: Option<bool>,
827 package: Option<bool>,
828) -> Result<RequirementSource, LoweringError> {
829 let path = path.as_ref();
830 let base = match origin {
831 RequirementOrigin::Project => project_dir,
832 RequirementOrigin::Workspace => workspace_root,
833 };
834 let url = VerbatimUrl::from_path(path, base)?.with_given(path.to_string_lossy());
835 let install_path = url
836 .to_file_path()
837 .map_err(|()| LoweringError::RelativeTo(io::Error::other("Invalid path in file URL")))?;
838
839 let is_dir = if let Ok(metadata) = install_path.metadata() {
840 metadata.is_dir()
841 } else {
842 install_path.extension().is_none()
843 };
844 if is_dir {
845 if let Some(git_member) = git_member {
846 return git_directory_source_from_path(install_path, git_member);
847 }
848
849 if editable == Some(true) {
850 Ok(RequirementSource::Directory {
851 install_path: install_path.into_boxed_path(),
852 url,
853 editable,
854 r#virtual: Some(false),
855 })
856 } else {
857 let is_package = package.unwrap_or_else(|| {
861 let pyproject_path = install_path.join("pyproject.toml");
862 fs_err::read_to_string(&pyproject_path)
863 .ok()
864 .and_then(|contents| PyProjectToml::from_string(contents, pyproject_path).ok())
865 .is_none_or(|pyproject_toml| pyproject_toml.is_package(false))
867 });
868
869 let r#virtual = !is_package;
871
872 Ok(RequirementSource::Directory {
873 install_path: install_path.into_boxed_path(),
874 url,
875 editable: Some(false),
876 r#virtual: Some(r#virtual),
877 })
878 }
879 } else {
880 if let Some(git_member) = git_member {
881 return git_archive_source_from_path(install_path, git_member);
882 }
883 if editable == Some(true) {
884 return Err(LoweringError::EditableFile(url.to_string()));
885 }
886 if package == Some(true) {
887 return Err(LoweringError::PackagedFile(url.to_string()));
888 }
889 Ok(RequirementSource::Path {
890 ext: DistExtension::from_path(&install_path)
891 .map_err(|err| ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err))?,
892 install_path: install_path.into_boxed_path(),
893 url,
894 })
895 }
896}
897
898fn git_directory_source_from_path(
899 install_path: impl AsRef<Path>,
900 git_member: &GitWorkspaceMember,
901) -> Result<RequirementSource, LoweringError> {
902 let git = git_member.git_source.git.clone();
903 let install_path = git_path(install_path.as_ref())?;
904 let fetch_root = git_path(git_member.fetch_root)?;
905 let subdirectory =
906 uv_fs::relative_to(install_path, fetch_root).map_err(LoweringError::RelativeTo)?;
907 let subdirectory = normalize_path(subdirectory);
908 let subdirectory = if subdirectory == PathBuf::new() {
909 None
910 } else {
911 Some(subdirectory.into_owned().into_boxed_path())
912 };
913 let url = DisplaySafeUrl::from(ParsedGitDirectoryUrl {
914 url: git.clone(),
915 subdirectory: subdirectory.clone(),
916 });
917 Ok(RequirementSource::GitDirectory {
918 git,
919 subdirectory,
920 url: VerbatimUrl::from_url(url),
921 })
922}
923
924fn git_archive_source_from_path(
925 install_path: impl AsRef<Path>,
926 git_member: &GitWorkspaceMember,
927) -> Result<RequirementSource, LoweringError> {
928 let git = git_member.git_source.git.clone();
929 let install_path = git_path(install_path.as_ref())?;
930 let fetch_root = git_path(git_member.fetch_root)?;
931 let install_path =
932 uv_fs::relative_to(install_path, fetch_root).map_err(LoweringError::RelativeTo)?;
933 let install_path = normalize_path(install_path).into_owned();
934 let ext = DistExtension::from_path(&install_path)
935 .map_err(|err| ParsedUrlError::MissingExtensionPath(install_path.clone(), err))?;
936 let url = DisplaySafeUrl::from(ParsedGitPathUrl {
937 url: git.clone(),
938 install_path: install_path.clone(),
939 ext,
940 });
941 Ok(RequirementSource::GitPath {
942 git,
943 install_path,
944 ext,
945 url: VerbatimUrl::from_url(url),
946 })
947}
948
949fn git_path(path: &Path) -> Result<PathBuf, LoweringError> {
950 path.simple_canonicalize()
951 .or_else(|_| normalize_absolute_path(path))
952 .map_err(LoweringError::RelativeTo)
953}