Skip to content

Commit ede40ac

Browse files
committed
Add support for uv export --all
1 parent 5ce9f9e commit ede40ac

File tree

8 files changed

+216
-81
lines changed

8 files changed

+216
-81
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3343,10 +3343,20 @@ pub struct ExportArgs {
33433343
#[arg(long, value_enum, default_value_t = ExportFormat::default())]
33443344
pub format: ExportFormat,
33453345

3346+
/// Export the entire workspace.
3347+
///
3348+
/// The dependencies for all workspace members will be included in the
3349+
/// exported requirements file.
3350+
///
3351+
/// Any extras or groups specified via `--extra`, `--group`, or related options
3352+
/// will be applied to all workspace members.
3353+
#[arg(long, conflicts_with = "package")]
3354+
pub all_packages: bool,
3355+
33463356
/// Export the dependencies for a specific package in the workspace.
33473357
///
33483358
/// If the workspace member does not exist, uv will exit with an error.
3349-
#[arg(long)]
3359+
#[arg(long, conflicts_with = "all_packages")]
33503360
pub package: Option<PackageName>,
33513361

33523362
/// Include optional dependencies from the specified extra name.

crates/uv-resolver/src/lock/requirements_txt.rs

Lines changed: 63 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ use petgraph::{Directed, Graph};
1010
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
1111
use url::Url;
1212

13+
use crate::graph_ops::marker_reachability;
14+
use crate::lock::{Package, PackageId, Source};
15+
use crate::{Lock, LockError};
1316
use uv_configuration::{DevGroupsManifest, EditableMode, ExtrasSpecification, InstallOptions};
1417
use uv_distribution_filename::{DistExtension, SourceDistExtension};
1518
use uv_fs::Simplified;
1619
use uv_git::GitReference;
17-
use uv_normalize::{ExtraName, PackageName};
20+
use uv_normalize::ExtraName;
1821
use uv_pep508::MarkerTree;
1922
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl};
20-
21-
use crate::graph_ops::marker_reachability;
22-
use crate::lock::{Package, PackageId, Source};
23-
use crate::{Lock, LockError};
23+
use uv_workspace::InstallTarget;
2424

2525
type LockGraph<'lock> = Graph<Node<'lock>, Edge, Directed>;
2626

