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