Skip to content

Commit cd40a34

Browse files
zaniebjtfmumm
andcommitted
Build and install workspace members that are dependencies by default (#14663)
Regardless of the presence of a build system, as in #14413 --------- Co-authored-by: John Mumm <[email protected]>
1 parent 0077f23 commit cd40a34

File tree

15 files changed

+791
-67
lines changed

15 files changed

+791
-67
lines changed

crates/uv-distribution/src/metadata/lowering.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,10 @@ impl LoweredRequirement {
306306
},
307307
url,
308308
}
309-
} else if member.pyproject_toml().is_package() {
309+
} else if member
310+
.pyproject_toml()
311+
.is_package(!workspace.is_required_member(&requirement.name))
312+
{
310313
RequirementSource::Directory {
311314
install_path: install_path.into_boxed_path(),
312315
url,
@@ -736,7 +739,8 @@ fn path_source(
736739
fs_err::read_to_string(&pyproject_path)
737740
.ok()
738741
.and_then(|contents| PyProjectToml::from_string(contents).ok())
739-
.and_then(|pyproject_toml| pyproject_toml.tool_uv_package())
742+
// We don't require a build system for path dependencies
743+
.map(|pyproject_toml| pyproject_toml.is_package(false))
740744
.unwrap_or(true)
741745
});
742746

crates/uv-platform-tags/src/tags.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ mod tests {
771771
/// A reference list can be generated with:
772772
/// ```text
773773
/// $ python -c "from packaging import tags; [print(tag) for tag in tags.platform_tags()]"`
774-
/// ````
774+
/// ```
775775
#[test]
776776
fn test_platform_tags_manylinux() {
777777
let tags = compatible_tags(&Platform::new(

crates/uv-resolver/src/lock/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,7 @@ impl Lock {
12551255
root: &Path,
12561256
packages: &BTreeMap<PackageName, WorkspaceMember>,
12571257
members: &[PackageName],
1258+
required_members: &BTreeSet<PackageName>,
12581259
requirements: &[Requirement],
12591260
constraints: &[Requirement],
12601261
overrides: &[Requirement],
@@ -1282,7 +1283,10 @@ impl Lock {
12821283
// Validate that the member sources have not changed (e.g., that they've switched from
12831284
// virtual to non-virtual or vice versa).
12841285
for (name, member) in packages {
1285-
let expected = !member.pyproject_toml().is_package();
1286+
// We don't require a build system, if the workspace member is a dependency
1287+
let expected = !member
1288+
.pyproject_toml()
1289+
.is_package(!required_members.contains(name));
12861290
let actual = self
12871291
.find_by_name(name)
12881292
.ok()

crates/uv-workspace/src/pyproject.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ pub struct PyProjectToml {
6666

6767
/// Used to determine whether a `build-system` section is present.
6868
#[serde(default, skip_serializing)]
69-
build_system: Option<serde::de::IgnoredAny>,
69+
pub build_system: Option<serde::de::IgnoredAny>,
7070
}
7171

7272
impl PyProjectToml {
@@ -81,18 +81,18 @@ impl PyProjectToml {
8181

8282
/// Returns `true` if the project should be considered a Python package, as opposed to a
8383
/// non-package ("virtual") project.
84-
pub fn is_package(&self) -> bool {
84+
pub fn is_package(&self, require_build_system: bool) -> bool {
8585
// If `tool.uv.package` is set, defer to that explicit setting.
8686
if let Some(is_package) = self.tool_uv_package() {
8787
return is_package;
8888
}
8989

9090
// Otherwise, a project is assumed to be a package if `build-system` is present.
91-
self.build_system.is_some()
91+
self.build_system.is_some() || !require_build_system
9292
}
9393

9494
/// Returns the value of `tool.uv.package` if set.
95-
pub fn tool_uv_package(&self) -> Option<bool> {
95+
fn tool_uv_package(&self) -> Option<bool> {
9696
self.tool
9797
.as_ref()
9898
.and_then(|tool| tool.uv.as_ref())

crates/uv-workspace/src/workspace.rs

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use uv_warnings::warn_user_once;
2020

2121
use crate::dependency_groups::{DependencyGroupError, FlatDependencyGroup, FlatDependencyGroups};
2222
use crate::pyproject::{
23-
Project, PyProjectToml, PyprojectTomlError, Sources, ToolUvSources, ToolUvWorkspace,
23+
Project, PyProjectToml, PyprojectTomlError, Source, Sources, ToolUvSources, ToolUvWorkspace,
2424
};
2525

2626
type WorkspaceMembers = Arc<BTreeMap<PackageName, WorkspaceMember>>;
@@ -109,6 +109,8 @@ pub struct Workspace {
109109
install_path: PathBuf,
110110
/// The members of the workspace.
111111
packages: WorkspaceMembers,
112+
/// The workspace members that are required by other members.
113+
required_members: BTreeSet<PackageName>,
112114
/// The sources table from the workspace `pyproject.toml`.
113115
///
114116
/// This table is overridden by the project sources.
@@ -260,6 +262,7 @@ impl Workspace {
260262
pyproject_toml: PyProjectToml,
261263
) -> Option<Self> {
262264
let mut packages = self.packages;
265+
263266
let member = Arc::make_mut(&mut packages).get_mut(package_name)?;
264267

265268
if member.root == self.install_path {
@@ -279,17 +282,33 @@ impl Workspace {
279282
// Set the `pyproject.toml` for the member.
280283
member.pyproject_toml = pyproject_toml;
281284

285+
// Recompute required_members with the updated data
286+
let required_members = Self::collect_required_members(
287+
&packages,
288+
&workspace_sources,
289+
&workspace_pyproject_toml,
290+
);
291+
282292
Some(Self {
283293
pyproject_toml: workspace_pyproject_toml,
284294
sources: workspace_sources,
285295
packages,
296+
required_members,
286297
..self
287298
})
288299
} else {
289300
// Set the `pyproject.toml` for the member.
290301
member.pyproject_toml = pyproject_toml;
291302

292-
Some(Self { packages, ..self })
303+
// Recompute required_members with the updated member data
304+
let required_members =
305+
Self::collect_required_members(&packages, &self.sources, &self.pyproject_toml);
306+
307+
Some(Self {
308+
packages,
309+
required_members,
310+
..self
311+
})
293312
}
294313
}
295314

@@ -303,7 +322,7 @@ impl Workspace {
303322

304323
/// Returns the set of all workspace members.
305324
pub fn members_requirements(&self) -> impl Iterator<Item = Requirement> + '_ {
306-
self.packages.values().filter_map(|member| {
325+
self.packages.iter().filter_map(|(name, member)| {
307326
let url = VerbatimUrl::from_absolute_path(&member.root)
308327
.expect("path is valid URL")
309328
.with_given(member.root.to_string_lossy());
@@ -312,7 +331,10 @@ impl Workspace {
312331
extras: Box::new([]),
313332
groups: Box::new([]),
314333
marker: MarkerTree::TRUE,
315-
source: if member.pyproject_toml.is_package() {
334+
source: if member
335+
.pyproject_toml()
336+
.is_package(!self.is_required_member(name))
337+
{
316338
RequirementSource::Directory {
317339
install_path: member.root.clone().into_boxed_path(),
318340
editable: Some(true),
@@ -332,9 +354,65 @@ impl Workspace {
332354
})
333355
}
334356

357+
/// The workspace members that are required my another member of the workspace.
358+
pub fn required_members(&self) -> &BTreeSet<PackageName> {
359+
&self.required_members
360+
}
361+
362+
/// Compute the workspace members that are required by another member of the workspace.
363+
///
364+
/// N.B. this checks if a workspace member is required by inspecting `tool.uv.source` entries,
365+
/// but does not actually check if the source is _used_, which could result in false positives
366+
/// but is easier to compute.
367+
fn collect_required_members(
368+
packages: &BTreeMap<PackageName, WorkspaceMember>,
369+
sources: &BTreeMap<PackageName, Sources>,
370+
pyproject_toml: &PyProjectToml,
371+
) -> BTreeSet<PackageName> {
372+
sources
373+
.iter()
374+
.filter(|(name, _)| {
375+
pyproject_toml
376+
.project
377+
.as_ref()
378+
.is_none_or(|project| project.name != **name)
379+
})
380+
.chain(
381+
packages
382+
.iter()
383+
.filter_map(|(name, member)| {
384+
member
385+
.pyproject_toml
386+
.tool
387+
.as_ref()
388+
.and_then(|tool| tool.uv.as_ref())
389+
.and_then(|uv| uv.sources.as_ref())
390+
.map(ToolUvSources::inner)
391+
.map(move |sources| {
392+
sources
393+
.iter()
394+
.filter(move |(source_name, _)| name != *source_name)
395+
})
396+
})
397+
.flatten(),
398+
)
399+
.filter_map(|(package, sources)| {
400+
sources
401+
.iter()
402+
.any(|source| matches!(source, Source::Workspace { .. }))
403+
.then_some(package.clone())
404+
})
405+
.collect()
406+
}
407+
408+
/// Whether a given workspace member is required by another member.
409+
pub fn is_required_member(&self, name: &PackageName) -> bool {
410+
self.required_members().contains(name)
411+
}
412+
335413
/// Returns the set of all workspace member dependency groups.
336414
pub fn group_requirements(&self) -> impl Iterator<Item = Requirement> + '_ {
337-
self.packages.values().filter_map(|member| {
415+
self.packages.iter().filter_map(|(name, member)| {
338416
let url = VerbatimUrl::from_absolute_path(&member.root)
339417
.expect("path is valid URL")
340418
.with_given(member.root.to_string_lossy());
@@ -368,7 +446,10 @@ impl Workspace {
368446
extras: Box::new([]),
369447
groups: groups.into_boxed_slice(),
370448
marker: MarkerTree::TRUE,
371-
source: if member.pyproject_toml.is_package() {
449+
source: if member
450+
.pyproject_toml()
451+
.is_package(!self.is_required_member(name))
452+
{
372453
RequirementSource::Directory {
373454
install_path: member.root.clone().into_boxed_path(),
374455
editable: Some(true),
@@ -746,9 +827,16 @@ impl Workspace {
746827
.and_then(|uv| uv.index)
747828
.unwrap_or_default();
748829

830+
let required_members = Self::collect_required_members(
831+
&workspace_members,
832+
&workspace_sources,
833+
&workspace_pyproject_toml,
834+
);
835+
749836
Ok(Workspace {
750837
install_path: workspace_root,
751838
packages: workspace_members,
839+
required_members,
752840
sources: workspace_sources,
753841
indexes: workspace_indexes,
754842
pyproject_toml: workspace_pyproject_toml,
@@ -1232,15 +1320,23 @@ impl ProjectWorkspace {
12321320
project.name.clone(),
12331321
current_project,
12341322
)]));
1323+
let workspace_sources = BTreeMap::default();
1324+
let required_members = Workspace::collect_required_members(
1325+
&current_project_as_members,
1326+
&workspace_sources,
1327+
project_pyproject_toml,
1328+
);
1329+
12351330
return Ok(Self {
12361331
project_root: project_path.clone(),
12371332
project_name: project.name.clone(),
12381333
workspace: Workspace {
12391334
install_path: project_path.clone(),
12401335
packages: current_project_as_members,
1336+
required_members,
12411337
// There may be package sources, but we don't need to duplicate them into the
12421338
// workspace sources.
1243-
sources: BTreeMap::default(),
1339+
sources: workspace_sources,
12441340
indexes: Vec::default(),
12451341
pyproject_toml: project_pyproject_toml.clone(),
12461342
},
@@ -1692,6 +1788,7 @@ mod tests {
16921788
"pyproject_toml": "[PYPROJECT_TOML]"
16931789
}
16941790
},
1791+
"required_members": [],
16951792
"sources": {},
16961793
"indexes": [],
16971794
"pyproject_toml": {
@@ -1745,6 +1842,7 @@ mod tests {
17451842
"pyproject_toml": "[PYPROJECT_TOML]"
17461843
}
17471844
},
1845+
"required_members": [],
17481846
"sources": {},
17491847
"indexes": [],
17501848
"pyproject_toml": {
@@ -1825,6 +1923,10 @@ mod tests {
18251923
"pyproject_toml": "[PYPROJECT_TOML]"
18261924
}
18271925
},
1926+
"required_members": [
1927+
"bird-feeder",
1928+
"seeds"
1929+
],
18281930
"sources": {
18291931
"bird-feeder": [
18301932
{
@@ -1946,6 +2048,10 @@ mod tests {
19462048
"pyproject_toml": "[PYPROJECT_TOML]"
19472049
}
19482050
},
2051+
"required_members": [
2052+
"bird-feeder",
2053+
"seeds"
2054+
],
19492055
"sources": {},
19502056
"indexes": [],
19512057
"pyproject_toml": {
@@ -2013,6 +2119,7 @@ mod tests {
20132119
"pyproject_toml": "[PYPROJECT_TOML]"
20142120
}
20152121
},
2122+
"required_members": [],
20162123
"sources": {},
20172124
"indexes": [],
20182125
"pyproject_toml": {
@@ -2147,6 +2254,7 @@ mod tests {
21472254
"pyproject_toml": "[PYPROJECT_TOML]"
21482255
}
21492256
},
2257+
"required_members": [],
21502258
"sources": {},
21512259
"indexes": [],
21522260
"pyproject_toml": {
@@ -2254,6 +2362,7 @@ mod tests {
22542362
"pyproject_toml": "[PYPROJECT_TOML]"
22552363
}
22562364
},
2365+
"required_members": [],
22572366
"sources": {},
22582367
"indexes": [],
22592368
"pyproject_toml": {
@@ -2375,6 +2484,7 @@ mod tests {
23752484
"pyproject_toml": "[PYPROJECT_TOML]"
23762485
}
23772486
},
2487+
"required_members": [],
23782488
"sources": {},
23792489
"indexes": [],
23802490
"pyproject_toml": {
@@ -2470,6 +2580,7 @@ mod tests {
24702580
"pyproject_toml": "[PYPROJECT_TOML]"
24712581
}
24722582
},
2583+
"required_members": [],
24732584
"sources": {},
24742585
"indexes": [],
24752586
"pyproject_toml": {

crates/uv/src/commands/build_frontend.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ async fn build_impl(
263263
.get(package)
264264
.ok_or_else(|| anyhow::anyhow!("Package `{package}` not found in workspace"))?;
265265

266-
if !package.pyproject_toml().is_package() {
266+
if !package.pyproject_toml().is_package(true) {
267267
let name = &package.project().name;
268268
let pyproject_toml = package.root().join("pyproject.toml");
269269
return Err(anyhow::anyhow!(
@@ -300,7 +300,7 @@ async fn build_impl(
300300
let packages: Vec<_> = workspace
301301
.packages()
302302
.values()
303-
.filter(|package| package.pyproject_toml().is_package())
303+
.filter(|package| package.pyproject_toml().is_package(true))
304304
.map(|package| AnnotatedSource {
305305
source: Source::Directory(Cow::Borrowed(package.root())),
306306
package: Some(package.project().name.clone()),

0 commit comments

Comments
 (0)