Skip to content

Commit

Permalink
Add support for uv export --all
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Nov 2, 2024
1 parent 3808b61 commit a7ac22e
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 81 deletions.
12 changes: 11 additions & 1 deletion crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
120 changes: 63 additions & 57 deletions crates/uv-resolver/src/lock/requirements_txt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>;

Expand All @@ -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,
Expand All @@ -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");

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)));
}
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);

// 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)));
// 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)));
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 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 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(),
);
// 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)));
// 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)));
}
}
}
}
Expand Down Expand Up @@ -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,
Expand Down
38 changes: 24 additions & 14 deletions crates/uv/src/commands/project/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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"));
};

Expand Down Expand Up @@ -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());

Expand All @@ -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,
Expand Down
16 changes: 8 additions & 8 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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())?;
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,7 @@ async fn run_project(
commands::export(
project_dir,
args.format,
args.all_packages,
args.package,
args.hashes,
args.install_options,
Expand Down
5 changes: 4 additions & 1 deletion crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down
Loading

0 comments on commit a7ac22e

Please sign in to comment.