Skip to content

Commit

Permalink
Add support for uv sync --all-packages (#8739)
Browse files Browse the repository at this point in the history
## Summary

This PR enables `uv sync --all-packages` to sync all packages in a
workspace. It removes a common use-case for the legacy non-`[project]`
packages that we're trying to move away from.

Closes #8724.
  • Loading branch information
charliermarsh authored Nov 2, 2024
1 parent 58a9811 commit 3c9dd97
Show file tree
Hide file tree
Showing 16 changed files with 407 additions and 83 deletions.
18 changes: 14 additions & 4 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2119,7 +2119,7 @@ pub struct BuildArgs {
/// directory if no source directory is provided.
///
/// If the workspace member does not exist, uv will exit with an error.
#[arg(long, conflicts_with("all"))]
#[arg(long, conflicts_with("all_packages"))]
pub package: Option<PackageName>,

/// Builds all packages in the workspace.
Expand All @@ -2128,8 +2128,8 @@ pub struct BuildArgs {
/// directory if no source directory is provided.
///
/// If the workspace member does not exist, uv will exit with an error.
#[arg(long, conflicts_with("package"))]
pub all: bool,
#[arg(long, alias = "all", conflicts_with("package"))]
pub all_packages: bool,

/// The output directory to which distributions should be written.
///
Expand Down Expand Up @@ -2912,13 +2912,23 @@ pub struct SyncArgs {
#[command(flatten)]
pub refresh: RefreshArgs,

/// Sync all packages in the workspace.
///
/// The workspace's environment (`.venv`) is updated to include all workspace
/// members.
///
/// Any extras or groups specified via `--extra`, `--group`, or related options
/// will be applied to all workspace members.
#[arg(long, conflicts_with = "package")]
pub all_packages: bool,

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

/// The Python interpreter to use for the project environment.
Expand Down
10 changes: 5 additions & 5 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ impl Lock {
/// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root.
pub fn to_resolution(
&self,
project: InstallTarget<'_>,
target: InstallTarget<'_>,
marker_env: &ResolverMarkerEnvironment,
tags: &Tags,
extras: &ExtrasSpecification,
Expand All @@ -588,7 +588,7 @@ impl Lock {
let mut seen = FxHashSet::default();

// Add the workspace packages to the queue.
for root_name in project.packages() {
for root_name in target.packages() {
let root = self
.find_by_name(root_name)
.map_err(|_| LockErrorKind::MultipleRootPackages {
Expand Down Expand Up @@ -638,7 +638,7 @@ impl Lock {

// Add any dependency groups that are exclusive to the workspace root (e.g., dev
// dependencies in (legacy) non-project workspace roots).
let groups = project
let groups = target
.groups()
.map_err(|err| LockErrorKind::DependencyGroup { err })?;
for group in dev.iter() {
Expand Down Expand Up @@ -688,13 +688,13 @@ impl Lock {
}
if install_options.include_package(
&dist.id.name,
project.project_name(),
target.project_name(),
&self.manifest.members,
) {
map.insert(
dist.id.name.clone(),
ResolvedDist::Installable(dist.to_dist(
project.workspace().install_path(),
target.workspace().install_path(),
TagPolicy::Required(tags),
build_options,
)?),
Expand Down
44 changes: 27 additions & 17 deletions crates/uv-workspace/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1496,35 +1496,39 @@ impl VirtualProject {
/// A target that can be installed.
#[derive(Debug, Copy, Clone)]
pub enum InstallTarget<'env> {
/// An entire workspace.
Workspace(&'env Workspace),
/// A (legacy) non-project workspace root.
NonProjectWorkspace(&'env Workspace),
/// A project (which could be a workspace root or member).
Project(&'env ProjectWorkspace),
/// A (legacy) non-project workspace root.
NonProject(&'env Workspace),
/// A frozen member within a [`Workspace`].
FrozenMember(&'env Workspace, &'env PackageName),
FrozenProject(&'env Workspace, &'env PackageName),
}

impl<'env> InstallTarget<'env> {
/// Create an [`InstallTarget`] for a frozen member within a workspace.
pub fn frozen_member(project: &'env VirtualProject, package_name: &'env PackageName) -> Self {
Self::FrozenMember(project.workspace(), package_name)
pub fn frozen(project: &'env VirtualProject, package_name: &'env PackageName) -> Self {
Self::FrozenProject(project.workspace(), package_name)
}

/// Return the [`Workspace`] of the target.
pub fn workspace(&self) -> &Workspace {
match self {
Self::Workspace(workspace) => workspace,
Self::Project(project) => project.workspace(),
Self::NonProject(workspace) => workspace,
Self::FrozenMember(workspace, _) => workspace,
Self::NonProjectWorkspace(workspace) => workspace,
Self::FrozenProject(workspace, _) => workspace,
}
}

/// Return the [`PackageName`] of the target.
pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
match self {
Self::Workspace(workspace) => Either::Right(workspace.packages().keys()),
Self::Project(project) => Either::Left(std::iter::once(project.project_name())),
Self::NonProject(workspace) => Either::Right(workspace.packages().keys()),
Self::FrozenMember(_, package_name) => Either::Left(std::iter::once(*package_name)),
Self::NonProjectWorkspace(workspace) => Either::Right(workspace.packages().keys()),
Self::FrozenProject(_, package_name) => Either::Left(std::iter::once(*package_name)),
}
}

Expand All @@ -1540,8 +1544,8 @@ impl<'env> InstallTarget<'env> {
DependencyGroupError,
> {
match self {
Self::Project(_) | Self::FrozenMember(..) => Ok(BTreeMap::new()),
Self::NonProject(workspace) => {
Self::Workspace(_) | Self::Project(_) | Self::FrozenProject(..) => Ok(BTreeMap::new()),
Self::NonProjectWorkspace(workspace) => {
// For non-projects, we might have `dependency-groups` or `tool.uv.dev-dependencies`
// that are attached to the workspace root (which isn't a member).

Expand Down Expand Up @@ -1591,18 +1595,24 @@ impl<'env> InstallTarget<'env> {
/// Return the [`PackageName`] of the target, if available.
pub fn project_name(&self) -> Option<&PackageName> {
match self {
Self::Workspace(_) => None,
Self::Project(project) => Some(project.project_name()),
Self::NonProject(_) => None,
Self::FrozenMember(_, package_name) => Some(package_name),
Self::NonProjectWorkspace(_) => None,
Self::FrozenProject(_, package_name) => Some(package_name),
}
}

pub fn from_workspace(workspace: &'env VirtualProject) -> Self {
match workspace {
VirtualProject::Project(project) => Self::Workspace(project.workspace()),
VirtualProject::NonProject(workspace) => Self::NonProjectWorkspace(workspace),
}
}
}

impl<'env> From<&'env VirtualProject> for InstallTarget<'env> {
fn from(project: &'env VirtualProject) -> Self {
pub fn from_project(project: &'env VirtualProject) -> Self {
match project {
VirtualProject::Project(project) => Self::Project(project),
VirtualProject::NonProject(workspace) => Self::NonProject(workspace),
VirtualProject::NonProject(workspace) => Self::NonProjectWorkspace(workspace),
}
}
}
Expand Down
14 changes: 7 additions & 7 deletions crates/uv/src/commands/build_frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub(crate) async fn build_frontend(
project_dir: &Path,
src: Option<PathBuf>,
package: Option<PackageName>,
all: bool,
all_packages: bool,
output_dir: Option<PathBuf>,
sdist: bool,
wheel: bool,
Expand All @@ -65,7 +65,7 @@ pub(crate) async fn build_frontend(
project_dir,
src.as_deref(),
package.as_ref(),
all,
all_packages,
output_dir.as_deref(),
sdist,
wheel,
Expand Down Expand Up @@ -105,7 +105,7 @@ async fn build_impl(
project_dir: &Path,
src: Option<&Path>,
package: Option<&PackageName>,
all: bool,
all_packages: bool,
output_dir: Option<&Path>,
sdist: bool,
wheel: bool,
Expand Down Expand Up @@ -171,7 +171,7 @@ async fn build_impl(
// Attempt to discover the workspace; on failure, save the error for later.
let workspace = Workspace::discover(src.directory(), &DiscoveryOptions::default()).await;

// If a `--package` or `--all` was provided, adjust the source directory.
// If a `--package` or `--all-packages` was provided, adjust the source directory.
let packages = if let Some(package) = package {
if matches!(src, Source::File(_)) {
return Err(anyhow::anyhow!(
Expand Down Expand Up @@ -201,18 +201,18 @@ async fn build_impl(
vec![AnnotatedSource::from(Source::Directory(Cow::Borrowed(
package.root(),
)))]
} else if all {
} else if all_packages {
if matches!(src, Source::File(_)) {
return Err(anyhow::anyhow!(
"Cannot specify `--all` when building from a file"
"Cannot specify `--all-packages` when building from a file"
));
}

let workspace = match workspace {
Ok(ref workspace) => workspace,
Err(err) => {
return Err(anyhow::Error::from(err)
.context("`--all` was provided, but no workspace was found"));
.context("`--all-packages` was provided, but no workspace was found"));
}
};

Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@ async fn lock_and_sync(
};

project::sync::do_sync(
InstallTarget::from(&project),
InstallTarget::from_project(&project),
venv,
&lock,
&extras,
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use uv_configuration::{
use uv_normalize::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_resolver::RequirementsTxtExport;
use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace};
use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, VirtualProject, Workspace};

use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::project::lock::{do_safe_lock, LockMode};
Expand Down Expand Up @@ -73,7 +73,7 @@ pub(crate) async fn export(
};

// Determine the default groups to include.
validate_dependency_groups(&project, &dev)?;
validate_dependency_groups(InstallTarget::from_project(&project), &dev)?;
let defaults = default_dependency_groups(project.pyproject_toml())?;

let VirtualProject::Project(project) = project else {
Expand Down
34 changes: 11 additions & 23 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::pyproject::PyProjectToml;
use uv_workspace::{VirtualProject, Workspace};
use uv_workspace::{InstallTarget, Workspace};

use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
use crate::commands::pip::operations::{Changelog, Modifications};
Expand Down Expand Up @@ -1369,16 +1369,22 @@ pub(crate) async fn script_python_requirement(
/// Validate the dependency groups requested by the [`DevGroupsSpecification`].
#[allow(clippy::result_large_err)]
pub(crate) fn validate_dependency_groups(
project: &VirtualProject,
target: InstallTarget<'_>,
dev: &DevGroupsSpecification,
) -> Result<(), ProjectError> {
for group in dev
.groups()
.into_iter()
.flat_map(GroupsSpecification::names)
{
match project {
VirtualProject::Project(project) => {
match target {
InstallTarget::Workspace(workspace) | InstallTarget::NonProjectWorkspace(workspace) => {
// The group must be defined in the workspace.
if !workspace.groups().contains(group) {
return Err(ProjectError::MissingGroupWorkspace(group.clone()));
}
}
InstallTarget::Project(project) => {
// The group must be defined in the target project.
if !project
.current_project()
Expand All @@ -1390,25 +1396,7 @@ pub(crate) fn validate_dependency_groups(
return Err(ProjectError::MissingGroupProject(group.clone()));
}
}
VirtualProject::NonProject(workspace) => {
// The group must be defined in at least one workspace package.
if !workspace
.pyproject_toml()
.dependency_groups
.as_ref()
.is_some_and(|groups| groups.contains_key(group))
{
if workspace.packages().values().all(|package| {
!package
.pyproject_toml()
.dependency_groups
.as_ref()
.is_some_and(|groups| groups.contains_key(group))
}) {
return Err(ProjectError::MissingGroupWorkspace(group.clone()));
}
}
}
InstallTarget::FrozenProject(_, _) => {}
}
}
Ok(())
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ pub(crate) async fn remove(
let defaults = default_dependency_groups(project.pyproject_toml())?;

project::sync::do_sync(
InstallTarget::from(&project),
InstallTarget::from_project(&project),
&venv,
&lock,
&extras,
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ pub(crate) async fn run(
}
} else {
// Determine the default groups to include.
validate_dependency_groups(&project, &dev)?;
validate_dependency_groups(InstallTarget::from_project(&project), &dev)?;
let defaults = default_dependency_groups(project.pyproject_toml())?;

// Determine the lock mode.
Expand Down Expand Up @@ -607,7 +607,7 @@ pub(crate) async fn run(
let install_options = InstallOptions::default();

project::sync::do_sync(
InstallTarget::from(&project),
InstallTarget::from_project(&project),
&venv,
result.lock(),
&extras,
Expand Down
Loading

0 comments on commit 3c9dd97

Please sign in to comment.