diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 1d5d57c46a9d..9de510806661 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -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, /// Builds all packages in the workspace. @@ -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. /// @@ -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, /// The Python interpreter to use for the project environment. diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 937b17a78944..20c4015e7803 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -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, @@ -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 { @@ -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() { @@ -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, )?), diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 69254d38c705..8dec546cf9d7 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -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 { 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)), } } @@ -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). @@ -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), } } } diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 51d11b1400b2..0aad44afd19b 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -43,7 +43,7 @@ pub(crate) async fn build_frontend( project_dir: &Path, src: Option, package: Option, - all: bool, + all_packages: bool, output_dir: Option, sdist: bool, wheel: bool, @@ -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, @@ -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, @@ -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!( @@ -201,10 +201,10 @@ 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" )); } @@ -212,7 +212,7 @@ async fn build_impl( 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")); } }; diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index acf94ee0ba52..bf25a05dddfd 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -870,7 +870,7 @@ async fn lock_and_sync( }; project::sync::do_sync( - InstallTarget::from(&project), + InstallTarget::from_project(&project), venv, &lock, &extras, diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 20fca2bce6da..d02a3a6fdd29 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -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}; @@ -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 { diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index d11320c1ac0b..dadeb22f9acf 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -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}; @@ -1369,7 +1369,7 @@ 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 @@ -1377,8 +1377,14 @@ pub(crate) fn validate_dependency_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() @@ -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(()) diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 2c098b4a2ec2..de6dbbd000f6 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -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, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 6f20a4df1621..d8812403bebc 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -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. @@ -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, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 7070db979936..8dd92be302a3 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -33,7 +33,7 @@ use crate::commands::project::lock::{do_safe_lock, LockMode}; use crate::commands::project::{ 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::settings::{InstallerSettingsRef, ResolverInstallerSettings}; @@ -43,6 +43,7 @@ pub(crate) async fn sync( project_dir: &Path, locked: bool, frozen: bool, + all_packages: bool, package: Option, extras: ExtrasSpecification, dev: DevGroupsSpecification, @@ -60,7 +61,7 @@ pub(crate) async fn sync( printer: Printer, ) -> Result { // Identify the project. - let project = if frozen { + let project = if frozen && !all_packages { VirtualProject::discover( project_dir, &DiscoveryOptions { @@ -82,9 +83,11 @@ pub(crate) async fn sync( // Identify the target. 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 { - InstallTarget::from(&project) + InstallTarget::from_project(&project) }; // TODO(lucab): improve warning content @@ -96,7 +99,7 @@ pub(crate) async fn sync( } // Determine the default groups to include. - validate_dependency_groups(&project, &dev)?; + validate_dependency_groups(target, &dev)?; let defaults = default_dependency_groups(project.pyproject_toml())?; // Discover or create the virtual environment. @@ -363,7 +366,7 @@ pub(super) async fn do_sync( let site_packages = SitePackages::from_environment(venv)?; // Sync the environment. - pip::operations::install( + operations::install( &resolution, site_packages, modifications, diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index c40392402309..3b209e3f2684 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -9,7 +9,7 @@ use uv_configuration::{Concurrency, DevGroupsSpecification, LowerBound, TargetTr use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion}; 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::resolution_markers; @@ -50,7 +50,7 @@ pub(crate) async fn tree( let workspace = Workspace::discover(project_dir, &DiscoveryOptions::default()).await?; // 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())?; // Find an interpreter for the project, unless `--frozen` and `--universal` are both set. diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index addb2e679255..e8a6ceced005 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -712,7 +712,7 @@ async fn run(mut cli: Cli) -> Result { &project_dir, args.src, args.package, - args.all, + args.all_packages, args.out_dir, args.sdist, args.wheel, @@ -1327,6 +1327,7 @@ async fn run_project( project_dir, args.locked, args.frozen, + args.all_packages, args.package, args.extras, args.dev, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index d1a4fbc68fc0..e508477285cb 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -720,6 +720,7 @@ pub(crate) struct SyncSettings { pub(crate) editable: EditableMode, pub(crate) install_options: InstallOptions, pub(crate) modifications: Modifications, + pub(crate) all_packages: bool, pub(crate) package: Option, pub(crate) python: Option, pub(crate) refresh: Refresh, @@ -751,6 +752,7 @@ impl SyncSettings { installer, build, refresh, + all_packages, package, python, } = args; @@ -781,6 +783,7 @@ impl SyncSettings { } else { Modifications::Sufficient }, + all_packages, package, python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), @@ -1797,7 +1800,7 @@ impl PipCheckSettings { pub(crate) struct BuildSettings { pub(crate) src: Option, pub(crate) package: Option, - pub(crate) all: bool, + pub(crate) all_packages: bool, pub(crate) out_dir: Option, pub(crate) sdist: bool, pub(crate) wheel: bool, @@ -1816,7 +1819,7 @@ impl BuildSettings { src, out_dir, package, - all, + all_packages, sdist, wheel, build_constraint, @@ -1835,7 +1838,7 @@ impl BuildSettings { Self { src, package, - all, + all_packages, out_dir, sdist, wheel, diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index 9c84e6446c84..8f8b5e30a0ae 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -1206,7 +1206,7 @@ fn workspace() -> Result<()> { ----- stdout ----- ----- 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 "###); diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 7725743af7bc..d44729ab61d9 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -263,7 +263,7 @@ fn package() -> Result<()> { name = "child" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig>1"] + dependencies = ["iniconfig>=1"] [build-system] requires = ["setuptools>=42"] @@ -415,7 +415,7 @@ fn sync_legacy_non_project_dev_dependencies() -> Result<()> { name = "child" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig>1"] + dependencies = ["iniconfig>=1"] [build-system] requires = ["setuptools>=42"] @@ -500,7 +500,7 @@ fn sync_legacy_non_project_group() -> Result<()> { name = "child" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig>1"] + dependencies = ["iniconfig>=1"] [dependency-groups] baz = ["typing-extensions"] @@ -1805,7 +1805,7 @@ fn no_install_workspace() -> Result<()> { name = "child" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig>1"] + dependencies = ["iniconfig>=1"] [build-system] requires = ["setuptools>=42"] @@ -3073,7 +3073,7 @@ fn transitive_dev() -> Result<()> { build-backend = "setuptools.build_meta" [tool.uv] - dev-dependencies = ["iniconfig>1"] + dev-dependencies = ["iniconfig>=1"] "#, )?; @@ -3425,7 +3425,7 @@ fn build_system_requires_workspace() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig>1"] + dependencies = ["iniconfig>=1"] [build-system] requires = ["setuptools>=42", "backend==0.1.0"] @@ -3508,7 +3508,7 @@ fn build_system_requires_path() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig>1"] + dependencies = ["iniconfig>=1"] [build-system] requires = ["setuptools>=42", "backend==0.1.0"] @@ -3798,3 +3798,306 @@ fn sync_explicit() -> Result<()> { 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(()) +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 057d220d74df..c934c10e8111 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1369,6 +1369,12 @@ uv sync [OPTIONS]

Note that all optional dependencies are always included in the resolution; this option only affects the selection of packages to install.

+
--all-packages

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.

+
--allow-insecure-host allow-insecure-host

Allow insecure connections to a host.

Can be provided multiple times.

@@ -7260,7 +7266,7 @@ uv build [OPTIONS] [SRC]

Options

-
--all

Builds all packages in the workspace.

+
--all-packages

Builds all packages in the workspace.

The workspace will be discovered from the provided source directory, or the current directory if no source directory is provided.