mirror of https://github.com/astral-sh/uv
Add support for `uv export --all-packages` (#8742)
## Summary Same as the other PRs, but for `uv export`.
This commit is contained in:
parent
3808b61fc1
commit
b36ae6d5ae
|
|
@ -3343,10 +3343,20 @@ pub struct ExportArgs {
|
|||
#[arg(long, value_enum, default_value_t = ExportFormat::default())]
|
||||
pub format: ExportFormat,
|
||||
|
||||
/// Export the entire workspace.
|
||||
///
|
||||
/// The dependencies for all workspace members will be included in the
|
||||
/// exported requirements file.
|
||||
///
|
||||
/// 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,
|
||||
|
||||
/// Export the dependencies for a specific package in the workspace.
|
||||
///
|
||||
/// 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>,
|
||||
|
||||
/// Include optional dependencies from the specified extra name.
|
||||
|
|
|
|||
|
|
@ -10,17 +10,17 @@ use petgraph::{Directed, Graph};
|
|||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||
use url::Url;
|
||||
|
||||
use crate::graph_ops::marker_reachability;
|
||||
use crate::lock::{Package, PackageId, Source};
|
||||
use crate::{Lock, LockError};
|
||||
use uv_configuration::{DevGroupsManifest, EditableMode, ExtrasSpecification, InstallOptions};
|
||||
use uv_distribution_filename::{DistExtension, SourceDistExtension};
|
||||
use uv_fs::Simplified;
|
||||
use uv_git::GitReference;
|
||||
use uv_normalize::{ExtraName, PackageName};
|
||||
use uv_normalize::ExtraName;
|
||||
use uv_pep508::MarkerTree;
|
||||
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl};
|
||||
|
||||
use crate::graph_ops::marker_reachability;
|
||||
use crate::lock::{Package, PackageId, Source};
|
||||
use crate::{Lock, LockError};
|
||||
use uv_workspace::InstallTarget;
|
||||
|
||||
type LockGraph<'lock> = Graph<Node<'lock>, Edge, Directed>;
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ pub struct RequirementsTxtExport<'lock> {
|
|||
impl<'lock> RequirementsTxtExport<'lock> {
|
||||
pub fn from_lock(
|
||||
lock: &'lock Lock,
|
||||
root_name: &PackageName,
|
||||
target: InstallTarget<'lock>,
|
||||
extras: &ExtrasSpecification,
|
||||
dev: &DevGroupsManifest,
|
||||
editable: EditableMode,
|
||||
|
|
@ -52,65 +52,67 @@ impl<'lock> RequirementsTxtExport<'lock> {
|
|||
let root = petgraph.add_node(Node::Root);
|
||||
|
||||
// Add the workspace package to the queue.
|
||||
let dist = lock
|
||||
.find_by_name(root_name)
|
||||
.expect("found too many packages matching root")
|
||||
.expect("could not find root");
|
||||
for root_name in target.packages() {
|
||||
let dist = lock
|
||||
.find_by_name(root_name)
|
||||
.expect("found too many packages matching root")
|
||||
.expect("could not find root");
|
||||
|
||||
if dev.prod() {
|
||||
// Add the workspace package to the graph.
|
||||
if let Entry::Vacant(entry) = inverse.entry(&dist.id) {
|
||||
entry.insert(petgraph.add_node(Node::Package(dist)));
|
||||
}
|
||||
|
||||
// Add an edge from the root.
|
||||
let index = inverse[&dist.id];
|
||||
petgraph.add_edge(root, index, MarkerTree::TRUE);
|
||||
|
||||
// Push its dependencies on the queue.
|
||||
queue.push_back((dist, None));
|
||||
match extras {
|
||||
ExtrasSpecification::None => {}
|
||||
ExtrasSpecification::All => {
|
||||
for extra in dist.optional_dependencies.keys() {
|
||||
queue.push_back((dist, Some(extra)));
|
||||
}
|
||||
}
|
||||
ExtrasSpecification::Some(extras) => {
|
||||
for extra in extras {
|
||||
queue.push_back((dist, Some(extra)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any development dependencies.
|
||||
for group in dev.iter() {
|
||||
for dep in dist.dependency_groups.get(group).into_iter().flatten() {
|
||||
let dep_dist = lock.find_by_id(&dep.package_id);
|
||||
|
||||
// Add the dependency to the graph.
|
||||
if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) {
|
||||
entry.insert(petgraph.add_node(Node::Package(dep_dist)));
|
||||
if dev.prod() {
|
||||
// Add the workspace package to the graph.
|
||||
if let Entry::Vacant(entry) = inverse.entry(&dist.id) {
|
||||
entry.insert(petgraph.add_node(Node::Package(dist)));
|
||||
}
|
||||
|
||||
// Add an edge from the root. Development dependencies may be installed without
|
||||
// installing the workspace package itself (which can never have markers on it
|
||||
// anyway), so they're directly connected to the root.
|
||||
let dep_index = inverse[&dep.package_id];
|
||||
petgraph.add_edge(
|
||||
root,
|
||||
dep_index,
|
||||
dep.simplified_marker.as_simplified_marker_tree().clone(),
|
||||
);
|
||||
// Add an edge from the root.
|
||||
let index = inverse[&dist.id];
|
||||
petgraph.add_edge(root, index, MarkerTree::TRUE);
|
||||
|
||||
// Push its dependencies on the queue.
|
||||
if seen.insert((&dep.package_id, None)) {
|
||||
queue.push_back((dep_dist, None));
|
||||
queue.push_back((dist, None));
|
||||
match extras {
|
||||
ExtrasSpecification::None => {}
|
||||
ExtrasSpecification::All => {
|
||||
for extra in dist.optional_dependencies.keys() {
|
||||
queue.push_back((dist, Some(extra)));
|
||||
}
|
||||
}
|
||||
ExtrasSpecification::Some(extras) => {
|
||||
for extra in extras {
|
||||
queue.push_back((dist, Some(extra)));
|
||||
}
|
||||
}
|
||||
}
|
||||
for extra in &dep.extra {
|
||||
if seen.insert((&dep.package_id, Some(extra))) {
|
||||
queue.push_back((dep_dist, Some(extra)));
|
||||
}
|
||||
|
||||
// Add any development dependencies.
|
||||
for group in dev.iter() {
|
||||
for dep in dist.dependency_groups.get(group).into_iter().flatten() {
|
||||
let dep_dist = lock.find_by_id(&dep.package_id);
|
||||
|
||||
// Add the dependency to the graph.
|
||||
if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) {
|
||||
entry.insert(petgraph.add_node(Node::Package(dep_dist)));
|
||||
}
|
||||
|
||||
// Add an edge from the root. Development dependencies may be installed without
|
||||
// installing the workspace package itself (which can never have markers on it
|
||||
// anyway), so they're directly connected to the root.
|
||||
let dep_index = inverse[&dep.package_id];
|
||||
petgraph.add_edge(
|
||||
root,
|
||||
dep_index,
|
||||
dep.simplified_marker.as_simplified_marker_tree().clone(),
|
||||
);
|
||||
|
||||
// Push its dependencies on the queue.
|
||||
if seen.insert((&dep.package_id, None)) {
|
||||
queue.push_back((dep_dist, None));
|
||||
}
|
||||
for extra in &dep.extra {
|
||||
if seen.insert((&dep.package_id, Some(extra))) {
|
||||
queue.push_back((dep_dist, Some(extra)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -170,7 +172,11 @@ impl<'lock> RequirementsTxtExport<'lock> {
|
|||
Node::Package(package) => Some((index, package)),
|
||||
})
|
||||
.filter(|(_index, package)| {
|
||||
install_options.include_package(&package.id.name, Some(root_name), lock.members())
|
||||
install_options.include_package(
|
||||
&package.id.name,
|
||||
target.project_name(),
|
||||
lock.members(),
|
||||
)
|
||||
})
|
||||
.map(|(index, package)| Requirement {
|
||||
package,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ use crate::settings::ResolverSettings;
|
|||
pub(crate) async fn export(
|
||||
project_dir: &Path,
|
||||
format: ExportFormat,
|
||||
all_packages: bool,
|
||||
package: Option<PackageName>,
|
||||
hashes: bool,
|
||||
install_options: InstallOptions,
|
||||
|
|
@ -52,14 +53,7 @@ pub(crate) async fn export(
|
|||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
// Identify the project.
|
||||
let project = if let Some(package) = package {
|
||||
VirtualProject::Project(
|
||||
Workspace::discover(project_dir, &DiscoveryOptions::default())
|
||||
.await?
|
||||
.with_current_project(package.clone())
|
||||
.with_context(|| format!("Package `{package}` not found in workspace"))?,
|
||||
)
|
||||
} else if frozen {
|
||||
let project = if frozen && !all_packages {
|
||||
VirtualProject::discover(
|
||||
project_dir,
|
||||
&DiscoveryOptions {
|
||||
|
|
@ -68,15 +62,18 @@ pub(crate) async fn export(
|
|||
},
|
||||
)
|
||||
.await?
|
||||
} else if let Some(package) = package.as_ref() {
|
||||
VirtualProject::Project(
|
||||
Workspace::discover(project_dir, &DiscoveryOptions::default())
|
||||
.await?
|
||||
.with_current_project(package.clone())
|
||||
.with_context(|| format!("Package `{package}` not found in workspace"))?,
|
||||
)
|
||||
} else {
|
||||
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
|
||||
};
|
||||
|
||||
// Determine the default groups to include.
|
||||
validate_dependency_groups(InstallTarget::from_project(&project), &dev)?;
|
||||
let defaults = default_dependency_groups(project.pyproject_toml())?;
|
||||
|
||||
let VirtualProject::Project(project) = project else {
|
||||
if project.is_non_project() {
|
||||
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"));
|
||||
};
|
||||
|
||||
|
|
@ -147,6 +144,19 @@ pub(crate) async fn export(
|
|||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
// Identify the target.
|
||||
let target = if let Some(package) = package.as_ref().filter(|_| frozen) {
|
||||
InstallTarget::frozen(&project, package)
|
||||
} else if all_packages {
|
||||
InstallTarget::from_workspace(&project)
|
||||
} else {
|
||||
InstallTarget::from_project(&project)
|
||||
};
|
||||
|
||||
// Determine the default groups to include.
|
||||
validate_dependency_groups(target, &dev)?;
|
||||
let defaults = default_dependency_groups(project.pyproject_toml())?;
|
||||
|
||||
// Write the resolved dependencies to the output channel.
|
||||
let mut writer = OutputWriter::new(!quiet || output_file.is_none(), output_file.as_deref());
|
||||
|
||||
|
|
@ -155,7 +165,7 @@ pub(crate) async fn export(
|
|||
ExportFormat::RequirementsTxt => {
|
||||
let export = RequirementsTxtExport::from_lock(
|
||||
&lock,
|
||||
project.project_name(),
|
||||
target,
|
||||
&extras,
|
||||
&dev.with_defaults(defaults),
|
||||
editable,
|
||||
|
|
|
|||
|
|
@ -81,6 +81,14 @@ pub(crate) async fn sync(
|
|||
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
|
||||
};
|
||||
|
||||
// TODO(lucab): improve warning content
|
||||
// <https://github.com/astral-sh/uv/issues/7428>
|
||||
if project.workspace().pyproject_toml().has_scripts()
|
||||
&& !project.workspace().pyproject_toml().is_package()
|
||||
{
|
||||
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`");
|
||||
}
|
||||
|
||||
// Identify the target.
|
||||
let target = if let Some(package) = package.as_ref().filter(|_| frozen) {
|
||||
InstallTarget::frozen(&project, package)
|
||||
|
|
@ -90,14 +98,6 @@ pub(crate) async fn sync(
|
|||
InstallTarget::from_project(&project)
|
||||
};
|
||||
|
||||
// TODO(lucab): improve warning content
|
||||
// <https://github.com/astral-sh/uv/issues/7428>
|
||||
if project.workspace().pyproject_toml().has_scripts()
|
||||
&& !project.workspace().pyproject_toml().is_package()
|
||||
{
|
||||
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`");
|
||||
}
|
||||
|
||||
// Determine the default groups to include.
|
||||
validate_dependency_groups(target, &dev)?;
|
||||
let defaults = default_dependency_groups(project.pyproject_toml())?;
|
||||
|
|
|
|||
|
|
@ -1510,6 +1510,7 @@ async fn run_project(
|
|||
commands::export(
|
||||
project_dir,
|
||||
args.format,
|
||||
args.all_packages,
|
||||
args.package,
|
||||
args.hashes,
|
||||
args.install_options,
|
||||
|
|
|
|||
|
|
@ -1094,6 +1094,7 @@ impl TreeSettings {
|
|||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ExportSettings {
|
||||
pub(crate) format: ExportFormat,
|
||||
pub(crate) all_packages: bool,
|
||||
pub(crate) package: Option<PackageName>,
|
||||
pub(crate) extras: ExtrasSpecification,
|
||||
pub(crate) dev: DevGroupsSpecification,
|
||||
|
|
@ -1115,6 +1116,7 @@ impl ExportSettings {
|
|||
pub(crate) fn resolve(args: ExportArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||
let ExportArgs {
|
||||
format,
|
||||
all_packages,
|
||||
package,
|
||||
extra,
|
||||
all_extras,
|
||||
|
|
@ -1143,8 +1145,9 @@ impl ExportSettings {
|
|||
} = args;
|
||||
|
||||
Self {
|
||||
package,
|
||||
format,
|
||||
all_packages,
|
||||
package,
|
||||
extras: ExtrasSpecification::from_args(
|
||||
flag(all_extras, no_all_extras).unwrap_or_default(),
|
||||
extra.unwrap_or_default(),
|
||||
|
|
|
|||
|
|
@ -503,6 +503,105 @@ fn non_root() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn 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.7.0", "child"]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = ["child"]
|
||||
|
||||
[tool.uv.sources]
|
||||
child = { workspace = true }
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
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>=2"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context.lock().assert().success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.export().arg("--all-packages"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv export --cache-dir [CACHE_DIR] --all-packages
|
||||
-e .
|
||||
-e ./child
|
||||
anyio==3.7.0 \
|
||||
--hash=sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce \
|
||||
--hash=sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0
|
||||
idna==3.6 \
|
||||
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
|
||||
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
|
||||
iniconfig==2.0.0 \
|
||||
--hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
|
||||
--hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
|
||||
sniffio==1.3.1 \
|
||||
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc \
|
||||
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_project() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[tool.uv.workspace]
|
||||
members = []
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = ["anyio"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context.lock().assert().success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.export().arg("--dev"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Legacy non-project roots are not supported in `uv export`; add a `[project]` table to your `pyproject.toml` to enable exports
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_path() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
|
|||
|
|
@ -2034,6 +2034,12 @@ uv export [OPTIONS]
|
|||
|
||||
<dl class="cli-reference"><dt><code>--all-extras</code></dt><dd><p>Include all optional dependencies</p>
|
||||
|
||||
</dd><dt><code>--all-packages</code></dt><dd><p>Export the entire workspace.</p>
|
||||
|
||||
<p>The dependencies for all workspace members will be included in the exported requirements file.</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>
|
||||
|
||||
<p>Can be provided multiple times.</p>
|
||||
|
|
|
|||
Loading…
Reference in New Issue