mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-20 03:49:54 +00:00
Accept multiple packages in uv sync (#16543)
## Summary Closes https://github.com/astral-sh/uv/issues/12130. --------- Co-authored-by: Zanie Blue <contact@zanie.dev> Co-authored-by: konsti <konstin@mailbox.org>
This commit is contained in:
parent
60a811e715
commit
9a6eafc043
8 changed files with 265 additions and 86 deletions
|
|
@ -3669,14 +3669,14 @@ pub struct SyncArgs {
|
||||||
#[arg(long, conflicts_with = "package")]
|
#[arg(long, conflicts_with = "package")]
|
||||||
pub all_packages: bool,
|
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
|
/// 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")]
|
#[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.
|
/// Sync the environment for a Python script, rather than the current project.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ pub(crate) enum InstallTarget<'lock> {
|
||||||
name: &'lock PackageName,
|
name: &'lock PackageName,
|
||||||
lock: &'lock Lock,
|
lock: &'lock Lock,
|
||||||
},
|
},
|
||||||
|
/// Multiple specific projects in a workspace.
|
||||||
|
Projects {
|
||||||
|
workspace: &'lock Workspace,
|
||||||
|
names: &'lock [PackageName],
|
||||||
|
lock: &'lock Lock,
|
||||||
|
},
|
||||||
/// An entire workspace.
|
/// An entire workspace.
|
||||||
Workspace {
|
Workspace {
|
||||||
workspace: &'lock Workspace,
|
workspace: &'lock Workspace,
|
||||||
|
|
@ -47,6 +53,7 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> {
|
||||||
fn install_path(&self) -> &'lock Path {
|
fn install_path(&self) -> &'lock Path {
|
||||||
match self {
|
match self {
|
||||||
Self::Project { workspace, .. } => workspace.install_path(),
|
Self::Project { workspace, .. } => workspace.install_path(),
|
||||||
|
Self::Projects { workspace, .. } => workspace.install_path(),
|
||||||
Self::Workspace { workspace, .. } => workspace.install_path(),
|
Self::Workspace { workspace, .. } => workspace.install_path(),
|
||||||
Self::NonProjectWorkspace { workspace, .. } => workspace.install_path(),
|
Self::NonProjectWorkspace { workspace, .. } => workspace.install_path(),
|
||||||
Self::Script { script, .. } => script.path.parent().unwrap(),
|
Self::Script { script, .. } => script.path.parent().unwrap(),
|
||||||
|
|
@ -56,36 +63,38 @@ impl<'lock> Installable<'lock> for InstallTarget<'lock> {
|
||||||
fn lock(&self) -> &'lock Lock {
|
fn lock(&self) -> &'lock Lock {
|
||||||
match self {
|
match self {
|
||||||
Self::Project { lock, .. } => lock,
|
Self::Project { lock, .. } => lock,
|
||||||
|
Self::Projects { lock, .. } => lock,
|
||||||
Self::Workspace { lock, .. } => lock,
|
Self::Workspace { lock, .. } => lock,
|
||||||
Self::NonProjectWorkspace { lock, .. } => lock,
|
Self::NonProjectWorkspace { lock, .. } => lock,
|
||||||
Self::Script { 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 {
|
match self {
|
||||||
Self::Project { name, .. } => Either::Left(Either::Left(std::iter::once(*name))),
|
Self::Project { name, .. } => Box::new(std::iter::once(*name)),
|
||||||
Self::NonProjectWorkspace { lock, .. } => {
|
Self::Projects { names, .. } => Box::new(names.iter()),
|
||||||
Either::Left(Either::Right(lock.members().iter()))
|
Self::NonProjectWorkspace { lock, .. } => Box::new(lock.members().iter()),
|
||||||
}
|
|
||||||
Self::Workspace { lock, .. } => {
|
Self::Workspace { lock, .. } => {
|
||||||
// Identify the workspace members.
|
// Identify the workspace members.
|
||||||
//
|
//
|
||||||
// The members are encoded directly in the lockfile, unless the workspace contains a
|
// 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.
|
// single member at the root, in which case, we identify it by its source.
|
||||||
if lock.members().is_empty() {
|
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 {
|
} 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> {
|
fn project_name(&self) -> Option<&PackageName> {
|
||||||
match self {
|
match self {
|
||||||
Self::Project { name, .. } => Some(name),
|
Self::Project { name, .. } => Some(name),
|
||||||
|
Self::Projects { .. } => None,
|
||||||
Self::Workspace { .. } => None,
|
Self::Workspace { .. } => None,
|
||||||
Self::NonProjectWorkspace { .. } => None,
|
Self::NonProjectWorkspace { .. } => None,
|
||||||
Self::Script { .. } => None,
|
Self::Script { .. } => None,
|
||||||
|
|
@ -98,6 +107,7 @@ impl<'lock> InstallTarget<'lock> {
|
||||||
pub(crate) fn indexes(self) -> impl Iterator<Item = &'lock Index> {
|
pub(crate) fn indexes(self) -> impl Iterator<Item = &'lock Index> {
|
||||||
match self {
|
match self {
|
||||||
Self::Project { workspace, .. }
|
Self::Project { workspace, .. }
|
||||||
|
| Self::Projects { workspace, .. }
|
||||||
| Self::Workspace { workspace, .. }
|
| Self::Workspace { workspace, .. }
|
||||||
| Self::NonProjectWorkspace { workspace, .. } => {
|
| Self::NonProjectWorkspace { workspace, .. } => {
|
||||||
Either::Left(workspace.indexes().iter().chain(
|
Either::Left(workspace.indexes().iter().chain(
|
||||||
|
|
@ -130,6 +140,7 @@ impl<'lock> InstallTarget<'lock> {
|
||||||
pub(crate) fn sources(&self) -> impl Iterator<Item = &Source> {
|
pub(crate) fn sources(&self) -> impl Iterator<Item = &Source> {
|
||||||
match self {
|
match self {
|
||||||
Self::Project { workspace, .. }
|
Self::Project { workspace, .. }
|
||||||
|
| Self::Projects { workspace, .. }
|
||||||
| Self::Workspace { workspace, .. }
|
| Self::Workspace { workspace, .. }
|
||||||
| Self::NonProjectWorkspace { workspace, .. } => {
|
| Self::NonProjectWorkspace { workspace, .. } => {
|
||||||
Either::Left(workspace.sources().values().flat_map(Sources::iter).chain(
|
Either::Left(workspace.sources().values().flat_map(Sources::iter).chain(
|
||||||
|
|
@ -158,6 +169,7 @@ impl<'lock> InstallTarget<'lock> {
|
||||||
) -> impl Iterator<Item = Cow<'lock, uv_pep508::Requirement<VerbatimParsedUrl>>> {
|
) -> impl Iterator<Item = Cow<'lock, uv_pep508::Requirement<VerbatimParsedUrl>>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Project { workspace, .. }
|
Self::Project { workspace, .. }
|
||||||
|
| Self::Projects { workspace, .. }
|
||||||
| Self::Workspace { workspace, .. }
|
| Self::Workspace { workspace, .. }
|
||||||
| Self::NonProjectWorkspace { workspace, .. } => {
|
| Self::NonProjectWorkspace { workspace, .. } => {
|
||||||
Either::Left(
|
Either::Left(
|
||||||
|
|
@ -256,6 +268,7 @@ impl<'lock> InstallTarget<'lock> {
|
||||||
}
|
}
|
||||||
match self {
|
match self {
|
||||||
Self::Project { lock, .. }
|
Self::Project { lock, .. }
|
||||||
|
| Self::Projects { lock, .. }
|
||||||
| Self::Workspace { lock, .. }
|
| Self::Workspace { lock, .. }
|
||||||
| Self::NonProjectWorkspace { lock, .. } => {
|
| Self::NonProjectWorkspace { lock, .. } => {
|
||||||
if !lock.supports_provides_extra() {
|
if !lock.supports_provides_extra() {
|
||||||
|
|
@ -281,7 +294,10 @@ impl<'lock> InstallTarget<'lock> {
|
||||||
Self::Project { .. } => {
|
Self::Project { .. } => {
|
||||||
Err(ProjectError::MissingExtraProject(extra.clone()))
|
Err(ProjectError::MissingExtraProject(extra.clone()))
|
||||||
}
|
}
|
||||||
_ => Err(ProjectError::MissingExtraWorkspace(extra.clone())),
|
Self::Projects { .. } => {
|
||||||
|
Err(ProjectError::MissingExtraProjects(extra.clone()))
|
||||||
|
}
|
||||||
|
_ => Err(ProjectError::MissingExtraProjects(extra.clone())),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -337,11 +353,11 @@ impl<'lock> InstallTarget<'lock> {
|
||||||
|
|
||||||
for group in groups.explicit_names() {
|
for group in groups.explicit_names() {
|
||||||
if !known_groups.contains(group) {
|
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 roots = self.roots().collect::<FxHashSet<_>>();
|
||||||
let member_packages: Vec<&Package> = lock
|
let member_packages: Vec<&Package> = lock
|
||||||
.packages()
|
.packages()
|
||||||
|
|
@ -349,7 +365,7 @@ impl<'lock> InstallTarget<'lock> {
|
||||||
.filter(|package| roots.contains(package.name()))
|
.filter(|package| roots.contains(package.name()))
|
||||||
.collect();
|
.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
|
let known_groups = member_packages
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|package| package.dependency_groups().keys())
|
.flat_map(|package| package.dependency_groups().keys())
|
||||||
|
|
@ -357,7 +373,15 @@ impl<'lock> InstallTarget<'lock> {
|
||||||
|
|
||||||
for group in groups.explicit_names() {
|
for group in groups.explicit_names() {
|
||||||
if !known_groups.contains(group) {
|
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!(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -380,59 +404,71 @@ impl<'lock> InstallTarget<'lock> {
|
||||||
groups: &DependencyGroupsWithDefaults,
|
groups: &DependencyGroupsWithDefaults,
|
||||||
) -> BTreeSet<&PackageName> {
|
) -> BTreeSet<&PackageName> {
|
||||||
match self {
|
match self {
|
||||||
Self::Project { name, lock, .. } => {
|
Self::Project { lock, .. } | Self::Projects { lock, .. } => {
|
||||||
// Collect the packages by name for efficient lookup
|
let roots = self.roots().collect::<FxHashSet<_>>();
|
||||||
|
|
||||||
|
// Collect the packages by name for efficient lookup.
|
||||||
let packages = lock
|
let packages = lock
|
||||||
.packages()
|
.packages()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| (p.name(), p))
|
.map(|package| (package.name(), package))
|
||||||
.collect::<BTreeMap<_, _>>();
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
|
||||||
// We'll include the project itself
|
// We'll include all specified projects
|
||||||
let mut required_members = BTreeSet::new();
|
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 queue: VecDeque<(&PackageName, Option<&ExtraName>)> = VecDeque::new();
|
||||||
let mut seen: FxHashSet<(&PackageName, Option<&ExtraName>)> = FxHashSet::default();
|
let mut seen: FxHashSet<(&PackageName, Option<&ExtraName>)> = FxHashSet::default();
|
||||||
|
|
||||||
let Some(root_package) = packages.get(name) else {
|
for name in roots {
|
||||||
return required_members;
|
let Some(root_package) = packages.get(name) else {
|
||||||
};
|
|
||||||
|
|
||||||
if groups.prod() {
|
|
||||||
// Add the root package
|
|
||||||
queue.push_back((name, None));
|
|
||||||
seen.insert((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 activated dependency groups for the root package
|
|
||||||
for (group_name, dependencies) in root_package.resolved_dependency_groups() {
|
|
||||||
if !groups.contains(group_name) {
|
|
||||||
continue;
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for dependency in dependencies {
|
|
||||||
let name = dependency.package_name();
|
// Add activated dependency groups for the root package
|
||||||
queue.push_back((name, None));
|
for (group_name, dependencies) in root_package.resolved_dependency_groups() {
|
||||||
for extra in dependency.extra() {
|
if !groups.contains(group_name) {
|
||||||
queue.push_back((name, Some(extra)));
|
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() {
|
while let Some((package_name, extra)) = queue.pop_front() {
|
||||||
if lock.members().contains(pkg_name) {
|
if lock.members().contains(package_name) {
|
||||||
required_members.insert(pkg_name);
|
required_members.insert(package_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(package) = packages.get(pkg_name) else {
|
let Some(package) = packages.get(package_name) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ pub(crate) enum ProjectError {
|
||||||
MissingGroupProject(GroupName),
|
MissingGroupProject(GroupName),
|
||||||
|
|
||||||
#[error("Group `{0}` is not defined in any project's `dependency-groups` table")]
|
#[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")]
|
#[error("PEP 723 scripts do not support dependency groups, but group `{0}` was specified")]
|
||||||
MissingGroupScript(GroupName),
|
MissingGroupScript(GroupName),
|
||||||
|
|
@ -175,7 +175,7 @@ pub(crate) enum ProjectError {
|
||||||
MissingExtraProject(ExtraName),
|
MissingExtraProject(ExtraName),
|
||||||
|
|
||||||
#[error("Extra `{0}` is not defined in any project's `optional-dependencies` table")]
|
#[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")]
|
#[error("PEP 723 scripts do not support optional dependencies, but extra `{0}` was specified")]
|
||||||
MissingExtraScript(ExtraName),
|
MissingExtraScript(ExtraName),
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ pub(crate) async fn sync(
|
||||||
dry_run: DryRun,
|
dry_run: DryRun,
|
||||||
active: Option<bool>,
|
active: Option<bool>,
|
||||||
all_packages: bool,
|
all_packages: bool,
|
||||||
package: Option<PackageName>,
|
package: Vec<PackageName>,
|
||||||
extras: ExtrasSpecification,
|
extras: ExtrasSpecification,
|
||||||
groups: DependencyGroups,
|
groups: DependencyGroups,
|
||||||
editable: Option<EditableMode>,
|
editable: Option<EditableMode>,
|
||||||
|
|
@ -109,16 +109,28 @@ pub(crate) async fn sync(
|
||||||
&workspace_cache,
|
&workspace_cache,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else if let Some(package) = package.as_ref() {
|
} else if let [name] = package.as_slice() {
|
||||||
VirtualProject::Project(
|
VirtualProject::Project(
|
||||||
Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
|
Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
|
||||||
.await?
|
.await?
|
||||||
.with_current_project(package.clone())
|
.with_current_project(name.clone())
|
||||||
.with_context(|| format!("Package `{package}` not found in workspace"))?,
|
.with_context(|| format!("Package `{name}` not found in workspace"))?,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
|
let project = VirtualProject::discover(
|
||||||
.await?
|
project_dir,
|
||||||
|
&DiscoveryOptions::default(),
|
||||||
|
&workspace_cache,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
for name in &package {
|
||||||
|
if !project.workspace().packages().contains_key(name) {
|
||||||
|
return Err(anyhow::anyhow!("Package `{name}` not found in workspace"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
project
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO(lucab): improve warning content
|
// TODO(lucab): improve warning content
|
||||||
|
|
@ -379,8 +391,7 @@ pub(crate) async fn sync(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identify the installation target.
|
// Identify the installation target.
|
||||||
let sync_target =
|
let sync_target = identify_installation_target(&target, outcome.lock(), all_packages, &package);
|
||||||
identify_installation_target(&target, outcome.lock(), all_packages, package.as_ref());
|
|
||||||
|
|
||||||
let state = state.fork();
|
let state = state.fork();
|
||||||
|
|
||||||
|
|
@ -459,7 +470,7 @@ fn identify_installation_target<'a>(
|
||||||
target: &'a SyncTarget,
|
target: &'a SyncTarget,
|
||||||
lock: &'a Lock,
|
lock: &'a Lock,
|
||||||
all_packages: bool,
|
all_packages: bool,
|
||||||
package: Option<&'a PackageName>,
|
package: &'a [PackageName],
|
||||||
) -> InstallTarget<'a> {
|
) -> InstallTarget<'a> {
|
||||||
match &target {
|
match &target {
|
||||||
SyncTarget::Project(project) => {
|
SyncTarget::Project(project) => {
|
||||||
|
|
@ -470,33 +481,45 @@ fn identify_installation_target<'a>(
|
||||||
workspace: project.workspace(),
|
workspace: project.workspace(),
|
||||||
lock,
|
lock,
|
||||||
}
|
}
|
||||||
} else if let Some(package) = package {
|
|
||||||
InstallTarget::Project {
|
|
||||||
workspace: project.workspace(),
|
|
||||||
name: package,
|
|
||||||
lock,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// By default, install the root package.
|
match package {
|
||||||
InstallTarget::Project {
|
// By default, install the root project.
|
||||||
workspace: project.workspace(),
|
[] => InstallTarget::Project {
|
||||||
name: project.project_name(),
|
workspace: project.workspace(),
|
||||||
lock,
|
name: project.project_name(),
|
||||||
|
lock,
|
||||||
|
},
|
||||||
|
[name] => InstallTarget::Project {
|
||||||
|
workspace: project.workspace(),
|
||||||
|
name,
|
||||||
|
lock,
|
||||||
|
},
|
||||||
|
names => InstallTarget::Projects {
|
||||||
|
workspace: project.workspace(),
|
||||||
|
names,
|
||||||
|
lock,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
VirtualProject::NonProject(workspace) => {
|
VirtualProject::NonProject(workspace) => {
|
||||||
if all_packages {
|
if all_packages {
|
||||||
InstallTarget::NonProjectWorkspace { workspace, lock }
|
InstallTarget::NonProjectWorkspace { workspace, lock }
|
||||||
} else if let Some(package) = package {
|
|
||||||
InstallTarget::Project {
|
|
||||||
workspace,
|
|
||||||
name: package,
|
|
||||||
lock,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// By default, install the entire workspace.
|
match package {
|
||||||
InstallTarget::NonProjectWorkspace { workspace, lock }
|
// By default, install the entire workspace.
|
||||||
|
[] => InstallTarget::NonProjectWorkspace { workspace, lock },
|
||||||
|
[name] => InstallTarget::Project {
|
||||||
|
workspace,
|
||||||
|
name,
|
||||||
|
lock,
|
||||||
|
},
|
||||||
|
names => InstallTarget::Projects {
|
||||||
|
workspace,
|
||||||
|
names,
|
||||||
|
lock,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -613,6 +636,7 @@ pub(super) async fn do_sync(
|
||||||
let extra_build_requires = match &target {
|
let extra_build_requires = match &target {
|
||||||
InstallTarget::Workspace { workspace, .. }
|
InstallTarget::Workspace { workspace, .. }
|
||||||
| InstallTarget::Project { workspace, .. }
|
| InstallTarget::Project { workspace, .. }
|
||||||
|
| InstallTarget::Projects { workspace, .. }
|
||||||
| InstallTarget::NonProjectWorkspace { workspace, .. } => {
|
| InstallTarget::NonProjectWorkspace { workspace, .. } => {
|
||||||
LoweredExtraBuildDependencies::from_workspace(
|
LoweredExtraBuildDependencies::from_workspace(
|
||||||
extra_build_dependencies.clone(),
|
extra_build_dependencies.clone(),
|
||||||
|
|
|
||||||
|
|
@ -1311,7 +1311,7 @@ pub(crate) struct SyncSettings {
|
||||||
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) all_packages: bool,
|
||||||
pub(crate) package: Option<PackageName>,
|
pub(crate) package: Vec<PackageName>,
|
||||||
pub(crate) python: Option<String>,
|
pub(crate) python: Option<String>,
|
||||||
pub(crate) python_platform: Option<TargetTriple>,
|
pub(crate) python_platform: Option<TargetTriple>,
|
||||||
pub(crate) install_mirrors: PythonInstallMirrors,
|
pub(crate) install_mirrors: PythonInstallMirrors,
|
||||||
|
|
|
||||||
|
|
@ -3260,6 +3260,16 @@ fn lock_conflicting_workspace_members() -> Result<()> {
|
||||||
error: Package `example` and package `subexample` are incompatible with the declared conflicts: {example, subexample}
|
error: Package `example` and package `subexample` are incompatible with the declared conflicts: {example, subexample}
|
||||||
");
|
");
|
||||||
|
|
||||||
|
// Attempt to install them together, i.e., with `--package`
|
||||||
|
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--package").arg("example").arg("--package").arg("subexample"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: Package `example` and package `subexample` are incompatible with the declared conflicts: {example, subexample}
|
||||||
|
");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,115 @@ fn package() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sync multiple packages within a workspace.
|
||||||
|
#[test]
|
||||||
|
fn multiple_packages() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "root"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["foo", "bar", "baz"]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
foo = { workspace = true }
|
||||||
|
bar = { workspace = true }
|
||||||
|
baz = { workspace = true }
|
||||||
|
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["packages/*"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
context
|
||||||
|
.temp_dir
|
||||||
|
.child("packages")
|
||||||
|
.child("foo")
|
||||||
|
.child("pyproject.toml")
|
||||||
|
.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "foo"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["anyio"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
context
|
||||||
|
.temp_dir
|
||||||
|
.child("packages")
|
||||||
|
.child("bar")
|
||||||
|
.child("pyproject.toml")
|
||||||
|
.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "bar"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["typing-extensions"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
context
|
||||||
|
.temp_dir
|
||||||
|
.child("packages")
|
||||||
|
.child("baz")
|
||||||
|
.child("pyproject.toml")
|
||||||
|
.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "baz"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = ["iniconfig"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Sync `foo` and `bar`.
|
||||||
|
uv_snapshot!(context.filters(), context.sync()
|
||||||
|
.arg("--package").arg("foo")
|
||||||
|
.arg("--package").arg("bar"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 9 packages in [TIME]
|
||||||
|
Prepared 6 packages in [TIME]
|
||||||
|
Installed 6 packages in [TIME]
|
||||||
|
+ anyio==4.3.0
|
||||||
|
+ bar==0.1.0 (from file://[TEMP_DIR]/packages/bar)
|
||||||
|
+ foo==0.1.0 (from file://[TEMP_DIR]/packages/foo)
|
||||||
|
+ idna==3.6
|
||||||
|
+ sniffio==1.3.1
|
||||||
|
+ typing-extensions==4.10.0
|
||||||
|
");
|
||||||
|
|
||||||
|
// Sync `foo`, `bar`, and `baz`.
|
||||||
|
uv_snapshot!(context.filters(), context.sync()
|
||||||
|
.arg("--package").arg("foo")
|
||||||
|
.arg("--package").arg("bar")
|
||||||
|
.arg("--package").arg("baz"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 9 packages in [TIME]
|
||||||
|
Prepared 2 packages in [TIME]
|
||||||
|
Installed 2 packages in [TIME]
|
||||||
|
+ baz==0.1.0 (from file://[TEMP_DIR]/packages/baz)
|
||||||
|
+ iniconfig==2.0.0
|
||||||
|
");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Test json output
|
/// Test json output
|
||||||
#[test]
|
#[test]
|
||||||
fn sync_json() -> Result<()> {
|
fn sync_json() -> Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -1513,9 +1513,9 @@ uv sync [OPTIONS]
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>text</code>: Display the result in a human-readable format</li>
|
<li><code>text</code>: Display the result in a human-readable format</li>
|
||||||
<li><code>json</code>: Display the result in JSON format</li>
|
<li><code>json</code>: Display the result in JSON format</li>
|
||||||
</ul></dd><dt id="uv-sync--package"><a href="#uv-sync--package"><code>--package</code></a> <i>package</i></dt><dd><p>Sync for a specific package in the workspace.</p>
|
</ul></dd><dt id="uv-sync--package"><a href="#uv-sync--package"><code>--package</code></a> <i>package</i></dt><dd><p>Sync for specific packages in the workspace.</p>
|
||||||
<p>The workspace's environment (<code>.venv</code>) is updated to reflect the subset of dependencies declared by the specified workspace member package.</p>
|
<p>The workspace's environment (<code>.venv</code>) is updated to reflect the subset of dependencies declared by the specified workspace member packages.</p>
|
||||||
<p>If the workspace member does not exist, uv will exit with an error.</p>
|
<p>If any workspace member does not exist, uv will exit with an error.</p>
|
||||||
</dd><dt id="uv-sync--prerelease"><a href="#uv-sync--prerelease"><code>--prerelease</code></a> <i>prerelease</i></dt><dd><p>The strategy to use when considering pre-release versions.</p>
|
</dd><dt id="uv-sync--prerelease"><a href="#uv-sync--prerelease"><code>--prerelease</code></a> <i>prerelease</i></dt><dd><p>The strategy to use when considering pre-release versions.</p>
|
||||||
<p>By default, uv will accept pre-releases for packages that <em>only</em> publish pre-releases, along with first-party requirements that contain an explicit pre-release marker in the declared specifiers (<code>if-necessary-or-explicit</code>).</p>
|
<p>By default, uv will accept pre-releases for packages that <em>only</em> publish pre-releases, along with first-party requirements that contain an explicit pre-release marker in the declared specifiers (<code>if-necessary-or-explicit</code>).</p>
|
||||||
<p>May also be set with the <code>UV_PRERELEASE</code> environment variable.</p><p>Possible values:</p>
|
<p>May also be set with the <code>UV_PRERELEASE</code> environment variable.</p><p>Possible values:</p>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue