Add support for uv sync --all-packages (#8739)

## 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 https://github.com/astral-sh/uv/issues/8724.
This commit is contained in:
Charlie Marsh 2024-11-01 21:55:08 -04:00 committed by GitHub
parent 58a9811881
commit 3c9dd97fe9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 407 additions and 83 deletions

View file

@ -2119,7 +2119,7 @@ pub struct BuildArgs {
/// directory if no source directory is provided. /// directory if no source directory is provided.
/// ///
/// If the workspace member does not exist, uv will exit with an error. /// 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>, pub package: Option<PackageName>,
/// Builds all packages in the workspace. /// Builds all packages in the workspace.
@ -2128,8 +2128,8 @@ pub struct BuildArgs {
/// directory if no source directory is provided. /// directory if no source directory is provided.
/// ///
/// If the workspace member does not exist, uv will exit with an error. /// If the workspace member does not exist, uv will exit with an error.
#[arg(long, conflicts_with("package"))] #[arg(long, alias = "all", conflicts_with("package"))]
pub all: bool, pub all_packages: bool,
/// The output directory to which distributions should be written. /// The output directory to which distributions should be written.
/// ///
@ -2912,13 +2912,23 @@ pub struct SyncArgs {
#[command(flatten)] #[command(flatten)]
pub refresh: RefreshArgs, 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. /// Sync for a specific package in the workspace.
/// ///
/// The workspace's environment (`.venv`) is updated to reflect the subset /// The workspace's environment (`.venv`) is updated to reflect the subset
/// of dependencies declared by the specified workspace member package. /// of dependencies declared by the specified workspace member package.
/// ///
/// If the workspace member does not exist, uv will exit with an error. /// 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>, pub package: Option<PackageName>,
/// The Python interpreter to use for the project environment. /// The Python interpreter to use for the project environment.

View file

@ -576,7 +576,7 @@ impl Lock {
/// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root. /// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root.
pub fn to_resolution( pub fn to_resolution(
&self, &self,
project: InstallTarget<'_>, target: InstallTarget<'_>,
marker_env: &ResolverMarkerEnvironment, marker_env: &ResolverMarkerEnvironment,
tags: &Tags, tags: &Tags,
extras: &ExtrasSpecification, extras: &ExtrasSpecification,
@ -588,7 +588,7 @@ impl Lock {
let mut seen = FxHashSet::default(); let mut seen = FxHashSet::default();
// Add the workspace packages to the queue. // Add the workspace packages to the queue.
for root_name in project.packages() { for root_name in target.packages() {
let root = self let root = self
.find_by_name(root_name) .find_by_name(root_name)
.map_err(|_| LockErrorKind::MultipleRootPackages { .map_err(|_| LockErrorKind::MultipleRootPackages {
@ -638,7 +638,7 @@ impl Lock {
// Add any dependency groups that are exclusive to the workspace root (e.g., dev // Add any dependency groups that are exclusive to the workspace root (e.g., dev
// dependencies in (legacy) non-project workspace roots). // dependencies in (legacy) non-project workspace roots).
let groups = project let groups = target
.groups() .groups()
.map_err(|err| LockErrorKind::DependencyGroup { err })?; .map_err(|err| LockErrorKind::DependencyGroup { err })?;
for group in dev.iter() { for group in dev.iter() {
@ -688,13 +688,13 @@ impl Lock {
} }
if install_options.include_package( if install_options.include_package(
&dist.id.name, &dist.id.name,
project.project_name(), target.project_name(),
&self.manifest.members, &self.manifest.members,
) { ) {
map.insert( map.insert(
dist.id.name.clone(), dist.id.name.clone(),
ResolvedDist::Installable(dist.to_dist( ResolvedDist::Installable(dist.to_dist(
project.workspace().install_path(), target.workspace().install_path(),
TagPolicy::Required(tags), TagPolicy::Required(tags),
build_options, build_options,
)?), )?),

View file

@ -1496,35 +1496,39 @@ impl VirtualProject {
/// A target that can be installed. /// A target that can be installed.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub enum InstallTarget<'env> { 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). /// A project (which could be a workspace root or member).
Project(&'env ProjectWorkspace), Project(&'env ProjectWorkspace),
/// A (legacy) non-project workspace root.
NonProject(&'env Workspace),
/// A frozen member within a [`Workspace`]. /// A frozen member within a [`Workspace`].
FrozenMember(&'env Workspace, &'env PackageName), FrozenProject(&'env Workspace, &'env PackageName),
} }
impl<'env> InstallTarget<'env> { impl<'env> InstallTarget<'env> {
/// Create an [`InstallTarget`] for a frozen member within a workspace. /// Create an [`InstallTarget`] for a frozen member within a workspace.
pub fn frozen_member(project: &'env VirtualProject, package_name: &'env PackageName) -> Self { pub fn frozen(project: &'env VirtualProject, package_name: &'env PackageName) -> Self {
Self::FrozenMember(project.workspace(), package_name) Self::FrozenProject(project.workspace(), package_name)
} }
/// Return the [`Workspace`] of the target. /// Return the [`Workspace`] of the target.
pub fn workspace(&self) -> &Workspace { pub fn workspace(&self) -> &Workspace {
match self { match self {
Self::Workspace(workspace) => workspace,
Self::Project(project) => project.workspace(), Self::Project(project) => project.workspace(),
Self::NonProject(workspace) => workspace, Self::NonProjectWorkspace(workspace) => workspace,
Self::FrozenMember(workspace, _) => workspace, Self::FrozenProject(workspace, _) => workspace,
} }
} }
/// Return the [`PackageName`] of the target. /// Return the [`PackageName`] of the target.
pub fn packages(&self) -> impl Iterator<Item = &PackageName> { pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
match self { match self {
Self::Workspace(workspace) => Either::Right(workspace.packages().keys()),
Self::Project(project) => Either::Left(std::iter::once(project.project_name())), Self::Project(project) => Either::Left(std::iter::once(project.project_name())),
Self::NonProject(workspace) => Either::Right(workspace.packages().keys()), Self::NonProjectWorkspace(workspace) => Either::Right(workspace.packages().keys()),
Self::FrozenMember(_, package_name) => Either::Left(std::iter::once(*package_name)), Self::FrozenProject(_, package_name) => Either::Left(std::iter::once(*package_name)),
} }
} }
@ -1540,8 +1544,8 @@ impl<'env> InstallTarget<'env> {
DependencyGroupError, DependencyGroupError,
> { > {
match self { match self {
Self::Project(_) | Self::FrozenMember(..) => Ok(BTreeMap::new()), Self::Workspace(_) | Self::Project(_) | Self::FrozenProject(..) => Ok(BTreeMap::new()),
Self::NonProject(workspace) => { Self::NonProjectWorkspace(workspace) => {
// For non-projects, we might have `dependency-groups` or `tool.uv.dev-dependencies` // 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). // that are attached to the workspace root (which isn't a member).
@ -1591,18 +1595,24 @@ impl<'env> InstallTarget<'env> {
/// Return the [`PackageName`] of the target, if available. /// Return the [`PackageName`] of the target, if available.
pub fn project_name(&self) -> Option<&PackageName> { pub fn project_name(&self) -> Option<&PackageName> {
match self { match self {
Self::Workspace(_) => None,
Self::Project(project) => Some(project.project_name()), Self::Project(project) => Some(project.project_name()),
Self::NonProject(_) => None, Self::NonProjectWorkspace(_) => None,
Self::FrozenMember(_, package_name) => Some(package_name), Self::FrozenProject(_, package_name) => Some(package_name),
}
} }
} }
impl<'env> From<&'env VirtualProject> for InstallTarget<'env> { pub fn from_workspace(workspace: &'env VirtualProject) -> Self {
fn from(project: &'env VirtualProject) -> Self { match workspace {
VirtualProject::Project(project) => Self::Workspace(project.workspace()),
VirtualProject::NonProject(workspace) => Self::NonProjectWorkspace(workspace),
}
}
pub fn from_project(project: &'env VirtualProject) -> Self {
match project { match project {
VirtualProject::Project(project) => Self::Project(project), VirtualProject::Project(project) => Self::Project(project),
VirtualProject::NonProject(workspace) => Self::NonProject(workspace), VirtualProject::NonProject(workspace) => Self::NonProjectWorkspace(workspace),
} }
} }
} }

View file

@ -43,7 +43,7 @@ pub(crate) async fn build_frontend(
project_dir: &Path, project_dir: &Path,
src: Option<PathBuf>, src: Option<PathBuf>,
package: Option<PackageName>, package: Option<PackageName>,
all: bool, all_packages: bool,
output_dir: Option<PathBuf>, output_dir: Option<PathBuf>,
sdist: bool, sdist: bool,
wheel: bool, wheel: bool,
@ -65,7 +65,7 @@ pub(crate) async fn build_frontend(
project_dir, project_dir,
src.as_deref(), src.as_deref(),
package.as_ref(), package.as_ref(),
all, all_packages,
output_dir.as_deref(), output_dir.as_deref(),
sdist, sdist,
wheel, wheel,
@ -105,7 +105,7 @@ async fn build_impl(
project_dir: &Path, project_dir: &Path,
src: Option<&Path>, src: Option<&Path>,
package: Option<&PackageName>, package: Option<&PackageName>,
all: bool, all_packages: bool,
output_dir: Option<&Path>, output_dir: Option<&Path>,
sdist: bool, sdist: bool,
wheel: bool, wheel: bool,
@ -171,7 +171,7 @@ async fn build_impl(
// Attempt to discover the workspace; on failure, save the error for later. // Attempt to discover the workspace; on failure, save the error for later.
let workspace = Workspace::discover(src.directory(), &DiscoveryOptions::default()).await; 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 { let packages = if let Some(package) = package {
if matches!(src, Source::File(_)) { if matches!(src, Source::File(_)) {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
@ -201,10 +201,10 @@ async fn build_impl(
vec![AnnotatedSource::from(Source::Directory(Cow::Borrowed( vec![AnnotatedSource::from(Source::Directory(Cow::Borrowed(
package.root(), package.root(),
)))] )))]
} else if all { } else if all_packages {
if matches!(src, Source::File(_)) { if matches!(src, Source::File(_)) {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"Cannot specify `--all` when building from a file" "Cannot specify `--all-packages` when building from a file"
)); ));
} }
@ -212,7 +212,7 @@ async fn build_impl(
Ok(ref workspace) => workspace, Ok(ref workspace) => workspace,
Err(err) => { Err(err) => {
return Err(anyhow::Error::from(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"));
} }
}; };

View file

@ -870,7 +870,7 @@ async fn lock_and_sync(
}; };
project::sync::do_sync( project::sync::do_sync(
InstallTarget::from(&project), InstallTarget::from_project(&project),
venv, venv,
&lock, &lock,
&extras, &extras,

View file

@ -14,7 +14,7 @@ use uv_configuration::{
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_resolver::RequirementsTxtExport; 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::pip::loggers::DefaultResolveLogger;
use crate::commands::project::lock::{do_safe_lock, LockMode}; use crate::commands::project::lock::{do_safe_lock, LockMode};
@ -73,7 +73,7 @@ pub(crate) async fn export(
}; };
// Determine the default groups to include. // 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 defaults = default_dependency_groups(project.pyproject_toml())?;
let VirtualProject::Project(project) = project else { let VirtualProject::Project(project) = project else {

View file

@ -38,7 +38,7 @@ use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy};
use uv_warnings::{warn_user, warn_user_once}; use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::dependency_groups::DependencyGroupError;
use uv_workspace::pyproject::PyProjectToml; 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::loggers::{InstallLogger, ResolveLogger};
use crate::commands::pip::operations::{Changelog, Modifications}; use crate::commands::pip::operations::{Changelog, Modifications};
@ -1369,7 +1369,7 @@ pub(crate) async fn script_python_requirement(
/// Validate the dependency groups requested by the [`DevGroupsSpecification`]. /// Validate the dependency groups requested by the [`DevGroupsSpecification`].
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
pub(crate) fn validate_dependency_groups( pub(crate) fn validate_dependency_groups(
project: &VirtualProject, target: InstallTarget<'_>,
dev: &DevGroupsSpecification, dev: &DevGroupsSpecification,
) -> Result<(), ProjectError> { ) -> Result<(), ProjectError> {
for group in dev for group in dev
@ -1377,8 +1377,14 @@ pub(crate) fn validate_dependency_groups(
.into_iter() .into_iter()
.flat_map(GroupsSpecification::names) .flat_map(GroupsSpecification::names)
{ {
match project { match target {
VirtualProject::Project(project) => { 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. // The group must be defined in the target project.
if !project if !project
.current_project() .current_project()
@ -1390,25 +1396,7 @@ pub(crate) fn validate_dependency_groups(
return Err(ProjectError::MissingGroupProject(group.clone())); return Err(ProjectError::MissingGroupProject(group.clone()));
} }
} }
VirtualProject::NonProject(workspace) => { InstallTarget::FrozenProject(_, _) => {}
// 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()));
}
}
}
} }
} }
Ok(()) Ok(())

View file

@ -236,7 +236,7 @@ pub(crate) async fn remove(
let defaults = default_dependency_groups(project.pyproject_toml())?; let defaults = default_dependency_groups(project.pyproject_toml())?;
project::sync::do_sync( project::sync::do_sync(
InstallTarget::from(&project), InstallTarget::from_project(&project),
&venv, &venv,
&lock, &lock,
&extras, &extras,

View file

@ -551,7 +551,7 @@ pub(crate) async fn run(
} }
} else { } else {
// Determine the default groups to include. // 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 defaults = default_dependency_groups(project.pyproject_toml())?;
// Determine the lock mode. // Determine the lock mode.
@ -607,7 +607,7 @@ pub(crate) async fn run(
let install_options = InstallOptions::default(); let install_options = InstallOptions::default();
project::sync::do_sync( project::sync::do_sync(
InstallTarget::from(&project), InstallTarget::from_project(&project),
&venv, &venv,
result.lock(), result.lock(),
&extras, &extras,

View file

@ -33,7 +33,7 @@ use crate::commands::project::lock::{do_safe_lock, LockMode};
use crate::commands::project::{ use crate::commands::project::{
default_dependency_groups, validate_dependency_groups, ProjectError, SharedState, default_dependency_groups, validate_dependency_groups, ProjectError, SharedState,
}; };
use crate::commands::{diagnostics, pip, project, ExitStatus}; use crate::commands::{diagnostics, project, ExitStatus};
use crate::printer::Printer; use crate::printer::Printer;
use crate::settings::{InstallerSettingsRef, ResolverInstallerSettings}; use crate::settings::{InstallerSettingsRef, ResolverInstallerSettings};
@ -43,6 +43,7 @@ pub(crate) async fn sync(
project_dir: &Path, project_dir: &Path,
locked: bool, locked: bool,
frozen: bool, frozen: bool,
all_packages: bool,
package: Option<PackageName>, package: Option<PackageName>,
extras: ExtrasSpecification, extras: ExtrasSpecification,
dev: DevGroupsSpecification, dev: DevGroupsSpecification,
@ -60,7 +61,7 @@ pub(crate) async fn sync(
printer: Printer, printer: Printer,
) -> Result<ExitStatus> { ) -> Result<ExitStatus> {
// Identify the project. // Identify the project.
let project = if frozen { let project = if frozen && !all_packages {
VirtualProject::discover( VirtualProject::discover(
project_dir, project_dir,
&DiscoveryOptions { &DiscoveryOptions {
@ -82,9 +83,11 @@ pub(crate) async fn sync(
// Identify the target. // Identify the target.
let target = if let Some(package) = package.as_ref().filter(|_| frozen) { let target = if let Some(package) = package.as_ref().filter(|_| frozen) {
InstallTarget::frozen_member(&project, package) InstallTarget::frozen(&project, package)
} else if all_packages {
InstallTarget::from_workspace(&project)
} else { } else {
InstallTarget::from(&project) InstallTarget::from_project(&project)
}; };
// TODO(lucab): improve warning content // TODO(lucab): improve warning content
@ -96,7 +99,7 @@ pub(crate) async fn sync(
} }
// Determine the default groups to include. // Determine the default groups to include.
validate_dependency_groups(&project, &dev)?; validate_dependency_groups(target, &dev)?;
let defaults = default_dependency_groups(project.pyproject_toml())?; let defaults = default_dependency_groups(project.pyproject_toml())?;
// Discover or create the virtual environment. // Discover or create the virtual environment.
@ -363,7 +366,7 @@ pub(super) async fn do_sync(
let site_packages = SitePackages::from_environment(venv)?; let site_packages = SitePackages::from_environment(venv)?;
// Sync the environment. // Sync the environment.
pip::operations::install( operations::install(
&resolution, &resolution,
site_packages, site_packages,
modifications, modifications,

View file

@ -9,7 +9,7 @@ use uv_configuration::{Concurrency, DevGroupsSpecification, LowerBound, TargetTr
use uv_pep508::PackageName; use uv_pep508::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion}; use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion};
use uv_resolver::TreeDisplay; use uv_resolver::TreeDisplay;
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace}; use uv_workspace::{DiscoveryOptions, InstallTarget, Workspace};
use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::resolution_markers; use crate::commands::pip::resolution_markers;
@ -50,7 +50,7 @@ pub(crate) async fn tree(
let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?; let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?;
// Determine the default groups to include. // Determine the default groups to include.
validate_dependency_groups(&VirtualProject::NonProject(workspace.clone()), &dev)?; validate_dependency_groups(InstallTarget::Workspace(&workspace), &dev)?;
let defaults = default_dependency_groups(workspace.pyproject_toml())?; let defaults = default_dependency_groups(workspace.pyproject_toml())?;
// Find an interpreter for the project, unless `--frozen` and `--universal` are both set. // Find an interpreter for the project, unless `--frozen` and `--universal` are both set.

View file

@ -712,7 +712,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
&project_dir, &project_dir,
args.src, args.src,
args.package, args.package,
args.all, args.all_packages,
args.out_dir, args.out_dir,
args.sdist, args.sdist,
args.wheel, args.wheel,
@ -1327,6 +1327,7 @@ async fn run_project(
project_dir, project_dir,
args.locked, args.locked,
args.frozen, args.frozen,
args.all_packages,
args.package, args.package,
args.extras, args.extras,
args.dev, args.dev,

View file

@ -720,6 +720,7 @@ pub(crate) struct SyncSettings {
pub(crate) editable: EditableMode, pub(crate) editable: EditableMode,
pub(crate) install_options: InstallOptions, pub(crate) install_options: InstallOptions,
pub(crate) modifications: Modifications, pub(crate) modifications: Modifications,
pub(crate) all_packages: bool,
pub(crate) package: Option<PackageName>, pub(crate) package: Option<PackageName>,
pub(crate) python: Option<String>, pub(crate) python: Option<String>,
pub(crate) refresh: Refresh, pub(crate) refresh: Refresh,
@ -751,6 +752,7 @@ impl SyncSettings {
installer, installer,
build, build,
refresh, refresh,
all_packages,
package, package,
python, python,
} = args; } = args;
@ -781,6 +783,7 @@ impl SyncSettings {
} else { } else {
Modifications::Sufficient Modifications::Sufficient
}, },
all_packages,
package, package,
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
refresh: Refresh::from(refresh), refresh: Refresh::from(refresh),
@ -1797,7 +1800,7 @@ impl PipCheckSettings {
pub(crate) struct BuildSettings { pub(crate) struct BuildSettings {
pub(crate) src: Option<PathBuf>, pub(crate) src: Option<PathBuf>,
pub(crate) package: Option<PackageName>, pub(crate) package: Option<PackageName>,
pub(crate) all: bool, pub(crate) all_packages: bool,
pub(crate) out_dir: Option<PathBuf>, pub(crate) out_dir: Option<PathBuf>,
pub(crate) sdist: bool, pub(crate) sdist: bool,
pub(crate) wheel: bool, pub(crate) wheel: bool,
@ -1816,7 +1819,7 @@ impl BuildSettings {
src, src,
out_dir, out_dir,
package, package,
all, all_packages,
sdist, sdist,
wheel, wheel,
build_constraint, build_constraint,
@ -1835,7 +1838,7 @@ impl BuildSettings {
Self { Self {
src, src,
package, package,
all, all_packages,
out_dir, out_dir,
sdist, sdist,
wheel, wheel,

View file

@ -1206,7 +1206,7 @@ fn workspace() -> Result<()> {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: `--all` was provided, but no workspace was found error: `--all-packages` was provided, but no workspace was found
Caused by: No `pyproject.toml` found in current directory or any parent directory Caused by: No `pyproject.toml` found in current directory or any parent directory
"###); "###);

View file

@ -263,7 +263,7 @@ fn package() -> Result<()> {
name = "child" name = "child"
version = "0.1.0" version = "0.1.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = ["iniconfig>1"] dependencies = ["iniconfig>=1"]
[build-system] [build-system]
requires = ["setuptools>=42"] requires = ["setuptools>=42"]
@ -415,7 +415,7 @@ fn sync_legacy_non_project_dev_dependencies() -> Result<()> {
name = "child" name = "child"
version = "0.1.0" version = "0.1.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = ["iniconfig>1"] dependencies = ["iniconfig>=1"]
[build-system] [build-system]
requires = ["setuptools>=42"] requires = ["setuptools>=42"]
@ -500,7 +500,7 @@ fn sync_legacy_non_project_group() -> Result<()> {
name = "child" name = "child"
version = "0.1.0" version = "0.1.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = ["iniconfig>1"] dependencies = ["iniconfig>=1"]
[dependency-groups] [dependency-groups]
baz = ["typing-extensions"] baz = ["typing-extensions"]
@ -1805,7 +1805,7 @@ fn no_install_workspace() -> Result<()> {
name = "child" name = "child"
version = "0.1.0" version = "0.1.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = ["iniconfig>1"] dependencies = ["iniconfig>=1"]
[build-system] [build-system]
requires = ["setuptools>=42"] requires = ["setuptools>=42"]
@ -3073,7 +3073,7 @@ fn transitive_dev() -> Result<()> {
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.uv] [tool.uv]
dev-dependencies = ["iniconfig>1"] dev-dependencies = ["iniconfig>=1"]
"#, "#,
)?; )?;
@ -3425,7 +3425,7 @@ fn build_system_requires_workspace() -> Result<()> {
name = "project" name = "project"
version = "0.1.0" version = "0.1.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = ["iniconfig>1"] dependencies = ["iniconfig>=1"]
[build-system] [build-system]
requires = ["setuptools>=42", "backend==0.1.0"] requires = ["setuptools>=42", "backend==0.1.0"]
@ -3508,7 +3508,7 @@ fn build_system_requires_path() -> Result<()> {
name = "project" name = "project"
version = "0.1.0" version = "0.1.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = ["iniconfig>1"] dependencies = ["iniconfig>=1"]
[build-system] [build-system]
requires = ["setuptools>=42", "backend==0.1.0"] requires = ["setuptools>=42", "backend==0.1.0"]
@ -3798,3 +3798,306 @@ fn sync_explicit() -> Result<()> {
Ok(()) Ok(())
} }
/// Sync all members in a workspace.
#[test]
fn sync_all() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["anyio>3", "child"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.uv.workspace]
members = ["child"]
[tool.uv.sources]
child = { workspace = true }
"#,
)?;
context
.temp_dir
.child("src")
.child("project")
.child("__init__.py")
.touch()?;
// Add a workspace member.
let child = context.temp_dir.child("child");
child.child("pyproject.toml").write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>=1"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
child
.child("src")
.child("child")
.child("__init__.py")
.touch()?;
// Generate a lockfile.
context.lock().assert().success();
// Sync all workspace members.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 6 packages in [TIME]
Prepared 6 packages in [TIME]
Installed 6 packages in [TIME]
+ anyio==4.3.0
+ child==0.1.0 (from file://[TEMP_DIR]/child)
+ idna==3.6
+ iniconfig==2.0.0
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"###);
Ok(())
}
/// Sync all members in a workspace with extras attached.
#[test]
fn sync_all_extras() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["child"]
[project.optional-dependencies]
types = ["sniffio>1"]
async = ["anyio>3"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.uv.workspace]
members = ["child"]
[tool.uv.sources]
child = { workspace = true }
"#,
)?;
context
.temp_dir
.child("src")
.child("project")
.child("__init__.py")
.touch()?;
// Add a workspace member.
let child = context.temp_dir.child("child");
child.child("pyproject.toml").write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>=1"]
[project.optional-dependencies]
types = ["typing-extensions>=4"]
testing = ["packaging>=24"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
child
.child("src")
.child("child")
.child("__init__.py")
.touch()?;
// Generate a lockfile.
context.lock().assert().success();
// Sync an extra that exists in both the parent and child.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("types"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 5 packages in [TIME]
Installed 5 packages in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/child)
+ iniconfig==2.0.0
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);
// Sync an extra that only exists in the child.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--extra").arg("testing"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 1 package in [TIME]
Uninstalled 2 packages in [TIME]
Installed 1 package in [TIME]
+ packaging==24.0
- sniffio==1.3.1
- typing-extensions==4.10.0
"###);
// Sync all extras.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--all-extras"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);
Ok(())
}
/// Sync all members in a workspace with dependency groups attached.
#[test]
fn sync_all_groups() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["child"]
[dependency-groups]
types = ["sniffio>=1"]
async = ["anyio>=3"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[tool.uv.workspace]
members = ["child"]
[tool.uv.sources]
child = { workspace = true }
"#,
)?;
context
.temp_dir
.child("src")
.child("project")
.child("__init__.py")
.touch()?;
// Add a workspace member.
let child = context.temp_dir.child("child");
child.child("pyproject.toml").write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["iniconfig>=1"]
[dependency-groups]
types = ["typing-extensions>=4"]
testing = ["packaging>=24"]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#,
)?;
child
.child("src")
.child("child")
.child("__init__.py")
.touch()?;
// Generate a lockfile.
context.lock().assert().success();
// Sync a group that exists in both the parent and child.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("types"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 5 packages in [TIME]
Installed 5 packages in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/child)
+ iniconfig==2.0.0
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);
// Sync a group that only exists in the child.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("testing"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 8 packages in [TIME]
Prepared 1 package in [TIME]
Uninstalled 2 packages in [TIME]
Installed 1 package in [TIME]
+ packaging==24.0
- sniffio==1.3.1
- typing-extensions==4.10.0
"###);
// Sync a group that doesn't exist.
uv_snapshot!(context.filters(), context.sync().arg("--all-packages").arg("--group").arg("foo"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Group `foo` is not defined in any project's `dependency-group` table
"###);
Ok(())
}

View file

@ -1369,6 +1369,12 @@ uv sync [OPTIONS]
<p>Note that all optional dependencies are always included in the resolution; this option only affects the selection of packages to install.</p> <p>Note that all optional dependencies are always included in the resolution; this option only affects the selection of packages to install.</p>
</dd><dt><code>--all-packages</code></dt><dd><p>Sync all packages in the workspace.</p>
<p>The workspace&#8217;s environment (<code>.venv</code>) is updated to include all workspace members.</p>
<p>Any extras or groups specified via <code>--extra</code>, <code>--group</code>, or related options will be applied to all workspace members.</p>
</dd><dt><code>--allow-insecure-host</code> <i>allow-insecure-host</i></dt><dd><p>Allow insecure connections to a host.</p> </dd><dt><code>--allow-insecure-host</code> <i>allow-insecure-host</i></dt><dd><p>Allow insecure connections to a host.</p>
<p>Can be provided multiple times.</p> <p>Can be provided multiple times.</p>
@ -7260,7 +7266,7 @@ uv build [OPTIONS] [SRC]
<h3 class="cli-reference">Options</h3> <h3 class="cli-reference">Options</h3>
<dl class="cli-reference"><dt><code>--all</code></dt><dd><p>Builds all packages in the workspace.</p> <dl class="cli-reference"><dt><code>--all-packages</code></dt><dd><p>Builds all packages in the workspace.</p>
<p>The workspace will be discovered from the provided source directory, or the current directory if no source directory is provided.</p> <p>The workspace will be discovered from the provided source directory, or the current directory if no source directory is provided.</p>