@@ -35,7 +35,7 @@ pub struct RequirementsTxtExport<'lock> {
3535
impl<'lock> RequirementsTxtExport<'lock> {
3636
pub fn from_lock(
3737
lock: &'lock Lock,
38-
root_name: &PackageName,
38+
target: InstallTarget<'lock>,
3939
extras: &ExtrasSpecification,
4040
dev: &DevGroupsManifest,
4141
editable: EditableMode,
@@ -52,65 +52,67 @@ impl<'lock> RequirementsTxtExport<'lock> {
5252
let root = petgraph.add_node(Node::Root);
5353

5454
// Add the workspace package to the queue.
55-
let dist = lock
56-
.find_by_name(root_name)
57-
.expect("found too many packages matching root")
58-
.expect("could not find root");
59-
60-
if dev.prod() {
61-
// Add the workspace package to the graph.
62-
if let Entry::Vacant(entry) = inverse.entry(&dist.id) {
63-
entry.insert(petgraph.add_node(Node::Package(dist)));
64-
}
55+
for root_name in target.packages() {
56+
let dist = lock
57+
.find_by_name(root_name)
58+
.expect("found too many packages matching root")
59+
.expect("could not find root");
60+
61+
if dev.prod() {
62+
// Add the workspace package to the graph.
63+
if let Entry::Vacant(entry) = inverse.entry(&dist.id) {
64+
entry.insert(petgraph.add_node(Node::Package(dist)));
65+
}
66+
67+
// Add an edge from the root.
68+
let index = inverse[&dist.id];
69+
petgraph.add_edge(root, index, MarkerTree::TRUE);
6570

66-
// Add an edge from the root.
67-
let index = inverse[&dist.id];
68-
petgraph.add_edge(root, index, MarkerTree::TRUE);
69-
70-
// Push its dependencies on the queue.
71-
queue.push_back((dist, None));
72-
match extras {
73-
ExtrasSpecification::None => {}
74-
ExtrasSpecification::All => {
75-
for extra in dist.optional_dependencies.keys() {
76-
queue.push_back((dist, Some(extra)));
71+
// Push its dependencies on the queue.
72+
queue.push_back((dist, None));
73+
match extras {
74+
ExtrasSpecification::None => {}
75+
ExtrasSpecification::All => {
76+
for extra in dist.optional_dependencies.keys() {
77+
queue.push_back((dist, Some(extra)));
78+
}
7779
}
78-
}
79-
ExtrasSpecification::Some(extras) => {
80-
for extra in extras {
81-
queue.push_back((dist, Some(extra)));
80+
ExtrasSpecification::Some(extras) => {
81+
for extra in extras {
82+
queue.push_back((dist, Some(extra)));
83+
}
8284
}
8385
}
8486
}
85-
}
8687

87-
// Add any development dependencies.
88-
for group in dev.iter() {
89-
for dep in dist.dependency_groups.get(group).into_iter().flatten() {
90-
let dep_dist = lock.find_by_id(&dep.package_id);
88+
// Add any development dependencies.
89+
for group in dev.iter() {
90+
for dep in dist.dependency_groups.get(group).into_iter().flatten() {
91+
let dep_dist = lock.find_by_id(&dep.package_id);
9192

92-
// Add the dependency to the graph.
93-
if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) {
94-
entry.insert(petgraph.add_node(Node::Package(dep_dist)));
95-
}
93+
// Add the dependency to the graph.
94+
if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) {
95+
entry.insert(petgraph.add_node(Node::Package(dep_dist)));
96+
}
9697

97-
// Add an edge from the root. Development dependencies may be installed without
98-
// installing the workspace package itself (which can never have markers on it
99-
// anyway), so they're directly connected to the root.
100-
let dep_index = inverse[&dep.package_id];
101-
petgraph.add_edge(
102-
root,
103-
dep_index,
104-
dep.simplified_marker.as_simplified_marker_tree().clone(),
105-
);
98+
// Add an edge from the root. Development dependencies may be installed without
99+
// installing the workspace package itself (which can never have markers on it
100+
// anyway), so they're directly connected to the root.
101+
let dep_index = inverse[&dep.package_id];
102+
petgraph.add_edge(
103+
root,
104+
dep_index,
105+
dep.simplified_marker.as_simplified_marker_tree().clone(),
106+
);
106107

107-
// Push its dependencies on the queue.
108-
if seen.insert((&dep.package_id, None)) {
109-
queue.push_back((dep_dist, None));
110-
}
111-
for extra in &dep.extra {
112-
if seen.insert((&dep.package_id, Some(extra))) {
113-
queue.push_back((dep_dist, Some(extra)));
108+
// Push its dependencies on the queue.
109+
if seen.insert((&dep.package_id, None)) {
110+
queue.push_back((dep_dist, None));
111+
}
112+
for extra in &dep.extra {
113+
if seen.insert((&dep.package_id, Some(extra))) {
114+
queue.push_back((dep_dist, Some(extra)));
115+
}
114116
}
115117
}
116118
}
@@ -170,7 +172,11 @@ impl<'lock> RequirementsTxtExport<'lock> {
170172
Node::Package(package) => Some((index, package)),
171173
})
172174
.filter(|(_index, package)| {
173-
install_options.include_package(&package.id.name, Some(root_name), lock.members())
175+
install_options.include_package(
176+
&package.id.name,
177+
target.project_name(),
178+
lock.members(),
179+
)
174180
})
175181
.map(|(index, package)| Requirement {
176182
package,

crates/uv/src/commands/project/export.rs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use crate::settings::ResolverSettings;
3030
pub(crate) async fn export(
3131
project_dir: &Path,
3232
format: ExportFormat,
33+
all_packages: bool,
3334
package: Option<PackageName>,
3435
hashes: bool,
3536
install_options: InstallOptions,
@@ -52,14 +53,7 @@ pub(crate) async fn export(
5253
printer: Printer,
5354
) -> Result<ExitStatus> {
5455
// Identify the project.
55-
let project = if let Some(package) = package {
56-
VirtualProject::Project(
57-
Workspace::discover(project_dir, &DiscoveryOptions::default())
58-
.await?
59-
.with_current_project(package.clone())
60-
.with_context(|| format!("Package `{package}` not found in workspace"))?,
61-
)
62-
} else if frozen {
56+
let project = if frozen && !all_packages {
6357
VirtualProject::discover(
6458
project_dir,
6559
&DiscoveryOptions {
@@ -68,15 +62,18 @@ pub(crate) async fn export(
6862
},
6963
)
7064
.await?
65+
} else if let Some(package) = package.as_ref() {
66+
VirtualProject::Project(
67+
Workspace::discover(project_dir, &DiscoveryOptions::default())
68+
.await?
69+
.with_current_project(package.clone())
70+
.with_context(|| format!("Package `{package}` not found in workspace"))?,
71+
)
7172
} else {
7273
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
7374
};
7475

75-
// Determine the default groups to include.
76-
validate_dependency_groups(InstallTarget::from_project(&project), &dev)?;
77-
let defaults = default_dependency_groups(project.pyproject_toml())?;
78-
79-
let VirtualProject::Project(project) = project else {
76+
if project.is_non_project() {
8077
return Err(anyhow::anyhow!("Legacy non-project roots are not supported in `uv export`; add a `[project]` table to your `pyproject.toml` to enable exports"));
8178
};
8279

@@ -147,6 +144,19 @@ pub(crate) async fn export(
147144
Err(err) => return Err(err.into()),
148145
};
149146

147+
// Identify the target.
148+
let target = if let Some(package) = package.as_ref().filter(|_| frozen) {
149+
InstallTarget::frozen(&project, package)
150+
} else if all_packages {
151+
InstallTarget::from_workspace(&project)
152+
} else {
153+
InstallTarget::from_project(&project)
154+
};
155+
156+
// Determine the default groups to include.
157+
validate_dependency_groups(target, &dev)?;
158+
let defaults = default_dependency_groups(project.pyproject_toml())?;
159+
150160
// Write the resolved dependencies to the output channel.
151161
let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file.as_deref());
152162

@@ -155,7 +165,7 @@ pub(crate) async fn export(
155165
ExportFormat::RequirementsTxt => {
156166
let export = RequirementsTxtExport::from_lock(
157167
&lock,
158-
project.project_name(),
168+
target,
159169
&extras,
160170
&dev.with_defaults(defaults),
161171
editable,

crates/uv/src/commands/project/sync.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ pub(crate) async fn sync(
8181
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
8282
};
8383

84+
// TODO(lucab): improve warning content
85+
// <https://github.com/astral-sh/uv/issues/7428>
86+
if project.workspace().pyproject_toml().has_scripts()
87+
&& !project.workspace().pyproject_toml().is_package()
88+
{
89+
warn_user!("Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`");
90+
}
91+
8492
// Identify the target.
8593
let target = if let Some(package) = package.as_ref().filter(|_| frozen) {
8694
InstallTarget::frozen(&project, package)
@@ -90,14 +98,6 @@ pub(crate) async fn sync(
9098
InstallTarget::from_project(&project)
9199
};
92100

93-
// TODO(lucab): improve warning content
94-
// <https://github.com/astral-sh/uv/issues/7428>
95-
if project.workspace().pyproject_toml().has_scripts()
96-
&& !project.workspace().pyproject_toml().is_package()
97-
{
98-
warn_user!("Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`");
99-
}
100-
101101
// Determine the default groups to include.
102102
validate_dependency_groups(target, &dev)?;
103103
let defaults = default_dependency_groups(project.pyproject_toml())?;

crates/uv/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,6 +1510,7 @@ async fn run_project(
15101510
commands::export(
15111511
project_dir,
15121512
args.format,
1513+
args.all_packages,
15131514
args.package,
15141515
args.hashes,
15151516
args.install_options,

crates/uv/src/settings.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,7 @@ impl TreeSettings {
10941094
#[derive(Debug, Clone)]
10951095
pub(crate) struct ExportSettings {
10961096
pub(crate) format: ExportFormat,
1097+
pub(crate) all_packages: bool,
10971098
pub(crate) package: Option<PackageName>,
10981099
pub(crate) extras: ExtrasSpecification,
10991100
pub(crate) dev: DevGroupsSpecification,
@@ -1115,6 +1116,7 @@ impl ExportSettings {
11151116
pub(crate) fn resolve(args: ExportArgs, filesystem: Option<FilesystemOptions>) -> Self {
11161117
let ExportArgs {
11171118
format,
1119+
all_packages,
11181120
package,
11191121
extra,
11201122
all_extras,
@@ -1143,8 +1145,9 @@ impl ExportSettings {
11431145
} = args;
11441146

11451147
Self {
1146-
package,
11471148
format,
1149+
all_packages,
1150+
package,
11481151
extras: ExtrasSpecification::from_args(
11491152
flag(all_extras, no_all_extras).unwrap_or_default(),
11501153
extra.unwrap_or_default(),

0 commit comments

Comments
 (0)