Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3669,14 +3669,14 @@ pub struct SyncArgs {
#[arg(long, conflicts_with = "package")]
pub all_packages: bool,

/// Sync for a specific package in the workspace.
/// Sync for specific packages in the workspace.
///
/// The workspace's environment (`.venv`) is updated to reflect the subset of dependencies
/// declared by the specified workspace member package.
/// declared by the specified workspace member packages.
///
/// If the workspace member does not exist, uv will exit with an error.
/// If any workspace member does not exist, uv will exit with an error.
#[arg(long, conflicts_with = "all_packages")]
pub package: Option<PackageName>,
pub package: Vec<PackageName>,

/// Sync the environment for a Python script, rather than the current project.
///
Expand Down
126 changes: 81 additions & 45 deletions crates/uv/src/commands/project/install_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ pub(crate) enum InstallTarget<'lock> {
name: &'lock PackageName,
lock: &'lock Lock,
},
/// Multiple specific projects in a workspace.
Projects {
workspace: &'lock Workspace,
names: &'lock [PackageName],
lock: &'lock Lock,
},
/// An entire workspace.
Workspace {
workspace: &'lock Workspace,
Expand All @@ -47,6 +53,7 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> {
fn install_path(&self) -> &'lock Path {
match self {
Self::Project { workspace, .. } => workspace.install_path(),
Self::Projects { workspace, .. } => workspace.install_path(),
Self::Workspace { workspace, .. } => workspace.install_path(),
Self::NonProjectWorkspace { workspace, .. } => workspace.install_path(),
Self::Script { script, .. } => script.path.parent().unwrap(),
Expand All @@ -56,36 +63,38 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> {
fn lock(&self) -> &'lock Lock {
match self {
Self::Project { lock, .. } => lock,
Self::Projects { lock, .. } => lock,
Self::Workspace { lock, .. } => lock,
Self::NonProjectWorkspace { lock, .. } => lock,
Self::Script { lock, .. } => lock,
}
}

fn roots(&self) -> impl Iterator<Item = &PackageName> {
#[allow(refining_impl_trait)]
fn roots(&self) -> Box<dyn Iterator<Item = &PackageName> + '_> {
match self {
Self::Project { name, .. } => Either::Left(Either::Left(std::iter::once(*name))),
Self::NonProjectWorkspace { lock, .. } => {
Either::Left(Either::Right(lock.members().iter()))
}
Self::Project { name, .. } => Box::new(std::iter::once(*name)),
Self::Projects { names, .. } => Box::new(names.iter()),
Self::NonProjectWorkspace { lock, .. } => Box::new(lock.members().iter()),
Self::Workspace { lock, .. } => {
// Identify the workspace members.
//
// The members are encoded directly in the lockfile, unless the workspace contains a
// single member at the root, in which case, we identify it by its source.
if lock.members().is_empty() {
Either::Right(Either::Left(lock.root().into_iter().map(Package::name)))
Box::new(lock.root().into_iter().map(Package::name))
} else {
Either::Left(Either::Right(lock.members().iter()))
Box::new(lock.members().iter())
}
}
Self::Script { .. } => Either::Right(Either::Right(std::iter::empty())),
Self::Script { .. } => Box::new(std::iter::empty()),
}
}

fn project_name(&self) -> Option<&PackageName> {
match self {
Self::Project { name, .. } => Some(name),
Self::Projects { .. } => None,
Self::Workspace { .. } => None,
Self::NonProjectWorkspace { .. } => None,
Self::Script { .. } => None,
Expand All @@ -98,6 +107,7 @@ impl<'lock> InstallTarget<'lock> {
pub(crate) fn indexes(self) -> impl Iterator<Item = &'lock Index> {
match self {
Self::Project { workspace, .. }
| Self::Projects { workspace, .. }
| Self::Workspace { workspace, .. }
| Self::NonProjectWorkspace { workspace, .. } => {
Either::Left(workspace.indexes().iter().chain(
Expand Down Expand Up @@ -130,6 +140,7 @@ impl<'lock> InstallTarget<'lock> {
pub(crate) fn sources(&self) -> impl Iterator<Item = &Source> {
match self {
Self::Project { workspace, .. }
| Self::Projects { workspace, .. }
| Self::Workspace { workspace, .. }
| Self::NonProjectWorkspace { workspace, .. } => {
Either::Left(workspace.sources().values().flat_map(Sources::iter).chain(
Expand Down Expand Up @@ -158,6 +169,7 @@ impl<'lock> InstallTarget<'lock> {
) -> impl Iterator<Item = Cow<'lock, uv_pep508::Requirement<VerbatimParsedUrl>>> {
match self {
Self::Project { workspace, .. }
| Self::Projects { workspace, .. }
| Self::Workspace { workspace, .. }
| Self::NonProjectWorkspace { workspace, .. } => {
Either::Left(
Expand Down Expand Up @@ -256,6 +268,7 @@ impl<'lock> InstallTarget<'lock> {
}
match self {
Self::Project { lock, .. }
| Self::Projects { lock, .. }
| Self::Workspace { lock, .. }
| Self::NonProjectWorkspace { lock, .. } => {
if !lock.supports_provides_extra() {
Expand All @@ -281,7 +294,10 @@ impl<'lock> InstallTarget<'lock> {
Self::Project { .. } => {
Err(ProjectError::MissingExtraProject(extra.clone()))
}
_ => Err(ProjectError::MissingExtraWorkspace(extra.clone())),
Self::Projects { .. } => {
Err(ProjectError::MissingExtraProjects(extra.clone()))
}
_ => Err(ProjectError::MissingExtraProjects(extra.clone())),
};
}
}
Expand Down Expand Up @@ -337,27 +353,35 @@ impl<'lock> InstallTarget<'lock> {

for group in groups.explicit_names() {
if !known_groups.contains(group) {
return Err(ProjectError::MissingGroupWorkspace(group.clone()));
return Err(ProjectError::MissingGroupProjects(group.clone()));
}
}
}
Self::Project { lock, .. } => {
Self::Project { lock, .. } | Self::Projects { lock, .. } => {
let roots = self.roots().collect::<FxHashSet<_>>();
let member_packages: Vec<&Package> = lock
.packages()
.iter()
.filter(|package| roots.contains(package.name()))
.collect();

// Extract the dependency groups defined in the relevant member.
// Extract the dependency groups defined in the relevant member(s).
let known_groups = member_packages
.iter()
.flat_map(|package| package.dependency_groups().keys())
.collect::<FxHashSet<_>>();

for group in groups.explicit_names() {
if !known_groups.contains(group) {
return Err(ProjectError::MissingGroupProject(group.clone()));
return match self {
Self::Project { .. } => {
Err(ProjectError::MissingGroupProject(group.clone()))
}
Self::Projects { .. } => {
Err(ProjectError::MissingGroupProjects(group.clone()))
}
_ => unreachable!(),
};
}
}
}
Expand All @@ -380,59 +404,71 @@ impl<'lock> InstallTarget<'lock> {
groups: &DependencyGroupsWithDefaults,
) -> BTreeSet<&PackageName> {
match self {
Self::Project { name, lock, .. } => {
// Collect the packages by name for efficient lookup
Self::Project { lock, .. } | Self::Projects { lock, .. } => {
let roots = self.roots().collect::<FxHashSet<_>>();

// Collect the packages by name for efficient lookup.
let packages = lock
.packages()
.iter()
.map(|p| (p.name(), p))
.map(|package| (package.name(), package))
.collect::<BTreeMap<_, _>>();

// We'll include the project itself
// We'll include all specified projects
let mut required_members = BTreeSet::new();
required_members.insert(*name);
for name in &roots {
required_members.insert(*name);
}

// Find all workspace member dependencies recursively
// Find all workspace member dependencies recursively for all specified packages
let mut queue: VecDeque<(&PackageName, Option<&ExtraName>)> = VecDeque::new();
let mut seen: FxHashSet<(&PackageName, Option<&ExtraName>)> = FxHashSet::default();

let Some(root_package) = packages.get(name) else {
return required_members;
};
for name in roots {
let Some(root_package) = packages.get(name) else {
continue;
};

if groups.prod() {
// Add the root package
queue.push_back((name, None));
seen.insert((name, None));
if groups.prod() {
// Add the root package
if seen.insert((name, None)) {
queue.push_back((name, None));
}

// Add explicitly activated extras for the root package
for extra in extras.extra_names(root_package.optional_dependencies().keys()) {
if seen.insert((name, Some(extra))) {
queue.push_back((name, Some(extra)));
// Add explicitly activated extras for the root package
for extra in extras.extra_names(root_package.optional_dependencies().keys())
{
if seen.insert((name, Some(extra))) {
queue.push_back((name, Some(extra)));
}
}
}
}

// Add activated dependency groups for the root package
for (group_name, dependencies) in root_package.resolved_dependency_groups() {
if !groups.contains(group_name) {
continue;
}
for dependency in dependencies {
let name = dependency.package_name();
queue.push_back((name, None));
for extra in dependency.extra() {
queue.push_back((name, Some(extra)));
// Add activated dependency groups for the root package
for (group_name, dependencies) in root_package.resolved_dependency_groups() {
if !groups.contains(group_name) {
continue;
}
for dependency in dependencies {
let dep_name = dependency.package_name();
if seen.insert((dep_name, None)) {
queue.push_back((dep_name, None));
}
for extra in dependency.extra() {
if seen.insert((dep_name, Some(extra))) {
queue.push_back((dep_name, Some(extra)));
}
}
}
}
}

while let Some((pkg_name, extra)) = queue.pop_front() {
if lock.members().contains(pkg_name) {
required_members.insert(pkg_name);
while let Some((package_name, extra)) = queue.pop_front() {
if lock.members().contains(package_name) {
required_members.insert(package_name);
}

let Some(package) = packages.get(pkg_name) else {
let Some(package) = packages.get(package_name) else {
continue;
};

Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ pub(crate) enum ProjectError {
MissingGroupProject(GroupName),

#[error("Group `{0}` is not defined in any project's `dependency-groups` table")]
MissingGroupWorkspace(GroupName),
MissingGroupProjects(GroupName),

#[error("PEP 723 scripts do not support dependency groups, but group `{0}` was specified")]
MissingGroupScript(GroupName),
Expand All @@ -175,7 +175,7 @@ pub(crate) enum ProjectError {
MissingExtraProject(ExtraName),

#[error("Extra `{0}` is not defined in any project's `optional-dependencies` table")]
MissingExtraWorkspace(ExtraName),
MissingExtraProjects(ExtraName),

#[error("PEP 723 scripts do not support optional dependencies, but extra `{0}` was specified")]
MissingExtraScript(ExtraName),
Expand Down
Loading
Loading