1use std::collections::BTreeMap;
2use std::io;
3use std::path::{Path, PathBuf};
4
5use either::Either;
6use futures::future::join_all;
7
8use thiserror::Error;
9use uv_auth::CredentialsCache;
10use uv_cache::Cache;
11use uv_distribution_filename::DistExtension;
12use uv_distribution_types::{
13 Index, IndexCredentialsError, IndexLocations, IndexMetadata, IndexName, Origin, Requirement,
14 RequirementScope, RequirementSource,
15};
16use uv_fs::{Simplified, normalize_absolute_path, normalize_path};
17use uv_git_types::{GitLfs, GitReference, GitUrl, GitUrlParseError};
18use uv_macros::DebugNoInline;
19use uv_normalize::{ExtraName, GroupName, PackageName};
20use uv_pep440::VersionSpecifiers;
21use uv_pep508::{MarkerTree, VerbatimUrl, VersionOrUrl, looks_like_git_repository};
22use uv_pypi_types::{
23 ConflictItem, ParsedGitDirectoryUrl, ParsedGitPathUrl, ParsedUrl, ParsedUrlError,
24 VerbatimParsedUrl,
25};
26use uv_redacted::policy::Raw;
27use uv_redacted::{SafeUrl, SafeUrlError};
28use uv_workspace::pyproject::{PyProjectToml, Source, Sources, WorkspaceReference};
29use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceCache, WorkspaceError};
30
31use crate::metadata::GitWorkspaceMember;
32
33#[derive(Debug, Clone)]
34pub struct LoweredRequirement(Requirement);
35
36#[derive(Debug, Clone, Copy)]
37enum RequirementOrigin {
38 Project,
40 Workspace,
42}
43
44impl LoweredRequirement {
45 pub(crate) async fn from_requirement<'data>(
47 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
48 project_name: Option<&'data PackageName>,
49 project_dir: &'data Path,
50 project_sources: &'data BTreeMap<PackageName, Sources>,
51 project_indexes: &'data [Index],
52 extra: Option<&ExtraName>,
53 group: Option<&GroupName>,
54 locations: &'data IndexLocations,
55 workspace: &'data Workspace,
56 git_member: Option<&'data GitWorkspaceMember<'data>>,
57 editable: bool,
58 cache: &'data Cache,
59 workspace_cache: &'data WorkspaceCache,
60 credentials_cache: &'data CredentialsCache,
61 ) -> impl Iterator<Item = Result<Self, LoweringError>> + use<'data> + 'data {
62 let (sources, origin) = if let Some(source) = project_sources.get(&requirement.name) {
64 (Some(source), RequirementOrigin::Project)
65 } else if let Some(source) = workspace.sources().get(&requirement.name) {
66 (Some(source), RequirementOrigin::Workspace)
67 } else {
68 (None, RequirementOrigin::Project)
69 };
70
71 let sources = sources.map(|sources| {
73 sources
74 .iter()
75 .filter(|source| {
76 if let Some(target) = source.extra()
77 && extra != Some(target)
78 {
79 return false;
80 }
81
82 if let Some(target) = source.group()
83 && group != Some(target)
84 {
85 return false;
86 }
87
88 true
89 })
90 .cloned()
91 .collect::<Sources>()
92 });
93
94 if workspace.packages().contains_key(&requirement.name) {
96 if project_name.is_none_or(|project_name| *project_name != requirement.name) {
99 let Some(sources) = sources.as_ref() else {
101 return Either::Left(std::iter::once(Err(
103 LoweringError::MissingWorkspaceSource(requirement.name.clone()),
104 )));
105 };
106
107 for source in sources.iter() {
108 match source {
109 Source::Git { .. } => {
110 return Either::Left(std::iter::once(Err(
111 LoweringError::NonWorkspaceSource(
112 requirement.name.clone(),
113 SourceKind::Git,
114 ),
115 )));
116 }
117 Source::Url { .. } => {
118 return Either::Left(std::iter::once(Err(
119 LoweringError::NonWorkspaceSource(
120 requirement.name.clone(),
121 SourceKind::Url,
122 ),
123 )));
124 }
125 Source::Path { .. } => {
126 return Either::Left(std::iter::once(Err(
127 LoweringError::NonWorkspaceSource(
128 requirement.name.clone(),
129 SourceKind::Path,
130 ),
131 )));
132 }
133 Source::Registry { .. } => {
134 return Either::Left(std::iter::once(Err(
135 LoweringError::NonWorkspaceSource(
136 requirement.name.clone(),
137 SourceKind::Registry,
138 ),
139 )));
140 }
141 Source::Workspace {
142 workspace: WorkspaceReference::Bool(true),
143 ..
144 } => {
145 }
147 Source::Workspace { .. } => {
148 return Either::Left(std::iter::once(Err(
149 LoweringError::InvalidWorkspaceSource(requirement.name.clone()),
150 )));
151 }
152 }
153 }
154 }
155 }
156
157 let Some(sources) = sources else {
158 return Either::Left(std::iter::once(Self::preserve_git_source(
159 requirement,
160 git_member,
161 )));
162 };
163
164 let remaining = {
167 let mut total = MarkerTree::FALSE;
169 for source in sources.iter() {
170 total = total.or(source.marker());
171 }
172
173 let mut remaining = total.negate();
175 remaining = remaining.and(requirement.marker);
176
177 Self(Requirement {
178 marker: remaining,
179 ..Requirement::from(requirement.clone())
180 })
181 };
182
183 Either::Right(
184 join_all(sources.into_iter().map(|source| {
185 let requirement = &requirement;
186 async move {
187 let (source, mut marker) = match source {
188 Source::Git {
189 git,
190 subdirectory,
191 path,
192 rev,
193 tag,
194 branch,
195 lfs,
196 marker,
197 ..
198 } => {
199 let source = git_source(
200 git.into_url(),
201 subdirectory.map(Box::<Path>::from),
202 path.map(Box::<Path>::from).map(PathBuf::from),
203 rev,
204 tag,
205 branch,
206 lfs,
207 )?;
208 (source, marker)
209 }
210 Source::Url {
211 url,
212 subdirectory,
213 marker,
214 ..
215 } => {
216 let source =
217 url_source(requirement, url, subdirectory.map(Box::<Path>::from))?;
218 (source, marker)
219 }
220 Source::Path {
221 path,
222 editable,
223 package,
224 marker,
225 ..
226 } => {
227 let source = path_source(
228 path,
229 git_member,
230 origin,
231 project_dir,
232 workspace.install_path(),
233 editable,
234 package,
235 true,
236 )?;
237 (source, marker)
238 }
239 Source::Registry {
240 index,
241 marker,
242 extra,
243 group,
244 } => {
245 let Some(index) = locations
248 .indexes()
249 .filter(|index| matches!(index.origin, Some(Origin::Cli)))
250 .chain(project_indexes.iter())
251 .chain(workspace.indexes().iter())
252 .find(|Index { name, .. }| {
253 name.as_ref().is_some_and(|name| *name == index)
254 })
255 else {
256 let hint = missing_index_hint(locations, &index);
257 return Err(LoweringError::MissingIndex {
258 package: requirement.name.clone(),
259 index,
260 hint,
261 });
262 };
263 if let Some(credentials) = index.credentials()? {
264 credentials_cache.store_credentials(index.raw_url(), credentials);
265 }
266 let index = IndexMetadata {
267 url: index.url.clone(),
268 format: index.format,
269 };
270 let conflict = project_name.and_then(|project_name| {
271 if let Some(extra) = extra {
272 Some(ConflictItem::from((project_name.clone(), extra)))
273 } else {
274 group.map(|group| {
275 ConflictItem::from((project_name.clone(), group))
276 })
277 }
278 });
279 let source = registry_source(requirement, index, conflict);
280 (source, marker)
281 }
282 Source::Workspace {
283 workspace: workspace_ref,
284 editable: source_editable,
285 marker,
286 ..
287 } => {
288 let source = workspace_source(
289 requirement,
290 &workspace_ref,
291 source_editable,
292 editable,
293 origin,
294 project_dir,
295 workspace.install_path(),
296 Some(workspace),
297 git_member,
298 cache,
299 workspace_cache,
300 )
301 .await?;
302 (source, marker)
303 }
304 };
305
306 marker = marker.and(requirement.marker);
307
308 Ok(Self(Requirement {
309 name: requirement.name.clone(),
310 extras: requirement.extras.clone(),
311 groups: Box::new([]),
312 marker,
313 source,
314 scope: RequirementScope::Global,
315 origin: requirement.origin.clone(),
316 }))
317 }
318 }))
319 .await
320 .into_iter()
321 .chain(std::iter::once(Ok(remaining)))
322 .filter(|requirement| match requirement {
323 Ok(requirement) => !requirement.0.marker.is_false(),
324 Err(_) => true,
325 }),
326 )
327 }
328
329 pub async fn from_non_workspace_requirement<'data>(
332 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
333 dir: &'data Path,
334 sources: &'data BTreeMap<PackageName, Sources>,
335 indexes: &'data [Index],
336 locations: &'data IndexLocations,
337 cache: &'data Cache,
338 workspace_cache: &'data WorkspaceCache,
339 credentials_cache: &'data CredentialsCache,
340 ) -> impl Iterator<Item = Result<Self, LoweringError>> + 'data {
341 let source = sources.get(&requirement.name).cloned();
342
343 let Some(source) = source else {
344 return Either::Left(std::iter::once(Ok(Self(Requirement::from(requirement)))));
345 };
346
347 let source = source
349 .iter()
350 .filter(|source| {
351 source.extra().is_none_or(|target| {
352 requirement
353 .marker
354 .top_level_extra_name()
355 .is_some_and(|extra| &*extra == target)
356 })
357 })
358 .cloned()
359 .collect::<Sources>();
360
361 let remaining = {
364 let mut total = MarkerTree::FALSE;
366 for source in source.iter() {
367 total = total.or(source.marker());
368 }
369
370 let mut remaining = total.negate();
372 remaining = remaining.and(requirement.marker);
373
374 Self(Requirement {
375 marker: remaining,
376 ..Requirement::from(requirement.clone())
377 })
378 };
379
380 Either::Right(
381 join_all(source.into_iter().map(|source| {
382 let requirement = &requirement;
383 async move {
384 let (source, mut marker) = match source {
385 Source::Git {
386 git,
387 subdirectory,
388 path,
389 rev,
390 tag,
391 branch,
392 lfs,
393 marker,
394 ..
395 } => {
396 let source = git_source(
397 git.into_url(),
398 subdirectory.map(Box::<Path>::from),
399 path.map(Box::<Path>::from).map(PathBuf::from),
400 rev,
401 tag,
402 branch,
403 lfs,
404 )?;
405 (source, marker)
406 }
407 Source::Url {
408 url,
409 subdirectory,
410 marker,
411 ..
412 } => {
413 let source =
414 url_source(requirement, url, subdirectory.map(Box::<Path>::from))?;
415 (source, marker)
416 }
417 Source::Path {
418 path,
419 editable,
420 package,
421 marker,
422 ..
423 } => {
424 let source = path_source(
425 path,
426 None,
427 RequirementOrigin::Project,
428 dir,
429 dir,
430 editable,
431 package,
432 true,
433 )?;
434 (source, marker)
435 }
436 Source::Registry { index, marker, .. } => {
437 let Some(index) = locations
438 .indexes()
439 .filter(|index| matches!(index.origin, Some(Origin::Cli)))
440 .chain(indexes.iter())
441 .find(|Index { name, .. }| {
442 name.as_ref().is_some_and(|name| *name == index)
443 })
444 else {
445 let hint = missing_index_hint(locations, &index);
446 return Err(LoweringError::MissingIndex {
447 package: requirement.name.clone(),
448 index,
449 hint,
450 });
451 };
452 if let Some(credentials) = index.credentials()? {
453 credentials_cache.store_credentials(index.raw_url(), credentials);
454 }
455 let index = IndexMetadata {
456 url: index.url.clone(),
457 format: index.format,
458 };
459 let conflict = None;
460 let source = registry_source(requirement, index, conflict);
461 (source, marker)
462 }
463 Source::Workspace {
464 workspace: workspace_ref,
465 editable,
466 marker,
467 ..
468 } => {
469 let source = workspace_source(
470 requirement,
471 &workspace_ref,
472 editable,
473 true,
474 RequirementOrigin::Project,
475 dir,
476 dir,
477 None,
478 None,
479 cache,
480 workspace_cache,
481 )
482 .await?;
483 (source, marker)
484 }
485 };
486
487 marker = marker.and(requirement.marker);
488
489 Ok(Self(Requirement {
490 name: requirement.name.clone(),
491 extras: requirement.extras.clone(),
492 groups: Box::new([]),
493 marker,
494 source,
495 scope: RequirementScope::Global,
496 origin: requirement.origin.clone(),
497 }))
498 }
499 }))
500 .await
501 .into_iter()
502 .chain(std::iter::once(Ok(remaining)))
503 .filter(|requirement| match requirement {
504 Ok(requirement) => !requirement.0.marker.is_false(),
505 Err(_) => true,
506 }),
507 )
508 }
509
510 pub(crate) fn preserve_git_source(
513 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
514 git_member: Option<&GitWorkspaceMember>,
515 ) -> Result<Self, LoweringError> {
516 let Some(git_member) = git_member else {
517 return Ok(Self(Requirement::from(requirement)));
518 };
519
520 let Some(VersionOrUrl::Url(url)) = &requirement.version_or_url else {
521 return Ok(Self(Requirement::from(requirement)));
522 };
523
524 let (install_path, is_archive) = match &url.parsed_url {
525 ParsedUrl::Directory(directory) => (directory.install_path.as_ref(), false),
526 ParsedUrl::Path(path) => (path.install_path.as_ref(), true),
527 _ => return Ok(Self(Requirement::from(requirement))),
528 };
529
530 let install_path = git_path(install_path)?;
531 let fetch_root = git_path(git_member.fetch_root)?;
532 if !install_path.starts_with(&fetch_root) {
533 return Ok(Self(Requirement::from(requirement)));
534 }
535
536 Ok(Self(Requirement {
537 name: requirement.name,
538 groups: Box::new([]),
539 extras: requirement.extras,
540 marker: requirement.marker,
541 source: if is_archive {
542 git_archive_source_from_path(&install_path, git_member)?
543 } else {
544 git_directory_source_from_path(&install_path, git_member)?
545 },
546 scope: RequirementScope::Global,
547 origin: requirement.origin,
548 }))
549 }
550
551 pub fn into_inner(self) -> Requirement {
553 self.0
554 }
555}
556
557#[derive(DebugNoInline, Error)]
560pub enum LoweringError {
561 #[error(
562 "`{0}` is included as a workspace member, but is missing an entry in `tool.uv.sources` (e.g., `{0} = {{ workspace = true }}`)"
563 )]
564 MissingWorkspaceSource(PackageName),
565 #[error(
566 "`{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 }}`)."
567 )]
568 NonWorkspaceSource(PackageName, SourceKind),
569 #[error(
570 "`{0}` references a workspace in `tool.uv.sources` (e.g., `{0} = {{ workspace = true }}`), but is not a workspace member"
571 )]
572 UndeclaredWorkspacePackage(PackageName),
573 #[error(
574 "`{0}` is included as a workspace member, but does not use `workspace = true` in `tool.uv.sources`"
575 )]
576 InvalidWorkspaceSource(PackageName),
577 #[error("Can only specify one of: `rev`, `tag`, or `branch`")]
578 MoreThanOneGitRef,
579 #[error(transparent)]
580 GitUrlParse(#[from] GitUrlParseError),
581 #[error("Package `{package}` references an undeclared index: `{index}`")]
582 MissingIndex {
583 package: PackageName,
584 index: IndexName,
585 hint: Option<String>,
586 },
587 #[error("Workspace members are not allowed in non-workspace contexts")]
588 WorkspaceMember,
589 #[error(transparent)]
590 InvalidUrl(#[from] SafeUrlError),
591 #[error(transparent)]
592 IndexCredentials(#[from] IndexCredentialsError),
593 #[error(transparent)]
594 InvalidVerbatimUrl(#[from] uv_pep508::VerbatimUrlError),
595 #[error("Fragments are not allowed in URLs: {0}")]
596 ForbiddenFragment(SafeUrl),
597 #[error(
598 "`{0}` is associated with a URL source, but references a Git repository. Consider using a Git source instead (e.g., `{0} = {{ git = \"{1}\" }}`)"
599 )]
600 MissingGitSource(PackageName, SafeUrl),
601 #[error("`workspace = false` is not yet supported")]
602 WorkspaceFalse,
603 #[error(transparent)]
604 Workspace(#[from] WorkspaceError),
605 #[error(
606 "Workspace source path `{}` must point to a workspace root (found workspace at `{}`)", path.simplified_display(), root.simplified_display()
607 )]
608 WorkspaceSourceNotRoot { path: PathBuf, root: PathBuf },
609 #[error("Source with `editable = true` must refer to a local directory, not a file: {0}")]
610 EditableFile(String),
611 #[error("Source with `package = true` must refer to a local directory, not a file: {0}")]
612 PackagedFile(String),
613 #[error(
614 "Git repository references local file source, but only directories are supported as transitive Git dependencies: {0}"
615 )]
616 GitFile(String),
617 #[error("Git repository references local directory outside the repository: {0}")]
618 GitDirectory(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::Hinted 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: SafeUrl,
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 = SafeUrl::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 git = GitUrl::from_fields(git, reference, None, lfs)?;
731
732 if let Some(path) = path {
733 let ext = match DistExtension::from_path(&path) {
734 Ok(ext) => ext,
735 Err(err) => {
736 return Err(ParsedUrlError::MissingExtensionPath(path, err).into());
737 }
738 };
739 Ok(RequirementSource::GitPath {
740 url,
741 git,
742 install_path: path,
743 ext,
744 })
745 } else {
746 Ok(RequirementSource::GitDirectory {
747 url,
748 git,
749 subdirectory,
750 })
751 }
752}
753
754fn url_source(
756 requirement: &uv_pep508::Requirement<VerbatimParsedUrl>,
757 url: SafeUrl<Raw>,
758 subdirectory: Option<Box<Path>>,
759) -> Result<RequirementSource, LoweringError> {
760 let mut verbatim_url = url.as_url().clone();
761 if verbatim_url.fragment().is_some() {
762 return Err(LoweringError::ForbiddenFragment(url.into_url()));
763 }
764 if let Some(subdirectory) = subdirectory.as_ref() {
765 let subdirectory = subdirectory
766 .to_str()
767 .ok_or_else(|| LoweringError::NonUtf8Path(subdirectory.to_path_buf()))?;
768 verbatim_url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
769 }
770
771 let ext = match DistExtension::from_path(url.path()) {
772 Ok(ext) => ext,
773 Err(..) if looks_like_git_repository(&url) => {
774 return Err(LoweringError::MissingGitSource(
775 requirement.name.clone(),
776 url.as_url().clone(),
777 ));
778 }
779 Err(err) => {
780 return Err(ParsedUrlError::MissingExtensionUrl(url.to_string(), err).into());
781 }
782 };
783
784 let verbatim_url = VerbatimUrl::from_url(verbatim_url);
785 Ok(RequirementSource::Url {
786 location: url,
787 subdirectory,
788 ext,
789 url: verbatim_url,
790 })
791}
792
793fn registry_source(
795 requirement: &uv_pep508::Requirement<VerbatimParsedUrl>,
796 index: IndexMetadata,
797 conflict: Option<ConflictItem>,
798) -> RequirementSource {
799 match &requirement.version_or_url {
800 None => RequirementSource::Registry {
801 specifier: VersionSpecifiers::empty(),
802 index: Some(index),
803 conflict,
804 },
805 Some(VersionOrUrl::VersionSpecifier(version)) => RequirementSource::Registry {
806 specifier: version.clone(),
807 index: Some(index),
808 conflict,
809 },
810 Some(VersionOrUrl::Url(_)) => RequirementSource::Registry {
811 specifier: VersionSpecifiers::empty(),
812 index: Some(index),
813 conflict,
814 },
815 }
816}
817
818async fn workspace_source(
819 requirement: &uv_pep508::Requirement<VerbatimParsedUrl>,
820 workspace_ref: &WorkspaceReference,
821 source_editable: Option<bool>,
822 default_editable: bool,
823 origin: RequirementOrigin,
824 project_dir: &Path,
825 workspace_root: &Path,
826 current_workspace: Option<&Workspace>,
827 git_member: Option<&GitWorkspaceMember<'_>>,
828 cache: &Cache,
829 workspace_cache: &WorkspaceCache,
830) -> Result<RequirementSource, LoweringError> {
831 let base = match origin {
832 RequirementOrigin::Project => project_dir,
833 RequirementOrigin::Workspace => workspace_root,
834 };
835
836 match workspace_ref {
837 WorkspaceReference::Bool(false) => Err(LoweringError::WorkspaceFalse),
838 WorkspaceReference::Bool(true) => {
839 let workspace = current_workspace.ok_or(LoweringError::WorkspaceMember)?;
840 let member = workspace.packages().get(&requirement.name).ok_or_else(|| {
841 LoweringError::UndeclaredWorkspacePackage(requirement.name.clone())
842 })?;
843
844 let value = workspace.required_members().get(&requirement.name);
845 let is_required_member = value.is_some();
846 let is_package = member.pyproject_toml().is_package(!is_required_member);
847 let editable = if is_package {
848 Some(value.copied().flatten().unwrap_or(default_editable))
849 } else {
850 Some(false)
851 };
852 path_source(
853 member.root(),
854 git_member,
855 origin,
856 project_dir,
857 workspace_root,
858 editable,
859 Some(is_package),
860 false,
861 )
862 }
863 WorkspaceReference::Path(path) => {
864 let workspace_path = VerbatimUrl::from_path(path.as_ref(), base)?
865 .to_file_path()
866 .map_err(|()| {
867 LoweringError::RelativeTo(io::Error::other("Invalid path in file URL"))
868 })?;
869 let target_workspace = Workspace::discover(
870 &workspace_path,
871 &DiscoveryOptions::default(),
872 cache,
873 workspace_cache,
874 )
875 .await?;
876
877 if target_workspace.install_path() != &workspace_path {
878 return Err(LoweringError::WorkspaceSourceNotRoot {
879 path: workspace_path,
880 root: target_workspace.install_path().clone(),
881 });
882 }
883
884 let member = target_workspace
885 .packages()
886 .get(&requirement.name)
887 .ok_or_else(|| {
888 LoweringError::UndeclaredWorkspacePackage(requirement.name.clone())
889 })?;
890
891 let is_package = member.pyproject_toml().is_package(false);
892 let editable = if is_package {
893 Some(source_editable.unwrap_or(true))
894 } else {
895 Some(false)
896 };
897 let member_path =
898 uv_fs::relative_to(member.root(), base).unwrap_or_else(|_| member.root().into());
899
900 path_source(
901 member_path,
902 git_member,
903 origin,
904 project_dir,
905 workspace_root,
906 editable,
907 Some(is_package),
908 true,
909 )
910 }
911 }
912}
913
914fn path_source(
916 path: impl AsRef<Path>,
917 git_member: Option<&GitWorkspaceMember>,
918 origin: RequirementOrigin,
919 project_dir: &Path,
920 workspace_root: &Path,
921 editable: Option<bool>,
922 package: Option<bool>,
923 preserve_given: bool,
924) -> Result<RequirementSource, LoweringError> {
925 let path = path.as_ref();
926 let base = match origin {
927 RequirementOrigin::Project => project_dir,
928 RequirementOrigin::Workspace => workspace_root,
929 };
930 let url = VerbatimUrl::from_path(path, base)?;
931 let url = if preserve_given {
932 url.with_given(path.to_string_lossy())
933 } else {
934 url
935 };
936 let install_path = url
937 .to_file_path()
938 .map_err(|()| LoweringError::RelativeTo(io::Error::other("Invalid path in file URL")))?;
939
940 let is_dir = if let Ok(metadata) = install_path.metadata() {
941 metadata.is_dir()
942 } else {
943 install_path.extension().is_none()
944 };
945 if is_dir {
946 if let Some(git_member) = git_member {
947 return git_directory_source_from_path(install_path, git_member);
948 }
949
950 if editable == Some(true) {
951 Ok(RequirementSource::Directory {
952 install_path: install_path.into_boxed_path(),
953 url,
954 editable,
955 r#virtual: Some(false),
956 })
957 } else {
958 let is_package = package.unwrap_or_else(|| {
962 let pyproject_path = install_path.join("pyproject.toml");
963 fs_err::read_to_string(&pyproject_path)
964 .ok()
965 .and_then(|contents| PyProjectToml::from_string(contents, pyproject_path).ok())
966 .is_none_or(|pyproject_toml| pyproject_toml.is_package(false))
968 });
969
970 let r#virtual = !is_package;
972
973 Ok(RequirementSource::Directory {
974 install_path: install_path.into_boxed_path(),
975 url,
976 editable: Some(false),
977 r#virtual: Some(r#virtual),
978 })
979 }
980 } else {
981 if let Some(git_member) = git_member {
982 return git_archive_source_from_path(install_path, git_member);
983 }
984 if editable == Some(true) {
985 return Err(LoweringError::EditableFile(url.to_string()));
986 }
987 if package == Some(true) {
988 return Err(LoweringError::PackagedFile(url.to_string()));
989 }
990 Ok(RequirementSource::Path {
991 ext: DistExtension::from_path(&install_path)
992 .map_err(|err| ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err))?,
993 install_path: install_path.into_boxed_path(),
994 url,
995 })
996 }
997}
998
999fn git_directory_source_from_path(
1000 install_path: impl AsRef<Path>,
1001 git_member: &GitWorkspaceMember,
1002) -> Result<RequirementSource, LoweringError> {
1003 let git = git_member.git_source.git.clone();
1004 let install_path = git_path(install_path.as_ref())?;
1005 let fetch_root = git_path(git_member.fetch_root)?;
1006 let subdirectory = uv_fs::relative_to(&install_path, fetch_root)
1007 .map_err(|_| LoweringError::GitDirectory(install_path.display().to_string()))?;
1008 let subdirectory = normalize_path(subdirectory);
1009 let subdirectory = if subdirectory == PathBuf::new() {
1010 None
1011 } else {
1012 Some(subdirectory.into_owned().into_boxed_path())
1013 };
1014 let url = SafeUrl::from(ParsedGitDirectoryUrl {
1015 url: git.clone(),
1016 subdirectory: subdirectory.clone(),
1017 });
1018 Ok(RequirementSource::GitDirectory {
1019 git,
1020 subdirectory,
1021 url: VerbatimUrl::from_url(url),
1022 })
1023}
1024
1025fn git_archive_source_from_path(
1026 install_path: impl AsRef<Path>,
1027 git_member: &GitWorkspaceMember,
1028) -> Result<RequirementSource, LoweringError> {
1029 let git = git_member.git_source.git.clone();
1030 let install_path = git_path(install_path.as_ref())?;
1031 let fetch_root = git_path(git_member.fetch_root)?;
1032 let install_path =
1033 uv_fs::relative_to(install_path, fetch_root).map_err(LoweringError::RelativeTo)?;
1034 let install_path = normalize_path(install_path).into_owned();
1035 let ext = DistExtension::from_path(&install_path)
1036 .map_err(|err| ParsedUrlError::MissingExtensionPath(install_path.clone(), err))?;
1037 let url = SafeUrl::from(ParsedGitPathUrl {
1038 url: git.clone(),
1039 install_path: install_path.clone(),
1040 ext,
1041 });
1042 Ok(RequirementSource::GitPath {
1043 git,
1044 install_path,
1045 ext,
1046 url: VerbatimUrl::from_url(url),
1047 })
1048}
1049
1050fn git_path(path: &Path) -> Result<PathBuf, LoweringError> {
1051 path.simple_canonicalize()
1052 .or_else(|_| normalize_absolute_path(path))
1053 .map_err(LoweringError::RelativeTo)
1054}