From 387eaf59f6e356feefde6c25c2585d11ae590c3d Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Mon, 22 Sep 2025 16:59:22 +0100 Subject: [PATCH 01/36] Split out pip compile format options into new enum --- crates/uv-cli/src/lib.rs | 6 ++--- crates/uv-configuration/src/lib.rs | 2 ++ .../src/pip_compile_format.rs | 18 ++++++++++++++ crates/uv/src/commands/pip/compile.rs | 24 +++++++++---------- crates/uv/src/settings.rs | 7 +++--- 5 files changed, 39 insertions(+), 18 deletions(-) create mode 100644 crates/uv-configuration/src/pip_compile_format.rs diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 5a3946b936115..5c7bb5b0806f9 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -11,8 +11,8 @@ use clap::{Args, Parser, Subcommand}; use uv_auth::Service; use uv_cache::CacheArgs; use uv_configuration::{ - ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, ProjectBuildBackend, - TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem, + ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, PipCompileFormat, + ProjectBuildBackend, TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem, }; use uv_distribution_types::{ ConfigSettingEntry, ConfigSettingPackageEntry, Index, IndexUrl, Origin, PipExtraIndex, @@ -1328,7 +1328,7 @@ pub struct PipCompileArgs { /// uv will infer the output format from the file extension of the output file, if /// provided. Otherwise, defaults to `requirements.txt`. #[arg(long, value_enum)] - pub format: Option, + pub format: Option, /// Include extras in the output file. /// diff --git a/crates/uv-configuration/src/lib.rs b/crates/uv-configuration/src/lib.rs index 931634812bbb5..202867cc70c5e 100644 --- a/crates/uv-configuration/src/lib.rs +++ b/crates/uv-configuration/src/lib.rs @@ -14,6 +14,7 @@ pub use install_options::*; pub use name_specifiers::*; pub use overrides::*; pub use package_options::*; +pub use pip_compile_format::*; pub use project_build_backend::*; pub use required_version::*; pub use sources::*; @@ -39,6 +40,7 @@ mod install_options; mod name_specifiers; mod overrides; mod package_options; +mod pip_compile_format; mod project_build_backend; mod required_version; mod sources; diff --git a/crates/uv-configuration/src/pip_compile_format.rs b/crates/uv-configuration/src/pip_compile_format.rs new file mode 100644 index 0000000000000..16d5dc55e7787 --- /dev/null +++ b/crates/uv-configuration/src/pip_compile_format.rs @@ -0,0 +1,18 @@ +/// The format to use when compiling to a `requirements.txt` or `pylock.toml` file +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] +pub enum PipCompileFormat { + /// Export in `requirements.txt` format. + #[default] + #[serde(rename = "requirements.txt", alias = "requirements-txt")] + #[cfg_attr( + feature = "clap", + clap(name = "requirements.txt", alias = "requirements-txt") + )] + RequirementsTxt, + /// Export in `pylock.toml` format. + #[serde(rename = "pylock.toml", alias = "pylock-toml")] + #[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))] + PylockToml, +} diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 351f9228a9e87..72d71c50b9394 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -13,8 +13,8 @@ use tracing::debug; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - BuildIsolation, BuildOptions, Concurrency, Constraints, ExportFormat, ExtrasSpecification, - IndexStrategy, NoBinary, NoBuild, Reinstall, SourceStrategy, Upgrade, + BuildIsolation, BuildOptions, Concurrency, Constraints, ExtrasSpecification, IndexStrategy, + NoBinary, NoBuild, PipCompileFormat, Reinstall, SourceStrategy, Upgrade, }; use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_dispatch::{BuildDispatch, SharedState}; @@ -72,7 +72,7 @@ pub(crate) async fn pip_compile( extras: ExtrasSpecification, groups: GroupsSpecification, output_file: Option<&Path>, - format: Option, + format: Option, resolution_mode: ResolutionMode, prerelease_mode: PrereleaseMode, fork_strategy: ForkStrategy, @@ -143,16 +143,16 @@ pub(crate) async fn pip_compile( let format = format.unwrap_or_else(|| { let extension = output_file.and_then(Path::extension); if extension.is_some_and(|ext| ext.eq_ignore_ascii_case("txt")) { - ExportFormat::RequirementsTxt + PipCompileFormat::RequirementsTxt } else if extension.is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) { - ExportFormat::PylockToml + PipCompileFormat::PylockToml } else { - ExportFormat::RequirementsTxt + PipCompileFormat::RequirementsTxt } }); // If the user is exporting to PEP 751, ensure the filename matches the specification. - if matches!(format, ExportFormat::PylockToml) { + if matches!(format, PipCompileFormat::PylockToml) { if let Some(file_name) = output_file .and_then(Path::file_name) .and_then(OsStr::to_str) @@ -386,7 +386,7 @@ pub(crate) async fn pip_compile( // Generate, but don't enforce hashes for the requirements. PEP 751 _requires_ a hash to be // present, but otherwise, we omit them by default. - let hasher = if generate_hashes || matches!(format, ExportFormat::PylockToml) { + let hasher = if generate_hashes || matches!(format, PipCompileFormat::PylockToml) { HashStrategy::Generate(HashGeneration::All) } else { HashStrategy::None @@ -443,10 +443,10 @@ pub(crate) async fn pip_compile( let LockedRequirements { preferences, git } = if let Some(output_file) = output_file.filter(|output_file| output_file.exists()) { match format { - ExportFormat::RequirementsTxt => LockedRequirements::from_preferences( + PipCompileFormat::RequirementsTxt => LockedRequirements::from_preferences( read_requirements_txt(output_file, &upgrade).await?, ), - ExportFormat::PylockToml => { + PipCompileFormat::PylockToml => { read_pylock_toml_requirements(output_file, &upgrade).await? } } @@ -601,7 +601,7 @@ pub(crate) async fn pip_compile( } match format { - ExportFormat::RequirementsTxt => { + PipCompileFormat::RequirementsTxt => { if include_marker_expression { if let Some(marker_env) = resolver_env.marker_environment() { let relevant_markers = resolution.marker_tree(&top_level_index, marker_env)?; @@ -692,7 +692,7 @@ pub(crate) async fn pip_compile( ) )?; } - ExportFormat::PylockToml => { + PipCompileFormat::PylockToml => { if include_marker_expression { warn_user!( "The `--emit-marker-expression` option is not supported for `pylock.toml` output" diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 548c5c2383751..733a5aba766a2 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -26,8 +26,9 @@ use uv_client::Connectivity; use uv_configuration::{ BuildIsolation, BuildOptions, Concurrency, DependencyGroups, DryRun, EditableMode, EnvFile, ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, - KeyringProviderType, NoBinary, NoBuild, ProjectBuildBackend, Reinstall, RequiredVersion, - SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, + KeyringProviderType, NoBinary, NoBuild, PipCompileFormat, ProjectBuildBackend, Reinstall, + RequiredVersion, SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, + VersionControlSystem, }; use uv_distribution_types::{ ConfigSettings, DependencyMetadata, ExtraBuildVariables, Index, IndexLocations, IndexUrl, @@ -2121,7 +2122,7 @@ impl FormatSettings { /// The resolved settings to use for a `pip compile` invocation. #[derive(Debug, Clone)] pub(crate) struct PipCompileSettings { - pub(crate) format: Option, + pub(crate) format: Option, pub(crate) src_file: Vec, pub(crate) constraints: Vec, pub(crate) overrides: Vec, From b908656878a79dd9147c7a83b1479963ffb34e4a Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Mon, 22 Sep 2025 17:19:53 +0100 Subject: [PATCH 02/36] Add CDX export format with empty JSON export --- crates/uv-cli/src/lib.rs | 2 +- crates/uv-configuration/src/export_format.rs | 7 +++++++ crates/uv/src/commands/project/export.rs | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 5c7bb5b0806f9..ef03af373c671 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -4303,7 +4303,7 @@ pub struct TreeArgs { pub struct ExportArgs { /// The format to which `uv.lock` should be exported. /// - /// Supports both `requirements.txt` and `pylock.toml` (PEP 751) output formats. + /// Supports `requirements.txt`, `pylock.toml` (PEP 751) and `CycloneDX` v1.5 JSON output formats. /// /// uv will infer the output format from the file extension of the output file, if /// provided. Otherwise, defaults to `requirements.txt`. diff --git a/crates/uv-configuration/src/export_format.rs b/crates/uv-configuration/src/export_format.rs index c38218dc4422e..93ca921e90bea 100644 --- a/crates/uv-configuration/src/export_format.rs +++ b/crates/uv-configuration/src/export_format.rs @@ -15,4 +15,11 @@ pub enum ExportFormat { #[serde(rename = "pylock.toml", alias = "pylock-toml")] #[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))] PylockToml, + /// Export in `CycloneDX` v1.5 JSON format. + #[serde(rename = "cyclonedx1.5")] + #[cfg_attr( + feature = "clap", + clap(name = "cyclonedx1.5", alias = "cyclonedx1.5+json") + )] + CycloneDX1_5, } diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 1028feb85a0a3..ca85ef7dd2049 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -376,6 +376,9 @@ pub(crate) async fn export( } write!(writer, "{}", export.to_toml()?)?; } + ExportFormat::CycloneDX1_5 => { + write!(writer, "{{}}")?; + } } writer.commit().await?; From bbc5a7aca0645a1fdc8e8db1692f928a2b201398 Mon Sep 17 00:00:00 2001 From: Will Rollason Date: Tue, 23 Sep 2025 12:02:05 +0100 Subject: [PATCH 03/36] Export CDX with CDX metadata --- Cargo.lock | 116 ++++++++++++++++++ Cargo.toml | 1 + crates/uv-resolver/Cargo.toml | 2 + crates/uv-resolver/src/lib.rs | 2 +- .../src/lock/export/cyclonedx_json.rs | 26 ++++ crates/uv-resolver/src/lock/export/mod.rs | 1 + crates/uv-resolver/src/lock/mod.rs | 2 +- crates/uv/src/commands/project/export.rs | 19 ++- 8 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 crates/uv-resolver/src/lock/export/cyclonedx_json.rs diff --git a/Cargo.lock b/Cargo.lock index 0345601d7c841..3ed51bd1a1430 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1042,6 +1042,41 @@ dependencies = [ "windows-sys 0.61.0", ] +[[package]] +name = "cyclonedx-bom" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2ec98a191e17f63b92b132f6852462de9eaee03ca8dbf2df401b9fd809bcac" +dependencies = [ + "base64 0.21.7", + "cyclonedx-bom-macros", + "fluent-uri", + "indexmap", + "once_cell", + "ordered-float", + "purl", + "regex", + "serde", + "serde_json", + "spdx", + "strum", + "thiserror 1.0.69", + "time", + "uuid", + "xml-rs", +] + +[[package]] +name = "cyclonedx-bom-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50341f21df64b412b4f917e34b7aa786c092d64f3f905f478cb76950c7e980c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -1409,6 +1444,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2779,6 +2823,12 @@ checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", "hashbrown 0.14.5", +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", ] [[package]] @@ -3169,6 +3219,14 @@ checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", "serde", +name = "purl" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60ebe4262ae91ddd28c8721111a0a6e9e58860e211fc92116c4bb85c98fd96ad" +dependencies = [ + "hex", + "percent-encoding", + "thiserror 2.0.16", ] [[package]] @@ -4319,6 +4377,28 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4665,6 +4745,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", ] [[package]] @@ -6513,6 +6621,7 @@ version = "0.0.1" dependencies = [ "arcstr", "clap", + "cyclonedx-bom", "dashmap", "either", "fs-err", @@ -6539,6 +6648,7 @@ dependencies = [ "toml_edit 0.23.7", "tracing", "url", + "uuid", "uv-cache-key", "uv-client", "uv-configuration", @@ -7505,6 +7615,12 @@ dependencies = [ "rustix 1.0.8", ] +[[package]] +name = "xml-rs" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" + [[package]] name = "xmlparser" version = "0.13.6" diff --git a/Cargo.toml b/Cargo.toml index a9aad6bea722b..81f1a6ff07b1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,6 +103,7 @@ configparser = { version = "3.1.0" } console = { version = "0.16.0", default-features = false, features = ["std"] } csv = { version = "1.3.0" } ctrlc = { version = "3.4.5" } +cyclonedx-bom = { version = "0.8.0" } dashmap = { version = "6.1.0" } data-encoding = { version = "2.6.0" } dotenvy = { version = "0.15.7" } diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 158e2967ddd51..9fe7b29ff1d0b 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -46,6 +46,7 @@ uv-workspace = { workspace = true } arcstr = { workspace = true } clap = { workspace = true, features = ["derive"], optional = true } +cyclonedx-bom = { workspace = true } dashmap = { workspace = true } either = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } @@ -71,6 +72,7 @@ toml = { workspace = true } toml_edit = { workspace = true } tracing = { workspace = true } url = { workspace = true } +uuid = { workspace = true } [dev-dependencies] insta = { workspace = true } diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 00cb9732e5ae4..901f8366a8ae1 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -9,7 +9,7 @@ pub use fork_strategy::ForkStrategy; pub use lock::{ Installable, Lock, LockError, LockVersion, Package, PackageMap, PylockToml, PylockTomlErrorKind, RequirementsTxtExport, ResolverManifest, SatisfiesResult, TreeDisplay, - VERSION, + VERSION, cyclonedx_json, }; pub use manifest::Manifest; pub use options::{Flexibility, Options, OptionsBuilder}; diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs new file mode 100644 index 0000000000000..2a5657092a761 --- /dev/null +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -0,0 +1,26 @@ +use crate::Installable; +use crate::LockError; +use crate::lock::export::ExportableRequirements; +use cyclonedx_bom::prelude::Bom; +use uv_configuration::{ + DependencyGroupsWithDefaults, EditableMode, ExtrasSpecificationWithDefaults, InstallOptions, +}; +use uv_normalize::PackageName; + +pub fn from_lock<'lock>( + target: &impl Installable<'lock>, + prune: &[PackageName], + extras: &ExtrasSpecificationWithDefaults, + groups: &DependencyGroupsWithDefaults, + annotate: bool, + #[allow(unused_variables)] editable: Option, + install_options: &'lock InstallOptions, +) -> Result { + // Extract the packages from the lock file. + let ExportableRequirements(_nodes) = + ExportableRequirements::from_lock(target, prune, extras, groups, annotate, install_options); + + let bom = Bom { ..Bom::default() }; + + Ok(bom) +} diff --git a/crates/uv-resolver/src/lock/export/mod.rs b/crates/uv-resolver/src/lock/export/mod.rs index f1fd1610649e9..08779add29244 100644 --- a/crates/uv-resolver/src/lock/export/mod.rs +++ b/crates/uv-resolver/src/lock/export/mod.rs @@ -23,6 +23,7 @@ pub use crate::lock::export::requirements_txt::RequirementsTxtExport; use crate::universal_marker::resolve_conflicts; use crate::{Installable, LockError, Package}; +pub mod cyclonedx_json; mod pylock_toml; mod requirements_txt; diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 36124917937b3..f1bce15fe101f 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -52,7 +52,7 @@ use uv_workspace::{Editability, WorkspaceMember}; use crate::fork_strategy::ForkStrategy; pub(crate) use crate::lock::export::PylockTomlPackage; pub use crate::lock::export::RequirementsTxtExport; -pub use crate::lock::export::{PylockToml, PylockTomlErrorKind}; +pub use crate::lock::export::{PylockToml, PylockTomlErrorKind, cyclonedx_json}; pub use crate::lock::installable::Installable; pub use crate::lock::map::PackageMap; pub use crate::lock::tree::TreeDisplay; diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index ca85ef7dd2049..a3aa1d4c73610 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -15,7 +15,7 @@ use uv_normalize::{DefaultExtras, DefaultGroups, PackageName}; use uv_preview::Preview; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; use uv_requirements::is_pylock_toml; -use uv_resolver::{PylockToml, RequirementsTxtExport}; +use uv_resolver::{PylockToml, RequirementsTxtExport, cyclonedx_json}; use uv_scripts::Pep723Script; use uv_settings::PythonInstallMirrors; use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace, WorkspaceCache}; @@ -377,7 +377,22 @@ pub(crate) async fn export( write!(writer, "{}", export.to_toml()?)?; } ExportFormat::CycloneDX1_5 => { - write!(writer, "{{}}")?; + let export = cyclonedx_json::from_lock( + &target, + &prune, + &extras, + &groups, + include_annotations, + editable, + &install_options, + )?; + + let mut output = Vec::::new(); + + export.output_as_json_v1_5(&mut output)?; + + let output_str = String::from_utf8(output)?; + write!(writer, "{}", output_str)?; } } From b1872176346bd1fbbc97d043b889ae25493d796b Mon Sep 17 00:00:00 2001 From: Will Rollason Date: Tue, 23 Sep 2025 16:55:29 +0100 Subject: [PATCH 04/36] Add CDX initial metadata and component list --- .../src/lock/export/cyclonedx_json.rs | 158 +++++++++++++++++- 1 file changed, 155 insertions(+), 3 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 2a5657092a761..f3e2a25116e97 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -1,7 +1,12 @@ use crate::Installable; use crate::LockError; +use crate::lock::Package; +use crate::lock::Source; use crate::lock::export::ExportableRequirements; -use cyclonedx_bom::prelude::Bom; +use cyclonedx_bom::models::component::Classification; +use cyclonedx_bom::models::metadata::Metadata; +use cyclonedx_bom::prelude::NormalizedString; +use cyclonedx_bom::prelude::{Bom, Component, Components}; use uv_configuration::{ DependencyGroupsWithDefaults, EditableMode, ExtrasSpecificationWithDefaults, InstallOptions, }; @@ -17,10 +22,157 @@ pub fn from_lock<'lock>( install_options: &'lock InstallOptions, ) -> Result { // Extract the packages from the lock file. - let ExportableRequirements(_nodes) = + let ExportableRequirements(mut nodes) = ExportableRequirements::from_lock(target, prune, extras, groups, annotate, install_options); - let bom = Bom { ..Bom::default() }; + nodes.sort_unstable_by_key(|node| &node.package.id); + + let root = target.lock().root(); + + // ID counter for bom-ref generation + let mut id_counter = 1; + + let metadata = Metadata { + component: root.map(|package| { + let res = + create_component_from_package(package, Classification::Application, id_counter); + id_counter += 1; + res + }), + timestamp: cyclonedx_bom::prelude::DateTime::now().ok(), + ..Metadata::default() + }; + + let dependencies = nodes + .iter() + .filter(|node| root.is_none_or(|package| package.id != node.package.id)); + + // Convert dependency packages to CycloneDX components. + let components = dependencies + .map(|node| { + let res = + create_component_from_package(node.package, Classification::Library, id_counter); + id_counter += 1; + res + }) + .collect(); + + let bom = Bom { + metadata: Some(metadata), + components: Some(Components(components)), + ..Bom::default() + }; Ok(bom) } + +/// Creates a bom-ref string in the format "{id}-{package_name}@{version}" +/// or "{id}-{package_name}" if no version is provided. +fn create_bom_ref(id: usize, name: &str, version: Option<&str>) -> String { + if let Some(version) = version { + format!("{}-{}@{}", id, name, version) + } else { + format!("{}-{}", id, name) + } +} + +/// Extract version string from a package, returning empty string if no version +fn get_version_string(package: &Package) -> Option { + package.id.version.as_ref().map(|v| v.to_string()) +} + +/// Extract package name string from a package +fn get_package_name(package: &Package) -> String { + package.id.name.to_string() +} + +/// Generate a Package URL (PURL) from a package +fn create_purl(package: &Package) -> Option { + let name = get_package_name(package); + let version = get_version_string(package); + + match &package.id.source { + Source::Registry(_) => { + if let Some(version) = version { + Some(format!("pkg:pypi/{}@{}", name, version)) + } else { + Some(format!("pkg:pypi/{}", name)) + } + } + Source::Git(url, _) => { + if let Some(version) = version { + Some(format!( + "pkg:generic/{}@{}?download_url={}", + name, + version, + url.as_ref() + )) + } else { + Some(format!( + "pkg:generic/{}?download_url={}", + name, + url.as_ref() + )) + } + } + Source::Direct(url, _) => { + if let Some(version) = version { + Some(format!( + "pkg:generic/{}@{}?download_url={}", + name, + version, + url.as_ref() + )) + } else { + Some(format!( + "pkg:generic/{}?download_url={}", + name, + url.as_ref() + )) + } + } + // No PURL for local sources Path, Directory, Editable, Virtual. + Source::Path(_) | Source::Directory(_) | Source::Editable(_) | Source::Virtual(_) => None, + } +} + +/// Create a CycloneDX component from a package node with the given classification and ID +fn create_component_from_package( + package: &Package, + classification: Classification, + id: usize, +) -> Component { + let name = get_package_name(package); + let version = get_version_string(package); + let bom_ref = create_bom_ref(id, &name, version.as_deref()); + let purl = create_purl(package).and_then(|purl_string| purl_string.parse().ok()); + + Component { + component_type: classification, + name: NormalizedString::new(&name), + version: version.as_deref().map(NormalizedString::new), + bom_ref: Some(bom_ref), + purl, + mime_type: None, + supplier: None, + author: None, + publisher: None, + group: None, + description: None, + scope: None, + hashes: None, + licenses: None, + copyright: None, + cpe: None, + swid: None, + modified: None, + pedigree: None, + external_references: None, + properties: None, + components: None, + evidence: None, + signature: None, + model_card: None, + data: None, + } +} From bb4f0f77d46736f5bba03bd3f03987198ee30b35 Mon Sep 17 00:00:00 2001 From: Will Rollason Date: Tue, 23 Sep 2025 17:14:45 +0100 Subject: [PATCH 05/36] Add test for basic CDX export --- crates/uv/tests/it/export.rs | 81 ++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 186e66850c78d..bf606ddba8edd 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -3591,6 +3591,87 @@ fn pep_751_dependency() -> Result<()> { Ok(()) } +#[test] +fn cyclonedx_export() -> 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"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + // Add custom filters for CycloneDX dynamic values + let mut filters = context.filters(); + filters.push(( + r"urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + "[SERIAL_NUMBER]", + )); + filters.push(( + r#""timestamp": "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z""#, + r#""timestamp": "[TIMESTAMP]""#, + )); + + uv_snapshot!(filters, context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-anyio@3.7.0", + "name": "anyio", + "version": "3.7.0", + "purl": "pkg:pypi/anyio@3.7.0" + }, + { + "type": "library", + "bom-ref": "3-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "4-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + } + ] + } + ----- stderr ----- + Resolved 4 packages in [TIME] + "#); + + Ok(()) +} + #[test] fn pep_751_export_no_header() -> Result<()> { let context = TestContext::new("3.12"); From b0b0f25d49fe0a282cbdd88a75221d16453ca054 Mon Sep 17 00:00:00 2001 From: Will Rollason Date: Tue, 23 Sep 2025 17:39:58 +0100 Subject: [PATCH 06/36] Add tooling metadata field --- Cargo.lock | 1 + crates/uv-resolver/Cargo.toml | 1 + crates/uv-resolver/src/lock/export/cyclonedx_json.rs | 8 ++++++++ crates/uv/tests/it/export.rs | 12 ++++++++++++ 4 files changed, 22 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 3ed51bd1a1430..cc5535c31cfcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6674,6 +6674,7 @@ dependencies = [ "uv-static", "uv-torch", "uv-types", + "uv-version", "uv-warnings", "uv-workspace", ] diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 9fe7b29ff1d0b..32aa7893cd7a9 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -41,6 +41,7 @@ uv-small-str = { workspace = true } uv-static = { workspace = true } uv-torch = { workspace = true } uv-types = { workspace = true } +uv-version = { workspace = true } uv-warnings = { workspace = true } uv-workspace = { workspace = true } diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index f3e2a25116e97..12abe04958e63 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -5,6 +5,7 @@ use crate::lock::Source; use crate::lock::export::ExportableRequirements; use cyclonedx_bom::models::component::Classification; use cyclonedx_bom::models::metadata::Metadata; +use cyclonedx_bom::models::tool::{Tool, Tools}; use cyclonedx_bom::prelude::NormalizedString; use cyclonedx_bom::prelude::{Bom, Component, Components}; use uv_configuration::{ @@ -40,6 +41,13 @@ pub fn from_lock<'lock>( res }), timestamp: cyclonedx_bom::prelude::DateTime::now().ok(), + tools: Some(Tools::List(vec![Tool { + vendor: Some(NormalizedString::new("Astral Software Inc.")), + name: Some(NormalizedString::new("uv")), + version: Some(NormalizedString::new(uv_version::version())), + hashes: None, + external_references: None, + }])), ..Metadata::default() }; diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index bf606ddba8edd..374233e336ff1 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -3622,6 +3622,11 @@ fn cyclonedx_export() -> Result<()> { r#""timestamp": "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z""#, r#""timestamp": "[TIMESTAMP]""#, )); + filters.push(( + r#""name": "uv",\s*"version": "\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?""#, + r#""name": "uv", + "version": "[VERSION]""#, + )); uv_snapshot!(filters, context.export().arg("--format").arg("cyclonedx1.5"), @r#" success: true @@ -3634,6 +3639,13 @@ fn cyclonedx_export() -> Result<()> { "serialNumber": "[SERIAL_NUMBER]", "metadata": { "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], "component": { "type": "application", "bom-ref": "1-project@0.1.0", From 79f13be90951c2ef3f4f4e8d388f665c3acef3b3 Mon Sep 17 00:00:00 2001 From: Will Rollason Date: Wed, 24 Sep 2025 14:42:17 +0100 Subject: [PATCH 07/36] Add dependency tree to CDX output --- .../src/lock/export/cyclonedx_json.rs | 68 +++++++++++++++++++ crates/uv/tests/it/export.rs | 23 +++++++ 2 files changed, 91 insertions(+) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 12abe04958e63..845922be57110 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -1,13 +1,18 @@ use crate::Installable; use crate::LockError; use crate::lock::Package; +use crate::lock::PackageId; use crate::lock::Source; +use crate::lock::export::ExportableRequirement; use crate::lock::export::ExportableRequirements; use cyclonedx_bom::models::component::Classification; +use cyclonedx_bom::models::dependency::Dependencies; +use cyclonedx_bom::models::dependency::Dependency; use cyclonedx_bom::models::metadata::Metadata; use cyclonedx_bom::models::tool::{Tool, Tools}; use cyclonedx_bom::prelude::NormalizedString; use cyclonedx_bom::prelude::{Bom, Component, Components}; +use std::collections::HashMap; use uv_configuration::{ DependencyGroupsWithDefaults, EditableMode, ExtrasSpecificationWithDefaults, InstallOptions, }; @@ -32,11 +37,13 @@ pub fn from_lock<'lock>( // ID counter for bom-ref generation let mut id_counter = 1; + let mut package_to_bom_ref = HashMap::<&PackageId, Component>::new(); let metadata = Metadata { component: root.map(|package| { let res = create_component_from_package(package, Classification::Application, id_counter); + package_to_bom_ref.insert(&package.id, res.clone()); id_counter += 1; res }), @@ -60,14 +67,18 @@ pub fn from_lock<'lock>( .map(|node| { let res = create_component_from_package(node.package, Classification::Library, id_counter); + package_to_bom_ref.insert(&node.package.id, res.clone()); id_counter += 1; res }) .collect(); + let dependencies = create_dependencies_from_mapping(&nodes, &package_to_bom_ref); + let bom = Bom { metadata: Some(metadata), components: Some(Components(components)), + dependencies: Some(dependencies), ..Bom::default() }; @@ -184,3 +195,60 @@ fn create_component_from_package( data: None, } } + +fn create_dependencies_from_mapping<'lock>( + nodes: &[ExportableRequirement<'lock>], + package_to_component: &HashMap<&PackageId, Component>, +) -> Dependencies { + let mut dependencies = Vec::new(); + + // Add dependencies for all other packages + for node in nodes { + if let Some(package_bom_ref) = package_to_component.get(&node.package.id) { + let mut package_deps = Vec::new(); + + // Add regular dependencies + for dep in &node.package.dependencies { + if let Some(component) = package_to_component.get(&dep.package_id) { + package_deps.push(component); + } + } + + // Add optional dependencies (extras) + for (_extra_name, deps) in &node.package.optional_dependencies { + for dep in deps { + if let Some(component) = package_to_component.get(&dep.package_id) { + package_deps.push(component); + } + } + } + + // Add dependency groups + for (_group_name, deps) in &node.package.dependency_groups { + for dep in deps { + if let Some(component) = package_to_component.get(&dep.package_id) { + package_deps.push(component); + } + } + } + + // Remove duplicates and sort + // package_deps.sort(); + package_deps.sort_by_key(|p| &p.bom_ref); + let bom_refs = package_deps + .iter() + .map(|p| p.bom_ref.clone().expect("bom-ref should always exist")) + .collect(); + package_deps.dedup(); + + dependencies.push(Dependency { + dependency_ref: package_bom_ref + .bom_ref + .clone() + .expect("bom-ref should always exist"), + dependencies: bom_refs, + }); + } + } + Dependencies(dependencies) +} diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 374233e336ff1..6db1e30a2e172 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -3675,6 +3675,29 @@ fn cyclonedx_export() -> Result<()> { "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" } + ], + "dependencies": [ + { + "ref": "2-anyio@3.7.0", + "dependsOn": [ + "3-idna@3.6", + "4-sniffio@1.3.1" + ] + }, + { + "ref": "3-idna@3.6", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-anyio@3.7.0" + ] + }, + { + "ref": "4-sniffio@1.3.1", + "dependsOn": [] + } ] } ----- stderr ----- From b2c872f22495da63c4ac8176be6272e7be00d4d7 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Wed, 24 Sep 2025 15:27:42 +0100 Subject: [PATCH 08/36] Tidy up dependency building and imports --- Cargo.lock | 67 +++--- crates/uv-cli/src/lib.rs | 2 +- crates/uv-configuration/src/export_format.rs | 2 +- crates/uv-resolver/Cargo.toml | 1 - .../src/lock/export/cyclonedx_json.rs | 212 +++++++----------- crates/uv/src/commands/project/export.rs | 3 +- 6 files changed, 118 insertions(+), 169 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc5535c31cfcb..a0ecbb29abab1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2815,6 +2815,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -2823,12 +2832,6 @@ checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ "dlv-list", "hashbrown 0.14.5", -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", ] [[package]] @@ -3212,13 +3215,6 @@ dependencies = [ ] [[package]] -name = "quick-xml" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" -dependencies = [ - "memchr", - "serde", name = "purl" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3229,6 +3225,16 @@ dependencies = [ "thiserror 2.0.16", ] +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quinn" version = "0.11.8" @@ -4724,31 +4730,6 @@ name = "time" version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -4775,6 +4756,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tiny-skia" version = "0.8.4" @@ -6648,7 +6638,6 @@ dependencies = [ "toml_edit 0.23.7", "tracing", "url", - "uuid", "uv-cache-key", "uv-client", "uv-configuration", diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index ef03af373c671..0363af2b44a5f 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -4303,7 +4303,7 @@ pub struct TreeArgs { pub struct ExportArgs { /// The format to which `uv.lock` should be exported. /// - /// Supports `requirements.txt`, `pylock.toml` (PEP 751) and `CycloneDX` v1.5 JSON output formats. + /// Supports `requirements.txt`, `pylock.toml` (PEP 751) and CycloneDX v1.5 JSON output formats. /// /// uv will infer the output format from the file extension of the output file, if /// provided. Otherwise, defaults to `requirements.txt`. diff --git a/crates/uv-configuration/src/export_format.rs b/crates/uv-configuration/src/export_format.rs index 93ca921e90bea..e4b0016b8fc2b 100644 --- a/crates/uv-configuration/src/export_format.rs +++ b/crates/uv-configuration/src/export_format.rs @@ -15,7 +15,7 @@ pub enum ExportFormat { #[serde(rename = "pylock.toml", alias = "pylock-toml")] #[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))] PylockToml, - /// Export in `CycloneDX` v1.5 JSON format. + /// Export in CycloneDX v1.5 JSON format. #[serde(rename = "cyclonedx1.5")] #[cfg_attr( feature = "clap", diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 32aa7893cd7a9..26e0ec6b6390f 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -73,7 +73,6 @@ toml = { workspace = true } toml_edit = { workspace = true } tracing = { workspace = true } url = { workspace = true } -uuid = { workspace = true } [dev-dependencies] insta = { workspace = true } diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 845922be57110..5bed43e83f964 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -1,30 +1,27 @@ -use crate::Installable; -use crate::LockError; -use crate::lock::Package; -use crate::lock::PackageId; -use crate::lock::Source; -use crate::lock::export::ExportableRequirement; -use crate::lock::export::ExportableRequirements; +use std::collections::HashMap; + use cyclonedx_bom::models::component::Classification; -use cyclonedx_bom::models::dependency::Dependencies; -use cyclonedx_bom::models::dependency::Dependency; +use cyclonedx_bom::models::dependency::{Dependencies, Dependency}; use cyclonedx_bom::models::metadata::Metadata; use cyclonedx_bom::models::tool::{Tool, Tools}; -use cyclonedx_bom::prelude::NormalizedString; -use cyclonedx_bom::prelude::{Bom, Component, Components}; -use std::collections::HashMap; +use cyclonedx_bom::prelude::{Bom, Component, Components, NormalizedString}; +use itertools::Itertools; + use uv_configuration::{ - DependencyGroupsWithDefaults, EditableMode, ExtrasSpecificationWithDefaults, InstallOptions, + DependencyGroupsWithDefaults, ExtrasSpecificationWithDefaults, InstallOptions, }; use uv_normalize::PackageName; +use crate::lock::export::{ExportableRequirement, ExportableRequirements}; +use crate::lock::{Package, PackageId, Source}; +use crate::{Installable, LockError}; + pub fn from_lock<'lock>( target: &impl Installable<'lock>, prune: &[PackageName], extras: &ExtrasSpecificationWithDefaults, groups: &DependencyGroupsWithDefaults, annotate: bool, - #[allow(unused_variables)] editable: Option, install_options: &'lock InstallOptions, ) -> Result { // Extract the packages from the lock file. @@ -35,17 +32,18 @@ pub fn from_lock<'lock>( let root = target.lock().root(); - // ID counter for bom-ref generation + // Used as prefix in bom-ref generation, to ensure uniqueness let mut id_counter = 1; let mut package_to_bom_ref = HashMap::<&PackageId, Component>::new(); let metadata = Metadata { component: root.map(|package| { - let res = - create_component_from_package(package, Classification::Application, id_counter); - package_to_bom_ref.insert(&package.id, res.clone()); - id_counter += 1; - res + create_and_register_component( + package, + Classification::Application, + &mut id_counter, + &mut package_to_bom_ref, + ) }), timestamp: cyclonedx_bom::prelude::DateTime::now().ok(), tools: Some(Tools::List(vec![Tool { @@ -62,14 +60,14 @@ pub fn from_lock<'lock>( .iter() .filter(|node| root.is_none_or(|package| package.id != node.package.id)); - // Convert dependency packages to CycloneDX components. let components = dependencies .map(|node| { - let res = - create_component_from_package(node.package, Classification::Library, id_counter); - package_to_bom_ref.insert(&node.package.id, res.clone()); - id_counter += 1; - res + create_and_register_component( + node.package, + Classification::Library, + &mut id_counter, + &mut package_to_bom_ref, + ) }) .collect(); @@ -85,17 +83,29 @@ pub fn from_lock<'lock>( Ok(bom) } -/// Creates a bom-ref string in the format "{id}-{package_name}@{version}" -/// or "{id}-{package_name}" if no version is provided. +/// Create and register a CycloneDX component, updating the counter and map +fn create_and_register_component<'a>( + package: &'a Package, + classification: Classification, + id_counter: &mut usize, + package_to_bom_ref: &mut HashMap<&'a PackageId, Component>, +) -> Component { + let component = create_component_from_package(package, classification, *id_counter); + package_to_bom_ref.insert(&package.id, component.clone()); + *id_counter += 1; + component +} + +/// Creates a bom-ref string in the format "{id}-{package_name}@{version}" or "{id}-{package_name}" if no version is provided. fn create_bom_ref(id: usize, name: &str, version: Option<&str>) -> String { if let Some(version) = version { - format!("{}-{}@{}", id, name, version) + format!("{id}-{name}@{version}") } else { - format!("{}-{}", id, name) + format!("{id}-{name}") } } -/// Extract version string from a package, returning empty string if no version +/// Extract version string from a package fn get_version_string(package: &Package) -> Option { package.id.version.as_ref().map(|v| v.to_string()) } @@ -105,54 +115,27 @@ fn get_package_name(package: &Package) -> String { package.id.name.to_string() } -/// Generate a Package URL (PURL) from a package +/// Generate a Package URL (purl) from a package fn create_purl(package: &Package) -> Option { let name = get_package_name(package); let version = get_version_string(package); - match &package.id.source { - Source::Registry(_) => { - if let Some(version) = version { - Some(format!("pkg:pypi/{}@{}", name, version)) - } else { - Some(format!("pkg:pypi/{}", name)) - } - } - Source::Git(url, _) => { - if let Some(version) = version { - Some(format!( - "pkg:generic/{}@{}?download_url={}", - name, - version, - url.as_ref() - )) - } else { - Some(format!( - "pkg:generic/{}?download_url={}", - name, - url.as_ref() - )) - } + let (purl_type, qualifiers) = match &package.id.source { + Source::Registry(_) => ("pypi", String::new()), + Source::Git(url, _) | Source::Direct(url, _) => { + ("generic", format!("?download_url={}", url.as_ref())) } - Source::Direct(url, _) => { - if let Some(version) = version { - Some(format!( - "pkg:generic/{}@{}?download_url={}", - name, - version, - url.as_ref() - )) - } else { - Some(format!( - "pkg:generic/{}?download_url={}", - name, - url.as_ref() - )) - } + // No purl for local sources + Source::Path(_) | Source::Directory(_) | Source::Editable(_) | Source::Virtual(_) => { + return None; } - // No PURL for local sources Path, Directory, Editable, Virtual. - Source::Path(_) | Source::Directory(_) | Source::Editable(_) | Source::Virtual(_) => None, - } + }; + + let version_specifier = version.map_or_else(String::new, |v| format!("@{v}")); + + Some(format!( + "pkg:{purl_type}/{name}{version_specifier}{qualifiers}" + )) } /// Create a CycloneDX component from a package node with the given classification and ID @@ -196,59 +179,38 @@ fn create_component_from_package( } } -fn create_dependencies_from_mapping<'lock>( - nodes: &[ExportableRequirement<'lock>], +fn create_dependencies_from_mapping( + nodes: &[ExportableRequirement<'_>], package_to_component: &HashMap<&PackageId, Component>, ) -> Dependencies { - let mut dependencies = Vec::new(); - - // Add dependencies for all other packages - for node in nodes { - if let Some(package_bom_ref) = package_to_component.get(&node.package.id) { - let mut package_deps = Vec::new(); - - // Add regular dependencies - for dep in &node.package.dependencies { - if let Some(component) = package_to_component.get(&dep.package_id) { - package_deps.push(component); - } - } - - // Add optional dependencies (extras) - for (_extra_name, deps) in &node.package.optional_dependencies { - for dep in deps { - if let Some(component) = package_to_component.get(&dep.package_id) { - package_deps.push(component); - } - } - } - - // Add dependency groups - for (_group_name, deps) in &node.package.dependency_groups { - for dep in deps { - if let Some(component) = package_to_component.get(&dep.package_id) { - package_deps.push(component); - } + let dependencies = nodes.iter().filter_map(|node| { + package_to_component + .get(&node.package.id) + .map(|package_bom_ref| { + let immediate_deps = &node.package.dependencies; + let optional_deps = node.package.optional_dependencies.values().flatten(); + let dep_groups = node.package.dependency_groups.values().flatten(); + + let package_deps = immediate_deps + .iter() + .chain(optional_deps) + .chain(dep_groups) + .filter_map(|dep| package_to_component.get(&dep.package_id)); + + let bom_refs = package_deps + .map(|p| p.bom_ref.clone().expect("bom-ref should always exist")) + .sorted_unstable() + .unique() + .collect(); + + Dependency { + dependency_ref: package_bom_ref + .bom_ref + .clone() + .expect("bom-ref should always exist"), + dependencies: bom_refs, } - } - - // Remove duplicates and sort - // package_deps.sort(); - package_deps.sort_by_key(|p| &p.bom_ref); - let bom_refs = package_deps - .iter() - .map(|p| p.bom_ref.clone().expect("bom-ref should always exist")) - .collect(); - package_deps.dedup(); - - dependencies.push(Dependency { - dependency_ref: package_bom_ref - .bom_ref - .clone() - .expect("bom-ref should always exist"), - dependencies: bom_refs, - }); - } - } - Dependencies(dependencies) + }) + }); + Dependencies(dependencies.collect()) } diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index a3aa1d4c73610..de46589e74f19 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -383,7 +383,6 @@ pub(crate) async fn export( &extras, &groups, include_annotations, - editable, &install_options, )?; @@ -392,7 +391,7 @@ pub(crate) async fn export( export.output_as_json_v1_5(&mut output)?; let output_str = String::from_utf8(output)?; - write!(writer, "{}", output_str)?; + write!(writer, "{output_str}")?; } } From 557d3b76501f964e08b7b37cd1246a023ba282fb Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Wed, 24 Sep 2025 16:48:44 +0100 Subject: [PATCH 09/36] Add tests for git deps and no deps --- Cargo.lock | 15 +- crates/uv/tests/it/common/mod.rs | 21 + crates/uv/tests/it/export.rs | 3967 +++++++++++++++++++++++++++++- 3 files changed, 3884 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0ecbb29abab1..2643669350f78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1058,7 +1058,7 @@ dependencies = [ "regex", "serde", "serde_json", - "spdx", + "spdx 0.10.9", "strum", "thiserror 1.0.69", "time", @@ -3222,7 +3222,7 @@ checksum = "60ebe4262ae91ddd28c8721111a0a6e9e58860e211fc92116c4bb85c98fd96ad" dependencies = [ "hex", "percent-encoding", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -4337,6 +4337,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "spdx" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" +dependencies = [ + "smallvec", +] + [[package]] name = "spdx" version = "0.12.0" @@ -5591,7 +5600,7 @@ dependencies = [ "schemars", "serde", "sha2", - "spdx", + "spdx 0.12.0", "tar", "tempfile", "thiserror 2.0.17", diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index e3145a7e94975..9b38eadba2116 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -477,6 +477,27 @@ impl TestContext { self } + /// Adds filters for non-deterministic CycloneDX data + pub fn with_cyclonedx_filters(mut self) -> Self { + self.filters.push(( + r"urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}".to_string(), + "[SERIAL_NUMBER]".to_string(), + )); + self.filters.push(( + r#""timestamp": "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z""# + .to_string(), + r#""timestamp": "[TIMESTAMP]""#.to_string(), + )); + self.filters.push(( + r#""name": "uv",\s*"version": "\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?""# + .to_string(), + r#""name": "uv", + "version": "[VERSION]""# + .to_string(), + )); + self + } + /// Add a filter that collapses duplicate whitespace. #[must_use] pub fn with_collapsed_whitespace(mut self) -> Self { diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 6db1e30a2e172..1c57a8cc3a833 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -3591,122 +3591,6 @@ fn pep_751_dependency() -> Result<()> { Ok(()) } -#[test] -fn cyclonedx_export() -> 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"] - - [build-system] - requires = ["setuptools>=42"] - build-backend = "setuptools.build_meta" - "#, - )?; - - context.lock().assert().success(); - - // Add custom filters for CycloneDX dynamic values - let mut filters = context.filters(); - filters.push(( - r"urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", - "[SERIAL_NUMBER]", - )); - filters.push(( - r#""timestamp": "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z""#, - r#""timestamp": "[TIMESTAMP]""#, - )); - filters.push(( - r#""name": "uv",\s*"version": "\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?""#, - r#""name": "uv", - "version": "[VERSION]""#, - )); - - uv_snapshot!(filters, context.export().arg("--format").arg("cyclonedx1.5"), @r#" - success: true - exit_code: 0 - ----- stdout ----- - { - "bomFormat": "CycloneDX", - "specVersion": "1.5", - "version": 1, - "serialNumber": "[SERIAL_NUMBER]", - "metadata": { - "timestamp": "[TIMESTAMP]", - "tools": [ - { - "vendor": "Astral Software Inc.", - "name": "uv", - "version": "[VERSION]" - } - ], - "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", - "name": "project", - "version": "0.1.0" - } - }, - "components": [ - { - "type": "library", - "bom-ref": "2-anyio@3.7.0", - "name": "anyio", - "version": "3.7.0", - "purl": "pkg:pypi/anyio@3.7.0" - }, - { - "type": "library", - "bom-ref": "3-idna@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6" - }, - { - "type": "library", - "bom-ref": "4-sniffio@1.3.1", - "name": "sniffio", - "version": "1.3.1", - "purl": "pkg:pypi/sniffio@1.3.1" - } - ], - "dependencies": [ - { - "ref": "2-anyio@3.7.0", - "dependsOn": [ - "3-idna@3.6", - "4-sniffio@1.3.1" - ] - }, - { - "ref": "3-idna@3.6", - "dependsOn": [] - }, - { - "ref": "1-project@0.1.0", - "dependsOn": [ - "2-anyio@3.7.0" - ] - }, - { - "ref": "4-sniffio@1.3.1", - "dependsOn": [] - } - ] - } - ----- stderr ----- - Resolved 4 packages in [TIME] - "#); - - Ok(()) -} - #[test] fn pep_751_export_no_header() -> Result<()> { let context = TestContext::new("3.12"); @@ -4920,3 +4804,3854 @@ fn multiple_packages() -> Result<()> { Ok(()) } + +#[test] +fn cyclonedx_export() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + 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"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-anyio@3.7.0", + "name": "anyio", + "version": "3.7.0", + "purl": "pkg:pypi/anyio@3.7.0" + }, + { + "type": "library", + "bom-ref": "3-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "4-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + } + ], + "dependencies": [ + { + "ref": "2-anyio@3.7.0", + "dependsOn": [ + "3-idna@3.6", + "4-sniffio@1.3.1" + ] + }, + { + "ref": "3-idna@3.6", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-anyio@3.7.0" + ] + }, + { + "ref": "4-sniffio@1.3.1", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 4 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_direct_url() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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 = ["idna @ https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:generic/idna@3.6?download_url=https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl" + } + ], + "dependencies": [ + { + "ref": "2-idna@3.6", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-idna@3.6" + ] + } + ] + } + ----- stderr ----- + Resolved 2 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_git_dependency() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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 = ["flask @ git+https://github.com/pallets/flask.git@2.3.3"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-blinker@1.7.0", + "name": "blinker", + "version": "1.7.0", + "purl": "pkg:pypi/blinker@1.7.0" + }, + { + "type": "library", + "bom-ref": "3-click@8.1.7", + "name": "click", + "version": "8.1.7", + "purl": "pkg:pypi/click@8.1.7" + }, + { + "type": "library", + "bom-ref": "4-colorama@0.4.6", + "name": "colorama", + "version": "0.4.6", + "purl": "pkg:pypi/colorama@0.4.6", + "properties": [ + { + "name": "uv:package:marker", + "value": "sys_platform == 'win32'" + } + ] + }, + { + "type": "library", + "bom-ref": "5-flask@2.3.3", + "name": "flask", + "version": "2.3.3", + "purl": "pkg:generic/flask@2.3.3?vcs_url=https://github.com/pallets/flask.git%3Frev%3D2.3.3%233205b53c7cf69d17fee49cac6b84978175b7dd73" + }, + { + "type": "library", + "bom-ref": "6-itsdangerous@2.1.2", + "name": "itsdangerous", + "version": "2.1.2", + "purl": "pkg:pypi/itsdangerous@2.1.2" + }, + { + "type": "library", + "bom-ref": "7-jinja2@3.1.3", + "name": "jinja2", + "version": "3.1.3", + "purl": "pkg:pypi/jinja2@3.1.3" + }, + { + "type": "library", + "bom-ref": "8-markupsafe@2.1.5", + "name": "markupsafe", + "version": "2.1.5", + "purl": "pkg:pypi/markupsafe@2.1.5" + }, + { + "type": "library", + "bom-ref": "9-werkzeug@3.0.1", + "name": "werkzeug", + "version": "3.0.1", + "purl": "pkg:pypi/werkzeug@3.0.1" + } + ], + "dependencies": [ + { + "ref": "2-blinker@1.7.0", + "dependsOn": [] + }, + { + "ref": "3-click@8.1.7", + "dependsOn": [ + "4-colorama@0.4.6" + ] + }, + { + "ref": "4-colorama@0.4.6", + "dependsOn": [] + }, + { + "ref": "5-flask@2.3.3", + "dependsOn": [ + "2-blinker@1.7.0", + "3-click@8.1.7", + "6-itsdangerous@2.1.2", + "7-jinja2@3.1.3", + "9-werkzeug@3.0.1" + ] + }, + { + "ref": "6-itsdangerous@2.1.2", + "dependsOn": [] + }, + { + "ref": "7-jinja2@3.1.3", + "dependsOn": [ + "8-markupsafe@2.1.5" + ] + }, + { + "ref": "8-markupsafe@2.1.5", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "5-flask@2.3.3" + ] + }, + { + "ref": "9-werkzeug@3.0.1", + "dependsOn": [ + "8-markupsafe@2.1.5" + ] + } + ] + } + ----- stderr ----- + Resolved 9 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_no_dependencies() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "standalone-project" + version = "1.0.0" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-standalone-project@1.0.0", + "name": "standalone-project", + "version": "1.0.0" + } + }, + "components": [], + "dependencies": [ + { + "ref": "1-standalone-project@1.0.0", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 1 package in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_mixed_source_types() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "mixed-project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "requests==2.31.0", # PyPI registry package + "flask @ git+https://github.com/pallets/flask.git@2.3.3", # Git package + "iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" # Direct URL package + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-mixed-project@0.1.0", + "name": "mixed-project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-blinker@1.7.0", + "name": "blinker", + "version": "1.7.0", + "purl": "pkg:pypi/blinker@1.7.0" + }, + { + "type": "library", + "bom-ref": "3-certifi@2024.2.2", + "name": "certifi", + "version": "2024.2.2", + "purl": "pkg:pypi/certifi@2024.2.2" + }, + { + "type": "library", + "bom-ref": "4-charset-normalizer@3.3.2", + "name": "charset-normalizer", + "version": "3.3.2", + "purl": "pkg:pypi/charset-normalizer@3.3.2" + }, + { + "type": "library", + "bom-ref": "5-click@8.1.7", + "name": "click", + "version": "8.1.7", + "purl": "pkg:pypi/click@8.1.7" + }, + { + "type": "library", + "bom-ref": "6-colorama@0.4.6", + "name": "colorama", + "version": "0.4.6", + "purl": "pkg:pypi/colorama@0.4.6", + "properties": [ + { + "name": "uv:package:marker", + "value": "sys_platform == 'win32'" + } + ] + }, + { + "type": "library", + "bom-ref": "7-flask@2.3.3", + "name": "flask", + "version": "2.3.3", + "purl": "pkg:generic/flask@2.3.3?vcs_url=https://github.com/pallets/flask.git%3Frev%3D2.3.3%233205b53c7cf69d17fee49cac6b84978175b7dd73" + }, + { + "type": "library", + "bom-ref": "8-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "9-iniconfig@2.0.0", + "name": "iniconfig", + "version": "2.0.0", + "purl": "pkg:generic/iniconfig@2.0.0?download_url=https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" + }, + { + "type": "library", + "bom-ref": "10-itsdangerous@2.1.2", + "name": "itsdangerous", + "version": "2.1.2", + "purl": "pkg:pypi/itsdangerous@2.1.2" + }, + { + "type": "library", + "bom-ref": "11-jinja2@3.1.3", + "name": "jinja2", + "version": "3.1.3", + "purl": "pkg:pypi/jinja2@3.1.3" + }, + { + "type": "library", + "bom-ref": "12-markupsafe@2.1.5", + "name": "markupsafe", + "version": "2.1.5", + "purl": "pkg:pypi/markupsafe@2.1.5" + }, + { + "type": "library", + "bom-ref": "13-requests@2.31.0", + "name": "requests", + "version": "2.31.0", + "purl": "pkg:pypi/requests@2.31.0" + }, + { + "type": "library", + "bom-ref": "14-urllib3@2.2.1", + "name": "urllib3", + "version": "2.2.1", + "purl": "pkg:pypi/urllib3@2.2.1" + }, + { + "type": "library", + "bom-ref": "15-werkzeug@3.0.1", + "name": "werkzeug", + "version": "3.0.1", + "purl": "pkg:pypi/werkzeug@3.0.1" + } + ], + "dependencies": [ + { + "ref": "2-blinker@1.7.0", + "dependsOn": [] + }, + { + "ref": "3-certifi@2024.2.2", + "dependsOn": [] + }, + { + "ref": "4-charset-normalizer@3.3.2", + "dependsOn": [] + }, + { + "ref": "5-click@8.1.7", + "dependsOn": [ + "6-colorama@0.4.6" + ] + }, + { + "ref": "6-colorama@0.4.6", + "dependsOn": [] + }, + { + "ref": "7-flask@2.3.3", + "dependsOn": [ + "10-itsdangerous@2.1.2", + "11-jinja2@3.1.3", + "15-werkzeug@3.0.1", + "2-blinker@1.7.0", + "5-click@8.1.7" + ] + }, + { + "ref": "8-idna@3.6", + "dependsOn": [] + }, + { + "ref": "9-iniconfig@2.0.0", + "dependsOn": [] + }, + { + "ref": "10-itsdangerous@2.1.2", + "dependsOn": [] + }, + { + "ref": "11-jinja2@3.1.3", + "dependsOn": [ + "12-markupsafe@2.1.5" + ] + }, + { + "ref": "12-markupsafe@2.1.5", + "dependsOn": [] + }, + { + "ref": "1-mixed-project@0.1.0", + "dependsOn": [ + "13-requests@2.31.0", + "7-flask@2.3.3", + "9-iniconfig@2.0.0" + ] + }, + { + "ref": "13-requests@2.31.0", + "dependsOn": [ + "14-urllib3@2.2.1", + "3-certifi@2024.2.2", + "4-charset-normalizer@3.3.2", + "8-idna@3.6" + ] + }, + { + "ref": "14-urllib3@2.2.1", + "dependsOn": [] + }, + { + "ref": "15-werkzeug@3.0.1", + "dependsOn": [ + "12-markupsafe@2.1.5" + ] + } + ] + } + ----- stderr ----- + Resolved 15 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_project_extra() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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 = ["typing-extensions"] + + [project.optional-dependencies] + async = ["anyio==3.7.0"] + pytest = ["iniconfig"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-typing-extensions@4.10.0", + "name": "typing-extensions", + "version": "4.10.0", + "purl": "pkg:pypi/typing-extensions@4.10.0" + } + ], + "dependencies": [ + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-typing-extensions@4.10.0" + ] + }, + { + "ref": "2-typing-extensions@4.10.0", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 6 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_project_extra_with_optional_flag() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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 = ["typing-extensions"] + + [project.optional-dependencies] + async = ["anyio==3.7.0"] + pytest = ["iniconfig"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--all-extras"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-anyio@3.7.0", + "name": "anyio", + "version": "3.7.0", + "purl": "pkg:pypi/anyio@3.7.0" + }, + { + "type": "library", + "bom-ref": "3-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "4-iniconfig@2.0.0", + "name": "iniconfig", + "version": "2.0.0", + "purl": "pkg:pypi/iniconfig@2.0.0" + }, + { + "type": "library", + "bom-ref": "5-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + }, + { + "type": "library", + "bom-ref": "6-typing-extensions@4.10.0", + "name": "typing-extensions", + "version": "4.10.0", + "purl": "pkg:pypi/typing-extensions@4.10.0" + } + ], + "dependencies": [ + { + "ref": "2-anyio@3.7.0", + "dependsOn": [ + "3-idna@3.6", + "5-sniffio@1.3.1" + ] + }, + { + "ref": "3-idna@3.6", + "dependsOn": [] + }, + { + "ref": "4-iniconfig@2.0.0", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-anyio@3.7.0", + "4-iniconfig@2.0.0", + "6-typing-extensions@4.10.0" + ] + }, + { + "ref": "5-sniffio@1.3.1", + "dependsOn": [] + }, + { + "ref": "6-typing-extensions@4.10.0", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 6 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_with_workspace_member() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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", "child1", "child2"] + + [tool.uv.workspace] + members = ["child1", "packages/*"] + + [tool.uv.sources] + child1 = { workspace = true } + child2 = { workspace = true } + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + let child1 = context.temp_dir.child("child1"); + child1.child("pyproject.toml").write_str( + r#" + [project] + name = "child1" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig>=2"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + let child2 = context.temp_dir.child("packages").child("child2"); + child2.child("pyproject.toml").write_str( + r#" + [project] + name = "child2" + version = "0.2.9" + requires-python = ">=3.11" + dependencies = [] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--all-extras"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-anyio@3.7.0", + "name": "anyio", + "version": "3.7.0", + "purl": "pkg:pypi/anyio@3.7.0" + }, + { + "type": "application", + "bom-ref": "3-child1@0.1.0", + "name": "child1", + "version": "0.1.0", + "properties": [ + { + "name": "uv:workspace:path", + "value": "child1" + } + ] + }, + { + "type": "application", + "bom-ref": "4-child2@0.2.9", + "name": "child2", + "version": "0.2.9", + "properties": [ + { + "name": "uv:workspace:path", + "value": "packages/child2" + } + ] + }, + { + "type": "library", + "bom-ref": "5-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "6-iniconfig@2.0.0", + "name": "iniconfig", + "version": "2.0.0", + "purl": "pkg:pypi/iniconfig@2.0.0" + }, + { + "type": "library", + "bom-ref": "7-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + } + ], + "dependencies": [ + { + "ref": "2-anyio@3.7.0", + "dependsOn": [ + "5-idna@3.6", + "7-sniffio@1.3.1" + ] + }, + { + "ref": "3-child1@0.1.0", + "dependsOn": [ + "6-iniconfig@2.0.0" + ] + }, + { + "ref": "4-child2@0.2.9", + "dependsOn": [] + }, + { + "ref": "5-idna@3.6", + "dependsOn": [] + }, + { + "ref": "6-iniconfig@2.0.0", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-anyio@3.7.0", + "3-child1@0.1.0", + "4-child2@0.2.9" + ] + }, + { + "ref": "7-sniffio@1.3.1", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 7 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_workspace_non_root() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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("--format").arg("cyclonedx1.5").arg("--package").arg("child"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-child@0.1.0", + "name": "child", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-iniconfig@2.0.0", + "name": "iniconfig", + "version": "2.0.0", + "purl": "pkg:pypi/iniconfig@2.0.0" + } + ], + "dependencies": [ + { + "ref": "1-child@0.1.0", + "dependsOn": [ + "2-iniconfig@2.0.0" + ] + }, + { + "ref": "2-iniconfig@2.0.0", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 6 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_workspace_with_extras() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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"] + + [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 = ["typing-extensions"] + + [project.optional-dependencies] + async = ["anyio==3.7.0"] + test = ["iniconfig"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "application", + "bom-ref": "2-child@0.1.0", + "name": "child", + "version": "0.1.0", + "properties": [ + { + "name": "uv:workspace:path", + "value": "child" + } + ] + }, + { + "type": "library", + "bom-ref": "3-typing-extensions@4.10.0", + "name": "typing-extensions", + "version": "4.10.0", + "purl": "pkg:pypi/typing-extensions@4.10.0" + } + ], + "dependencies": [ + { + "ref": "2-child@0.1.0", + "dependsOn": [ + "3-typing-extensions@4.10.0" + ] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-child@0.1.0" + ] + }, + { + "ref": "3-typing-extensions@4.10.0", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 7 packages in [TIME] + "#); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--all-extras"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "application", + "bom-ref": "2-child@0.1.0", + "name": "child", + "version": "0.1.0", + "properties": [ + { + "name": "uv:workspace:path", + "value": "child" + } + ] + }, + { + "type": "library", + "bom-ref": "3-typing-extensions@4.10.0", + "name": "typing-extensions", + "version": "4.10.0", + "purl": "pkg:pypi/typing-extensions@4.10.0" + } + ], + "dependencies": [ + { + "ref": "2-child@0.1.0", + "dependsOn": [ + "3-typing-extensions@4.10.0" + ] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-child@0.1.0" + ] + }, + { + "ref": "3-typing-extensions@4.10.0", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 7 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_workspace_frozen() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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(); + + // Remove the child `pyproject.toml`. + fs_err::remove_dir_all(child.path())?; + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--all-packages"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to build `project @ file://[TEMP_DIR]/` + ├─▶ Failed to parse entry: `child` + ╰─▶ `child` references a workspace in `tool.uv.sources` (e.g., `child = { workspace = true }`), but is not a workspace member + "###); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--all-packages").arg("--frozen"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-anyio@3.7.0", + "name": "anyio", + "version": "3.7.0", + "purl": "pkg:pypi/anyio@3.7.0" + }, + { + "type": "application", + "bom-ref": "3-child@0.1.0", + "name": "child", + "version": "0.1.0", + "properties": [ + { + "name": "uv:workspace:path", + "value": "child" + } + ] + }, + { + "type": "library", + "bom-ref": "4-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "5-iniconfig@2.0.0", + "name": "iniconfig", + "version": "2.0.0", + "purl": "pkg:pypi/iniconfig@2.0.0" + }, + { + "type": "library", + "bom-ref": "6-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + } + ], + "dependencies": [ + { + "ref": "2-anyio@3.7.0", + "dependsOn": [ + "4-idna@3.6", + "6-sniffio@1.3.1" + ] + }, + { + "ref": "3-child@0.1.0", + "dependsOn": [ + "5-iniconfig@2.0.0" + ] + }, + { + "ref": "4-idna@3.6", + "dependsOn": [] + }, + { + "ref": "5-iniconfig@2.0.0", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-anyio@3.7.0", + "3-child@0.1.0" + ] + }, + { + "ref": "6-sniffio@1.3.1", + "dependsOn": [] + } + ] + } + ----- stderr ----- + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_workspace_all_packages() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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"] + + [tool.uv.workspace] + members = ["child1", "child2"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + let child1 = context.temp_dir.child("child1"); + child1.child("pyproject.toml").write_str( + r#" + [project] + name = "child1" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig>=2"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + let child2 = context.temp_dir.child("child2"); + child2.child("pyproject.toml").write_str( + r#" + [project] + name = "child2" + version = "0.2.0" + requires-python = ">=3.12" + dependencies = ["sniffio"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--all-packages"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-anyio@3.7.0", + "name": "anyio", + "version": "3.7.0", + "purl": "pkg:pypi/anyio@3.7.0" + }, + { + "type": "application", + "bom-ref": "3-child1@0.1.0", + "name": "child1", + "version": "0.1.0", + "properties": [ + { + "name": "uv:workspace:path", + "value": "child1" + } + ] + }, + { + "type": "application", + "bom-ref": "4-child2@0.2.0", + "name": "child2", + "version": "0.2.0", + "properties": [ + { + "name": "uv:workspace:path", + "value": "child2" + } + ] + }, + { + "type": "library", + "bom-ref": "5-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "6-iniconfig@2.0.0", + "name": "iniconfig", + "version": "2.0.0", + "purl": "pkg:pypi/iniconfig@2.0.0" + }, + { + "type": "library", + "bom-ref": "7-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + } + ], + "dependencies": [ + { + "ref": "2-anyio@3.7.0", + "dependsOn": [ + "5-idna@3.6", + "7-sniffio@1.3.1" + ] + }, + { + "ref": "3-child1@0.1.0", + "dependsOn": [ + "6-iniconfig@2.0.0" + ] + }, + { + "ref": "4-child2@0.2.0", + "dependsOn": [ + "7-sniffio@1.3.1" + ] + }, + { + "ref": "5-idna@3.6", + "dependsOn": [] + }, + { + "ref": "6-iniconfig@2.0.0", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-anyio@3.7.0" + ] + }, + { + "ref": "7-sniffio@1.3.1", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 7 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_workspace_complex_dependencies() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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 = ["child1", "anyio==3.7.0"] + + [tool.uv.workspace] + members = ["child1", "child2"] + + [tool.uv.sources] + child1 = { workspace = true } + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + let child1 = context.temp_dir.child("child1"); + child1.child("pyproject.toml").write_str( + r#" + [project] + name = "child1" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["child2", "iniconfig>=2"] + + [tool.uv.sources] + child2 = { workspace = true } + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + let child2 = context.temp_dir.child("child2"); + child2.child("pyproject.toml").write_str( + r#" + [project] + name = "child2" + version = "0.2.0" + requires-python = ">=3.12" + dependencies = ["sniffio"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-anyio@3.7.0", + "name": "anyio", + "version": "3.7.0", + "purl": "pkg:pypi/anyio@3.7.0" + }, + { + "type": "application", + "bom-ref": "3-child1@0.1.0", + "name": "child1", + "version": "0.1.0", + "properties": [ + { + "name": "uv:workspace:path", + "value": "child1" + } + ] + }, + { + "type": "application", + "bom-ref": "4-child2@0.2.0", + "name": "child2", + "version": "0.2.0", + "properties": [ + { + "name": "uv:workspace:path", + "value": "child2" + } + ] + }, + { + "type": "library", + "bom-ref": "5-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "6-iniconfig@2.0.0", + "name": "iniconfig", + "version": "2.0.0", + "purl": "pkg:pypi/iniconfig@2.0.0" + }, + { + "type": "library", + "bom-ref": "7-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + } + ], + "dependencies": [ + { + "ref": "2-anyio@3.7.0", + "dependsOn": [ + "5-idna@3.6", + "7-sniffio@1.3.1" + ] + }, + { + "ref": "3-child1@0.1.0", + "dependsOn": [ + "4-child2@0.2.0", + "6-iniconfig@2.0.0" + ] + }, + { + "ref": "4-child2@0.2.0", + "dependsOn": [ + "7-sniffio@1.3.1" + ] + }, + { + "ref": "5-idna@3.6", + "dependsOn": [] + }, + { + "ref": "6-iniconfig@2.0.0", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-anyio@3.7.0", + "3-child1@0.1.0" + ] + }, + { + "ref": "7-sniffio@1.3.1", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 7 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_dependency_marker() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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 ; sys_platform == 'darwin'", "iniconfig"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-anyio@4.3.0", + "name": "anyio", + "version": "4.3.0", + "purl": "pkg:pypi/anyio@4.3.0", + "properties": [ + { + "name": "uv:package:marker", + "value": "sys_platform == 'darwin'" + } + ] + }, + { + "type": "library", + "bom-ref": "3-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6", + "properties": [ + { + "name": "uv:package:marker", + "value": "sys_platform == 'darwin'" + } + ] + }, + { + "type": "library", + "bom-ref": "4-iniconfig@2.0.0", + "name": "iniconfig", + "version": "2.0.0", + "purl": "pkg:pypi/iniconfig@2.0.0" + }, + { + "type": "library", + "bom-ref": "5-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1", + "properties": [ + { + "name": "uv:package:marker", + "value": "sys_platform == 'darwin'" + } + ] + } + ], + "dependencies": [ + { + "ref": "2-anyio@4.3.0", + "dependsOn": [ + "3-idna@3.6", + "5-sniffio@1.3.1" + ] + }, + { + "ref": "3-idna@3.6", + "dependsOn": [] + }, + { + "ref": "4-iniconfig@2.0.0", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-anyio@4.3.0", + "4-iniconfig@2.0.0" + ] + }, + { + "ref": "5-sniffio@1.3.1", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 5 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.10" + dependencies = [ + "trio ; python_version > '3.11'", + "trio ; sys_platform == 'win32'", + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-attrs@23.2.0", + "name": "attrs", + "version": "23.2.0", + "purl": "pkg:pypi/attrs@23.2.0", + "properties": [ + { + "name": "uv:package:marker", + "value": "python_full_version >= '3.12' or sys_platform == 'win32'" + } + ] + }, + { + "type": "library", + "bom-ref": "3-cffi@1.16.0", + "name": "cffi", + "version": "1.16.0", + "purl": "pkg:pypi/cffi@1.16.0", + "properties": [ + { + "name": "uv:package:marker", + "value": "(python_full_version >= '3.12' and implementation_name != 'pypy' and os_name == 'nt') or (implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'win32')" + } + ] + }, + { + "type": "library", + "bom-ref": "4-exceptiongroup@1.2.0", + "name": "exceptiongroup", + "version": "1.2.0", + "purl": "pkg:pypi/exceptiongroup@1.2.0", + "properties": [ + { + "name": "uv:package:marker", + "value": "python_full_version < '3.11' and sys_platform == 'win32'" + } + ] + }, + { + "type": "library", + "bom-ref": "5-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6", + "properties": [ + { + "name": "uv:package:marker", + "value": "python_full_version >= '3.12' or sys_platform == 'win32'" + } + ] + }, + { + "type": "library", + "bom-ref": "6-outcome@1.3.0.post0", + "name": "outcome", + "version": "1.3.0.post0", + "purl": "pkg:pypi/outcome@1.3.0.post0", + "properties": [ + { + "name": "uv:package:marker", + "value": "python_full_version >= '3.12' or sys_platform == 'win32'" + } + ] + }, + { + "type": "library", + "bom-ref": "7-pycparser@2.21", + "name": "pycparser", + "version": "2.21", + "purl": "pkg:pypi/pycparser@2.21", + "properties": [ + { + "name": "uv:package:marker", + "value": "(python_full_version >= '3.12' and implementation_name != 'pypy' and os_name == 'nt') or (implementation_name != 'pypy' and os_name == 'nt' and sys_platform == 'win32')" + } + ] + }, + { + "type": "library", + "bom-ref": "8-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1", + "properties": [ + { + "name": "uv:package:marker", + "value": "python_full_version >= '3.12' or sys_platform == 'win32'" + } + ] + }, + { + "type": "library", + "bom-ref": "9-sortedcontainers@2.4.0", + "name": "sortedcontainers", + "version": "2.4.0", + "purl": "pkg:pypi/sortedcontainers@2.4.0", + "properties": [ + { + "name": "uv:package:marker", + "value": "python_full_version >= '3.12' or sys_platform == 'win32'" + } + ] + }, + { + "type": "library", + "bom-ref": "10-trio@0.25.0", + "name": "trio", + "version": "0.25.0", + "purl": "pkg:pypi/trio@0.25.0", + "properties": [ + { + "name": "uv:package:marker", + "value": "python_full_version >= '3.12' or sys_platform == 'win32'" + } + ] + } + ], + "dependencies": [ + { + "ref": "2-attrs@23.2.0", + "dependsOn": [] + }, + { + "ref": "3-cffi@1.16.0", + "dependsOn": [ + "7-pycparser@2.21" + ] + }, + { + "ref": "4-exceptiongroup@1.2.0", + "dependsOn": [] + }, + { + "ref": "5-idna@3.6", + "dependsOn": [] + }, + { + "ref": "6-outcome@1.3.0.post0", + "dependsOn": [ + "2-attrs@23.2.0" + ] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "10-trio@0.25.0" + ] + }, + { + "ref": "7-pycparser@2.21", + "dependsOn": [] + }, + { + "ref": "8-sniffio@1.3.1", + "dependsOn": [] + }, + { + "ref": "9-sortedcontainers@2.4.0", + "dependsOn": [] + }, + { + "ref": "10-trio@0.25.0", + "dependsOn": [ + "2-attrs@23.2.0", + "3-cffi@1.16.0", + "4-exceptiongroup@1.2.0", + "5-idna@3.6", + "6-outcome@1.3.0.post0", + "8-sniffio@1.3.1", + "9-sortedcontainers@2.4.0" + ] + } + ] + } + ----- stderr ----- + Resolved 10 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_dependency_extra() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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 = ["flask[dotenv]"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-blinker@1.7.0", + "name": "blinker", + "version": "1.7.0", + "purl": "pkg:pypi/blinker@1.7.0" + }, + { + "type": "library", + "bom-ref": "3-click@8.1.7", + "name": "click", + "version": "8.1.7", + "purl": "pkg:pypi/click@8.1.7" + }, + { + "type": "library", + "bom-ref": "4-colorama@0.4.6", + "name": "colorama", + "version": "0.4.6", + "purl": "pkg:pypi/colorama@0.4.6", + "properties": [ + { + "name": "uv:package:marker", + "value": "sys_platform == 'win32'" + } + ] + }, + { + "type": "library", + "bom-ref": "5-flask@3.0.2", + "name": "flask", + "version": "3.0.2", + "purl": "pkg:pypi/flask@3.0.2" + }, + { + "type": "library", + "bom-ref": "6-itsdangerous@2.1.2", + "name": "itsdangerous", + "version": "2.1.2", + "purl": "pkg:pypi/itsdangerous@2.1.2" + }, + { + "type": "library", + "bom-ref": "7-jinja2@3.1.3", + "name": "jinja2", + "version": "3.1.3", + "purl": "pkg:pypi/jinja2@3.1.3" + }, + { + "type": "library", + "bom-ref": "8-markupsafe@2.1.5", + "name": "markupsafe", + "version": "2.1.5", + "purl": "pkg:pypi/markupsafe@2.1.5" + }, + { + "type": "library", + "bom-ref": "9-python-dotenv@1.0.1", + "name": "python-dotenv", + "version": "1.0.1", + "purl": "pkg:pypi/python-dotenv@1.0.1" + }, + { + "type": "library", + "bom-ref": "10-werkzeug@3.0.1", + "name": "werkzeug", + "version": "3.0.1", + "purl": "pkg:pypi/werkzeug@3.0.1" + } + ], + "dependencies": [ + { + "ref": "2-blinker@1.7.0", + "dependsOn": [] + }, + { + "ref": "3-click@8.1.7", + "dependsOn": [ + "4-colorama@0.4.6" + ] + }, + { + "ref": "4-colorama@0.4.6", + "dependsOn": [] + }, + { + "ref": "5-flask@3.0.2", + "dependsOn": [ + "10-werkzeug@3.0.1", + "2-blinker@1.7.0", + "3-click@8.1.7", + "6-itsdangerous@2.1.2", + "7-jinja2@3.1.3", + "9-python-dotenv@1.0.1" + ] + }, + { + "ref": "6-itsdangerous@2.1.2", + "dependsOn": [] + }, + { + "ref": "7-jinja2@3.1.3", + "dependsOn": [ + "8-markupsafe@2.1.5" + ] + }, + { + "ref": "8-markupsafe@2.1.5", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "5-flask@3.0.2" + ] + }, + { + "ref": "9-python-dotenv@1.0.1", + "dependsOn": [] + }, + { + "ref": "10-werkzeug@3.0.1", + "dependsOn": [ + "8-markupsafe@2.1.5" + ] + } + ] + } + ----- stderr ----- + Resolved 10 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_prune() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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 = [ + "jupyter-client" + ] + "#, + )?; + + context.lock().assert().success(); + + // project v0.1.0 + // └── jupyter-client v8.6.1 + // ├── jupyter-core v5.7.2 + // │ ├── platformdirs v4.2.0 + // │ └── traitlets v5.14.2 + // ├── python-dateutil v2.9.0.post0 + // │ └── six v1.16.0 + // ├── pyzmq v25.1.2 + // ├── tornado v6.4 + // └── traitlets v5.14.2 + + uv_snapshot!( + context.filters(), + context.export() + .arg("--format") + .arg("cyclonedx1.5") + .arg("--prune") + .arg("jupyter-core"), + @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-cffi@1.16.0", + "name": "cffi", + "version": "1.16.0", + "purl": "pkg:pypi/cffi@1.16.0", + "properties": [ + { + "name": "uv:package:marker", + "value": "implementation_name == 'pypy'" + } + ] + }, + { + "type": "library", + "bom-ref": "3-jupyter-client@8.6.1", + "name": "jupyter-client", + "version": "8.6.1", + "purl": "pkg:pypi/jupyter-client@8.6.1" + }, + { + "type": "library", + "bom-ref": "4-pycparser@2.21", + "name": "pycparser", + "version": "2.21", + "purl": "pkg:pypi/pycparser@2.21", + "properties": [ + { + "name": "uv:package:marker", + "value": "implementation_name == 'pypy'" + } + ] + }, + { + "type": "library", + "bom-ref": "5-python-dateutil@2.9.0.post0", + "name": "python-dateutil", + "version": "2.9.0.post0", + "purl": "pkg:pypi/python-dateutil@2.9.0.post0" + }, + { + "type": "library", + "bom-ref": "6-pyzmq@25.1.2", + "name": "pyzmq", + "version": "25.1.2", + "purl": "pkg:pypi/pyzmq@25.1.2" + }, + { + "type": "library", + "bom-ref": "7-six@1.16.0", + "name": "six", + "version": "1.16.0", + "purl": "pkg:pypi/six@1.16.0" + }, + { + "type": "library", + "bom-ref": "8-tornado@6.4", + "name": "tornado", + "version": "6.4", + "purl": "pkg:pypi/tornado@6.4" + }, + { + "type": "library", + "bom-ref": "9-traitlets@5.14.2", + "name": "traitlets", + "version": "5.14.2", + "purl": "pkg:pypi/traitlets@5.14.2" + } + ], + "dependencies": [ + { + "ref": "2-cffi@1.16.0", + "dependsOn": [ + "4-pycparser@2.21" + ] + }, + { + "ref": "3-jupyter-client@8.6.1", + "dependsOn": [ + "5-python-dateutil@2.9.0.post0", + "6-pyzmq@25.1.2", + "8-tornado@6.4", + "9-traitlets@5.14.2" + ] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "3-jupyter-client@8.6.1" + ] + }, + { + "ref": "4-pycparser@2.21", + "dependsOn": [] + }, + { + "ref": "5-python-dateutil@2.9.0.post0", + "dependsOn": [ + "7-six@1.16.0" + ] + }, + { + "ref": "6-pyzmq@25.1.2", + "dependsOn": [ + "2-cffi@1.16.0" + ] + }, + { + "ref": "7-six@1.16.0", + "dependsOn": [] + }, + { + "ref": "8-tornado@6.4", + "dependsOn": [] + }, + { + "ref": "9-traitlets@5.14.2", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 12 packages in [TIME] + "# + ); + + Ok(()) +} + +#[test] +fn cyclonedx_export_group() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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 = ["typing-extensions"] + + [dependency-groups] + foo = ["anyio ; sys_platform == 'darwin'"] + bar = ["iniconfig"] + dev = ["sniffio"] + "#, + )?; + + context.lock().assert().success(); + + // Default exports include dev group + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + }, + { + "type": "library", + "bom-ref": "3-typing-extensions@4.10.0", + "name": "typing-extensions", + "version": "4.10.0", + "purl": "pkg:pypi/typing-extensions@4.10.0" + } + ], + "dependencies": [ + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-sniffio@1.3.1", + "3-typing-extensions@4.10.0" + ] + }, + { + "ref": "2-sniffio@1.3.1", + "dependsOn": [] + }, + { + "ref": "3-typing-extensions@4.10.0", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 6 packages in [TIME] + "#); + + // Export only specific group + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--only-group").arg("bar"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-iniconfig@2.0.0", + "name": "iniconfig", + "version": "2.0.0", + "purl": "pkg:pypi/iniconfig@2.0.0" + } + ], + "dependencies": [ + { + "ref": "2-iniconfig@2.0.0", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 6 packages in [TIME] + "#); + + // Export with additional group + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--group").arg("foo"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-anyio@4.3.0", + "name": "anyio", + "version": "4.3.0", + "purl": "pkg:pypi/anyio@4.3.0", + "properties": [ + { + "name": "uv:package:marker", + "value": "sys_platform == 'darwin'" + } + ] + }, + { + "type": "library", + "bom-ref": "3-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6", + "properties": [ + { + "name": "uv:package:marker", + "value": "sys_platform == 'darwin'" + } + ] + }, + { + "type": "library", + "bom-ref": "4-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + }, + { + "type": "library", + "bom-ref": "5-typing-extensions@4.10.0", + "name": "typing-extensions", + "version": "4.10.0", + "purl": "pkg:pypi/typing-extensions@4.10.0" + } + ], + "dependencies": [ + { + "ref": "2-anyio@4.3.0", + "dependsOn": [ + "3-idna@3.6", + "4-sniffio@1.3.1" + ] + }, + { + "ref": "3-idna@3.6", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-anyio@4.3.0", + "4-sniffio@1.3.1", + "5-typing-extensions@4.10.0" + ] + }, + { + "ref": "4-sniffio@1.3.1", + "dependsOn": [] + }, + { + "ref": "5-typing-extensions@4.10.0", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 6 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_non_project() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [tool.uv.workspace] + members = [] + + [dependency-groups] + async = ["anyio"] + "#, + )?; + + context.lock().assert().success(); + + // Default export with no project section + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ] + }, + "components": [], + "dependencies": [] + } + ----- stderr ----- + warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`. + Resolved 3 packages in [TIME] + "#); + + // Export with group specified + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--group").arg("async"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "1-anyio@4.3.0", + "name": "anyio", + "version": "4.3.0", + "purl": "pkg:pypi/anyio@4.3.0" + }, + { + "type": "library", + "bom-ref": "2-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "3-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + } + ], + "dependencies": [ + { + "ref": "1-anyio@4.3.0", + "dependsOn": [ + "2-idna@3.6", + "3-sniffio@1.3.1" + ] + }, + { + "ref": "2-idna@3.6", + "dependsOn": [] + }, + { + "ref": "3-sniffio@1.3.1", + "dependsOn": [] + } + ] + } + ----- stderr ----- + warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`. + Resolved 3 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_no_emit() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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(); + + // Exclude `anyio`. + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--no-emit-package").arg("anyio"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "application", + "bom-ref": "2-child@0.1.0", + "name": "child", + "version": "0.1.0", + "properties": [ + { + "name": "uv:workspace:path", + "value": "child" + } + ] + }, + { + "type": "library", + "bom-ref": "3-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "4-iniconfig@2.0.0", + "name": "iniconfig", + "version": "2.0.0", + "purl": "pkg:pypi/iniconfig@2.0.0" + }, + { + "type": "library", + "bom-ref": "5-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + } + ], + "dependencies": [ + { + "ref": "2-child@0.1.0", + "dependsOn": [ + "4-iniconfig@2.0.0" + ] + }, + { + "ref": "3-idna@3.6", + "dependsOn": [] + }, + { + "ref": "4-iniconfig@2.0.0", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-child@0.1.0" + ] + }, + { + "ref": "5-sniffio@1.3.1", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 6 packages in [TIME] + "#); + + // Exclude `project`. + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--no-emit-project"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-anyio@3.7.0", + "name": "anyio", + "version": "3.7.0", + "purl": "pkg:pypi/anyio@3.7.0" + }, + { + "type": "application", + "bom-ref": "3-child@0.1.0", + "name": "child", + "version": "0.1.0", + "properties": [ + { + "name": "uv:workspace:path", + "value": "child" + } + ] + }, + { + "type": "library", + "bom-ref": "4-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "5-iniconfig@2.0.0", + "name": "iniconfig", + "version": "2.0.0", + "purl": "pkg:pypi/iniconfig@2.0.0" + }, + { + "type": "library", + "bom-ref": "6-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + } + ], + "dependencies": [ + { + "ref": "2-anyio@3.7.0", + "dependsOn": [ + "4-idna@3.6", + "6-sniffio@1.3.1" + ] + }, + { + "ref": "3-child@0.1.0", + "dependsOn": [ + "5-iniconfig@2.0.0" + ] + }, + { + "ref": "4-idna@3.6", + "dependsOn": [] + }, + { + "ref": "5-iniconfig@2.0.0", + "dependsOn": [] + }, + { + "ref": "6-sniffio@1.3.1", + "dependsOn": [] + } + ] + } + ----- stderr ----- + Resolved 6 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_relative_path() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + let dependency = context.temp_dir.child("dependency"); + dependency.child("pyproject.toml").write_str( + r#" + [project] + name = "dependency" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig>=2"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + let project = context.temp_dir.child("project"); + project.child("pyproject.toml").write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["dependency"] + + [tool.uv.sources] + dependency = { path = "../dependency" } + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().current_dir(&project).assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").current_dir(&project), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-dependency@0.1.0", + "name": "dependency", + "version": "0.1.0" + }, + { + "type": "library", + "bom-ref": "3-iniconfig@2.0.0", + "name": "iniconfig", + "version": "2.0.0", + "purl": "pkg:pypi/iniconfig@2.0.0" + } + ], + "dependencies": [ + { + "ref": "2-dependency@0.1.0", + "dependsOn": [ + "3-iniconfig@2.0.0" + ] + }, + { + "ref": "3-iniconfig@2.0.0", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-dependency@0.1.0" + ] + } + ] + } + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 3 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_cyclic_dependencies() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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 = [ + "testtools==2.3.0", + "fixtures==3.0.0", + ] + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-argparse@1.4.0", + "name": "argparse", + "version": "1.4.0", + "purl": "pkg:pypi/argparse@1.4.0" + }, + { + "type": "library", + "bom-ref": "3-extras@1.0.0", + "name": "extras", + "version": "1.0.0", + "purl": "pkg:pypi/extras@1.0.0" + }, + { + "type": "library", + "bom-ref": "4-fixtures@3.0.0", + "name": "fixtures", + "version": "3.0.0", + "purl": "pkg:pypi/fixtures@3.0.0" + }, + { + "type": "library", + "bom-ref": "5-linecache2@1.0.0", + "name": "linecache2", + "version": "1.0.0", + "purl": "pkg:pypi/linecache2@1.0.0" + }, + { + "type": "library", + "bom-ref": "6-pbr@6.0.0", + "name": "pbr", + "version": "6.0.0", + "purl": "pkg:pypi/pbr@6.0.0" + }, + { + "type": "library", + "bom-ref": "7-python-mimeparse@1.6.0", + "name": "python-mimeparse", + "version": "1.6.0", + "purl": "pkg:pypi/python-mimeparse@1.6.0" + }, + { + "type": "library", + "bom-ref": "8-six@1.16.0", + "name": "six", + "version": "1.16.0", + "purl": "pkg:pypi/six@1.16.0" + }, + { + "type": "library", + "bom-ref": "9-testtools@2.3.0", + "name": "testtools", + "version": "2.3.0", + "purl": "pkg:pypi/testtools@2.3.0" + }, + { + "type": "library", + "bom-ref": "10-traceback2@1.4.0", + "name": "traceback2", + "version": "1.4.0", + "purl": "pkg:pypi/traceback2@1.4.0" + }, + { + "type": "library", + "bom-ref": "11-unittest2@1.1.0", + "name": "unittest2", + "version": "1.1.0", + "purl": "pkg:pypi/unittest2@1.1.0" + } + ], + "dependencies": [ + { + "ref": "2-argparse@1.4.0", + "dependsOn": [] + }, + { + "ref": "3-extras@1.0.0", + "dependsOn": [] + }, + { + "ref": "4-fixtures@3.0.0", + "dependsOn": [ + "6-pbr@6.0.0", + "8-six@1.16.0", + "9-testtools@2.3.0" + ] + }, + { + "ref": "5-linecache2@1.0.0", + "dependsOn": [] + }, + { + "ref": "6-pbr@6.0.0", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "4-fixtures@3.0.0", + "9-testtools@2.3.0" + ] + }, + { + "ref": "7-python-mimeparse@1.6.0", + "dependsOn": [] + }, + { + "ref": "8-six@1.16.0", + "dependsOn": [] + }, + { + "ref": "9-testtools@2.3.0", + "dependsOn": [ + "10-traceback2@1.4.0", + "11-unittest2@1.1.0", + "3-extras@1.0.0", + "4-fixtures@3.0.0", + "6-pbr@6.0.0", + "7-python-mimeparse@1.6.0", + "8-six@1.16.0" + ] + }, + { + "ref": "10-traceback2@1.4.0", + "dependsOn": [ + "5-linecache2@1.0.0" + ] + }, + { + "ref": "11-unittest2@1.1.0", + "dependsOn": [ + "10-traceback2@1.4.0", + "2-argparse@1.4.0", + "8-six@1.16.0" + ] + } + ] + } + ----- stderr ----- + Resolved 11 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_dev_dependencies() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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 = ["typing-extensions"] + + [tool.uv] + dev-dependencies = ["anyio"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + // Default export includes dev dependencies + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-anyio@4.3.0", + "name": "anyio", + "version": "4.3.0", + "purl": "pkg:pypi/anyio@4.3.0" + }, + { + "type": "library", + "bom-ref": "3-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "4-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + }, + { + "type": "library", + "bom-ref": "5-typing-extensions@4.10.0", + "name": "typing-extensions", + "version": "4.10.0", + "purl": "pkg:pypi/typing-extensions@4.10.0" + } + ], + "dependencies": [ + { + "ref": "2-anyio@4.3.0", + "dependsOn": [ + "3-idna@3.6", + "4-sniffio@1.3.1" + ] + }, + { + "ref": "3-idna@3.6", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-anyio@4.3.0", + "5-typing-extensions@4.10.0" + ] + }, + { + "ref": "4-sniffio@1.3.1", + "dependsOn": [] + }, + { + "ref": "5-typing-extensions@4.10.0", + "dependsOn": [] + } + ] + } + ----- stderr ----- + warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead + Resolved 5 packages in [TIME] + "#); + + // Export without dev dependencies + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--no-dev"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-typing-extensions@4.10.0", + "name": "typing-extensions", + "version": "4.10.0", + "purl": "pkg:pypi/typing-extensions@4.10.0" + } + ], + "dependencies": [ + { + "ref": "1-project@0.1.0", + "dependsOn": [ + "2-typing-extensions@4.10.0" + ] + }, + { + "ref": "2-typing-extensions@4.10.0", + "dependsOn": [] + } + ] + } + ----- stderr ----- + warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead + Resolved 5 packages in [TIME] + "#); + + // Export only dev dependencies + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--only-dev"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "library", + "bom-ref": "2-anyio@4.3.0", + "name": "anyio", + "version": "4.3.0", + "purl": "pkg:pypi/anyio@4.3.0" + }, + { + "type": "library", + "bom-ref": "3-idna@3.6", + "name": "idna", + "version": "3.6", + "purl": "pkg:pypi/idna@3.6" + }, + { + "type": "library", + "bom-ref": "4-sniffio@1.3.1", + "name": "sniffio", + "version": "1.3.1", + "purl": "pkg:pypi/sniffio@1.3.1" + } + ], + "dependencies": [ + { + "ref": "2-anyio@4.3.0", + "dependsOn": [ + "3-idna@3.6", + "4-sniffio@1.3.1" + ] + }, + { + "ref": "3-idna@3.6", + "dependsOn": [] + }, + { + "ref": "4-sniffio@1.3.1", + "dependsOn": [] + } + ] + } + ----- stderr ----- + warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead + Resolved 5 packages in [TIME] + "#); + + Ok(()) +} + +#[test] +fn cyclonedx_export_all_packages_conflicting_workspace_members() -> Result<()> { + let context = TestContext::new("3.12").with_cyclonedx_filters(); + + 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 = ["sortedcontainers==2.3.0"] + + [tool.uv.workspace] + members = ["child"] + + [tool.uv] + conflicts = [ + [ + { package = "project" }, + { package = "child" }, + ], + ] + + [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 = ["sortedcontainers==2.4.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + // Export with --all-packages to CycloneDX format should succeed as conflict detection is skipped + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--all-packages"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "[SERIAL_NUMBER]", + "metadata": { + "timestamp": "[TIMESTAMP]", + "tools": [ + { + "vendor": "Astral Software Inc.", + "name": "uv", + "version": "[VERSION]" + } + ], + "component": { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" + } + }, + "components": [ + { + "type": "application", + "bom-ref": "2-child@0.1.0", + "name": "child", + "version": "0.1.0", + "properties": [ + { + "name": "uv:workspace:path", + "value": "child" + } + ] + } + ], + "dependencies": [ + { + "ref": "2-child@0.1.0", + "dependsOn": [] + }, + { + "ref": "1-project@0.1.0", + "dependsOn": [] + } + ] + } + ----- stderr ----- + warning: Declaring conflicts for packages (`package = ...`) is experimental and may change without warning. Pass `--preview-features package-conflicts` to disable this warning. + Resolved 4 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. + "#); + + // Should fail when exporting to `requirements.txt` or `pylock.toml`as conflict detection is enabled for these formats + uv_snapshot!(context.filters(), context.export().arg("--format").arg("requirements-txt").arg("--all-packages"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: Declaring conflicts for packages (`package = ...`) is experimental and may change without warning. Pass `--preview-features package-conflicts` to disable this warning. + Resolved 4 packages in [TIME] + error: Package `child` and package `project` are incompatible with the declared conflicts: {child, project} + "); + + uv_snapshot!(context.filters(), context.export().arg("--format").arg("pylock.toml").arg("--all-packages"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: Declaring conflicts for packages (`package = ...`) is experimental and may change without warning. Pass `--preview-features package-conflicts` to disable this warning. + Resolved 4 packages in [TIME] + error: Package `child` and package `project` are incompatible with the declared conflicts: {child, project} + "); + Ok(()) +} From 45db004e19286ee17f2e1b2f45e2ffa539bbc66c Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Wed, 24 Sep 2025 17:00:58 +0100 Subject: [PATCH 10/36] Update docs --- docs/reference/cli.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 11ef15ebeae7f..4e244ecacc051 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1845,12 +1845,13 @@ uv export [OPTIONS]
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • --format format

    The format to which uv.lock should be exported.

    -

    Supports both requirements.txt and pylock.toml (PEP 751) output formats.

    +

    Supports requirements.txt, pylock.toml (PEP 751) and CycloneDX v1.5 JSON output formats.

    uv will infer the output format from the file extension of the output file, if provided. Otherwise, defaults to requirements.txt.

    Possible values:

    • requirements.txt: Export in requirements.txt format
    • pylock.toml: Export in pylock.toml format
    • +
    • cyclonedx1.5: Export in CycloneDX v1.5 JSON format
    --frozen

    Do not update the uv.lock before exporting.

    If a uv.lock does not exist, uv will exit with an error.

    May also be set with the UV_FROZEN environment variable.

    --group group

    Include dependencies from the specified dependency group.

    From 1f2ba66bd8ce7b950a4345bb8a2356e68dce667e Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Wed, 15 Oct 2025 16:15:28 +0100 Subject: [PATCH 11/36] Add workspace information --- .../src/lock/export/cyclonedx_json.rs | 97 ++++++++++++++++--- 1 file changed, 82 insertions(+), 15 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 16ebfe3a504c5..a1d135cd80fcc 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -1,16 +1,24 @@ use std::collections::HashMap; +use std::path::Path; -use cyclonedx_bom::models::component::Classification; -use cyclonedx_bom::models::dependency::{Dependencies, Dependency}; -use cyclonedx_bom::models::metadata::Metadata; -use cyclonedx_bom::models::tool::{Tool, Tools}; -use cyclonedx_bom::prelude::{Bom, Component, Components, NormalizedString}; +use cyclonedx_bom::{ + models::{ + component::Classification, + dependency::{Dependencies, Dependency}, + metadata::Metadata, + property::{Properties, Property}, + tool::{Tool, Tools}, + }, + prelude::{Bom, Component, Components, NormalizedString}, +}; use itertools::Itertools; use percent_encoding::{AsciiSet, CONTROLS, percent_encode}; +use rustc_hash::FxHashSet; use uv_configuration::{ DependencyGroupsWithDefaults, ExtrasSpecificationWithDefaults, InstallOptions, }; +use uv_fs::PortablePath; use uv_normalize::PackageName; use crate::lock::export::{ExportableRequirement, ExportableRequirements}; @@ -53,7 +61,17 @@ pub fn from_lock<'lock>( nodes.sort_unstable_by_key(|node| &node.package.id); - let root = target.lock().root(); + // CycloneDX requires exactly one root component in `metadata.component`. + let root = match target.roots().collect::>().as_slice() { + // Single root: use it directly + [single_root] => nodes + .iter() + .find(|node| &node.package.id.name == *single_root) + .map(|node| node.package), + // Multiple roots or no roots: use fallback + _ => None, + } + .or_else(|| target.lock().root()); // Fallback to project root // Used as prefix in bom-ref generation, to ensure uniqueness let mut id_counter = 1; @@ -63,7 +81,7 @@ pub fn from_lock<'lock>( component: root.map(|package| { create_and_register_component( package, - Classification::Application, + PackageType::Root, &mut id_counter, &mut package_to_bom_ref, ) @@ -79,15 +97,44 @@ pub fn from_lock<'lock>( ..Metadata::default() }; - let dependencies = nodes + let workspace_member_ids = nodes .iter() - .filter(|node| root.is_none_or(|package| package.id != node.package.id)); + .filter_map(|node| { + if target.lock().members().contains(&node.package.id.name) + && node.package.id.source.is_local() + { + Some(&node.package.id) + } else { + None + } + }) + .collect::>(); - let components = dependencies + let components = nodes + .iter() + .filter(|node| root.is_none_or(|root_pkg| root_pkg.id != node.package.id)) // Filter out root package as this is included in `metadata` .map(|node| { + let package_type = if workspace_member_ids.contains(&node.package.id) { + let path = match &node.package.id.source { + Source::Path(path) + | Source::Directory(path) + | Source::Editable(path) + | Source::Virtual(path) => path, + Source::Registry(_) | Source::Git(_, _) | Source::Direct(_, _) => { + // Workspace packages are always local dependencies + unreachable!( + "Workspace member {:?} has non-local source {:?}", + node.package.id.name, node.package.id.source, + ) + } + }; + PackageType::Workspace(path) + } else { + PackageType::Dependency + }; create_and_register_component( node.package, - Classification::Library, + package_type, &mut id_counter, &mut package_to_bom_ref, ) @@ -106,14 +153,21 @@ pub fn from_lock<'lock>( Ok(bom) } +#[derive(Clone, Debug, Eq, PartialEq)] +enum PackageType<'a> { + Root, + Workspace(&'a Path), + Dependency, +} + /// Create and register a `CycloneDX` component, updating the counter and map. fn create_and_register_component<'a>( package: &'a Package, - classification: Classification, + package_type: PackageType, id_counter: &mut usize, package_to_bom_ref: &mut HashMap<&'a PackageId, Component>, ) -> Component { - let component = create_component_from_package(package, classification, *id_counter); + let component = create_component_from_package(package, package_type, *id_counter); package_to_bom_ref.insert(&package.id, component.clone()); *id_counter += 1; component @@ -183,9 +237,10 @@ fn format_qualifiers(qualifiers: &[(&str, &str)]) -> String { } /// Create a `CycloneDX` component from a package node with the given classification and ID. +#[allow(clippy::needless_pass_by_value)] fn create_component_from_package( package: &Package, - classification: Classification, + package_type: PackageType, id: usize, ) -> Component { let name = get_package_name(package); @@ -193,6 +248,18 @@ fn create_component_from_package( let bom_ref = create_bom_ref(id, name, version.as_deref()); let purl = create_purl(package).and_then(|purl_string| purl_string.parse().ok()); + let (classification, properties) = match package_type { + PackageType::Root => (Classification::Application, None), + PackageType::Workspace(path) => { + let properties = vec![ + Property::new("uv:workspace", "true"), + Property::new("uv:workspace_path", &PortablePath::from(path).to_string()), + ]; + (Classification::Application, Some(Properties(properties))) + } + PackageType::Dependency => (Classification::Library, None), + }; + Component { component_type: classification, name: NormalizedString::new(name), @@ -214,7 +281,7 @@ fn create_component_from_package( modified: None, pedigree: None, external_references: None, - properties: None, + properties, components: None, evidence: None, signature: None, From 3c4a00d0327c68469f5c87602d16db4508e90dbd Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Wed, 24 Sep 2025 17:07:43 +0100 Subject: [PATCH 12/36] Fix linting errors --- crates/uv-cli/src/lib.rs | 2 +- crates/uv-configuration/src/export_format.rs | 2 +- crates/uv-resolver/src/lock/export/cyclonedx_json.rs | 10 +++++++--- crates/uv/tests/it/common/mod.rs | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 0363af2b44a5f..ef03af373c671 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -4303,7 +4303,7 @@ pub struct TreeArgs { pub struct ExportArgs { /// The format to which `uv.lock` should be exported. /// - /// Supports `requirements.txt`, `pylock.toml` (PEP 751) and CycloneDX v1.5 JSON output formats. + /// Supports `requirements.txt`, `pylock.toml` (PEP 751) and `CycloneDX` v1.5 JSON output formats. /// /// uv will infer the output format from the file extension of the output file, if /// provided. Otherwise, defaults to `requirements.txt`. diff --git a/crates/uv-configuration/src/export_format.rs b/crates/uv-configuration/src/export_format.rs index e4b0016b8fc2b..93ca921e90bea 100644 --- a/crates/uv-configuration/src/export_format.rs +++ b/crates/uv-configuration/src/export_format.rs @@ -15,7 +15,7 @@ pub enum ExportFormat { #[serde(rename = "pylock.toml", alias = "pylock-toml")] #[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))] PylockToml, - /// Export in CycloneDX v1.5 JSON format. + /// Export in `CycloneDX` v1.5 JSON format. #[serde(rename = "cyclonedx1.5")] #[cfg_attr( feature = "clap", diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 5bed43e83f964..3ce52e16afcee 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -83,7 +83,7 @@ pub fn from_lock<'lock>( Ok(bom) } -/// Create and register a CycloneDX component, updating the counter and map +/// Create and register a `CycloneDX` component, updating the counter and map fn create_and_register_component<'a>( package: &'a Package, classification: Classification, @@ -107,7 +107,11 @@ fn create_bom_ref(id: usize, name: &str, version: Option<&str>) -> String { /// Extract version string from a package fn get_version_string(package: &Package) -> Option { - package.id.version.as_ref().map(|v| v.to_string()) + package + .id + .version + .as_ref() + .map(std::string::ToString::to_string) } /// Extract package name string from a package @@ -138,7 +142,7 @@ fn create_purl(package: &Package) -> Option { )) } -/// Create a CycloneDX component from a package node with the given classification and ID +/// Create a `CycloneDX` component from a package node with the given classification and ID fn create_component_from_package( package: &Package, classification: Classification, diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 9b38eadba2116..6cba11a66321c 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -477,7 +477,7 @@ impl TestContext { self } - /// Adds filters for non-deterministic CycloneDX data + /// Adds filters for non-deterministic `CycloneDX` data pub fn with_cyclonedx_filters(mut self) -> Self { self.filters.push(( r"urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}".to_string(), From faff7781b81e981fe4f8c8a0f97a6ac85b8479c4 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Thu, 16 Oct 2025 13:18:03 +0100 Subject: [PATCH 13/36] Update component lookup --- .../src/lock/export/cyclonedx_json.rs | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index a1d135cd80fcc..c3fd45df05738 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -75,7 +75,7 @@ pub fn from_lock<'lock>( // Used as prefix in bom-ref generation, to ensure uniqueness let mut id_counter = 1; - let mut package_to_bom_ref = HashMap::<&PackageId, Component>::new(); + let mut package_to_component_map = HashMap::<&PackageId, Component>::new(); let metadata = Metadata { component: root.map(|package| { @@ -83,7 +83,7 @@ pub fn from_lock<'lock>( package, PackageType::Root, &mut id_counter, - &mut package_to_bom_ref, + &mut package_to_component_map, ) }), timestamp: cyclonedx_bom::prelude::DateTime::now().ok(), @@ -136,12 +136,12 @@ pub fn from_lock<'lock>( node.package, package_type, &mut id_counter, - &mut package_to_bom_ref, + &mut package_to_component_map, ) }) .collect(); - let dependencies = create_dependencies_from_mapping(&nodes, &package_to_bom_ref); + let dependencies = create_dependencies_from_mapping(&nodes, &package_to_component_map); let bom = Bom { metadata: Some(metadata), @@ -292,36 +292,36 @@ fn create_component_from_package( fn create_dependencies_from_mapping( nodes: &[ExportableRequirement<'_>], - package_to_component: &HashMap<&PackageId, Component>, + package_to_component_map: &HashMap<&PackageId, Component>, ) -> Dependencies { - let dependencies = nodes.iter().filter_map(|node| { - package_to_component + let dependencies = nodes.iter().map(|node| { + let package_bom_ref = package_to_component_map .get(&node.package.id) - .map(|package_bom_ref| { - let immediate_deps = &node.package.dependencies; - let optional_deps = node.package.optional_dependencies.values().flatten(); - let dep_groups = node.package.dependency_groups.values().flatten(); - - let package_deps = immediate_deps - .iter() - .chain(optional_deps) - .chain(dep_groups) - .filter_map(|dep| package_to_component.get(&dep.package_id)); - - let bom_refs = package_deps - .map(|p| p.bom_ref.clone().expect("bom-ref should always exist")) - .sorted_unstable() - .unique() - .collect(); - - Dependency { - dependency_ref: package_bom_ref - .bom_ref - .clone() - .expect("bom-ref should always exist"), - dependencies: bom_refs, - } - }) + .expect("All nodes should have been added to package_to_bom_ref"); + + let immediate_deps = &node.package.dependencies; + let optional_deps = node.package.optional_dependencies.values().flatten(); + let dep_groups = node.package.dependency_groups.values().flatten(); + + let package_deps = immediate_deps + .iter() + .chain(optional_deps) + .chain(dep_groups) + .filter_map(|dep| package_to_component_map.get(&dep.package_id)); + + let bom_refs = package_deps + .map(|p| p.bom_ref.clone().expect("bom-ref should always exist")) + .sorted_unstable() + .unique() + .collect(); + + Dependency { + dependency_ref: package_bom_ref + .bom_ref + .clone() + .expect("bom-ref should always exist"), + dependencies: bom_refs, + } }); Dependencies(dependencies.collect()) } From 5abe29376b957e3a4eeb6e6a1b5153c9ea931074 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Thu, 25 Sep 2025 10:20:54 +0100 Subject: [PATCH 14/36] Move PipCompileFormat into export_format.rs --- crates/uv-configuration/src/export_format.rs | 19 +++++++++++++++++++ crates/uv-configuration/src/lib.rs | 2 -- .../src/pip_compile_format.rs | 18 ------------------ 3 files changed, 19 insertions(+), 20 deletions(-) delete mode 100644 crates/uv-configuration/src/pip_compile_format.rs diff --git a/crates/uv-configuration/src/export_format.rs b/crates/uv-configuration/src/export_format.rs index 93ca921e90bea..af04af8d9d61b 100644 --- a/crates/uv-configuration/src/export_format.rs +++ b/crates/uv-configuration/src/export_format.rs @@ -23,3 +23,22 @@ pub enum ExportFormat { )] CycloneDX1_5, } + +/// The format to use when compiling to a `requirements.txt` or `pylock.toml` file. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] +pub enum PipCompileFormat { + /// Export in `requirements.txt` format. + #[default] + #[serde(rename = "requirements.txt", alias = "requirements-txt")] + #[cfg_attr( + feature = "clap", + clap(name = "requirements.txt", alias = "requirements-txt") + )] + RequirementsTxt, + /// Export in `pylock.toml` format. + #[serde(rename = "pylock.toml", alias = "pylock-toml")] + #[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))] + PylockToml, +} diff --git a/crates/uv-configuration/src/lib.rs b/crates/uv-configuration/src/lib.rs index 202867cc70c5e..931634812bbb5 100644 --- a/crates/uv-configuration/src/lib.rs +++ b/crates/uv-configuration/src/lib.rs @@ -14,7 +14,6 @@ pub use install_options::*; pub use name_specifiers::*; pub use overrides::*; pub use package_options::*; -pub use pip_compile_format::*; pub use project_build_backend::*; pub use required_version::*; pub use sources::*; @@ -40,7 +39,6 @@ mod install_options; mod name_specifiers; mod overrides; mod package_options; -mod pip_compile_format; mod project_build_backend; mod required_version; mod sources; diff --git a/crates/uv-configuration/src/pip_compile_format.rs b/crates/uv-configuration/src/pip_compile_format.rs deleted file mode 100644 index 16d5dc55e7787..0000000000000 --- a/crates/uv-configuration/src/pip_compile_format.rs +++ /dev/null @@ -1,18 +0,0 @@ -/// The format to use when compiling to a `requirements.txt` or `pylock.toml` file -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields, rename_all = "kebab-case")] -#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] -pub enum PipCompileFormat { - /// Export in `requirements.txt` format. - #[default] - #[serde(rename = "requirements.txt", alias = "requirements-txt")] - #[cfg_attr( - feature = "clap", - clap(name = "requirements.txt", alias = "requirements-txt") - )] - RequirementsTxt, - /// Export in `pylock.toml` format. - #[serde(rename = "pylock.toml", alias = "pylock-toml")] - #[cfg_attr(feature = "clap", clap(name = "pylock.toml", alias = "pylock-toml"))] - PylockToml, -} From fc0bc1bcb48b9ec12942b6db7bb1edb5554c57d0 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Thu, 16 Oct 2025 14:29:14 +0100 Subject: [PATCH 15/36] Add markers --- .../src/lock/export/cyclonedx_json.rs | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index c3fd45df05738..87f4907489683 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -20,6 +20,7 @@ use uv_configuration::{ }; use uv_fs::PortablePath; use uv_normalize::PackageName; +use uv_pep508::MarkerTree; use crate::lock::export::{ExportableRequirement, ExportableRequirements}; use crate::lock::{Package, PackageId, Source}; @@ -82,6 +83,7 @@ pub fn from_lock<'lock>( create_and_register_component( package, PackageType::Root, + None, &mut id_counter, &mut package_to_component_map, ) @@ -135,6 +137,7 @@ pub fn from_lock<'lock>( create_and_register_component( node.package, package_type, + Some(&node.marker), &mut id_counter, &mut package_to_component_map, ) @@ -164,10 +167,11 @@ enum PackageType<'a> { fn create_and_register_component<'a>( package: &'a Package, package_type: PackageType, + marker: Option<&MarkerTree>, id_counter: &mut usize, package_to_bom_ref: &mut HashMap<&'a PackageId, Component>, ) -> Component { - let component = create_component_from_package(package, package_type, *id_counter); + let component = create_component_from_package(package, package_type, marker, *id_counter); package_to_bom_ref.insert(&package.id, component.clone()); *id_counter += 1; component @@ -241,25 +245,35 @@ fn format_qualifiers(qualifiers: &[(&str, &str)]) -> String { fn create_component_from_package( package: &Package, package_type: PackageType, + marker: Option<&MarkerTree>, id: usize, ) -> Component { let name = get_package_name(package); let version = get_version_string(package); let bom_ref = create_bom_ref(id, name, version.as_deref()); let purl = create_purl(package).and_then(|purl_string| purl_string.parse().ok()); + let mut properties = vec![]; - let (classification, properties) = match package_type { - PackageType::Root => (Classification::Application, None), + let classification = match package_type { + PackageType::Root => Classification::Application, PackageType::Workspace(path) => { - let properties = vec![ - Property::new("uv:workspace", "true"), - Property::new("uv:workspace_path", &PortablePath::from(path).to_string()), - ]; - (Classification::Application, Some(Properties(properties))) + properties.push(Property::new("uv:workspace", "true")); + properties.push(Property::new( + "uv:workspace_path", + &PortablePath::from(path).to_string(), + )); + Classification::Application } - PackageType::Dependency => (Classification::Library, None), + PackageType::Dependency => Classification::Library, }; + if let Some(marker_contents) = marker.and_then(|marker| marker.contents()) { + properties.push(Property::new( + "python:environment_marker", + &marker_contents.to_string(), + )); + } + Component { component_type: classification, name: NormalizedString::new(name), @@ -281,7 +295,11 @@ fn create_component_from_package( modified: None, pedigree: None, external_references: None, - properties, + properties: if !properties.is_empty() { + Some(Properties(properties)) + } else { + None + }, components: None, evidence: None, signature: None, From 59e897f4ae31d788c80d34d589af9bb6cdd2ccf0 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Thu, 25 Sep 2025 15:46:10 +0100 Subject: [PATCH 16/36] Update comments and return &str for package name --- crates/uv-configuration/src/export_format.rs | 2 +- .../src/lock/export/cyclonedx_json.rs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/uv-configuration/src/export_format.rs b/crates/uv-configuration/src/export_format.rs index af04af8d9d61b..c1e5c57fb9c17 100644 --- a/crates/uv-configuration/src/export_format.rs +++ b/crates/uv-configuration/src/export_format.rs @@ -24,7 +24,7 @@ pub enum ExportFormat { CycloneDX1_5, } -/// The format to use when compiling to a `requirements.txt` or `pylock.toml` file. +/// The output format to use in `uv pip compile`. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 3ce52e16afcee..07ee4a59c1f64 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -83,7 +83,7 @@ pub fn from_lock<'lock>( Ok(bom) } -/// Create and register a `CycloneDX` component, updating the counter and map +/// Create and register a `CycloneDX` component, updating the counter and map. fn create_and_register_component<'a>( package: &'a Package, classification: Classification, @@ -105,7 +105,7 @@ fn create_bom_ref(id: usize, name: &str, version: Option<&str>) -> String { } } -/// Extract version string from a package +/// Extract version string from a package. fn get_version_string(package: &Package) -> Option { package .id @@ -114,12 +114,12 @@ fn get_version_string(package: &Package) -> Option { .map(std::string::ToString::to_string) } -/// Extract package name string from a package -fn get_package_name(package: &Package) -> String { - package.id.name.to_string() +/// Extract package name string from a package. +fn get_package_name(package: &Package) -> &str { + package.id.name.as_str() } -/// Generate a Package URL (purl) from a package +/// Generate a Package URL (purl) from a package. Returns `None` for local sources. fn create_purl(package: &Package) -> Option { let name = get_package_name(package); let version = get_version_string(package); @@ -142,7 +142,7 @@ fn create_purl(package: &Package) -> Option { )) } -/// Create a `CycloneDX` component from a package node with the given classification and ID +/// Create a `CycloneDX` component from a package node with the given classification and ID. fn create_component_from_package( package: &Package, classification: Classification, @@ -150,12 +150,12 @@ fn create_component_from_package( ) -> Component { let name = get_package_name(package); let version = get_version_string(package); - let bom_ref = create_bom_ref(id, &name, version.as_deref()); + let bom_ref = create_bom_ref(id, name, version.as_deref()); let purl = create_purl(package).and_then(|purl_string| purl_string.parse().ok()); Component { component_type: classification, - name: NormalizedString::new(&name), + name: NormalizedString::new(name), version: version.as_deref().map(NormalizedString::new), bom_ref: Some(bom_ref), purl, From 271daa94cdbca43ed3d7753283f3c9b829619a59 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Fri, 17 Oct 2025 09:42:09 +0100 Subject: [PATCH 17/36] Replace python:environment_marker with uv:marker --- crates/uv-resolver/src/lock/export/cyclonedx_json.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 87f4907489683..2e606a44a5f48 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -269,7 +269,7 @@ fn create_component_from_package( if let Some(marker_contents) = marker.and_then(|marker| marker.contents()) { properties.push(Property::new( - "python:environment_marker", + "uv:marker", &marker_contents.to_string(), )); } From 4518fcc4526309499dd466a00b2451580ea44bb4 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Mon, 29 Sep 2025 16:29:16 +0100 Subject: [PATCH 18/36] Add purl encoding --- Cargo.lock | 1 + crates/uv-resolver/Cargo.toml | 1 + .../src/lock/export/cyclonedx_json.rs | 60 +++++++++++++++---- crates/uv/tests/it/export.rs | 1 + 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2643669350f78..48c8a563a781f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6631,6 +6631,7 @@ dependencies = [ "itertools 0.14.0", "jiff", "owo-colors", + "percent-encoding", "petgraph", "pubgrub", "rkyv", diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 26e0ec6b6390f..016d77d407794 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -57,6 +57,7 @@ indexmap = { workspace = true } itertools = { workspace = true } jiff = { workspace = true, features = ["serde"] } owo-colors = { workspace = true } +percent-encoding = { workspace = true } petgraph = { workspace = true } pubgrub = { workspace = true } rkyv = { workspace = true } diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 07ee4a59c1f64..16ebfe3a504c5 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -6,6 +6,7 @@ use cyclonedx_bom::models::metadata::Metadata; use cyclonedx_bom::models::tool::{Tool, Tools}; use cyclonedx_bom::prelude::{Bom, Component, Components, NormalizedString}; use itertools::Itertools; +use percent_encoding::{AsciiSet, CONTROLS, percent_encode}; use uv_configuration::{ DependencyGroupsWithDefaults, ExtrasSpecificationWithDefaults, InstallOptions, @@ -16,6 +17,28 @@ use crate::lock::export::{ExportableRequirement, ExportableRequirements}; use crate::lock::{Package, PackageId, Source}; use crate::{Installable, LockError}; +/// Character set for percent-encoding PURL components, copied from packageurl.rs. +const PURL_ENCODE_SET: &AsciiSet = &CONTROLS + .add(b' ') + .add(b'"') + .add(b'#') + .add(b'%') + .add(b'<') + .add(b'>') + .add(b'`') + .add(b'?') + .add(b'{') + .add(b'}') + .add(b';') + .add(b'=') + .add(b'+') + .add(b'@') + .add(b'\\') + .add(b'[') + .add(b']') + .add(b'^') + .add(b'|'); + pub fn from_lock<'lock>( target: &impl Installable<'lock>, prune: &[PackageName], @@ -121,25 +144,42 @@ fn get_package_name(package: &Package) -> &str { /// Generate a Package URL (purl) from a package. Returns `None` for local sources. fn create_purl(package: &Package) -> Option { - let name = get_package_name(package); - let version = get_version_string(package); + let name = percent_encode(get_package_name(package).as_bytes(), PURL_ENCODE_SET); + + let version = get_version_string(package).map_or_else(String::new, |v| { + format!("@{}", percent_encode(v.as_bytes(), PURL_ENCODE_SET)) + }); let (purl_type, qualifiers) = match &package.id.source { - Source::Registry(_) => ("pypi", String::new()), - Source::Git(url, _) | Source::Direct(url, _) => { - ("generic", format!("?download_url={}", url.as_ref())) - } + Source::Registry(_) => ("pypi", vec![]), + Source::Git(url, _) => ("generic", vec![("vcs_url", url.as_ref())]), + Source::Direct(url, _) => ("generic", vec![("download_url", url.as_ref())]), // No purl for local sources Source::Path(_) | Source::Directory(_) | Source::Editable(_) | Source::Virtual(_) => { return None; } }; - let version_specifier = version.map_or_else(String::new, |v| format!("@{v}")); + let qualifiers = if qualifiers.is_empty() { + String::new() + } else { + format_qualifiers(&qualifiers) + }; + + Some(format!("pkg:{purl_type}/{name}{version}{qualifiers}")) +} - Some(format!( - "pkg:{purl_type}/{name}{version_specifier}{qualifiers}" - )) +fn format_qualifiers(qualifiers: &[(&str, &str)]) -> String { + let joined_qualifiers = qualifiers + .iter() + .map(|(key, value)| { + format!( + "{key}={}", + percent_encode(value.as_bytes(), PURL_ENCODE_SET) + ) + }) + .join("&"); + format!("?{joined_qualifiers}") } /// Create a `CycloneDX` component from a package node with the given classification and ID. diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 1c57a8cc3a833..ba19faf17874a 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -4979,6 +4979,7 @@ fn cyclonedx_export_direct_url() -> Result<()> { Ok(()) } + #[test] fn cyclonedx_export_git_dependency() -> Result<()> { let context = TestContext::new("3.12").with_cyclonedx_filters(); From ea4ca0f3e1a763d8c4da2e3666f74e313121add2 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Sun, 19 Oct 2025 17:42:49 +0200 Subject: [PATCH 19/36] More fixes to properties --- crates/uv-resolver/src/lock/export/cyclonedx_json.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 2e606a44a5f48..9d507684f01ef 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -257,9 +257,8 @@ fn create_component_from_package( let classification = match package_type { PackageType::Root => Classification::Application, PackageType::Workspace(path) => { - properties.push(Property::new("uv:workspace", "true")); properties.push(Property::new( - "uv:workspace_path", + "uv:workspace:path", &PortablePath::from(path).to_string(), )); Classification::Application @@ -269,7 +268,7 @@ fn create_component_from_package( if let Some(marker_contents) = marker.and_then(|marker| marker.contents()) { properties.push(Property::new( - "uv:marker", + "cdx:python:package:marker", &marker_contents.to_string(), )); } From 1fd62b902def64441cc3ba7adc1b733cac44fc2b Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Mon, 29 Sep 2025 17:02:02 +0100 Subject: [PATCH 20/36] Add more tests --- crates/uv/tests/it/export.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index ba19faf17874a..1c57a8cc3a833 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -4979,7 +4979,6 @@ fn cyclonedx_export_direct_url() -> Result<()> { Ok(()) } - #[test] fn cyclonedx_export_git_dependency() -> Result<()> { let context = TestContext::new("3.12").with_cyclonedx_filters(); From 3498b42280cea4f1ed2a13dad05f0a1a5b76c928 Mon Sep 17 00:00:00 2001 From: Will Rollason Date: Mon, 27 Oct 2025 11:21:28 +0000 Subject: [PATCH 21/36] docs: uv export documentation --- docs/concepts/projects/sync.md | 15 +++-- docs/guides/export.md | 100 +++++++++++++++++++++++++++++++++ docs/guides/projects.md | 2 +- mkdocs.template.yml | 2 + 4 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 docs/guides/export.md diff --git a/docs/concepts/projects/sync.md b/docs/concepts/projects/sync.md index a5cfcc122a081..054cb5f8667bd 100644 --- a/docs/concepts/projects/sync.md +++ b/docs/concepts/projects/sync.md @@ -186,12 +186,17 @@ environment. ## Exporting the lockfile -If you need to integrate uv with other tools or workflows, you can export `uv.lock` to the -`requirements.txt` format with `uv export --format requirements-txt`. The generated -`requirements.txt` file can then be installed via `uv pip install`, or with other tools like `pip`. +If you need to integrate uv with other tools or workflows, you can export `uv.lock` to different +formats including `requirements.txt`, `pylock.toml` (PEP 751), and CycloneDX SBOM. -In general, we recommend against using both a `uv.lock` and a `requirements.txt` file. If you find -yourself exporting a `uv.lock` file, consider opening an issue to discuss your use case. +```console +$ uv export --format requirements.txt +$ uv export --format pylock.toml +$ uv export --format cyclonedx1.5 +``` + +See the [export guide](../../guides/export.md) for comprehensive documentation on all export formats +and their use cases. ## Partial installations diff --git a/docs/guides/export.md b/docs/guides/export.md new file mode 100644 index 0000000000000..8c4219e816cb6 --- /dev/null +++ b/docs/guides/export.md @@ -0,0 +1,100 @@ +--- +title: Exporting a lockfile +description: Exporting a lockfile to different formats +--- + +# Exporting a lockfile + +uv can export a lockfile to different formats for integration with other tools and workflows. The `uv export` command supports multiple output formats, each suited to different use cases. + +For more details on lockfiles and how they're created, see the +[project layout](../concepts/projects/layout.md) and +[locking and syncing](../concepts/projects/sync.md) documentation. + +## Overview of export formats + +uv supports three export formats: + +- `requirements.txt`: The traditional pip-compatible [requirements file format](https://pip.pypa.io/en/stable/reference/requirements-file-format/). +- `pylock.toml`: The standardized Python lockfile format defined in [PEP 751](https://peps.python.org/pep-0751/). +- `CycloneDX`: An industry-standard [Software Bill of Materials (SBOM)](https://cyclonedx.org/) format. + +The format can be specified with the `--format` flag: + +```console +$ uv export --format requirements.txt +$ uv export --format pylock.toml +$ uv export --format cyclonedx1.5 +``` + +!!! tip + + By default, `uv export` prints to stdout. Use `--output-file` to write to a file for any format: + + ```console + $ uv export --format requirements.txt --output-file requirements.txt + $ uv export --format pylock.toml --output-file pylock.toml + $ uv export --format cyclonedx1.5 --output-file sbom.json + ``` + +## `requirements.txt` format + +The `requirements.txt` format is the most widely supported format for Python dependencies. It can be used with `pip` and other Python package managers. + +### Basic usage + +```console +$ uv export --format requirements.txt +``` + +The generated `requirements.txt` file can then be installed via `uv pip install`, or with other tools like `pip`. + +!!! note + + In general, we recommend against using both a `uv.lock` and a `requirements.txt` file. The + `uv.lock` format is more powerful and includes features that cannot be expressed in + `requirements.txt`. If you find yourself exporting a `uv.lock` file, consider opening an issue + to discuss your use case. + +## `pylock.toml` format + +[PEP 751](https://peps.python.org/pep-0751/) defines a TOML-based lockfile format for Python dependencies. uv can export your project's dependency lockfile to this format. + +### Basic usage + +```console +$ uv export --format pylock.toml +``` + +## CycloneDX SBOM format + +uv can export your project's dependency lockfile as a Software Bill of Materials (SBOM) in CycloneDX format. SBOMs provide a comprehensive inventory of all software components in your application, which is useful for security auditing, compliance, and supply chain transparency. + +### What is CycloneDX? + +[CycloneDX](https://cyclonedx.org/) is an industry-standard format for creating Software Bill of Materials. CycloneDX is machine readable and widely supported by security scanning tools, vulnerability databases, and Software Composition Analysis (SCA) platforms. + +### Basic usage + +To export your project's lockfile as a CycloneDX SBOM: + +```console +$ uv export --format cyclonedx1.5 +``` + +This will generate a JSON-encoded CycloneDX v1.5 document containing your project and all of its dependencies. + +### SBOM Structure + +The generated SBOM follows the [CycloneDX specification](https://cyclonedx.org/specification/overview/). uv also includes the following custom properties on components: + +- `uv:package:marker`: Environment markers (e.g., `python_version >= "3.8"`) +- `uv:workspace:path`: Relative path for workspace members + +## Next steps + +To learn more about lockfiles and exporting, see the +[locking and syncing](../concepts/projects/sync.md) documentation and the +[command reference](../reference/cli.md#uv-export). + +Or, read on to learn how to [build and publish your project to a package index](./package.md). diff --git a/docs/guides/projects.md b/docs/guides/projects.md index cafa0102c4d96..0170ba1e1164b 100644 --- a/docs/guides/projects.md +++ b/docs/guides/projects.md @@ -272,4 +272,4 @@ To learn more about working on projects with uv, see the [projects concept](../concepts/projects/index.md) page and the [command reference](../reference/cli.md#uv). -Or, read on to learn how to [build and publish your project to a package index](./package.md). +Or, read on to learn how to [export a uv lockfile to different formats](./export.md). diff --git a/mkdocs.template.yml b/mkdocs.template.yml index 50f9f4e01ff54..fc18c300e940c 100644 --- a/mkdocs.template.yml +++ b/mkdocs.template.yml @@ -101,6 +101,7 @@ plugins: - guides/scripts.md - guides/tools.md - guides/projects.md + - guides/export.md - guides/package.md Integrations: - guides/integration/docker.md @@ -176,6 +177,7 @@ nav: - Running scripts: guides/scripts.md - Using tools: guides/tools.md - Working on projects: guides/projects.md + - Exporting lockfiles: guides/export.md - Publishing packages: guides/package.md - Migration: - guides/migration/index.md From b159e68ce9afb3808c808690b4e39acb0a430fc2 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Mon, 27 Oct 2025 18:10:24 +0000 Subject: [PATCH 22/36] Guard sbom export behind preview flag --- Cargo.lock | 1 + crates/uv-preview/src/lib.rs | 4 ++++ crates/uv-resolver/Cargo.toml | 1 + crates/uv-resolver/src/lock/export/cyclonedx_json.rs | 10 ++++++++++ crates/uv/src/commands/project/export.rs | 1 + 5 files changed, 17 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 48c8a563a781f..a1288fa0dc333 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6665,6 +6665,7 @@ dependencies = [ "uv-pep440", "uv-pep508", "uv-platform-tags", + "uv-preview", "uv-pypi-types", "uv-python", "uv-redacted", diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index a2453e4d80c33..99fabef3aa640 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -21,6 +21,7 @@ bitflags::bitflags! { const NATIVE_AUTH = 1 << 9; const S3_ENDPOINT = 1 << 10; const CACHE_SIZE = 1 << 11; + const SBOM_EXPORT = 1 << 12; } } @@ -42,6 +43,7 @@ impl PreviewFeatures { Self::NATIVE_AUTH => "native-auth", Self::S3_ENDPOINT => "s3-endpoint", Self::CACHE_SIZE => "cache-size", + Self::SBOM_EXPORT => "sbom-export", _ => panic!("`flag_as_str` can only be used for exactly one feature flag"), } } @@ -91,6 +93,7 @@ impl FromStr for PreviewFeatures { "native-auth" => Self::NATIVE_AUTH, "s3-endpoint" => Self::S3_ENDPOINT, "cache-size" => Self::CACHE_SIZE, + "sbom-export" => Self::SBOM_EXPORT, _ => { warn_user_once!("Unknown preview feature: `{part}`"); continue; @@ -267,6 +270,7 @@ mod tests { ); assert_eq!(PreviewFeatures::FORMAT.flag_as_str(), "format"); assert_eq!(PreviewFeatures::S3_ENDPOINT.flag_as_str(), "s3-endpoint"); + assert_eq!(PreviewFeatures::SBOM_EXPORT.flag_as_str(), "sbom-export"); } #[test] diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 016d77d407794..9e204df0eff09 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -33,6 +33,7 @@ uv-once-map = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } +uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-redacted = { workspace = true } diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 9d507684f01ef..8e61e3bdc7955 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -21,6 +21,8 @@ use uv_configuration::{ use uv_fs::PortablePath; use uv_normalize::PackageName; use uv_pep508::MarkerTree; +use uv_preview::{Preview, PreviewFeatures}; +use uv_warnings::warn_user; use crate::lock::export::{ExportableRequirement, ExportableRequirements}; use crate::lock::{Package, PackageId, Source}; @@ -55,7 +57,15 @@ pub fn from_lock<'lock>( groups: &DependencyGroupsWithDefaults, annotate: bool, install_options: &'lock InstallOptions, + preview: Preview, ) -> Result { + if !preview.is_enabled(PreviewFeatures::SBOM_EXPORT) { + warn_user!( + "`uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::SBOM_EXPORT + ); + } + // Extract the packages from the lock file. let ExportableRequirements(mut nodes) = ExportableRequirements::from_lock(target, prune, extras, groups, annotate, install_options); diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index de46589e74f19..171994480124e 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -384,6 +384,7 @@ pub(crate) async fn export( &groups, include_annotations, &install_options, + preview, )?; let mut output = Vec::::new(); From 8d8c19c5f5c3283fa4ceb92fec2bd7335dd27603 Mon Sep 17 00:00:00 2001 From: Will Rollason Date: Wed, 29 Oct 2025 16:24:13 +0000 Subject: [PATCH 23/36] chore: add preview message --- docs/guides/export.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/guides/export.md b/docs/guides/export.md index 8c4219e816cb6..a7e4a3bf4ac49 100644 --- a/docs/guides/export.md +++ b/docs/guides/export.md @@ -70,6 +70,11 @@ $ uv export --format pylock.toml uv can export your project's dependency lockfile as a Software Bill of Materials (SBOM) in CycloneDX format. SBOMs provide a comprehensive inventory of all software components in your application, which is useful for security auditing, compliance, and supply chain transparency. +!!! important + + Support for exporting to CycloneDX is in _preview_. This means the behavior is experimental + and subject to change. + ### What is CycloneDX? [CycloneDX](https://cyclonedx.org/) is an industry-standard format for creating Software Bill of Materials. CycloneDX is machine readable and widely supported by security scanning tools, vulnerability databases, and Software Composition Analysis (SCA) platforms. From 17a2dd0d5d79b2f3d61056da2c6201a6776610a2 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Tue, 28 Oct 2025 11:17:59 +0000 Subject: [PATCH 24/36] Update `cdx:python:package:marker` to `uv:package:marker` See PR here for reasoning: https://github.com/CycloneDX/cyclonedx-property-taxonomy/pull/142 --- crates/uv-resolver/src/lock/export/cyclonedx_json.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 8e61e3bdc7955..b7d2b337a2b6f 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -278,7 +278,7 @@ fn create_component_from_package( if let Some(marker_contents) = marker.and_then(|marker| marker.contents()) { properties.push(Property::new( - "cdx:python:package:marker", + "uv:package:marker", &marker_contents.to_string(), )); } From 93a65aec761fb96a9334463bfce39e2f9ec0618d Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Thu, 30 Oct 2025 15:21:54 +0000 Subject: [PATCH 25/36] fix: resolve conflicts after rebasing --- crates/uv-resolver/src/lock/export/cyclonedx_json.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 9642862004d60..22d98aa8caa54 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -67,8 +67,14 @@ pub fn from_lock<'lock>( } // Extract the packages from the lock file. - let ExportableRequirements(mut nodes) = - ExportableRequirements::from_lock(target, prune, extras, groups, annotate, install_options); + let ExportableRequirements(mut nodes) = ExportableRequirements::from_lock( + target, + prune, + extras, + groups, + annotate, + install_options, + )?; nodes.sort_unstable_by_key(|node| &node.package.id); From 164a64328a092f0ce3f5f0ae604902aff3cd5720 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Wed, 29 Oct 2025 12:50:51 +0000 Subject: [PATCH 26/36] Update comment --- crates/uv-resolver/src/lock/export/cyclonedx_json.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index b7d2b337a2b6f..9642862004d60 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -28,7 +28,7 @@ use crate::lock::export::{ExportableRequirement, ExportableRequirements}; use crate::lock::{Package, PackageId, Source}; use crate::{Installable, LockError}; -/// Character set for percent-encoding PURL components, copied from packageurl.rs. +/// Character set for percent-encoding PURL components, copied from packageurl.rs (). const PURL_ENCODE_SET: &AsciiSet = &CONTROLS .add(b' ') .add(b'"') From a0c12dc88647489270c3fcda144d08938789421b Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Thu, 30 Oct 2025 15:24:21 +0000 Subject: [PATCH 27/36] Add preview link to note --- docs/guides/export.md | 3 +-- docs/reference/cli.md | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/guides/export.md b/docs/guides/export.md index a7e4a3bf4ac49..42ec9b2fd6976 100644 --- a/docs/guides/export.md +++ b/docs/guides/export.md @@ -72,8 +72,7 @@ uv can export your project's dependency lockfile as a Software Bill of Materials !!! important - Support for exporting to CycloneDX is in _preview_. This means the behavior is experimental - and subject to change. + Support for exporting to CycloneDX is in [preview](../concepts/preview.md), so may be subject to change. ### What is CycloneDX? diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 4e244ecacc051..3add58292c250 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1845,13 +1845,13 @@ uv export [OPTIONS]
  • fewest: Optimize for selecting the fewest number of versions for each package. Older versions may be preferred if they are compatible with a wider range of supported Python versions or platforms
  • requires-python: Optimize for selecting latest supported version of each package, for each supported Python version
  • --format format

    The format to which uv.lock should be exported.

    -

    Supports requirements.txt, pylock.toml (PEP 751) and CycloneDX v1.5 JSON output formats.

    +

    Supports requirements.txt, pylock.toml (PEP 751) and CycloneDX v1.5 JSON output formats.

    uv will infer the output format from the file extension of the output file, if provided. Otherwise, defaults to requirements.txt.

    Possible values:

    • requirements.txt: Export in requirements.txt format
    • pylock.toml: Export in pylock.toml format
    • -
    • cyclonedx1.5: Export in CycloneDX v1.5 JSON format
    • +
    • cyclonedx1.5: Export in CycloneDX v1.5 JSON format
    --frozen

    Do not update the uv.lock before exporting.

    If a uv.lock does not exist, uv will exit with an error.

    May also be set with the UV_FROZEN environment variable.

    --group group

    Include dependencies from the specified dependency group.

    From ba46e590f3eda976e317250a384821c25bc3f608 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Wed, 29 Oct 2025 15:39:50 +0000 Subject: [PATCH 28/36] Skip conflict detection for SBOMs --- crates/uv/src/commands/project/export.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 171994480124e..7a914fbc4fa51 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -286,9 +286,6 @@ pub(crate) async fn export( }, }; - // Validate that the set of requested extras and development groups are compatible. - detect_conflicts(&target, &extras, &groups)?; - // Validate that the set of requested extras and development groups are defined in the lockfile. target.validate_extras(&extras)?; target.validate_groups(&groups)?; @@ -316,6 +313,11 @@ pub(crate) async fn export( } }); + // Skip conflict detection for CycloneDX exports, as SBOMs are meant to document all dependencies including conflicts. + if !matches!(format, ExportFormat::CycloneDX1_5) { + detect_conflicts(&target, &extras, &groups)?; + } + // If the user is exporting to PEP 751, ensure the filename matches the specification. if matches!(format, ExportFormat::PylockToml) { if let Some(file_name) = output_file From 50cc074ac622057e3f2fe49a1395ba18dc20296b Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Thu, 30 Oct 2025 16:27:47 +0000 Subject: [PATCH 29/36] Refactor component map --- .../src/lock/export/cyclonedx_json.rs | 392 +++++++++--------- 1 file changed, 192 insertions(+), 200 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index 22d98aa8caa54..dab1bbf63c592 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -50,6 +50,193 @@ const PURL_ENCODE_SET: &AsciiSet = &CONTROLS .add(b'^') .add(b'|'); +#[derive(Default)] +struct ComponentBuilder<'a> { + id_counter: usize, // Used as prefix in bom-ref generation, to ensure uniqueness + package_to_component_map: HashMap<&'a PackageId, Component>, +} + +impl<'a> ComponentBuilder<'a> { + /// Create and register a `CycloneDX` component, updating the counter and map. + fn create_component( + &mut self, + package: &'a Package, + package_type: PackageType, + marker: Option<&MarkerTree>, + ) -> Component { + let component = + Self::create_component_from_package(package, package_type, marker, self.id_counter); + self.package_to_component_map + .insert(&package.id, component.clone()); + self.id_counter += 1; + component + } + + /// Creates a bom-ref string in the format "{id}-{package_name}@{version}" or "{id}-{package_name}" if no version is provided. + fn create_bom_ref(id: usize, name: &str, version: Option<&str>) -> String { + if let Some(version) = version { + format!("{id}-{name}@{version}") + } else { + format!("{id}-{name}") + } + } + + /// Extract version string from a package. + fn get_version_string(package: &Package) -> Option { + package + .id + .version + .as_ref() + .map(std::string::ToString::to_string) + } + + /// Extract package name string from a package. + fn get_package_name(package: &Package) -> &str { + package.id.name.as_str() + } + + /// Generate a Package URL (purl) from a package. Returns `None` for local sources. + fn create_purl(package: &Package) -> Option { + let name = percent_encode(Self::get_package_name(package).as_bytes(), PURL_ENCODE_SET); + + let version = Self::get_version_string(package).map_or_else(String::new, |v| { + format!("@{}", percent_encode(v.as_bytes(), PURL_ENCODE_SET)) + }); + + let (purl_type, qualifiers) = match &package.id.source { + Source::Registry(_) => ("pypi", vec![]), + Source::Git(url, _) => ("generic", vec![("vcs_url", url.as_ref())]), + Source::Direct(url, _) => ("generic", vec![("download_url", url.as_ref())]), + // No purl for local sources + Source::Path(_) | Source::Directory(_) | Source::Editable(_) | Source::Virtual(_) => { + return None; + } + }; + + let qualifiers = if qualifiers.is_empty() { + String::new() + } else { + Self::format_qualifiers(&qualifiers) + }; + + Some(format!("pkg:{purl_type}/{name}{version}{qualifiers}")) + } + + fn format_qualifiers(qualifiers: &[(&str, &str)]) -> String { + let joined_qualifiers = qualifiers + .iter() + .map(|(key, value)| { + format!( + "{key}={}", + percent_encode(value.as_bytes(), PURL_ENCODE_SET) + ) + }) + .join("&"); + format!("?{joined_qualifiers}") + } + + /// Create a `CycloneDX` component from a package node with the given classification and ID. + #[allow(clippy::needless_pass_by_value)] + fn create_component_from_package( + package: &Package, + package_type: PackageType, + marker: Option<&MarkerTree>, + id: usize, + ) -> Component { + let name = Self::get_package_name(package); + let version = Self::get_version_string(package); + let bom_ref = Self::create_bom_ref(id, name, version.as_deref()); + let purl = Self::create_purl(package).and_then(|purl_string| purl_string.parse().ok()); + let mut properties = vec![]; + + let classification = match package_type { + PackageType::Root => Classification::Application, + PackageType::Workspace(path) => { + properties.push(Property::new( + "uv:workspace:path", + &PortablePath::from(path).to_string(), + )); + Classification::Application + } + PackageType::Dependency => Classification::Library, + }; + + if let Some(marker_contents) = marker.and_then(|marker| marker.contents()) { + properties.push(Property::new( + "uv:package:marker", + &marker_contents.to_string(), + )); + } + + Component { + component_type: classification, + name: NormalizedString::new(name), + version: version.as_deref().map(NormalizedString::new), + bom_ref: Some(bom_ref), + purl, + mime_type: None, + supplier: None, + author: None, + publisher: None, + group: None, + description: None, + scope: None, + hashes: None, + licenses: None, + copyright: None, + cpe: None, + swid: None, + modified: None, + pedigree: None, + external_references: None, + properties: if !properties.is_empty() { + Some(Properties(properties)) + } else { + None + }, + components: None, + evidence: None, + signature: None, + model_card: None, + data: None, + } + } + + fn create_dependencies(&self, nodes: &[ExportableRequirement<'_>]) -> Dependencies { + let dependencies = nodes.iter().map(|node| { + let package_bom_ref = self + .package_to_component_map + .get(&node.package.id) + .expect("All nodes should have been added to package_to_bom_ref"); + + let immediate_deps = &node.package.dependencies; + let optional_deps = node.package.optional_dependencies.values().flatten(); + let dep_groups = node.package.dependency_groups.values().flatten(); + + let package_deps = immediate_deps + .iter() + .chain(optional_deps) + .chain(dep_groups) + .filter_map(|dep| self.package_to_component_map.get(&dep.package_id)); + + let bom_refs = package_deps + .map(|p| p.bom_ref.clone().expect("bom-ref should always exist")) + .sorted_unstable() + .unique() + .collect(); + + Dependency { + dependency_ref: package_bom_ref + .bom_ref + .clone() + .expect("bom-ref should always exist"), + dependencies: bom_refs, + } + }); + Dependencies(dependencies.collect()) + } +} + pub fn from_lock<'lock>( target: &impl Installable<'lock>, prune: &[PackageName], @@ -90,20 +277,11 @@ pub fn from_lock<'lock>( } .or_else(|| target.lock().root()); // Fallback to project root - // Used as prefix in bom-ref generation, to ensure uniqueness - let mut id_counter = 1; - let mut package_to_component_map = HashMap::<&PackageId, Component>::new(); + let mut component_builder = ComponentBuilder::default(); let metadata = Metadata { - component: root.map(|package| { - create_and_register_component( - package, - PackageType::Root, - None, - &mut id_counter, - &mut package_to_component_map, - ) - }), + component: root + .map(|package| component_builder.create_component(package, PackageType::Root, None)), timestamp: cyclonedx_bom::prelude::DateTime::now().ok(), tools: Some(Tools::List(vec![Tool { vendor: Some(NormalizedString::new("Astral Software Inc.")), @@ -150,17 +328,11 @@ pub fn from_lock<'lock>( } else { PackageType::Dependency }; - create_and_register_component( - node.package, - package_type, - Some(&node.marker), - &mut id_counter, - &mut package_to_component_map, - ) + component_builder.create_component(node.package, package_type, Some(&node.marker)) }) .collect(); - let dependencies = create_dependencies_from_mapping(&nodes, &package_to_component_map); + let dependencies = component_builder.create_dependencies(&nodes); let bom = Bom { metadata: Some(metadata), @@ -178,183 +350,3 @@ enum PackageType<'a> { Workspace(&'a Path), Dependency, } - -/// Create and register a `CycloneDX` component, updating the counter and map. -fn create_and_register_component<'a>( - package: &'a Package, - package_type: PackageType, - marker: Option<&MarkerTree>, - id_counter: &mut usize, - package_to_bom_ref: &mut HashMap<&'a PackageId, Component>, -) -> Component { - let component = create_component_from_package(package, package_type, marker, *id_counter); - package_to_bom_ref.insert(&package.id, component.clone()); - *id_counter += 1; - component -} - -/// Creates a bom-ref string in the format "{id}-{package_name}@{version}" or "{id}-{package_name}" if no version is provided. -fn create_bom_ref(id: usize, name: &str, version: Option<&str>) -> String { - if let Some(version) = version { - format!("{id}-{name}@{version}") - } else { - format!("{id}-{name}") - } -} - -/// Extract version string from a package. -fn get_version_string(package: &Package) -> Option { - package - .id - .version - .as_ref() - .map(std::string::ToString::to_string) -} - -/// Extract package name string from a package. -fn get_package_name(package: &Package) -> &str { - package.id.name.as_str() -} - -/// Generate a Package URL (purl) from a package. Returns `None` for local sources. -fn create_purl(package: &Package) -> Option { - let name = percent_encode(get_package_name(package).as_bytes(), PURL_ENCODE_SET); - - let version = get_version_string(package).map_or_else(String::new, |v| { - format!("@{}", percent_encode(v.as_bytes(), PURL_ENCODE_SET)) - }); - - let (purl_type, qualifiers) = match &package.id.source { - Source::Registry(_) => ("pypi", vec![]), - Source::Git(url, _) => ("generic", vec![("vcs_url", url.as_ref())]), - Source::Direct(url, _) => ("generic", vec![("download_url", url.as_ref())]), - // No purl for local sources - Source::Path(_) | Source::Directory(_) | Source::Editable(_) | Source::Virtual(_) => { - return None; - } - }; - - let qualifiers = if qualifiers.is_empty() { - String::new() - } else { - format_qualifiers(&qualifiers) - }; - - Some(format!("pkg:{purl_type}/{name}{version}{qualifiers}")) -} - -fn format_qualifiers(qualifiers: &[(&str, &str)]) -> String { - let joined_qualifiers = qualifiers - .iter() - .map(|(key, value)| { - format!( - "{key}={}", - percent_encode(value.as_bytes(), PURL_ENCODE_SET) - ) - }) - .join("&"); - format!("?{joined_qualifiers}") -} - -/// Create a `CycloneDX` component from a package node with the given classification and ID. -#[allow(clippy::needless_pass_by_value)] -fn create_component_from_package( - package: &Package, - package_type: PackageType, - marker: Option<&MarkerTree>, - id: usize, -) -> Component { - let name = get_package_name(package); - let version = get_version_string(package); - let bom_ref = create_bom_ref(id, name, version.as_deref()); - let purl = create_purl(package).and_then(|purl_string| purl_string.parse().ok()); - let mut properties = vec![]; - - let classification = match package_type { - PackageType::Root => Classification::Application, - PackageType::Workspace(path) => { - properties.push(Property::new( - "uv:workspace:path", - &PortablePath::from(path).to_string(), - )); - Classification::Application - } - PackageType::Dependency => Classification::Library, - }; - - if let Some(marker_contents) = marker.and_then(|marker| marker.contents()) { - properties.push(Property::new( - "uv:package:marker", - &marker_contents.to_string(), - )); - } - - Component { - component_type: classification, - name: NormalizedString::new(name), - version: version.as_deref().map(NormalizedString::new), - bom_ref: Some(bom_ref), - purl, - mime_type: None, - supplier: None, - author: None, - publisher: None, - group: None, - description: None, - scope: None, - hashes: None, - licenses: None, - copyright: None, - cpe: None, - swid: None, - modified: None, - pedigree: None, - external_references: None, - properties: if !properties.is_empty() { - Some(Properties(properties)) - } else { - None - }, - components: None, - evidence: None, - signature: None, - model_card: None, - data: None, - } -} - -fn create_dependencies_from_mapping( - nodes: &[ExportableRequirement<'_>], - package_to_component_map: &HashMap<&PackageId, Component>, -) -> Dependencies { - let dependencies = nodes.iter().map(|node| { - let package_bom_ref = package_to_component_map - .get(&node.package.id) - .expect("All nodes should have been added to package_to_bom_ref"); - - let immediate_deps = &node.package.dependencies; - let optional_deps = node.package.optional_dependencies.values().flatten(); - let dep_groups = node.package.dependency_groups.values().flatten(); - - let package_deps = immediate_deps - .iter() - .chain(optional_deps) - .chain(dep_groups) - .filter_map(|dep| package_to_component_map.get(&dep.package_id)); - - let bom_refs = package_deps - .map(|p| p.bom_ref.clone().expect("bom-ref should always exist")) - .sorted_unstable() - .unique() - .collect(); - - Dependency { - dependency_ref: package_bom_ref - .bom_ref - .clone() - .expect("bom-ref should always exist"), - dependencies: bom_refs, - } - }); - Dependencies(dependencies.collect()) -} From 74a7286c201b7f120ca38886b5265b661c1e446f Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Thu, 30 Oct 2025 17:01:05 +0000 Subject: [PATCH 30/36] Insert synthetic root when using `--all-packages` flag --- .../src/lock/export/cyclonedx_json.rs | 187 ++++++++++++------ crates/uv/src/commands/project/export.rs | 1 + crates/uv/tests/it/export.rs | 85 +++++++- 3 files changed, 207 insertions(+), 66 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index dab1bbf63c592..c8f1f587abba6 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -13,8 +13,8 @@ use cyclonedx_bom::{ }; use itertools::Itertools; use percent_encoding::{AsciiSet, CONTROLS, percent_encode}; - use rustc_hash::FxHashSet; + use uv_configuration::{ DependencyGroupsWithDefaults, ExtrasSpecificationWithDefaults, InstallOptions, }; @@ -50,6 +50,8 @@ const PURL_ENCODE_SET: &AsciiSet = &CONTROLS .add(b'^') .add(b'|'); +/// Creates `CycloneDX` components, registering them in a `HashMap` so that they can be retrieved by `PackageId`. +/// Also ensures uniqueness when generating bom-refs by using a numeric prefix which is incremented for each component. #[derive(Default)] struct ComponentBuilder<'a> { id_counter: usize, // Used as prefix in bom-ref generation, to ensure uniqueness @@ -57,23 +59,10 @@ struct ComponentBuilder<'a> { } impl<'a> ComponentBuilder<'a> { - /// Create and register a `CycloneDX` component, updating the counter and map. - fn create_component( - &mut self, - package: &'a Package, - package_type: PackageType, - marker: Option<&MarkerTree>, - ) -> Component { - let component = - Self::create_component_from_package(package, package_type, marker, self.id_counter); - self.package_to_component_map - .insert(&package.id, component.clone()); - self.id_counter += 1; - component - } - /// Creates a bom-ref string in the format "{id}-{package_name}@{version}" or "{id}-{package_name}" if no version is provided. - fn create_bom_ref(id: usize, name: &str, version: Option<&str>) -> String { + fn create_bom_ref(&mut self, name: &str, version: Option<&str>) -> String { + self.id_counter += 1; + let id = self.id_counter; if let Some(version) = version { format!("{id}-{name}@{version}") } else { @@ -135,17 +124,63 @@ impl<'a> ComponentBuilder<'a> { format!("?{joined_qualifiers}") } - /// Create a `CycloneDX` component from a package node with the given classification and ID. + fn create_component( + &mut self, + package: &'a Package, + package_type: PackageType, + marker: Option<&MarkerTree>, + ) -> Component { + let component = self.create_component_from_package(package, package_type, marker); + self.package_to_component_map + .insert(&package.id, component.clone()); + component + } + + fn create_synthetic_root_component(&mut self) -> Component { + let name = "uv-workspace"; + let bom_ref = self.create_bom_ref(name, None); + + // No need to register as we manually add dependencies in `if all_packages` check in `from_lock` + Component { + component_type: Classification::Application, + name: NormalizedString::new(name), + version: None, + bom_ref: Some(bom_ref), + purl: None, + mime_type: None, + supplier: None, + author: None, + publisher: None, + group: None, + description: None, + scope: None, + hashes: None, + licenses: None, + copyright: None, + cpe: None, + swid: None, + modified: None, + pedigree: None, + external_references: None, + properties: None, + components: None, + evidence: None, + signature: None, + model_card: None, + data: None, + } + } + #[allow(clippy::needless_pass_by_value)] fn create_component_from_package( + &mut self, package: &Package, package_type: PackageType, marker: Option<&MarkerTree>, - id: usize, ) -> Component { let name = Self::get_package_name(package); let version = Self::get_version_string(package); - let bom_ref = Self::create_bom_ref(id, name, version.as_deref()); + let bom_ref = self.create_bom_ref(name, version.as_deref()); let purl = Self::create_purl(package).and_then(|purl_string| purl_string.parse().ok()); let mut properties = vec![]; @@ -202,38 +237,8 @@ impl<'a> ComponentBuilder<'a> { } } - fn create_dependencies(&self, nodes: &[ExportableRequirement<'_>]) -> Dependencies { - let dependencies = nodes.iter().map(|node| { - let package_bom_ref = self - .package_to_component_map - .get(&node.package.id) - .expect("All nodes should have been added to package_to_bom_ref"); - - let immediate_deps = &node.package.dependencies; - let optional_deps = node.package.optional_dependencies.values().flatten(); - let dep_groups = node.package.dependency_groups.values().flatten(); - - let package_deps = immediate_deps - .iter() - .chain(optional_deps) - .chain(dep_groups) - .filter_map(|dep| self.package_to_component_map.get(&dep.package_id)); - - let bom_refs = package_deps - .map(|p| p.bom_ref.clone().expect("bom-ref should always exist")) - .sorted_unstable() - .unique() - .collect(); - - Dependency { - dependency_ref: package_bom_ref - .bom_ref - .clone() - .expect("bom-ref should always exist"), - dependencies: bom_refs, - } - }); - Dependencies(dependencies.collect()) + fn get_component(&self, id: &PackageId) -> Option<&Component> { + self.package_to_component_map.get(id) } } @@ -245,6 +250,7 @@ pub fn from_lock<'lock>( annotate: bool, install_options: &'lock InstallOptions, preview: Preview, + all_packages: bool, ) -> Result { if !preview.is_enabled(PreviewFeatures::SBOM_EXPORT) { warn_user!( @@ -279,7 +285,7 @@ pub fn from_lock<'lock>( let mut component_builder = ComponentBuilder::default(); - let metadata = Metadata { + let mut metadata = Metadata { component: root .map(|package| component_builder.create_component(package, PackageType::Root, None)), timestamp: cyclonedx_bom::prelude::DateTime::now().ok(), @@ -306,7 +312,7 @@ pub fn from_lock<'lock>( }) .collect::>(); - let components = nodes + let mut components = nodes .iter() .filter(|node| root.is_none_or(|root_pkg| root_pkg.id != node.package.id)) // Filter out root package as this is included in `metadata` .map(|node| { @@ -330,20 +336,87 @@ pub fn from_lock<'lock>( }; component_builder.create_component(node.package, package_type, Some(&node.marker)) }) - .collect(); + .collect::>(); + + let mut dependencies = create_dependencies(&nodes, &component_builder); + + // With `--all-packages`, use synthetic root which depends on workspace root and all workspace members. + // This ensures that we don't have any dangling components resulting from workspace packages not depended on by the workspace root. + if all_packages { + let synthetic_root = component_builder.create_synthetic_root_component(); + let synthetic_root_bom_ref = synthetic_root + .bom_ref + .clone() + .expect("bom-ref should always exist"); + let workspace_root = metadata.component.replace(synthetic_root); + + if let Some(workspace_root) = workspace_root { + components.push(workspace_root); + } - let dependencies = component_builder.create_dependencies(&nodes); + dependencies.push(Dependency { + dependency_ref: synthetic_root_bom_ref, + dependencies: components + .iter() + .filter_map(|c| { + if c.component_type == Classification::Application { + c.bom_ref.clone() + } else { + None + } + }) + .collect(), + }); + } let bom = Bom { metadata: Some(metadata), components: Some(Components(components)), - dependencies: Some(dependencies), + dependencies: Some(Dependencies(dependencies)), ..Bom::default() }; Ok(bom) } +fn create_dependencies( + nodes: &[ExportableRequirement<'_>], + component_builder: &ComponentBuilder, +) -> Vec { + nodes + .iter() + .map(|node| { + let component = component_builder + .get_component(&node.package.id) + .expect("All nodes should have been added to map"); + + let immediate_deps = &node.package.dependencies; + let optional_deps = node.package.optional_dependencies.values().flatten(); + let dep_groups = node.package.dependency_groups.values().flatten(); + + let package_deps = immediate_deps + .iter() + .chain(optional_deps) + .chain(dep_groups) + .filter_map(|dep| component_builder.get_component(&dep.package_id)); + + let bom_refs = package_deps + .map(|p| p.bom_ref.clone().expect("bom-ref should always exist")) + .sorted_unstable() + .unique() + .collect(); + + Dependency { + dependency_ref: component + .bom_ref + .clone() + .expect("bom-ref should always exist"), + dependencies: bom_refs, + } + }) + .collect() +} + #[derive(Clone, Debug, Eq, PartialEq)] enum PackageType<'a> { Root, diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 7a914fbc4fa51..b18eeeeb17625 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -387,6 +387,7 @@ pub(crate) async fn export( include_annotations, &install_options, preview, + all_packages, )?; let mut output = Vec::::new(); diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 1c57a8cc3a833..d99d45221bba4 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -4899,6 +4899,7 @@ fn cyclonedx_export() -> Result<()> { } ----- stderr ----- Resolved 4 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -4974,6 +4975,7 @@ fn cyclonedx_export_direct_url() -> Result<()> { } ----- stderr ----- Resolved 2 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -5144,6 +5146,7 @@ fn cyclonedx_export_git_dependency() -> Result<()> { } ----- stderr ----- Resolved 9 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -5205,6 +5208,7 @@ fn cyclonedx_export_no_dependencies() -> Result<()> { } ----- stderr ----- Resolved 1 package in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -5452,6 +5456,7 @@ fn cyclonedx_export_mixed_source_types() -> Result<()> { } ----- stderr ----- Resolved 15 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -5531,6 +5536,7 @@ fn cyclonedx_export_project_extra() -> Result<()> { } ----- stderr ----- Resolved 6 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -5659,6 +5665,7 @@ fn cyclonedx_export_project_extra_with_optional_flag() -> Result<()> { } ----- stderr ----- Resolved 6 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -5843,6 +5850,7 @@ fn cyclonedx_export_with_workspace_member() -> Result<()> { } ----- stderr ----- Resolved 7 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -5939,6 +5947,7 @@ fn cyclonedx_export_workspace_non_root() -> Result<()> { } ----- stderr ----- Resolved 6 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -6057,6 +6066,7 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { } ----- stderr ----- Resolved 7 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--all-extras"), @r#" @@ -6126,6 +6136,7 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { } ----- stderr ----- Resolved 7 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -6207,9 +6218,8 @@ fn cyclonedx_export_workspace_frozen() -> Result<()> { ], "component": { "type": "application", - "bom-ref": "1-project@0.1.0", - "name": "project", - "version": "0.1.0" + "bom-ref": "7-uv-workspace", + "name": "uv-workspace" } }, "components": [ @@ -6252,6 +6262,12 @@ fn cyclonedx_export_workspace_frozen() -> Result<()> { "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" + }, + { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" } ], "dependencies": [ @@ -6286,10 +6302,18 @@ fn cyclonedx_export_workspace_frozen() -> Result<()> { { "ref": "6-sniffio@1.3.1", "dependsOn": [] + }, + { + "ref": "7-uv-workspace", + "dependsOn": [ + "3-child@0.1.0", + "1-project@0.1.0" + ] } ] } ----- stderr ----- + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -6369,9 +6393,8 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { ], "component": { "type": "application", - "bom-ref": "1-project@0.1.0", - "name": "project", - "version": "0.1.0" + "bom-ref": "8-uv-workspace", + "name": "uv-workspace" } }, "components": [ @@ -6426,6 +6449,12 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" + }, + { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" } ], "dependencies": [ @@ -6465,11 +6494,20 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { { "ref": "7-sniffio@1.3.1", "dependsOn": [] + }, + { + "ref": "8-uv-workspace", + "dependsOn": [ + "3-child1@0.1.0", + "4-child2@0.2.0", + "1-project@0.1.0" + ] } ] } ----- stderr ----- Resolved 7 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -6658,6 +6696,7 @@ fn cyclonedx_export_workspace_complex_dependencies() -> Result<()> { } ----- stderr ----- Resolved 7 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -6788,6 +6827,7 @@ fn cyclonedx_export_dependency_marker() -> Result<()> { } ----- stderr ----- Resolved 5 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -7020,6 +7060,7 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { } ----- stderr ----- Resolved 10 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -7202,6 +7243,7 @@ fn cyclonedx_export_dependency_extra() -> Result<()> { } ----- stderr ----- Resolved 10 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -7393,6 +7435,7 @@ fn cyclonedx_export_prune() -> Result<()> { } ----- stderr ----- Resolved 12 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "# ); @@ -7483,6 +7526,7 @@ fn cyclonedx_export_group() -> Result<()> { } ----- stderr ----- Resolved 6 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); // Export only specific group @@ -7529,6 +7573,7 @@ fn cyclonedx_export_group() -> Result<()> { } ----- stderr ----- Resolved 6 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); // Export with additional group @@ -7631,6 +7676,7 @@ fn cyclonedx_export_group() -> Result<()> { } ----- stderr ----- Resolved 6 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -7679,6 +7725,7 @@ fn cyclonedx_export_non_project() -> Result<()> { ----- stderr ----- warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`. Resolved 3 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); // Export with group specified @@ -7745,6 +7792,7 @@ fn cyclonedx_export_non_project() -> Result<()> { ----- stderr ----- warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`. Resolved 3 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -7882,6 +7930,7 @@ fn cyclonedx_export_no_emit() -> Result<()> { } ----- stderr ----- Resolved 6 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); // Exclude `project`. @@ -7982,6 +8031,7 @@ fn cyclonedx_export_no_emit() -> Result<()> { } ----- stderr ----- Resolved 6 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -8088,6 +8138,7 @@ fn cyclonedx_export_relative_path() -> Result<()> { ----- stderr ----- Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Resolved 3 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -8280,6 +8331,7 @@ fn cyclonedx_export_cyclic_dependencies() -> Result<()> { } ----- stderr ----- Resolved 11 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -8397,6 +8449,7 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { ----- stderr ----- warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead Resolved 5 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); // Export without dev dependencies @@ -8450,6 +8503,7 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { ----- stderr ----- warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead Resolved 5 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); // Export only dev dependencies @@ -8522,6 +8576,7 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { ----- stderr ----- warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead Resolved 5 packages in [TIME] + warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); Ok(()) @@ -8595,9 +8650,8 @@ fn cyclonedx_export_all_packages_conflicting_workspace_members() -> Result<()> { ], "component": { "type": "application", - "bom-ref": "1-project@0.1.0", - "name": "project", - "version": "0.1.0" + "bom-ref": "3-uv-workspace", + "name": "uv-workspace" } }, "components": [ @@ -8612,6 +8666,12 @@ fn cyclonedx_export_all_packages_conflicting_workspace_members() -> Result<()> { "value": "child" } ] + }, + { + "type": "application", + "bom-ref": "1-project@0.1.0", + "name": "project", + "version": "0.1.0" } ], "dependencies": [ @@ -8622,6 +8682,13 @@ fn cyclonedx_export_all_packages_conflicting_workspace_members() -> Result<()> { { "ref": "1-project@0.1.0", "dependsOn": [] + }, + { + "ref": "3-uv-workspace", + "dependsOn": [ + "2-child@0.1.0", + "1-project@0.1.0" + ] } ] } From bdf116ae8d5b2be70d0bea83af61ef0ed8227e6e Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Thu, 30 Oct 2025 19:37:15 +0000 Subject: [PATCH 31/36] Fix linting and tests --- crates/uv/tests/it/export.rs | 4 +++- crates/uv/tests/it/show_settings.rs | 4 ++-- docs/guides/export.md | 36 ++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index d99d45221bba4..5b3f0b0231d5e 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -4806,7 +4806,7 @@ fn multiple_packages() -> Result<()> { } #[test] -fn cyclonedx_export() -> Result<()> { +fn cyclonedx_export_basic() -> Result<()> { let context = TestContext::new("3.12").with_cyclonedx_filters(); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( @@ -4981,6 +4981,7 @@ fn cyclonedx_export_direct_url() -> Result<()> { Ok(()) } +#[cfg(feature = "git")] #[test] fn cyclonedx_export_git_dependency() -> Result<()> { let context = TestContext::new("3.12").with_cyclonedx_filters(); @@ -5214,6 +5215,7 @@ fn cyclonedx_export_no_dependencies() -> Result<()> { Ok(()) } +#[cfg(feature = "git")] #[test] fn cyclonedx_export_mixed_source_types() -> Result<()> { let context = TestContext::new("3.12").with_cyclonedx_filters(); diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 01b6703c9cf8a..d1ad284e9f15c 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -7831,7 +7831,7 @@ fn preview_features() { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE, + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | SBOM_EXPORT, ), }, python_preference: Managed, @@ -8059,7 +8059,7 @@ fn preview_features() { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE, + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | SBOM_EXPORT, ), }, python_preference: Managed, diff --git a/docs/guides/export.md b/docs/guides/export.md index 42ec9b2fd6976..c76a0b9b32cdd 100644 --- a/docs/guides/export.md +++ b/docs/guides/export.md @@ -5,7 +5,8 @@ description: Exporting a lockfile to different formats # Exporting a lockfile -uv can export a lockfile to different formats for integration with other tools and workflows. The `uv export` command supports multiple output formats, each suited to different use cases. +uv can export a lockfile to different formats for integration with other tools and workflows. The +`uv export` command supports multiple output formats, each suited to different use cases. For more details on lockfiles and how they're created, see the [project layout](../concepts/projects/layout.md) and @@ -15,9 +16,12 @@ For more details on lockfiles and how they're created, see the uv supports three export formats: -- `requirements.txt`: The traditional pip-compatible [requirements file format](https://pip.pypa.io/en/stable/reference/requirements-file-format/). -- `pylock.toml`: The standardized Python lockfile format defined in [PEP 751](https://peps.python.org/pep-0751/). -- `CycloneDX`: An industry-standard [Software Bill of Materials (SBOM)](https://cyclonedx.org/) format. +- `requirements.txt`: The traditional pip-compatible + [requirements file format](https://pip.pypa.io/en/stable/reference/requirements-file-format/). +- `pylock.toml`: The standardized Python lockfile format defined in + [PEP 751](https://peps.python.org/pep-0751/). +- `CycloneDX`: An industry-standard [Software Bill of Materials (SBOM)](https://cyclonedx.org/) + format. The format can be specified with the `--format` flag: @@ -39,7 +43,8 @@ $ uv export --format cyclonedx1.5 ## `requirements.txt` format -The `requirements.txt` format is the most widely supported format for Python dependencies. It can be used with `pip` and other Python package managers. +The `requirements.txt` format is the most widely supported format for Python dependencies. It can be +used with `pip` and other Python package managers. ### Basic usage @@ -47,7 +52,8 @@ The `requirements.txt` format is the most widely supported format for Python dep $ uv export --format requirements.txt ``` -The generated `requirements.txt` file can then be installed via `uv pip install`, or with other tools like `pip`. +The generated `requirements.txt` file can then be installed via `uv pip install`, or with other +tools like `pip`. !!! note @@ -58,7 +64,8 @@ The generated `requirements.txt` file can then be installed via `uv pip install` ## `pylock.toml` format -[PEP 751](https://peps.python.org/pep-0751/) defines a TOML-based lockfile format for Python dependencies. uv can export your project's dependency lockfile to this format. +[PEP 751](https://peps.python.org/pep-0751/) defines a TOML-based lockfile format for Python +dependencies. uv can export your project's dependency lockfile to this format. ### Basic usage @@ -68,7 +75,9 @@ $ uv export --format pylock.toml ## CycloneDX SBOM format -uv can export your project's dependency lockfile as a Software Bill of Materials (SBOM) in CycloneDX format. SBOMs provide a comprehensive inventory of all software components in your application, which is useful for security auditing, compliance, and supply chain transparency. +uv can export your project's dependency lockfile as a Software Bill of Materials (SBOM) in CycloneDX +format. SBOMs provide a comprehensive inventory of all software components in your application, +which is useful for security auditing, compliance, and supply chain transparency. !!! important @@ -76,7 +85,9 @@ uv can export your project's dependency lockfile as a Software Bill of Materials ### What is CycloneDX? -[CycloneDX](https://cyclonedx.org/) is an industry-standard format for creating Software Bill of Materials. CycloneDX is machine readable and widely supported by security scanning tools, vulnerability databases, and Software Composition Analysis (SCA) platforms. +[CycloneDX](https://cyclonedx.org/) is an industry-standard format for creating Software Bill of +Materials. CycloneDX is machine readable and widely supported by security scanning tools, +vulnerability databases, and Software Composition Analysis (SCA) platforms. ### Basic usage @@ -86,11 +97,14 @@ To export your project's lockfile as a CycloneDX SBOM: $ uv export --format cyclonedx1.5 ``` -This will generate a JSON-encoded CycloneDX v1.5 document containing your project and all of its dependencies. +This will generate a JSON-encoded CycloneDX v1.5 document containing your project and all of its +dependencies. ### SBOM Structure -The generated SBOM follows the [CycloneDX specification](https://cyclonedx.org/specification/overview/). uv also includes the following custom properties on components: +The generated SBOM follows the +[CycloneDX specification](https://cyclonedx.org/specification/overview/). uv also includes the +following custom properties on components: - `uv:package:marker`: Environment markers (e.g., `python_version >= "3.8"`) - `uv:workspace:path`: Relative path for workspace members From fd3dad3424276f82bf7705aa69bdcc78d6e0415c Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Fri, 31 Oct 2025 15:46:53 +0000 Subject: [PATCH 32/36] Use pypi rather than generic purl type, and naming updates --- .../src/lock/export/cyclonedx_json.rs | 37 +- crates/uv/tests/it/export.rs | 1027 +++++++++-------- 2 files changed, 529 insertions(+), 535 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index c8f1f587abba6..be08729e3dc62 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -64,9 +64,9 @@ impl<'a> ComponentBuilder<'a> { self.id_counter += 1; let id = self.id_counter; if let Some(version) = version { - format!("{id}-{name}@{version}") + format!("{name}-{id}@{version}") } else { - format!("{id}-{name}") + format!("{name}-{id}") } } @@ -94,8 +94,8 @@ impl<'a> ComponentBuilder<'a> { let (purl_type, qualifiers) = match &package.id.source { Source::Registry(_) => ("pypi", vec![]), - Source::Git(url, _) => ("generic", vec![("vcs_url", url.as_ref())]), - Source::Direct(url, _) => ("generic", vec![("download_url", url.as_ref())]), + Source::Git(url, _) => ("pypi", vec![("vcs_url", url.as_ref())]), + Source::Direct(url, _) => ("pypi", vec![("download_url", url.as_ref())]), // No purl for local sources Source::Path(_) | Source::Directory(_) | Source::Editable(_) | Source::Virtual(_) => { return None; @@ -136,13 +136,13 @@ impl<'a> ComponentBuilder<'a> { component } - fn create_synthetic_root_component(&mut self) -> Component { - let name = "uv-workspace"; + fn create_synthetic_root_component(&mut self, root: Option<&Package>) -> Component { + let name = root.map(Self::get_package_name).unwrap_or("uv-workspace"); let bom_ref = self.create_bom_ref(name, None); // No need to register as we manually add dependencies in `if all_packages` check in `from_lock` Component { - component_type: Classification::Application, + component_type: Classification::Library, name: NormalizedString::new(name), version: None, bom_ref: Some(bom_ref), @@ -184,17 +184,15 @@ impl<'a> ComponentBuilder<'a> { let purl = Self::create_purl(package).and_then(|purl_string| purl_string.parse().ok()); let mut properties = vec![]; - let classification = match package_type { - PackageType::Root => Classification::Application, + match package_type { PackageType::Workspace(path) => { properties.push(Property::new( "uv:workspace:path", &PortablePath::from(path).to_string(), )); - Classification::Application } - PackageType::Dependency => Classification::Library, - }; + PackageType::Root | PackageType::Dependency => {} + } if let Some(marker_contents) = marker.and_then(|marker| marker.contents()) { properties.push(Property::new( @@ -204,7 +202,7 @@ impl<'a> ComponentBuilder<'a> { } Component { - component_type: classification, + component_type: Classification::Library, name: NormalizedString::new(name), version: version.as_deref().map(NormalizedString::new), bom_ref: Some(bom_ref), @@ -343,7 +341,7 @@ pub fn from_lock<'lock>( // With `--all-packages`, use synthetic root which depends on workspace root and all workspace members. // This ensures that we don't have any dangling components resulting from workspace packages not depended on by the workspace root. if all_packages { - let synthetic_root = component_builder.create_synthetic_root_component(); + let synthetic_root = component_builder.create_synthetic_root_component(root); let synthetic_root_bom_ref = synthetic_root .bom_ref .clone() @@ -356,15 +354,10 @@ pub fn from_lock<'lock>( dependencies.push(Dependency { dependency_ref: synthetic_root_bom_ref, - dependencies: components + dependencies: workspace_member_ids .iter() - .filter_map(|c| { - if c.component_type == Classification::Application { - c.bom_ref.clone() - } else { - None - } - }) + .filter_map(|c| component_builder.get_component(c)) + .map(|c| c.bom_ref.clone().expect("bom-ref should always exist")) .collect(), }); } diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 5b3f0b0231d5e..92c37e200f469 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -4844,8 +4844,8 @@ fn cyclonedx_export_basic() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -4853,21 +4853,21 @@ fn cyclonedx_export_basic() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-anyio@3.7.0", + "bom-ref": "anyio-2@3.7.0", "name": "anyio", "version": "3.7.0", "purl": "pkg:pypi/anyio@3.7.0" }, { "type": "library", - "bom-ref": "3-idna@3.6", + "bom-ref": "idna-3@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6" }, { "type": "library", - "bom-ref": "4-sniffio@1.3.1", + "bom-ref": "sniffio-4@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" @@ -4875,24 +4875,24 @@ fn cyclonedx_export_basic() -> Result<()> { ], "dependencies": [ { - "ref": "2-anyio@3.7.0", + "ref": "anyio-2@3.7.0", "dependsOn": [ - "3-idna@3.6", - "4-sniffio@1.3.1" + "idna-3@3.6", + "sniffio-4@1.3.1" ] }, { - "ref": "3-idna@3.6", + "ref": "idna-3@3.6", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-anyio@3.7.0" + "anyio-2@3.7.0" ] }, { - "ref": "4-sniffio@1.3.1", + "ref": "sniffio-4@1.3.1", "dependsOn": [] } ] @@ -4945,8 +4945,8 @@ fn cyclonedx_export_direct_url() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -4954,21 +4954,21 @@ fn cyclonedx_export_direct_url() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-idna@3.6", + "bom-ref": "idna-2@3.6", "name": "idna", "version": "3.6", - "purl": "pkg:generic/idna@3.6?download_url=https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl" + "purl": "pkg:pypi/idna@3.6?download_url=https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl" } ], "dependencies": [ { - "ref": "2-idna@3.6", + "ref": "idna-2@3.6", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-idna@3.6" + "idna-2@3.6" ] } ] @@ -5022,8 +5022,8 @@ fn cyclonedx_export_git_dependency() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -5031,21 +5031,21 @@ fn cyclonedx_export_git_dependency() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-blinker@1.7.0", + "bom-ref": "blinker-2@1.7.0", "name": "blinker", "version": "1.7.0", "purl": "pkg:pypi/blinker@1.7.0" }, { "type": "library", - "bom-ref": "3-click@8.1.7", + "bom-ref": "click-3@8.1.7", "name": "click", "version": "8.1.7", "purl": "pkg:pypi/click@8.1.7" }, { "type": "library", - "bom-ref": "4-colorama@0.4.6", + "bom-ref": "colorama-4@0.4.6", "name": "colorama", "version": "0.4.6", "purl": "pkg:pypi/colorama@0.4.6", @@ -5058,35 +5058,35 @@ fn cyclonedx_export_git_dependency() -> Result<()> { }, { "type": "library", - "bom-ref": "5-flask@2.3.3", + "bom-ref": "flask-5@2.3.3", "name": "flask", "version": "2.3.3", - "purl": "pkg:generic/flask@2.3.3?vcs_url=https://github.com/pallets/flask.git%3Frev%3D2.3.3%233205b53c7cf69d17fee49cac6b84978175b7dd73" + "purl": "pkg:pypi/flask@2.3.3?vcs_url=https://github.com/pallets/flask.git%3Frev%3D2.3.3%233205b53c7cf69d17fee49cac6b84978175b7dd73" }, { "type": "library", - "bom-ref": "6-itsdangerous@2.1.2", + "bom-ref": "itsdangerous-6@2.1.2", "name": "itsdangerous", "version": "2.1.2", "purl": "pkg:pypi/itsdangerous@2.1.2" }, { "type": "library", - "bom-ref": "7-jinja2@3.1.3", + "bom-ref": "jinja2-7@3.1.3", "name": "jinja2", "version": "3.1.3", "purl": "pkg:pypi/jinja2@3.1.3" }, { "type": "library", - "bom-ref": "8-markupsafe@2.1.5", + "bom-ref": "markupsafe-8@2.1.5", "name": "markupsafe", "version": "2.1.5", "purl": "pkg:pypi/markupsafe@2.1.5" }, { "type": "library", - "bom-ref": "9-werkzeug@3.0.1", + "bom-ref": "werkzeug-9@3.0.1", "name": "werkzeug", "version": "3.0.1", "purl": "pkg:pypi/werkzeug@3.0.1" @@ -5094,53 +5094,53 @@ fn cyclonedx_export_git_dependency() -> Result<()> { ], "dependencies": [ { - "ref": "2-blinker@1.7.0", + "ref": "blinker-2@1.7.0", "dependsOn": [] }, { - "ref": "3-click@8.1.7", + "ref": "click-3@8.1.7", "dependsOn": [ - "4-colorama@0.4.6" + "colorama-4@0.4.6" ] }, { - "ref": "4-colorama@0.4.6", + "ref": "colorama-4@0.4.6", "dependsOn": [] }, { - "ref": "5-flask@2.3.3", + "ref": "flask-5@2.3.3", "dependsOn": [ - "2-blinker@1.7.0", - "3-click@8.1.7", - "6-itsdangerous@2.1.2", - "7-jinja2@3.1.3", - "9-werkzeug@3.0.1" + "blinker-2@1.7.0", + "click-3@8.1.7", + "itsdangerous-6@2.1.2", + "jinja2-7@3.1.3", + "werkzeug-9@3.0.1" ] }, { - "ref": "6-itsdangerous@2.1.2", + "ref": "itsdangerous-6@2.1.2", "dependsOn": [] }, { - "ref": "7-jinja2@3.1.3", + "ref": "jinja2-7@3.1.3", "dependsOn": [ - "8-markupsafe@2.1.5" + "markupsafe-8@2.1.5" ] }, { - "ref": "8-markupsafe@2.1.5", + "ref": "markupsafe-8@2.1.5", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "5-flask@2.3.3" + "flask-5@2.3.3" ] }, { - "ref": "9-werkzeug@3.0.1", + "ref": "werkzeug-9@3.0.1", "dependsOn": [ - "8-markupsafe@2.1.5" + "markupsafe-8@2.1.5" ] } ] @@ -5193,8 +5193,8 @@ fn cyclonedx_export_no_dependencies() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-standalone-project@1.0.0", + "type": "library", + "bom-ref": "standalone-project-1@1.0.0", "name": "standalone-project", "version": "1.0.0" } @@ -5202,7 +5202,7 @@ fn cyclonedx_export_no_dependencies() -> Result<()> { "components": [], "dependencies": [ { - "ref": "1-standalone-project@1.0.0", + "ref": "standalone-project-1@1.0.0", "dependsOn": [] } ] @@ -5260,8 +5260,8 @@ fn cyclonedx_export_mixed_source_types() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-mixed-project@0.1.0", + "type": "library", + "bom-ref": "mixed-project-1@0.1.0", "name": "mixed-project", "version": "0.1.0" } @@ -5269,35 +5269,35 @@ fn cyclonedx_export_mixed_source_types() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-blinker@1.7.0", + "bom-ref": "blinker-2@1.7.0", "name": "blinker", "version": "1.7.0", "purl": "pkg:pypi/blinker@1.7.0" }, { "type": "library", - "bom-ref": "3-certifi@2024.2.2", + "bom-ref": "certifi-3@2024.2.2", "name": "certifi", "version": "2024.2.2", "purl": "pkg:pypi/certifi@2024.2.2" }, { "type": "library", - "bom-ref": "4-charset-normalizer@3.3.2", + "bom-ref": "charset-normalizer-4@3.3.2", "name": "charset-normalizer", "version": "3.3.2", "purl": "pkg:pypi/charset-normalizer@3.3.2" }, { "type": "library", - "bom-ref": "5-click@8.1.7", + "bom-ref": "click-5@8.1.7", "name": "click", "version": "8.1.7", "purl": "pkg:pypi/click@8.1.7" }, { "type": "library", - "bom-ref": "6-colorama@0.4.6", + "bom-ref": "colorama-6@0.4.6", "name": "colorama", "version": "0.4.6", "purl": "pkg:pypi/colorama@0.4.6", @@ -5310,63 +5310,63 @@ fn cyclonedx_export_mixed_source_types() -> Result<()> { }, { "type": "library", - "bom-ref": "7-flask@2.3.3", + "bom-ref": "flask-7@2.3.3", "name": "flask", "version": "2.3.3", - "purl": "pkg:generic/flask@2.3.3?vcs_url=https://github.com/pallets/flask.git%3Frev%3D2.3.3%233205b53c7cf69d17fee49cac6b84978175b7dd73" + "purl": "pkg:pypi/flask@2.3.3?vcs_url=https://github.com/pallets/flask.git%3Frev%3D2.3.3%233205b53c7cf69d17fee49cac6b84978175b7dd73" }, { "type": "library", - "bom-ref": "8-idna@3.6", + "bom-ref": "idna-8@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6" }, { "type": "library", - "bom-ref": "9-iniconfig@2.0.0", + "bom-ref": "iniconfig-9@2.0.0", "name": "iniconfig", "version": "2.0.0", - "purl": "pkg:generic/iniconfig@2.0.0?download_url=https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" + "purl": "pkg:pypi/iniconfig@2.0.0?download_url=https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" }, { "type": "library", - "bom-ref": "10-itsdangerous@2.1.2", + "bom-ref": "itsdangerous-10@2.1.2", "name": "itsdangerous", "version": "2.1.2", "purl": "pkg:pypi/itsdangerous@2.1.2" }, { "type": "library", - "bom-ref": "11-jinja2@3.1.3", + "bom-ref": "jinja2-11@3.1.3", "name": "jinja2", "version": "3.1.3", "purl": "pkg:pypi/jinja2@3.1.3" }, { "type": "library", - "bom-ref": "12-markupsafe@2.1.5", + "bom-ref": "markupsafe-12@2.1.5", "name": "markupsafe", "version": "2.1.5", "purl": "pkg:pypi/markupsafe@2.1.5" }, { "type": "library", - "bom-ref": "13-requests@2.31.0", + "bom-ref": "requests-13@2.31.0", "name": "requests", "version": "2.31.0", "purl": "pkg:pypi/requests@2.31.0" }, { "type": "library", - "bom-ref": "14-urllib3@2.2.1", + "bom-ref": "urllib3-14@2.2.1", "name": "urllib3", "version": "2.2.1", "purl": "pkg:pypi/urllib3@2.2.1" }, { "type": "library", - "bom-ref": "15-werkzeug@3.0.1", + "bom-ref": "werkzeug-15@3.0.1", "name": "werkzeug", "version": "3.0.1", "purl": "pkg:pypi/werkzeug@3.0.1" @@ -5374,84 +5374,84 @@ fn cyclonedx_export_mixed_source_types() -> Result<()> { ], "dependencies": [ { - "ref": "2-blinker@1.7.0", + "ref": "blinker-2@1.7.0", "dependsOn": [] }, { - "ref": "3-certifi@2024.2.2", + "ref": "certifi-3@2024.2.2", "dependsOn": [] }, { - "ref": "4-charset-normalizer@3.3.2", + "ref": "charset-normalizer-4@3.3.2", "dependsOn": [] }, { - "ref": "5-click@8.1.7", + "ref": "click-5@8.1.7", "dependsOn": [ - "6-colorama@0.4.6" + "colorama-6@0.4.6" ] }, { - "ref": "6-colorama@0.4.6", + "ref": "colorama-6@0.4.6", "dependsOn": [] }, { - "ref": "7-flask@2.3.3", + "ref": "flask-7@2.3.3", "dependsOn": [ - "10-itsdangerous@2.1.2", - "11-jinja2@3.1.3", - "15-werkzeug@3.0.1", - "2-blinker@1.7.0", - "5-click@8.1.7" + "blinker-2@1.7.0", + "click-5@8.1.7", + "itsdangerous-10@2.1.2", + "jinja2-11@3.1.3", + "werkzeug-15@3.0.1" ] }, { - "ref": "8-idna@3.6", + "ref": "idna-8@3.6", "dependsOn": [] }, { - "ref": "9-iniconfig@2.0.0", + "ref": "iniconfig-9@2.0.0", "dependsOn": [] }, { - "ref": "10-itsdangerous@2.1.2", + "ref": "itsdangerous-10@2.1.2", "dependsOn": [] }, { - "ref": "11-jinja2@3.1.3", + "ref": "jinja2-11@3.1.3", "dependsOn": [ - "12-markupsafe@2.1.5" + "markupsafe-12@2.1.5" ] }, { - "ref": "12-markupsafe@2.1.5", + "ref": "markupsafe-12@2.1.5", "dependsOn": [] }, { - "ref": "1-mixed-project@0.1.0", + "ref": "mixed-project-1@0.1.0", "dependsOn": [ - "13-requests@2.31.0", - "7-flask@2.3.3", - "9-iniconfig@2.0.0" + "flask-7@2.3.3", + "iniconfig-9@2.0.0", + "requests-13@2.31.0" ] }, { - "ref": "13-requests@2.31.0", + "ref": "requests-13@2.31.0", "dependsOn": [ - "14-urllib3@2.2.1", - "3-certifi@2024.2.2", - "4-charset-normalizer@3.3.2", - "8-idna@3.6" + "certifi-3@2024.2.2", + "charset-normalizer-4@3.3.2", + "idna-8@3.6", + "urllib3-14@2.2.1" ] }, { - "ref": "14-urllib3@2.2.1", + "ref": "urllib3-14@2.2.1", "dependsOn": [] }, { - "ref": "15-werkzeug@3.0.1", + "ref": "werkzeug-15@3.0.1", "dependsOn": [ - "12-markupsafe@2.1.5" + "markupsafe-12@2.1.5" ] } ] @@ -5508,8 +5508,8 @@ fn cyclonedx_export_project_extra() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -5517,7 +5517,7 @@ fn cyclonedx_export_project_extra() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-typing-extensions@4.10.0", + "bom-ref": "typing-extensions-2@4.10.0", "name": "typing-extensions", "version": "4.10.0", "purl": "pkg:pypi/typing-extensions@4.10.0" @@ -5525,13 +5525,13 @@ fn cyclonedx_export_project_extra() -> Result<()> { ], "dependencies": [ { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-typing-extensions@4.10.0" + "typing-extensions-2@4.10.0" ] }, { - "ref": "2-typing-extensions@4.10.0", + "ref": "typing-extensions-2@4.10.0", "dependsOn": [] } ] @@ -5588,8 +5588,8 @@ fn cyclonedx_export_project_extra_with_optional_flag() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -5597,35 +5597,35 @@ fn cyclonedx_export_project_extra_with_optional_flag() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-anyio@3.7.0", + "bom-ref": "anyio-2@3.7.0", "name": "anyio", "version": "3.7.0", "purl": "pkg:pypi/anyio@3.7.0" }, { "type": "library", - "bom-ref": "3-idna@3.6", + "bom-ref": "idna-3@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6" }, { "type": "library", - "bom-ref": "4-iniconfig@2.0.0", + "bom-ref": "iniconfig-4@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "5-sniffio@1.3.1", + "bom-ref": "sniffio-5@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" }, { "type": "library", - "bom-ref": "6-typing-extensions@4.10.0", + "bom-ref": "typing-extensions-6@4.10.0", "name": "typing-extensions", "version": "4.10.0", "purl": "pkg:pypi/typing-extensions@4.10.0" @@ -5633,34 +5633,34 @@ fn cyclonedx_export_project_extra_with_optional_flag() -> Result<()> { ], "dependencies": [ { - "ref": "2-anyio@3.7.0", + "ref": "anyio-2@3.7.0", "dependsOn": [ - "3-idna@3.6", - "5-sniffio@1.3.1" + "idna-3@3.6", + "sniffio-5@1.3.1" ] }, { - "ref": "3-idna@3.6", + "ref": "idna-3@3.6", "dependsOn": [] }, { - "ref": "4-iniconfig@2.0.0", + "ref": "iniconfig-4@2.0.0", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-anyio@3.7.0", - "4-iniconfig@2.0.0", - "6-typing-extensions@4.10.0" + "anyio-2@3.7.0", + "iniconfig-4@2.0.0", + "typing-extensions-6@4.10.0" ] }, { - "ref": "5-sniffio@1.3.1", + "ref": "sniffio-5@1.3.1", "dependsOn": [] }, { - "ref": "6-typing-extensions@4.10.0", + "ref": "typing-extensions-6@4.10.0", "dependsOn": [] } ] @@ -5750,8 +5750,8 @@ fn cyclonedx_export_with_workspace_member() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -5759,14 +5759,14 @@ fn cyclonedx_export_with_workspace_member() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-anyio@3.7.0", + "bom-ref": "anyio-2@3.7.0", "name": "anyio", "version": "3.7.0", "purl": "pkg:pypi/anyio@3.7.0" }, { - "type": "application", - "bom-ref": "3-child1@0.1.0", + "type": "library", + "bom-ref": "child1-3@0.1.0", "name": "child1", "version": "0.1.0", "properties": [ @@ -5777,8 +5777,8 @@ fn cyclonedx_export_with_workspace_member() -> Result<()> { ] }, { - "type": "application", - "bom-ref": "4-child2@0.2.9", + "type": "library", + "bom-ref": "child2-4@0.2.9", "name": "child2", "version": "0.2.9", "properties": [ @@ -5790,21 +5790,21 @@ fn cyclonedx_export_with_workspace_member() -> Result<()> { }, { "type": "library", - "bom-ref": "5-idna@3.6", + "bom-ref": "idna-5@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6" }, { "type": "library", - "bom-ref": "6-iniconfig@2.0.0", + "bom-ref": "iniconfig-6@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "7-sniffio@1.3.1", + "bom-ref": "sniffio-7@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" @@ -5812,40 +5812,40 @@ fn cyclonedx_export_with_workspace_member() -> Result<()> { ], "dependencies": [ { - "ref": "2-anyio@3.7.0", + "ref": "anyio-2@3.7.0", "dependsOn": [ - "5-idna@3.6", - "7-sniffio@1.3.1" + "idna-5@3.6", + "sniffio-7@1.3.1" ] }, { - "ref": "3-child1@0.1.0", + "ref": "child1-3@0.1.0", "dependsOn": [ - "6-iniconfig@2.0.0" + "iniconfig-6@2.0.0" ] }, { - "ref": "4-child2@0.2.9", + "ref": "child2-4@0.2.9", "dependsOn": [] }, { - "ref": "5-idna@3.6", + "ref": "idna-5@3.6", "dependsOn": [] }, { - "ref": "6-iniconfig@2.0.0", + "ref": "iniconfig-6@2.0.0", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-anyio@3.7.0", - "3-child1@0.1.0", - "4-child2@0.2.9" + "anyio-2@3.7.0", + "child1-3@0.1.0", + "child2-4@0.2.9" ] }, { - "ref": "7-sniffio@1.3.1", + "ref": "sniffio-7@1.3.1", "dependsOn": [] } ] @@ -5919,8 +5919,8 @@ fn cyclonedx_export_workspace_non_root() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-child@0.1.0", + "type": "library", + "bom-ref": "child-1@0.1.0", "name": "child", "version": "0.1.0" } @@ -5928,7 +5928,7 @@ fn cyclonedx_export_workspace_non_root() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-iniconfig@2.0.0", + "bom-ref": "iniconfig-2@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" @@ -5936,13 +5936,13 @@ fn cyclonedx_export_workspace_non_root() -> Result<()> { ], "dependencies": [ { - "ref": "1-child@0.1.0", + "ref": "child-1@0.1.0", "dependsOn": [ - "2-iniconfig@2.0.0" + "iniconfig-2@2.0.0" ] }, { - "ref": "2-iniconfig@2.0.0", + "ref": "iniconfig-2@2.0.0", "dependsOn": [] } ] @@ -6020,16 +6020,16 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } }, "components": [ { - "type": "application", - "bom-ref": "2-child@0.1.0", + "type": "library", + "bom-ref": "child-2@0.1.0", "name": "child", "version": "0.1.0", "properties": [ @@ -6041,7 +6041,7 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { }, { "type": "library", - "bom-ref": "3-typing-extensions@4.10.0", + "bom-ref": "typing-extensions-3@4.10.0", "name": "typing-extensions", "version": "4.10.0", "purl": "pkg:pypi/typing-extensions@4.10.0" @@ -6049,19 +6049,19 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { ], "dependencies": [ { - "ref": "2-child@0.1.0", + "ref": "child-2@0.1.0", "dependsOn": [ - "3-typing-extensions@4.10.0" + "typing-extensions-3@4.10.0" ] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-child@0.1.0" + "child-2@0.1.0" ] }, { - "ref": "3-typing-extensions@4.10.0", + "ref": "typing-extensions-3@4.10.0", "dependsOn": [] } ] @@ -6090,16 +6090,16 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } }, "components": [ { - "type": "application", - "bom-ref": "2-child@0.1.0", + "type": "library", + "bom-ref": "child-2@0.1.0", "name": "child", "version": "0.1.0", "properties": [ @@ -6111,7 +6111,7 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { }, { "type": "library", - "bom-ref": "3-typing-extensions@4.10.0", + "bom-ref": "typing-extensions-3@4.10.0", "name": "typing-extensions", "version": "4.10.0", "purl": "pkg:pypi/typing-extensions@4.10.0" @@ -6119,19 +6119,19 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { ], "dependencies": [ { - "ref": "2-child@0.1.0", + "ref": "child-2@0.1.0", "dependsOn": [ - "3-typing-extensions@4.10.0" + "typing-extensions-3@4.10.0" ] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-child@0.1.0" + "child-2@0.1.0" ] }, { - "ref": "3-typing-extensions@4.10.0", + "ref": "typing-extensions-3@4.10.0", "dependsOn": [] } ] @@ -6219,22 +6219,22 @@ fn cyclonedx_export_workspace_frozen() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "7-uv-workspace", - "name": "uv-workspace" + "type": "library", + "bom-ref": "project-7", + "name": "project" } }, "components": [ { "type": "library", - "bom-ref": "2-anyio@3.7.0", + "bom-ref": "anyio-2@3.7.0", "name": "anyio", "version": "3.7.0", "purl": "pkg:pypi/anyio@3.7.0" }, { - "type": "application", - "bom-ref": "3-child@0.1.0", + "type": "library", + "bom-ref": "child-3@0.1.0", "name": "child", "version": "0.1.0", "properties": [ @@ -6246,70 +6246,70 @@ fn cyclonedx_export_workspace_frozen() -> Result<()> { }, { "type": "library", - "bom-ref": "4-idna@3.6", + "bom-ref": "idna-4@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6" }, { "type": "library", - "bom-ref": "5-iniconfig@2.0.0", + "bom-ref": "iniconfig-5@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "6-sniffio@1.3.1", + "bom-ref": "sniffio-6@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" }, { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } ], "dependencies": [ { - "ref": "2-anyio@3.7.0", + "ref": "anyio-2@3.7.0", "dependsOn": [ - "4-idna@3.6", - "6-sniffio@1.3.1" + "idna-4@3.6", + "sniffio-6@1.3.1" ] }, { - "ref": "3-child@0.1.0", + "ref": "child-3@0.1.0", "dependsOn": [ - "5-iniconfig@2.0.0" + "iniconfig-5@2.0.0" ] }, { - "ref": "4-idna@3.6", + "ref": "idna-4@3.6", "dependsOn": [] }, { - "ref": "5-iniconfig@2.0.0", + "ref": "iniconfig-5@2.0.0", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-anyio@3.7.0", - "3-child@0.1.0" + "anyio-2@3.7.0", + "child-3@0.1.0" ] }, { - "ref": "6-sniffio@1.3.1", + "ref": "sniffio-6@1.3.1", "dependsOn": [] }, { - "ref": "7-uv-workspace", + "ref": "project-7", "dependsOn": [ - "3-child@0.1.0", - "1-project@0.1.0" + "child-3@0.1.0", + "project-1@0.1.0" ] } ] @@ -6394,22 +6394,22 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "8-uv-workspace", - "name": "uv-workspace" + "type": "library", + "bom-ref": "project-8", + "name": "project" } }, "components": [ { "type": "library", - "bom-ref": "2-anyio@3.7.0", + "bom-ref": "anyio-2@3.7.0", "name": "anyio", "version": "3.7.0", "purl": "pkg:pypi/anyio@3.7.0" }, { - "type": "application", - "bom-ref": "3-child1@0.1.0", + "type": "library", + "bom-ref": "child1-3@0.1.0", "name": "child1", "version": "0.1.0", "properties": [ @@ -6420,8 +6420,8 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { ] }, { - "type": "application", - "bom-ref": "4-child2@0.2.0", + "type": "library", + "bom-ref": "child2-4@0.2.0", "name": "child2", "version": "0.2.0", "properties": [ @@ -6433,76 +6433,76 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { }, { "type": "library", - "bom-ref": "5-idna@3.6", + "bom-ref": "idna-5@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6" }, { "type": "library", - "bom-ref": "6-iniconfig@2.0.0", + "bom-ref": "iniconfig-6@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "7-sniffio@1.3.1", + "bom-ref": "sniffio-7@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" }, { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } ], "dependencies": [ { - "ref": "2-anyio@3.7.0", + "ref": "anyio-2@3.7.0", "dependsOn": [ - "5-idna@3.6", - "7-sniffio@1.3.1" + "idna-5@3.6", + "sniffio-7@1.3.1" ] }, { - "ref": "3-child1@0.1.0", + "ref": "child1-3@0.1.0", "dependsOn": [ - "6-iniconfig@2.0.0" + "iniconfig-6@2.0.0" ] }, { - "ref": "4-child2@0.2.0", + "ref": "child2-4@0.2.0", "dependsOn": [ - "7-sniffio@1.3.1" + "sniffio-7@1.3.1" ] }, { - "ref": "5-idna@3.6", + "ref": "idna-5@3.6", "dependsOn": [] }, { - "ref": "6-iniconfig@2.0.0", + "ref": "iniconfig-6@2.0.0", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-anyio@3.7.0" + "anyio-2@3.7.0" ] }, { - "ref": "7-sniffio@1.3.1", + "ref": "sniffio-7@1.3.1", "dependsOn": [] }, { - "ref": "8-uv-workspace", + "ref": "project-8", "dependsOn": [ - "3-child1@0.1.0", - "4-child2@0.2.0", - "1-project@0.1.0" + "child1-3@0.1.0", + "child2-4@0.2.0", + "project-1@0.1.0" ] } ] @@ -6515,8 +6515,9 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { Ok(()) } +// Contains a combination of combination of workspace and registry deps, with another workspace dep not depended on by the root #[test] -fn cyclonedx_export_workspace_complex_dependencies() -> Result<()> { +fn cyclonedx_export_workspace_mixed_dependencies() -> Result<()> { let context = TestContext::new("3.12").with_cyclonedx_filters(); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -6594,8 +6595,8 @@ fn cyclonedx_export_workspace_complex_dependencies() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -6603,14 +6604,14 @@ fn cyclonedx_export_workspace_complex_dependencies() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-anyio@3.7.0", + "bom-ref": "anyio-2@3.7.0", "name": "anyio", "version": "3.7.0", "purl": "pkg:pypi/anyio@3.7.0" }, { - "type": "application", - "bom-ref": "3-child1@0.1.0", + "type": "library", + "bom-ref": "child1-3@0.1.0", "name": "child1", "version": "0.1.0", "properties": [ @@ -6621,8 +6622,8 @@ fn cyclonedx_export_workspace_complex_dependencies() -> Result<()> { ] }, { - "type": "application", - "bom-ref": "4-child2@0.2.0", + "type": "library", + "bom-ref": "child2-4@0.2.0", "name": "child2", "version": "0.2.0", "properties": [ @@ -6634,21 +6635,21 @@ fn cyclonedx_export_workspace_complex_dependencies() -> Result<()> { }, { "type": "library", - "bom-ref": "5-idna@3.6", + "bom-ref": "idna-5@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6" }, { "type": "library", - "bom-ref": "6-iniconfig@2.0.0", + "bom-ref": "iniconfig-6@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "7-sniffio@1.3.1", + "bom-ref": "sniffio-7@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" @@ -6656,42 +6657,42 @@ fn cyclonedx_export_workspace_complex_dependencies() -> Result<()> { ], "dependencies": [ { - "ref": "2-anyio@3.7.0", + "ref": "anyio-2@3.7.0", "dependsOn": [ - "5-idna@3.6", - "7-sniffio@1.3.1" + "idna-5@3.6", + "sniffio-7@1.3.1" ] }, { - "ref": "3-child1@0.1.0", + "ref": "child1-3@0.1.0", "dependsOn": [ - "4-child2@0.2.0", - "6-iniconfig@2.0.0" + "child2-4@0.2.0", + "iniconfig-6@2.0.0" ] }, { - "ref": "4-child2@0.2.0", + "ref": "child2-4@0.2.0", "dependsOn": [ - "7-sniffio@1.3.1" + "sniffio-7@1.3.1" ] }, { - "ref": "5-idna@3.6", + "ref": "idna-5@3.6", "dependsOn": [] }, { - "ref": "6-iniconfig@2.0.0", + "ref": "iniconfig-6@2.0.0", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-anyio@3.7.0", - "3-child1@0.1.0" + "anyio-2@3.7.0", + "child1-3@0.1.0" ] }, { - "ref": "7-sniffio@1.3.1", + "ref": "sniffio-7@1.3.1", "dependsOn": [] } ] @@ -6744,8 +6745,8 @@ fn cyclonedx_export_dependency_marker() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -6753,7 +6754,7 @@ fn cyclonedx_export_dependency_marker() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-anyio@4.3.0", + "bom-ref": "anyio-2@4.3.0", "name": "anyio", "version": "4.3.0", "purl": "pkg:pypi/anyio@4.3.0", @@ -6766,7 +6767,7 @@ fn cyclonedx_export_dependency_marker() -> Result<()> { }, { "type": "library", - "bom-ref": "3-idna@3.6", + "bom-ref": "idna-3@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6", @@ -6779,14 +6780,14 @@ fn cyclonedx_export_dependency_marker() -> Result<()> { }, { "type": "library", - "bom-ref": "4-iniconfig@2.0.0", + "bom-ref": "iniconfig-4@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "5-sniffio@1.3.1", + "bom-ref": "sniffio-5@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1", @@ -6800,29 +6801,29 @@ fn cyclonedx_export_dependency_marker() -> Result<()> { ], "dependencies": [ { - "ref": "2-anyio@4.3.0", + "ref": "anyio-2@4.3.0", "dependsOn": [ - "3-idna@3.6", - "5-sniffio@1.3.1" + "idna-3@3.6", + "sniffio-5@1.3.1" ] }, { - "ref": "3-idna@3.6", + "ref": "idna-3@3.6", "dependsOn": [] }, { - "ref": "4-iniconfig@2.0.0", + "ref": "iniconfig-4@2.0.0", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-anyio@4.3.0", - "4-iniconfig@2.0.0" + "anyio-2@4.3.0", + "iniconfig-4@2.0.0" ] }, { - "ref": "5-sniffio@1.3.1", + "ref": "sniffio-5@1.3.1", "dependsOn": [] } ] @@ -6878,8 +6879,8 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -6887,7 +6888,7 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-attrs@23.2.0", + "bom-ref": "attrs-2@23.2.0", "name": "attrs", "version": "23.2.0", "purl": "pkg:pypi/attrs@23.2.0", @@ -6900,7 +6901,7 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { }, { "type": "library", - "bom-ref": "3-cffi@1.16.0", + "bom-ref": "cffi-3@1.16.0", "name": "cffi", "version": "1.16.0", "purl": "pkg:pypi/cffi@1.16.0", @@ -6913,7 +6914,7 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { }, { "type": "library", - "bom-ref": "4-exceptiongroup@1.2.0", + "bom-ref": "exceptiongroup-4@1.2.0", "name": "exceptiongroup", "version": "1.2.0", "purl": "pkg:pypi/exceptiongroup@1.2.0", @@ -6926,7 +6927,7 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { }, { "type": "library", - "bom-ref": "5-idna@3.6", + "bom-ref": "idna-5@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6", @@ -6939,7 +6940,7 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { }, { "type": "library", - "bom-ref": "6-outcome@1.3.0.post0", + "bom-ref": "outcome-6@1.3.0.post0", "name": "outcome", "version": "1.3.0.post0", "purl": "pkg:pypi/outcome@1.3.0.post0", @@ -6952,7 +6953,7 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { }, { "type": "library", - "bom-ref": "7-pycparser@2.21", + "bom-ref": "pycparser-7@2.21", "name": "pycparser", "version": "2.21", "purl": "pkg:pypi/pycparser@2.21", @@ -6965,7 +6966,7 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { }, { "type": "library", - "bom-ref": "8-sniffio@1.3.1", + "bom-ref": "sniffio-8@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1", @@ -6978,7 +6979,7 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { }, { "type": "library", - "bom-ref": "9-sortedcontainers@2.4.0", + "bom-ref": "sortedcontainers-9@2.4.0", "name": "sortedcontainers", "version": "2.4.0", "purl": "pkg:pypi/sortedcontainers@2.4.0", @@ -6991,7 +6992,7 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { }, { "type": "library", - "bom-ref": "10-trio@0.25.0", + "bom-ref": "trio-10@0.25.0", "name": "trio", "version": "0.25.0", "purl": "pkg:pypi/trio@0.25.0", @@ -7005,57 +7006,57 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { ], "dependencies": [ { - "ref": "2-attrs@23.2.0", + "ref": "attrs-2@23.2.0", "dependsOn": [] }, { - "ref": "3-cffi@1.16.0", + "ref": "cffi-3@1.16.0", "dependsOn": [ - "7-pycparser@2.21" + "pycparser-7@2.21" ] }, { - "ref": "4-exceptiongroup@1.2.0", + "ref": "exceptiongroup-4@1.2.0", "dependsOn": [] }, { - "ref": "5-idna@3.6", + "ref": "idna-5@3.6", "dependsOn": [] }, { - "ref": "6-outcome@1.3.0.post0", + "ref": "outcome-6@1.3.0.post0", "dependsOn": [ - "2-attrs@23.2.0" + "attrs-2@23.2.0" ] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "10-trio@0.25.0" + "trio-10@0.25.0" ] }, { - "ref": "7-pycparser@2.21", + "ref": "pycparser-7@2.21", "dependsOn": [] }, { - "ref": "8-sniffio@1.3.1", + "ref": "sniffio-8@1.3.1", "dependsOn": [] }, { - "ref": "9-sortedcontainers@2.4.0", + "ref": "sortedcontainers-9@2.4.0", "dependsOn": [] }, { - "ref": "10-trio@0.25.0", + "ref": "trio-10@0.25.0", "dependsOn": [ - "2-attrs@23.2.0", - "3-cffi@1.16.0", - "4-exceptiongroup@1.2.0", - "5-idna@3.6", - "6-outcome@1.3.0.post0", - "8-sniffio@1.3.1", - "9-sortedcontainers@2.4.0" + "attrs-2@23.2.0", + "cffi-3@1.16.0", + "exceptiongroup-4@1.2.0", + "idna-5@3.6", + "outcome-6@1.3.0.post0", + "sniffio-8@1.3.1", + "sortedcontainers-9@2.4.0" ] } ] @@ -7108,8 +7109,8 @@ fn cyclonedx_export_dependency_extra() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -7117,21 +7118,21 @@ fn cyclonedx_export_dependency_extra() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-blinker@1.7.0", + "bom-ref": "blinker-2@1.7.0", "name": "blinker", "version": "1.7.0", "purl": "pkg:pypi/blinker@1.7.0" }, { "type": "library", - "bom-ref": "3-click@8.1.7", + "bom-ref": "click-3@8.1.7", "name": "click", "version": "8.1.7", "purl": "pkg:pypi/click@8.1.7" }, { "type": "library", - "bom-ref": "4-colorama@0.4.6", + "bom-ref": "colorama-4@0.4.6", "name": "colorama", "version": "0.4.6", "purl": "pkg:pypi/colorama@0.4.6", @@ -7144,42 +7145,42 @@ fn cyclonedx_export_dependency_extra() -> Result<()> { }, { "type": "library", - "bom-ref": "5-flask@3.0.2", + "bom-ref": "flask-5@3.0.2", "name": "flask", "version": "3.0.2", "purl": "pkg:pypi/flask@3.0.2" }, { "type": "library", - "bom-ref": "6-itsdangerous@2.1.2", + "bom-ref": "itsdangerous-6@2.1.2", "name": "itsdangerous", "version": "2.1.2", "purl": "pkg:pypi/itsdangerous@2.1.2" }, { "type": "library", - "bom-ref": "7-jinja2@3.1.3", + "bom-ref": "jinja2-7@3.1.3", "name": "jinja2", "version": "3.1.3", "purl": "pkg:pypi/jinja2@3.1.3" }, { "type": "library", - "bom-ref": "8-markupsafe@2.1.5", + "bom-ref": "markupsafe-8@2.1.5", "name": "markupsafe", "version": "2.1.5", "purl": "pkg:pypi/markupsafe@2.1.5" }, { "type": "library", - "bom-ref": "9-python-dotenv@1.0.1", + "bom-ref": "python-dotenv-9@1.0.1", "name": "python-dotenv", "version": "1.0.1", "purl": "pkg:pypi/python-dotenv@1.0.1" }, { "type": "library", - "bom-ref": "10-werkzeug@3.0.1", + "bom-ref": "werkzeug-10@3.0.1", "name": "werkzeug", "version": "3.0.1", "purl": "pkg:pypi/werkzeug@3.0.1" @@ -7187,58 +7188,58 @@ fn cyclonedx_export_dependency_extra() -> Result<()> { ], "dependencies": [ { - "ref": "2-blinker@1.7.0", + "ref": "blinker-2@1.7.0", "dependsOn": [] }, { - "ref": "3-click@8.1.7", + "ref": "click-3@8.1.7", "dependsOn": [ - "4-colorama@0.4.6" + "colorama-4@0.4.6" ] }, { - "ref": "4-colorama@0.4.6", + "ref": "colorama-4@0.4.6", "dependsOn": [] }, { - "ref": "5-flask@3.0.2", + "ref": "flask-5@3.0.2", "dependsOn": [ - "10-werkzeug@3.0.1", - "2-blinker@1.7.0", - "3-click@8.1.7", - "6-itsdangerous@2.1.2", - "7-jinja2@3.1.3", - "9-python-dotenv@1.0.1" + "blinker-2@1.7.0", + "click-3@8.1.7", + "itsdangerous-6@2.1.2", + "jinja2-7@3.1.3", + "python-dotenv-9@1.0.1", + "werkzeug-10@3.0.1" ] }, { - "ref": "6-itsdangerous@2.1.2", + "ref": "itsdangerous-6@2.1.2", "dependsOn": [] }, { - "ref": "7-jinja2@3.1.3", + "ref": "jinja2-7@3.1.3", "dependsOn": [ - "8-markupsafe@2.1.5" + "markupsafe-8@2.1.5" ] }, { - "ref": "8-markupsafe@2.1.5", + "ref": "markupsafe-8@2.1.5", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "5-flask@3.0.2" + "flask-5@3.0.2" ] }, { - "ref": "9-python-dotenv@1.0.1", + "ref": "python-dotenv-9@1.0.1", "dependsOn": [] }, { - "ref": "10-werkzeug@3.0.1", + "ref": "werkzeug-10@3.0.1", "dependsOn": [ - "8-markupsafe@2.1.5" + "markupsafe-8@2.1.5" ] } ] @@ -7307,8 +7308,8 @@ fn cyclonedx_export_prune() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -7316,7 +7317,7 @@ fn cyclonedx_export_prune() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-cffi@1.16.0", + "bom-ref": "cffi-2@1.16.0", "name": "cffi", "version": "1.16.0", "purl": "pkg:pypi/cffi@1.16.0", @@ -7329,14 +7330,14 @@ fn cyclonedx_export_prune() -> Result<()> { }, { "type": "library", - "bom-ref": "3-jupyter-client@8.6.1", + "bom-ref": "jupyter-client-3@8.6.1", "name": "jupyter-client", "version": "8.6.1", "purl": "pkg:pypi/jupyter-client@8.6.1" }, { "type": "library", - "bom-ref": "4-pycparser@2.21", + "bom-ref": "pycparser-4@2.21", "name": "pycparser", "version": "2.21", "purl": "pkg:pypi/pycparser@2.21", @@ -7349,35 +7350,35 @@ fn cyclonedx_export_prune() -> Result<()> { }, { "type": "library", - "bom-ref": "5-python-dateutil@2.9.0.post0", + "bom-ref": "python-dateutil-5@2.9.0.post0", "name": "python-dateutil", "version": "2.9.0.post0", "purl": "pkg:pypi/python-dateutil@2.9.0.post0" }, { "type": "library", - "bom-ref": "6-pyzmq@25.1.2", + "bom-ref": "pyzmq-6@25.1.2", "name": "pyzmq", "version": "25.1.2", "purl": "pkg:pypi/pyzmq@25.1.2" }, { "type": "library", - "bom-ref": "7-six@1.16.0", + "bom-ref": "six-7@1.16.0", "name": "six", "version": "1.16.0", "purl": "pkg:pypi/six@1.16.0" }, { "type": "library", - "bom-ref": "8-tornado@6.4", + "bom-ref": "tornado-8@6.4", "name": "tornado", "version": "6.4", "purl": "pkg:pypi/tornado@6.4" }, { "type": "library", - "bom-ref": "9-traitlets@5.14.2", + "bom-ref": "traitlets-9@5.14.2", "name": "traitlets", "version": "5.14.2", "purl": "pkg:pypi/traitlets@5.14.2" @@ -7385,52 +7386,52 @@ fn cyclonedx_export_prune() -> Result<()> { ], "dependencies": [ { - "ref": "2-cffi@1.16.0", + "ref": "cffi-2@1.16.0", "dependsOn": [ - "4-pycparser@2.21" + "pycparser-4@2.21" ] }, { - "ref": "3-jupyter-client@8.6.1", + "ref": "jupyter-client-3@8.6.1", "dependsOn": [ - "5-python-dateutil@2.9.0.post0", - "6-pyzmq@25.1.2", - "8-tornado@6.4", - "9-traitlets@5.14.2" + "python-dateutil-5@2.9.0.post0", + "pyzmq-6@25.1.2", + "tornado-8@6.4", + "traitlets-9@5.14.2" ] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "3-jupyter-client@8.6.1" + "jupyter-client-3@8.6.1" ] }, { - "ref": "4-pycparser@2.21", + "ref": "pycparser-4@2.21", "dependsOn": [] }, { - "ref": "5-python-dateutil@2.9.0.post0", + "ref": "python-dateutil-5@2.9.0.post0", "dependsOn": [ - "7-six@1.16.0" + "six-7@1.16.0" ] }, { - "ref": "6-pyzmq@25.1.2", + "ref": "pyzmq-6@25.1.2", "dependsOn": [ - "2-cffi@1.16.0" + "cffi-2@1.16.0" ] }, { - "ref": "7-six@1.16.0", + "ref": "six-7@1.16.0", "dependsOn": [] }, { - "ref": "8-tornado@6.4", + "ref": "tornado-8@6.4", "dependsOn": [] }, { - "ref": "9-traitlets@5.14.2", + "ref": "traitlets-9@5.14.2", "dependsOn": [] } ] @@ -7486,8 +7487,8 @@ fn cyclonedx_export_group() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -7495,14 +7496,14 @@ fn cyclonedx_export_group() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-sniffio@1.3.1", + "bom-ref": "sniffio-2@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" }, { "type": "library", - "bom-ref": "3-typing-extensions@4.10.0", + "bom-ref": "typing-extensions-3@4.10.0", "name": "typing-extensions", "version": "4.10.0", "purl": "pkg:pypi/typing-extensions@4.10.0" @@ -7510,18 +7511,18 @@ fn cyclonedx_export_group() -> Result<()> { ], "dependencies": [ { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-sniffio@1.3.1", - "3-typing-extensions@4.10.0" + "sniffio-2@1.3.1", + "typing-extensions-3@4.10.0" ] }, { - "ref": "2-sniffio@1.3.1", + "ref": "sniffio-2@1.3.1", "dependsOn": [] }, { - "ref": "3-typing-extensions@4.10.0", + "ref": "typing-extensions-3@4.10.0", "dependsOn": [] } ] @@ -7551,8 +7552,8 @@ fn cyclonedx_export_group() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -7560,7 +7561,7 @@ fn cyclonedx_export_group() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-iniconfig@2.0.0", + "bom-ref": "iniconfig-2@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" @@ -7568,7 +7569,7 @@ fn cyclonedx_export_group() -> Result<()> { ], "dependencies": [ { - "ref": "2-iniconfig@2.0.0", + "ref": "iniconfig-2@2.0.0", "dependsOn": [] } ] @@ -7598,8 +7599,8 @@ fn cyclonedx_export_group() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -7607,7 +7608,7 @@ fn cyclonedx_export_group() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-anyio@4.3.0", + "bom-ref": "anyio-2@4.3.0", "name": "anyio", "version": "4.3.0", "purl": "pkg:pypi/anyio@4.3.0", @@ -7620,7 +7621,7 @@ fn cyclonedx_export_group() -> Result<()> { }, { "type": "library", - "bom-ref": "3-idna@3.6", + "bom-ref": "idna-3@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6", @@ -7633,14 +7634,14 @@ fn cyclonedx_export_group() -> Result<()> { }, { "type": "library", - "bom-ref": "4-sniffio@1.3.1", + "bom-ref": "sniffio-4@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" }, { "type": "library", - "bom-ref": "5-typing-extensions@4.10.0", + "bom-ref": "typing-extensions-5@4.10.0", "name": "typing-extensions", "version": "4.10.0", "purl": "pkg:pypi/typing-extensions@4.10.0" @@ -7648,30 +7649,30 @@ fn cyclonedx_export_group() -> Result<()> { ], "dependencies": [ { - "ref": "2-anyio@4.3.0", + "ref": "anyio-2@4.3.0", "dependsOn": [ - "3-idna@3.6", - "4-sniffio@1.3.1" + "idna-3@3.6", + "sniffio-4@1.3.1" ] }, { - "ref": "3-idna@3.6", + "ref": "idna-3@3.6", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-anyio@4.3.0", - "4-sniffio@1.3.1", - "5-typing-extensions@4.10.0" + "anyio-2@4.3.0", + "sniffio-4@1.3.1", + "typing-extensions-5@4.10.0" ] }, { - "ref": "4-sniffio@1.3.1", + "ref": "sniffio-4@1.3.1", "dependsOn": [] }, { - "ref": "5-typing-extensions@4.10.0", + "ref": "typing-extensions-5@4.10.0", "dependsOn": [] } ] @@ -7753,21 +7754,21 @@ fn cyclonedx_export_non_project() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "1-anyio@4.3.0", + "bom-ref": "anyio-1@4.3.0", "name": "anyio", "version": "4.3.0", "purl": "pkg:pypi/anyio@4.3.0" }, { "type": "library", - "bom-ref": "2-idna@3.6", + "bom-ref": "idna-2@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6" }, { "type": "library", - "bom-ref": "3-sniffio@1.3.1", + "bom-ref": "sniffio-3@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" @@ -7775,18 +7776,18 @@ fn cyclonedx_export_non_project() -> Result<()> { ], "dependencies": [ { - "ref": "1-anyio@4.3.0", + "ref": "anyio-1@4.3.0", "dependsOn": [ - "2-idna@3.6", - "3-sniffio@1.3.1" + "idna-2@3.6", + "sniffio-3@1.3.1" ] }, { - "ref": "2-idna@3.6", + "ref": "idna-2@3.6", "dependsOn": [] }, { - "ref": "3-sniffio@1.3.1", + "ref": "sniffio-3@1.3.1", "dependsOn": [] } ] @@ -7862,16 +7863,16 @@ fn cyclonedx_export_no_emit() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } }, "components": [ { - "type": "application", - "bom-ref": "2-child@0.1.0", + "type": "library", + "bom-ref": "child-2@0.1.0", "name": "child", "version": "0.1.0", "properties": [ @@ -7883,21 +7884,21 @@ fn cyclonedx_export_no_emit() -> Result<()> { }, { "type": "library", - "bom-ref": "3-idna@3.6", + "bom-ref": "idna-3@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6" }, { "type": "library", - "bom-ref": "4-iniconfig@2.0.0", + "bom-ref": "iniconfig-4@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "5-sniffio@1.3.1", + "bom-ref": "sniffio-5@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" @@ -7905,27 +7906,27 @@ fn cyclonedx_export_no_emit() -> Result<()> { ], "dependencies": [ { - "ref": "2-child@0.1.0", + "ref": "child-2@0.1.0", "dependsOn": [ - "4-iniconfig@2.0.0" + "iniconfig-4@2.0.0" ] }, { - "ref": "3-idna@3.6", + "ref": "idna-3@3.6", "dependsOn": [] }, { - "ref": "4-iniconfig@2.0.0", + "ref": "iniconfig-4@2.0.0", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-child@0.1.0" + "child-2@0.1.0" ] }, { - "ref": "5-sniffio@1.3.1", + "ref": "sniffio-5@1.3.1", "dependsOn": [] } ] @@ -7955,8 +7956,8 @@ fn cyclonedx_export_no_emit() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -7964,14 +7965,14 @@ fn cyclonedx_export_no_emit() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-anyio@3.7.0", + "bom-ref": "anyio-2@3.7.0", "name": "anyio", "version": "3.7.0", "purl": "pkg:pypi/anyio@3.7.0" }, { - "type": "application", - "bom-ref": "3-child@0.1.0", + "type": "library", + "bom-ref": "child-3@0.1.0", "name": "child", "version": "0.1.0", "properties": [ @@ -7983,21 +7984,21 @@ fn cyclonedx_export_no_emit() -> Result<()> { }, { "type": "library", - "bom-ref": "4-idna@3.6", + "bom-ref": "idna-4@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6" }, { "type": "library", - "bom-ref": "5-iniconfig@2.0.0", + "bom-ref": "iniconfig-5@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "6-sniffio@1.3.1", + "bom-ref": "sniffio-6@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" @@ -8005,28 +8006,28 @@ fn cyclonedx_export_no_emit() -> Result<()> { ], "dependencies": [ { - "ref": "2-anyio@3.7.0", + "ref": "anyio-2@3.7.0", "dependsOn": [ - "4-idna@3.6", - "6-sniffio@1.3.1" + "idna-4@3.6", + "sniffio-6@1.3.1" ] }, { - "ref": "3-child@0.1.0", + "ref": "child-3@0.1.0", "dependsOn": [ - "5-iniconfig@2.0.0" + "iniconfig-5@2.0.0" ] }, { - "ref": "4-idna@3.6", + "ref": "idna-4@3.6", "dependsOn": [] }, { - "ref": "5-iniconfig@2.0.0", + "ref": "iniconfig-5@2.0.0", "dependsOn": [] }, { - "ref": "6-sniffio@1.3.1", + "ref": "sniffio-6@1.3.1", "dependsOn": [] } ] @@ -8097,8 +8098,8 @@ fn cyclonedx_export_relative_path() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -8106,13 +8107,13 @@ fn cyclonedx_export_relative_path() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-dependency@0.1.0", + "bom-ref": "dependency-2@0.1.0", "name": "dependency", "version": "0.1.0" }, { "type": "library", - "bom-ref": "3-iniconfig@2.0.0", + "bom-ref": "iniconfig-3@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" @@ -8120,19 +8121,19 @@ fn cyclonedx_export_relative_path() -> Result<()> { ], "dependencies": [ { - "ref": "2-dependency@0.1.0", + "ref": "dependency-2@0.1.0", "dependsOn": [ - "3-iniconfig@2.0.0" + "iniconfig-3@2.0.0" ] }, { - "ref": "3-iniconfig@2.0.0", + "ref": "iniconfig-3@2.0.0", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-dependency@0.1.0" + "dependency-2@0.1.0" ] } ] @@ -8185,8 +8186,8 @@ fn cyclonedx_export_cyclic_dependencies() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -8194,70 +8195,70 @@ fn cyclonedx_export_cyclic_dependencies() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-argparse@1.4.0", + "bom-ref": "argparse-2@1.4.0", "name": "argparse", "version": "1.4.0", "purl": "pkg:pypi/argparse@1.4.0" }, { "type": "library", - "bom-ref": "3-extras@1.0.0", + "bom-ref": "extras-3@1.0.0", "name": "extras", "version": "1.0.0", "purl": "pkg:pypi/extras@1.0.0" }, { "type": "library", - "bom-ref": "4-fixtures@3.0.0", + "bom-ref": "fixtures-4@3.0.0", "name": "fixtures", "version": "3.0.0", "purl": "pkg:pypi/fixtures@3.0.0" }, { "type": "library", - "bom-ref": "5-linecache2@1.0.0", + "bom-ref": "linecache2-5@1.0.0", "name": "linecache2", "version": "1.0.0", "purl": "pkg:pypi/linecache2@1.0.0" }, { "type": "library", - "bom-ref": "6-pbr@6.0.0", + "bom-ref": "pbr-6@6.0.0", "name": "pbr", "version": "6.0.0", "purl": "pkg:pypi/pbr@6.0.0" }, { "type": "library", - "bom-ref": "7-python-mimeparse@1.6.0", + "bom-ref": "python-mimeparse-7@1.6.0", "name": "python-mimeparse", "version": "1.6.0", "purl": "pkg:pypi/python-mimeparse@1.6.0" }, { "type": "library", - "bom-ref": "8-six@1.16.0", + "bom-ref": "six-8@1.16.0", "name": "six", "version": "1.16.0", "purl": "pkg:pypi/six@1.16.0" }, { "type": "library", - "bom-ref": "9-testtools@2.3.0", + "bom-ref": "testtools-9@2.3.0", "name": "testtools", "version": "2.3.0", "purl": "pkg:pypi/testtools@2.3.0" }, { "type": "library", - "bom-ref": "10-traceback2@1.4.0", + "bom-ref": "traceback2-10@1.4.0", "name": "traceback2", "version": "1.4.0", "purl": "pkg:pypi/traceback2@1.4.0" }, { "type": "library", - "bom-ref": "11-unittest2@1.1.0", + "bom-ref": "unittest2-11@1.1.0", "name": "unittest2", "version": "1.1.0", "purl": "pkg:pypi/unittest2@1.1.0" @@ -8265,68 +8266,68 @@ fn cyclonedx_export_cyclic_dependencies() -> Result<()> { ], "dependencies": [ { - "ref": "2-argparse@1.4.0", + "ref": "argparse-2@1.4.0", "dependsOn": [] }, { - "ref": "3-extras@1.0.0", + "ref": "extras-3@1.0.0", "dependsOn": [] }, { - "ref": "4-fixtures@3.0.0", + "ref": "fixtures-4@3.0.0", "dependsOn": [ - "6-pbr@6.0.0", - "8-six@1.16.0", - "9-testtools@2.3.0" + "pbr-6@6.0.0", + "six-8@1.16.0", + "testtools-9@2.3.0" ] }, { - "ref": "5-linecache2@1.0.0", + "ref": "linecache2-5@1.0.0", "dependsOn": [] }, { - "ref": "6-pbr@6.0.0", + "ref": "pbr-6@6.0.0", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "4-fixtures@3.0.0", - "9-testtools@2.3.0" + "fixtures-4@3.0.0", + "testtools-9@2.3.0" ] }, { - "ref": "7-python-mimeparse@1.6.0", + "ref": "python-mimeparse-7@1.6.0", "dependsOn": [] }, { - "ref": "8-six@1.16.0", + "ref": "six-8@1.16.0", "dependsOn": [] }, { - "ref": "9-testtools@2.3.0", + "ref": "testtools-9@2.3.0", "dependsOn": [ - "10-traceback2@1.4.0", - "11-unittest2@1.1.0", - "3-extras@1.0.0", - "4-fixtures@3.0.0", - "6-pbr@6.0.0", - "7-python-mimeparse@1.6.0", - "8-six@1.16.0" + "extras-3@1.0.0", + "fixtures-4@3.0.0", + "pbr-6@6.0.0", + "python-mimeparse-7@1.6.0", + "six-8@1.16.0", + "traceback2-10@1.4.0", + "unittest2-11@1.1.0" ] }, { - "ref": "10-traceback2@1.4.0", + "ref": "traceback2-10@1.4.0", "dependsOn": [ - "5-linecache2@1.0.0" + "linecache2-5@1.0.0" ] }, { - "ref": "11-unittest2@1.1.0", + "ref": "unittest2-11@1.1.0", "dependsOn": [ - "10-traceback2@1.4.0", - "2-argparse@1.4.0", - "8-six@1.16.0" + "argparse-2@1.4.0", + "six-8@1.16.0", + "traceback2-10@1.4.0" ] } ] @@ -8383,8 +8384,8 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -8392,28 +8393,28 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-anyio@4.3.0", + "bom-ref": "anyio-2@4.3.0", "name": "anyio", "version": "4.3.0", "purl": "pkg:pypi/anyio@4.3.0" }, { "type": "library", - "bom-ref": "3-idna@3.6", + "bom-ref": "idna-3@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6" }, { "type": "library", - "bom-ref": "4-sniffio@1.3.1", + "bom-ref": "sniffio-4@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" }, { "type": "library", - "bom-ref": "5-typing-extensions@4.10.0", + "bom-ref": "typing-extensions-5@4.10.0", "name": "typing-extensions", "version": "4.10.0", "purl": "pkg:pypi/typing-extensions@4.10.0" @@ -8421,29 +8422,29 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { ], "dependencies": [ { - "ref": "2-anyio@4.3.0", + "ref": "anyio-2@4.3.0", "dependsOn": [ - "3-idna@3.6", - "4-sniffio@1.3.1" + "idna-3@3.6", + "sniffio-4@1.3.1" ] }, { - "ref": "3-idna@3.6", + "ref": "idna-3@3.6", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-anyio@4.3.0", - "5-typing-extensions@4.10.0" + "anyio-2@4.3.0", + "typing-extensions-5@4.10.0" ] }, { - "ref": "4-sniffio@1.3.1", + "ref": "sniffio-4@1.3.1", "dependsOn": [] }, { - "ref": "5-typing-extensions@4.10.0", + "ref": "typing-extensions-5@4.10.0", "dependsOn": [] } ] @@ -8474,8 +8475,8 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -8483,7 +8484,7 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-typing-extensions@4.10.0", + "bom-ref": "typing-extensions-2@4.10.0", "name": "typing-extensions", "version": "4.10.0", "purl": "pkg:pypi/typing-extensions@4.10.0" @@ -8491,13 +8492,13 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { ], "dependencies": [ { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "2-typing-extensions@4.10.0" + "typing-extensions-2@4.10.0" ] }, { - "ref": "2-typing-extensions@4.10.0", + "ref": "typing-extensions-2@4.10.0", "dependsOn": [] } ] @@ -8528,8 +8529,8 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } @@ -8537,21 +8538,21 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "2-anyio@4.3.0", + "bom-ref": "anyio-2@4.3.0", "name": "anyio", "version": "4.3.0", "purl": "pkg:pypi/anyio@4.3.0" }, { "type": "library", - "bom-ref": "3-idna@3.6", + "bom-ref": "idna-3@3.6", "name": "idna", "version": "3.6", "purl": "pkg:pypi/idna@3.6" }, { "type": "library", - "bom-ref": "4-sniffio@1.3.1", + "bom-ref": "sniffio-4@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" @@ -8559,18 +8560,18 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { ], "dependencies": [ { - "ref": "2-anyio@4.3.0", + "ref": "anyio-2@4.3.0", "dependsOn": [ - "3-idna@3.6", - "4-sniffio@1.3.1" + "idna-3@3.6", + "sniffio-4@1.3.1" ] }, { - "ref": "3-idna@3.6", + "ref": "idna-3@3.6", "dependsOn": [] }, { - "ref": "4-sniffio@1.3.1", + "ref": "sniffio-4@1.3.1", "dependsOn": [] } ] @@ -8651,15 +8652,15 @@ fn cyclonedx_export_all_packages_conflicting_workspace_members() -> Result<()> { } ], "component": { - "type": "application", - "bom-ref": "3-uv-workspace", - "name": "uv-workspace" + "type": "library", + "bom-ref": "project-3", + "name": "project" } }, "components": [ { - "type": "application", - "bom-ref": "2-child@0.1.0", + "type": "library", + "bom-ref": "child-2@0.1.0", "name": "child", "version": "0.1.0", "properties": [ @@ -8670,26 +8671,26 @@ fn cyclonedx_export_all_packages_conflicting_workspace_members() -> Result<()> { ] }, { - "type": "application", - "bom-ref": "1-project@0.1.0", + "type": "library", + "bom-ref": "project-1@0.1.0", "name": "project", "version": "0.1.0" } ], "dependencies": [ { - "ref": "2-child@0.1.0", + "ref": "child-2@0.1.0", "dependsOn": [] }, { - "ref": "1-project@0.1.0", + "ref": "project-1@0.1.0", "dependsOn": [] }, { - "ref": "3-uv-workspace", + "ref": "project-3", "dependsOn": [ - "2-child@0.1.0", - "1-project@0.1.0" + "child-2@0.1.0", + "project-1@0.1.0" ] } ] From 9d1f7b6ba4b2e6c22f135ccedade5d4d67fe3fd3 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Fri, 31 Oct 2025 23:09:35 +0000 Subject: [PATCH 33/36] Implement Copy on PackageType --- crates/uv-resolver/src/lock/export/cyclonedx_json.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index be08729e3dc62..bb1f11628734d 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -171,7 +171,6 @@ impl<'a> ComponentBuilder<'a> { } } - #[allow(clippy::needless_pass_by_value)] fn create_component_from_package( &mut self, package: &Package, @@ -410,7 +409,7 @@ fn create_dependencies( .collect() } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] enum PackageType<'a> { Root, Workspace(&'a Path), From bba0627a6e298bb5ca9cdfbb1bbc03b52b7ec795 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Fri, 31 Oct 2025 23:33:31 +0000 Subject: [PATCH 34/36] Replace `write_fmt` with `Write` implementation --- crates/uv/src/commands/mod.rs | 41 ++++++++++++++---------- crates/uv/src/commands/pip/compile.rs | 1 + crates/uv/src/commands/project/export.rs | 8 ++--- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index fc371530f24dd..83cf77352bf7a 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -223,23 +223,6 @@ impl<'a> OutputWriter<'a> { } } - /// Write the given arguments to both standard output and the output buffer, if present. - fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> { - use std::io::Write; - - // Write to the buffer. - if self.output_file.is_some() { - self.buffer.write_fmt(args)?; - } - - // Write to standard output. - if let Some(stdout) = &mut self.stdout { - write!(stdout, "{args}")?; - } - - Ok(()) - } - /// Commit the buffer to the output file. async fn commit(self) -> std::io::Result<()> { if let Some(output_file) = self.output_file { @@ -258,6 +241,30 @@ impl<'a> OutputWriter<'a> { } } +impl std::io::Write for OutputWriter<'_> { + /// Write to both standard output and the output buffer, if present. + fn write(&mut self, buf: &[u8]) -> std::io::Result { + // Write to the buffer. + if self.output_file.is_some() { + self.buffer.write_all(buf)?; + } + + // Write to standard output. + if let Some(stdout) = &mut self.stdout { + stdout.write_all(buf)?; + } + + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + if let Some(stdout) = &mut self.stdout { + stdout.flush()?; + } + Ok(()) + } +} + /// Given a list of names, return a conjunction of the names (e.g., "Alice, Bob, and Charlie"). pub(super) fn conjunction(names: Vec) -> String { let mut names = names.into_iter(); diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 72d71c50b9394..65732ba791dc5 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -1,6 +1,7 @@ use std::collections::BTreeSet; use std::env; use std::ffi::OsStr; +use std::io::Write; use std::path::Path; use std::str::FromStr; diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index b18eeeeb17625..32d68bb59dbbd 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -1,5 +1,6 @@ use std::env; use std::ffi::OsStr; +use std::io::Write; use std::path::{Path, PathBuf}; use anyhow::{Context, Result, anyhow}; @@ -390,12 +391,7 @@ pub(crate) async fn export( all_packages, )?; - let mut output = Vec::::new(); - - export.output_as_json_v1_5(&mut output)?; - - let output_str = String::from_utf8(output)?; - write!(writer, "{output_str}")?; + export.output_as_json_v1_5(&mut writer)?; } } From 4f8f01c722a8c8026891ccc0869c80640dbab624 Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Wed, 5 Nov 2025 22:48:19 +0000 Subject: [PATCH 35/36] Return error rather than panicking for non-local workspace packages --- .../src/lock/export/cyclonedx_json.rs | 16 ++++++++-------- crates/uv-resolver/src/lock/mod.rs | 6 ++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs index bb1f11628734d..b795d317c4795 100644 --- a/crates/uv-resolver/src/lock/export/cyclonedx_json.rs +++ b/crates/uv-resolver/src/lock/export/cyclonedx_json.rs @@ -25,7 +25,7 @@ use uv_preview::{Preview, PreviewFeatures}; use uv_warnings::warn_user; use crate::lock::export::{ExportableRequirement, ExportableRequirements}; -use crate::lock::{Package, PackageId, Source}; +use crate::lock::{LockErrorKind, Package, PackageId, Source}; use crate::{Installable, LockError}; /// Character set for percent-encoding PURL components, copied from packageurl.rs (). @@ -320,20 +320,20 @@ pub fn from_lock<'lock>( | Source::Editable(path) | Source::Virtual(path) => path, Source::Registry(_) | Source::Git(_, _) | Source::Direct(_, _) => { - // Workspace packages are always local dependencies - unreachable!( - "Workspace member {:?} has non-local source {:?}", - node.package.id.name, node.package.id.source, - ) + // Workspace packages should always be local dependencies + return Err(LockErrorKind::NonLocalWorkspaceMember { + id: node.package.id.clone(), + } + .into()); } }; PackageType::Workspace(path) } else { PackageType::Dependency }; - component_builder.create_component(node.package, package_type, Some(&node.marker)) + Ok(component_builder.create_component(node.package, package_type, Some(&node.marker))) }) - .collect::>(); + .collect::, LockError>>()?; let mut dependencies = create_dependencies(&nodes, &component_builder); diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index f1bce15fe101f..c723335fed532 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -5963,6 +5963,12 @@ enum LockErrorKind { #[source] err: toml::de::Error, }, + /// An error that occurs when a workspace member has a non-local source. + #[error("Workspace member `{id}` has non-local source", id = id.cyan())] + NonLocalWorkspaceMember { + /// The ID of the workspace member with an invalid source. + id: PackageId, + }, } /// An error that occurs when a source string could not be parsed. From baa1457c9ea4a8a7de68a6cc8c76fb9423b7d7df Mon Sep 17 00:00:00 2001 From: Thomas Schafer Date: Wed, 5 Nov 2025 23:16:26 +0000 Subject: [PATCH 36/36] Swap anyio with urllib3 in cyclonedx snapshot tests --- crates/uv/tests/it/export.rs | 715 ++++++++++------------------------- 1 file changed, 204 insertions(+), 511 deletions(-) diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 92c37e200f469..e3e4af3b22583 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -4815,7 +4815,7 @@ fn cyclonedx_export_basic() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio==3.7.0"] + dependencies = ["urllib3==2.2.0"] [build-system] requires = ["setuptools>=42"] @@ -4853,52 +4853,27 @@ fn cyclonedx_export_basic() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "anyio-2@3.7.0", - "name": "anyio", - "version": "3.7.0", - "purl": "pkg:pypi/anyio@3.7.0" - }, - { - "type": "library", - "bom-ref": "idna-3@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6" - }, - { - "type": "library", - "bom-ref": "sniffio-4@1.3.1", - "name": "sniffio", - "version": "1.3.1", - "purl": "pkg:pypi/sniffio@1.3.1" + "bom-ref": "urllib3-2@2.2.0", + "name": "urllib3", + "version": "2.2.0", + "purl": "pkg:pypi/urllib3@2.2.0" } ], "dependencies": [ - { - "ref": "anyio-2@3.7.0", - "dependsOn": [ - "idna-3@3.6", - "sniffio-4@1.3.1" - ] - }, - { - "ref": "idna-3@3.6", - "dependsOn": [] - }, { "ref": "project-1@0.1.0", "dependsOn": [ - "anyio-2@3.7.0" + "urllib3-2@2.2.0" ] }, { - "ref": "sniffio-4@1.3.1", + "ref": "urllib3-2@2.2.0", "dependsOn": [] } ] } ----- stderr ----- - Resolved 4 packages in [TIME] + Resolved 2 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -5478,7 +5453,7 @@ fn cyclonedx_export_project_extra() -> Result<()> { dependencies = ["typing-extensions"] [project.optional-dependencies] - async = ["anyio==3.7.0"] + url = ["urllib3==2.2.0"] pytest = ["iniconfig"] [build-system] @@ -5537,7 +5512,7 @@ fn cyclonedx_export_project_extra() -> Result<()> { ] } ----- stderr ----- - Resolved 6 packages in [TIME] + Resolved 4 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -5558,7 +5533,7 @@ fn cyclonedx_export_project_extra_with_optional_flag() -> Result<()> { dependencies = ["typing-extensions"] [project.optional-dependencies] - async = ["anyio==3.7.0"] + url = ["urllib3==2.2.0"] pytest = ["iniconfig"] [build-system] @@ -5597,76 +5572,51 @@ fn cyclonedx_export_project_extra_with_optional_flag() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "anyio-2@3.7.0", - "name": "anyio", - "version": "3.7.0", - "purl": "pkg:pypi/anyio@3.7.0" - }, - { - "type": "library", - "bom-ref": "idna-3@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6" - }, - { - "type": "library", - "bom-ref": "iniconfig-4@2.0.0", + "bom-ref": "iniconfig-2@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "sniffio-5@1.3.1", - "name": "sniffio", - "version": "1.3.1", - "purl": "pkg:pypi/sniffio@1.3.1" - }, - { - "type": "library", - "bom-ref": "typing-extensions-6@4.10.0", + "bom-ref": "typing-extensions-3@4.10.0", "name": "typing-extensions", "version": "4.10.0", "purl": "pkg:pypi/typing-extensions@4.10.0" + }, + { + "type": "library", + "bom-ref": "urllib3-4@2.2.0", + "name": "urllib3", + "version": "2.2.0", + "purl": "pkg:pypi/urllib3@2.2.0" } ], "dependencies": [ { - "ref": "anyio-2@3.7.0", - "dependsOn": [ - "idna-3@3.6", - "sniffio-5@1.3.1" - ] - }, - { - "ref": "idna-3@3.6", - "dependsOn": [] - }, - { - "ref": "iniconfig-4@2.0.0", + "ref": "iniconfig-2@2.0.0", "dependsOn": [] }, { "ref": "project-1@0.1.0", "dependsOn": [ - "anyio-2@3.7.0", - "iniconfig-4@2.0.0", - "typing-extensions-6@4.10.0" + "iniconfig-2@2.0.0", + "typing-extensions-3@4.10.0", + "urllib3-4@2.2.0" ] }, { - "ref": "sniffio-5@1.3.1", + "ref": "typing-extensions-3@4.10.0", "dependsOn": [] }, { - "ref": "typing-extensions-6@4.10.0", + "ref": "urllib3-4@2.2.0", "dependsOn": [] } ] } ----- stderr ----- - Resolved 6 packages in [TIME] + Resolved 4 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -5684,7 +5634,7 @@ fn cyclonedx_export_with_workspace_member() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio==3.7.0", "child1", "child2"] + dependencies = ["urllib3==2.2.0", "child1", "child2"] [tool.uv.workspace] members = ["child1", "packages/*"] @@ -5759,14 +5709,7 @@ fn cyclonedx_export_with_workspace_member() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "anyio-2@3.7.0", - "name": "anyio", - "version": "3.7.0", - "purl": "pkg:pypi/anyio@3.7.0" - }, - { - "type": "library", - "bom-ref": "child1-3@0.1.0", + "bom-ref": "child1-2@0.1.0", "name": "child1", "version": "0.1.0", "properties": [ @@ -5778,7 +5721,7 @@ fn cyclonedx_export_with_workspace_member() -> Result<()> { }, { "type": "library", - "bom-ref": "child2-4@0.2.9", + "bom-ref": "child2-3@0.2.9", "name": "child2", "version": "0.2.9", "properties": [ @@ -5790,68 +5733,50 @@ fn cyclonedx_export_with_workspace_member() -> Result<()> { }, { "type": "library", - "bom-ref": "idna-5@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6" - }, - { - "type": "library", - "bom-ref": "iniconfig-6@2.0.0", + "bom-ref": "iniconfig-4@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "sniffio-7@1.3.1", - "name": "sniffio", - "version": "1.3.1", - "purl": "pkg:pypi/sniffio@1.3.1" + "bom-ref": "urllib3-5@2.2.0", + "name": "urllib3", + "version": "2.2.0", + "purl": "pkg:pypi/urllib3@2.2.0" } ], "dependencies": [ { - "ref": "anyio-2@3.7.0", - "dependsOn": [ - "idna-5@3.6", - "sniffio-7@1.3.1" - ] - }, - { - "ref": "child1-3@0.1.0", + "ref": "child1-2@0.1.0", "dependsOn": [ - "iniconfig-6@2.0.0" + "iniconfig-4@2.0.0" ] }, { - "ref": "child2-4@0.2.9", - "dependsOn": [] - }, - { - "ref": "idna-5@3.6", + "ref": "child2-3@0.2.9", "dependsOn": [] }, { - "ref": "iniconfig-6@2.0.0", + "ref": "iniconfig-4@2.0.0", "dependsOn": [] }, { "ref": "project-1@0.1.0", "dependsOn": [ - "anyio-2@3.7.0", - "child1-3@0.1.0", - "child2-4@0.2.9" + "child1-2@0.1.0", + "child2-3@0.2.9", + "urllib3-5@2.2.0" ] }, { - "ref": "sniffio-7@1.3.1", + "ref": "urllib3-5@2.2.0", "dependsOn": [] } ] } ----- stderr ----- - Resolved 7 packages in [TIME] + Resolved 5 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -5869,7 +5794,7 @@ fn cyclonedx_export_workspace_non_root() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio==3.7.0", "child"] + dependencies = ["urllib3==2.2.0", "child"] [tool.uv.workspace] members = ["child"] @@ -5948,7 +5873,7 @@ fn cyclonedx_export_workspace_non_root() -> Result<()> { ] } ----- stderr ----- - Resolved 6 packages in [TIME] + Resolved 4 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -5990,7 +5915,7 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { dependencies = ["typing-extensions"] [project.optional-dependencies] - async = ["anyio==3.7.0"] + url = ["urllib3==2.2.0"] test = ["iniconfig"] [build-system] @@ -6067,7 +5992,7 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { ] } ----- stderr ----- - Resolved 7 packages in [TIME] + Resolved 5 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -6137,7 +6062,7 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { ] } ----- stderr ----- - Resolved 7 packages in [TIME] + Resolved 5 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -6155,7 +6080,7 @@ fn cyclonedx_export_workspace_frozen() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio==3.7.0", "child"] + dependencies = ["urllib3==2.2.0", "child"] [tool.uv.workspace] members = ["child"] @@ -6220,21 +6145,14 @@ fn cyclonedx_export_workspace_frozen() -> Result<()> { ], "component": { "type": "library", - "bom-ref": "project-7", + "bom-ref": "project-5", "name": "project" } }, "components": [ { "type": "library", - "bom-ref": "anyio-2@3.7.0", - "name": "anyio", - "version": "3.7.0", - "purl": "pkg:pypi/anyio@3.7.0" - }, - { - "type": "library", - "bom-ref": "child-3@0.1.0", + "bom-ref": "child-2@0.1.0", "name": "child", "version": "0.1.0", "properties": [ @@ -6246,24 +6164,17 @@ fn cyclonedx_export_workspace_frozen() -> Result<()> { }, { "type": "library", - "bom-ref": "idna-4@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6" - }, - { - "type": "library", - "bom-ref": "iniconfig-5@2.0.0", + "bom-ref": "iniconfig-3@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "sniffio-6@1.3.1", - "name": "sniffio", - "version": "1.3.1", - "purl": "pkg:pypi/sniffio@1.3.1" + "bom-ref": "urllib3-4@2.2.0", + "name": "urllib3", + "version": "2.2.0", + "purl": "pkg:pypi/urllib3@2.2.0" }, { "type": "library", @@ -6274,41 +6185,30 @@ fn cyclonedx_export_workspace_frozen() -> Result<()> { ], "dependencies": [ { - "ref": "anyio-2@3.7.0", - "dependsOn": [ - "idna-4@3.6", - "sniffio-6@1.3.1" - ] - }, - { - "ref": "child-3@0.1.0", + "ref": "child-2@0.1.0", "dependsOn": [ - "iniconfig-5@2.0.0" + "iniconfig-3@2.0.0" ] }, { - "ref": "idna-4@3.6", - "dependsOn": [] - }, - { - "ref": "iniconfig-5@2.0.0", + "ref": "iniconfig-3@2.0.0", "dependsOn": [] }, { "ref": "project-1@0.1.0", "dependsOn": [ - "anyio-2@3.7.0", - "child-3@0.1.0" + "child-2@0.1.0", + "urllib3-4@2.2.0" ] }, { - "ref": "sniffio-6@1.3.1", + "ref": "urllib3-4@2.2.0", "dependsOn": [] }, { - "ref": "project-7", + "ref": "project-5", "dependsOn": [ - "child-3@0.1.0", + "child-2@0.1.0", "project-1@0.1.0" ] } @@ -6332,7 +6232,7 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio==3.7.0"] + dependencies = ["urllib3==2.2.0"] [tool.uv.workspace] members = ["child1", "child2"] @@ -6395,21 +6295,14 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { ], "component": { "type": "library", - "bom-ref": "project-8", + "bom-ref": "project-7", "name": "project" } }, "components": [ { "type": "library", - "bom-ref": "anyio-2@3.7.0", - "name": "anyio", - "version": "3.7.0", - "purl": "pkg:pypi/anyio@3.7.0" - }, - { - "type": "library", - "bom-ref": "child1-3@0.1.0", + "bom-ref": "child1-2@0.1.0", "name": "child1", "version": "0.1.0", "properties": [ @@ -6421,7 +6314,7 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { }, { "type": "library", - "bom-ref": "child2-4@0.2.0", + "bom-ref": "child2-3@0.2.0", "name": "child2", "version": "0.2.0", "properties": [ @@ -6433,25 +6326,25 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { }, { "type": "library", - "bom-ref": "idna-5@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6" - }, - { - "type": "library", - "bom-ref": "iniconfig-6@2.0.0", + "bom-ref": "iniconfig-4@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "sniffio-7@1.3.1", + "bom-ref": "sniffio-5@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" }, + { + "type": "library", + "bom-ref": "urllib3-6@2.2.0", + "name": "urllib3", + "version": "2.2.0", + "purl": "pkg:pypi/urllib3@2.2.0" + }, { "type": "library", "bom-ref": "project-1@0.1.0", @@ -6461,54 +6354,47 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { ], "dependencies": [ { - "ref": "anyio-2@3.7.0", + "ref": "child1-2@0.1.0", "dependsOn": [ - "idna-5@3.6", - "sniffio-7@1.3.1" - ] - }, - { - "ref": "child1-3@0.1.0", - "dependsOn": [ - "iniconfig-6@2.0.0" + "iniconfig-4@2.0.0" ] }, { - "ref": "child2-4@0.2.0", + "ref": "child2-3@0.2.0", "dependsOn": [ - "sniffio-7@1.3.1" + "sniffio-5@1.3.1" ] }, { - "ref": "idna-5@3.6", - "dependsOn": [] - }, - { - "ref": "iniconfig-6@2.0.0", + "ref": "iniconfig-4@2.0.0", "dependsOn": [] }, { "ref": "project-1@0.1.0", "dependsOn": [ - "anyio-2@3.7.0" + "urllib3-6@2.2.0" ] }, { - "ref": "sniffio-7@1.3.1", + "ref": "sniffio-5@1.3.1", "dependsOn": [] }, { - "ref": "project-8", + "ref": "urllib3-6@2.2.0", + "dependsOn": [] + }, + { + "ref": "project-7", "dependsOn": [ - "child1-3@0.1.0", - "child2-4@0.2.0", + "child1-2@0.1.0", + "child2-3@0.2.0", "project-1@0.1.0" ] } ] } ----- stderr ----- - Resolved 7 packages in [TIME] + Resolved 6 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -6527,7 +6413,7 @@ fn cyclonedx_export_workspace_mixed_dependencies() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["child1", "anyio==3.7.0"] + dependencies = ["child1", "urllib3==2.2.0"] [tool.uv.workspace] members = ["child1", "child2"] @@ -6604,14 +6490,7 @@ fn cyclonedx_export_workspace_mixed_dependencies() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "anyio-2@3.7.0", - "name": "anyio", - "version": "3.7.0", - "purl": "pkg:pypi/anyio@3.7.0" - }, - { - "type": "library", - "bom-ref": "child1-3@0.1.0", + "bom-ref": "child1-2@0.1.0", "name": "child1", "version": "0.1.0", "properties": [ @@ -6623,7 +6502,7 @@ fn cyclonedx_export_workspace_mixed_dependencies() -> Result<()> { }, { "type": "library", - "bom-ref": "child2-4@0.2.0", + "bom-ref": "child2-3@0.2.0", "name": "child2", "version": "0.2.0", "properties": [ @@ -6635,70 +6514,63 @@ fn cyclonedx_export_workspace_mixed_dependencies() -> Result<()> { }, { "type": "library", - "bom-ref": "idna-5@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6" - }, - { - "type": "library", - "bom-ref": "iniconfig-6@2.0.0", + "bom-ref": "iniconfig-4@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "sniffio-7@1.3.1", + "bom-ref": "sniffio-5@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" + }, + { + "type": "library", + "bom-ref": "urllib3-6@2.2.0", + "name": "urllib3", + "version": "2.2.0", + "purl": "pkg:pypi/urllib3@2.2.0" } ], "dependencies": [ { - "ref": "anyio-2@3.7.0", + "ref": "child1-2@0.1.0", "dependsOn": [ - "idna-5@3.6", - "sniffio-7@1.3.1" - ] - }, - { - "ref": "child1-3@0.1.0", - "dependsOn": [ - "child2-4@0.2.0", - "iniconfig-6@2.0.0" + "child2-3@0.2.0", + "iniconfig-4@2.0.0" ] }, { - "ref": "child2-4@0.2.0", + "ref": "child2-3@0.2.0", "dependsOn": [ - "sniffio-7@1.3.1" + "sniffio-5@1.3.1" ] }, { - "ref": "idna-5@3.6", - "dependsOn": [] - }, - { - "ref": "iniconfig-6@2.0.0", + "ref": "iniconfig-4@2.0.0", "dependsOn": [] }, { "ref": "project-1@0.1.0", "dependsOn": [ - "anyio-2@3.7.0", - "child1-3@0.1.0" + "child1-2@0.1.0", + "urllib3-6@2.2.0" ] }, { - "ref": "sniffio-7@1.3.1", + "ref": "sniffio-5@1.3.1", + "dependsOn": [] + }, + { + "ref": "urllib3-6@2.2.0", "dependsOn": [] } ] } ----- stderr ----- - Resolved 7 packages in [TIME] + Resolved 6 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -6716,7 +6588,7 @@ fn cyclonedx_export_dependency_marker() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio ; sys_platform == 'darwin'", "iniconfig"] + dependencies = ["urllib3 ; sys_platform == 'darwin'", "iniconfig"] [build-system] requires = ["setuptools>=42"] @@ -6754,43 +6626,17 @@ fn cyclonedx_export_dependency_marker() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "anyio-2@4.3.0", - "name": "anyio", - "version": "4.3.0", - "purl": "pkg:pypi/anyio@4.3.0", - "properties": [ - { - "name": "uv:package:marker", - "value": "sys_platform == 'darwin'" - } - ] - }, - { - "type": "library", - "bom-ref": "idna-3@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6", - "properties": [ - { - "name": "uv:package:marker", - "value": "sys_platform == 'darwin'" - } - ] - }, - { - "type": "library", - "bom-ref": "iniconfig-4@2.0.0", + "bom-ref": "iniconfig-2@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "sniffio-5@1.3.1", - "name": "sniffio", - "version": "1.3.1", - "purl": "pkg:pypi/sniffio@1.3.1", + "bom-ref": "urllib3-3@2.2.1", + "name": "urllib3", + "version": "2.2.1", + "purl": "pkg:pypi/urllib3@2.2.1", "properties": [ { "name": "uv:package:marker", @@ -6801,35 +6647,24 @@ fn cyclonedx_export_dependency_marker() -> Result<()> { ], "dependencies": [ { - "ref": "anyio-2@4.3.0", - "dependsOn": [ - "idna-3@3.6", - "sniffio-5@1.3.1" - ] - }, - { - "ref": "idna-3@3.6", - "dependsOn": [] - }, - { - "ref": "iniconfig-4@2.0.0", + "ref": "iniconfig-2@2.0.0", "dependsOn": [] }, { "ref": "project-1@0.1.0", "dependsOn": [ - "anyio-2@4.3.0", - "iniconfig-4@2.0.0" + "iniconfig-2@2.0.0", + "urllib3-3@2.2.1" ] }, { - "ref": "sniffio-5@1.3.1", + "ref": "urllib3-3@2.2.1", "dependsOn": [] } ] } ----- stderr ----- - Resolved 5 packages in [TIME] + Resolved 3 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -7459,7 +7294,7 @@ fn cyclonedx_export_group() -> Result<()> { dependencies = ["typing-extensions"] [dependency-groups] - foo = ["anyio ; sys_platform == 'darwin'"] + foo = ["urllib3 ; sys_platform == 'darwin'"] bar = ["iniconfig"] dev = ["sniffio"] "#, @@ -7528,7 +7363,7 @@ fn cyclonedx_export_group() -> Result<()> { ] } ----- stderr ----- - Resolved 6 packages in [TIME] + Resolved 5 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -7575,7 +7410,7 @@ fn cyclonedx_export_group() -> Result<()> { ] } ----- stderr ----- - Resolved 6 packages in [TIME] + Resolved 5 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -7608,77 +7443,57 @@ fn cyclonedx_export_group() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "anyio-2@4.3.0", - "name": "anyio", - "version": "4.3.0", - "purl": "pkg:pypi/anyio@4.3.0", - "properties": [ - { - "name": "uv:package:marker", - "value": "sys_platform == 'darwin'" - } - ] - }, - { - "type": "library", - "bom-ref": "idna-3@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6", - "properties": [ - { - "name": "uv:package:marker", - "value": "sys_platform == 'darwin'" - } - ] - }, - { - "type": "library", - "bom-ref": "sniffio-4@1.3.1", + "bom-ref": "sniffio-2@1.3.1", "name": "sniffio", "version": "1.3.1", "purl": "pkg:pypi/sniffio@1.3.1" }, { "type": "library", - "bom-ref": "typing-extensions-5@4.10.0", + "bom-ref": "typing-extensions-3@4.10.0", "name": "typing-extensions", "version": "4.10.0", "purl": "pkg:pypi/typing-extensions@4.10.0" + }, + { + "type": "library", + "bom-ref": "urllib3-4@2.2.1", + "name": "urllib3", + "version": "2.2.1", + "purl": "pkg:pypi/urllib3@2.2.1", + "properties": [ + { + "name": "uv:package:marker", + "value": "sys_platform == 'darwin'" + } + ] } ], "dependencies": [ { - "ref": "anyio-2@4.3.0", + "ref": "project-1@0.1.0", "dependsOn": [ - "idna-3@3.6", - "sniffio-4@1.3.1" + "sniffio-2@1.3.1", + "typing-extensions-3@4.10.0", + "urllib3-4@2.2.1" ] }, { - "ref": "idna-3@3.6", + "ref": "sniffio-2@1.3.1", "dependsOn": [] }, { - "ref": "project-1@0.1.0", - "dependsOn": [ - "anyio-2@4.3.0", - "sniffio-4@1.3.1", - "typing-extensions-5@4.10.0" - ] - }, - { - "ref": "sniffio-4@1.3.1", + "ref": "typing-extensions-3@4.10.0", "dependsOn": [] }, { - "ref": "typing-extensions-5@4.10.0", + "ref": "urllib3-4@2.2.1", "dependsOn": [] } ] } ----- stderr ----- - Resolved 6 packages in [TIME] + Resolved 5 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -7696,7 +7511,7 @@ fn cyclonedx_export_non_project() -> Result<()> { members = [] [dependency-groups] - async = ["anyio"] + url = ["urllib3"] "#, )?; @@ -7727,12 +7542,12 @@ fn cyclonedx_export_non_project() -> Result<()> { } ----- stderr ----- warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`. - Resolved 3 packages in [TIME] + Resolved 1 package in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); // Export with group specified - uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--group").arg("async"), @r#" + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--group").arg("url"), @r#" success: true exit_code: 0 ----- stdout ----- @@ -7754,47 +7569,22 @@ fn cyclonedx_export_non_project() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "anyio-1@4.3.0", - "name": "anyio", - "version": "4.3.0", - "purl": "pkg:pypi/anyio@4.3.0" - }, - { - "type": "library", - "bom-ref": "idna-2@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6" - }, - { - "type": "library", - "bom-ref": "sniffio-3@1.3.1", - "name": "sniffio", - "version": "1.3.1", - "purl": "pkg:pypi/sniffio@1.3.1" + "bom-ref": "urllib3-1@2.2.1", + "name": "urllib3", + "version": "2.2.1", + "purl": "pkg:pypi/urllib3@2.2.1" } ], "dependencies": [ { - "ref": "anyio-1@4.3.0", - "dependsOn": [ - "idna-2@3.6", - "sniffio-3@1.3.1" - ] - }, - { - "ref": "idna-2@3.6", - "dependsOn": [] - }, - { - "ref": "sniffio-3@1.3.1", + "ref": "urllib3-1@2.2.1", "dependsOn": [] } ] } ----- stderr ----- warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`. - Resolved 3 packages in [TIME] + Resolved 1 package in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -7812,7 +7602,7 @@ fn cyclonedx_export_no_emit() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["anyio==3.7.0", "child"] + dependencies = ["urllib3==2.2.0", "child"] [tool.uv.workspace] members = ["child"] @@ -7843,8 +7633,8 @@ fn cyclonedx_export_no_emit() -> Result<()> { context.lock().assert().success(); - // Exclude `anyio`. - uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--no-emit-package").arg("anyio"), @r#" + // Exclude `urllib3`. + uv_snapshot!(context.filters(), context.export().arg("--format").arg("cyclonedx1.5").arg("--no-emit-package").arg("urllib3"), @r#" success: true exit_code: 0 ----- stdout ----- @@ -7884,39 +7674,21 @@ fn cyclonedx_export_no_emit() -> Result<()> { }, { "type": "library", - "bom-ref": "idna-3@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6" - }, - { - "type": "library", - "bom-ref": "iniconfig-4@2.0.0", + "bom-ref": "iniconfig-3@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" - }, - { - "type": "library", - "bom-ref": "sniffio-5@1.3.1", - "name": "sniffio", - "version": "1.3.1", - "purl": "pkg:pypi/sniffio@1.3.1" } ], "dependencies": [ { "ref": "child-2@0.1.0", "dependsOn": [ - "iniconfig-4@2.0.0" + "iniconfig-3@2.0.0" ] }, { - "ref": "idna-3@3.6", - "dependsOn": [] - }, - { - "ref": "iniconfig-4@2.0.0", + "ref": "iniconfig-3@2.0.0", "dependsOn": [] }, { @@ -7924,15 +7696,11 @@ fn cyclonedx_export_no_emit() -> Result<()> { "dependsOn": [ "child-2@0.1.0" ] - }, - { - "ref": "sniffio-5@1.3.1", - "dependsOn": [] } ] } ----- stderr ----- - Resolved 6 packages in [TIME] + Resolved 4 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -7965,14 +7733,7 @@ fn cyclonedx_export_no_emit() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "anyio-2@3.7.0", - "name": "anyio", - "version": "3.7.0", - "purl": "pkg:pypi/anyio@3.7.0" - }, - { - "type": "library", - "bom-ref": "child-3@0.1.0", + "bom-ref": "child-2@0.1.0", "name": "child", "version": "0.1.0", "properties": [ @@ -7984,56 +7745,38 @@ fn cyclonedx_export_no_emit() -> Result<()> { }, { "type": "library", - "bom-ref": "idna-4@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6" - }, - { - "type": "library", - "bom-ref": "iniconfig-5@2.0.0", + "bom-ref": "iniconfig-3@2.0.0", "name": "iniconfig", "version": "2.0.0", "purl": "pkg:pypi/iniconfig@2.0.0" }, { "type": "library", - "bom-ref": "sniffio-6@1.3.1", - "name": "sniffio", - "version": "1.3.1", - "purl": "pkg:pypi/sniffio@1.3.1" + "bom-ref": "urllib3-4@2.2.0", + "name": "urllib3", + "version": "2.2.0", + "purl": "pkg:pypi/urllib3@2.2.0" } ], "dependencies": [ { - "ref": "anyio-2@3.7.0", - "dependsOn": [ - "idna-4@3.6", - "sniffio-6@1.3.1" - ] - }, - { - "ref": "child-3@0.1.0", + "ref": "child-2@0.1.0", "dependsOn": [ - "iniconfig-5@2.0.0" + "iniconfig-3@2.0.0" ] }, { - "ref": "idna-4@3.6", - "dependsOn": [] - }, - { - "ref": "iniconfig-5@2.0.0", + "ref": "iniconfig-3@2.0.0", "dependsOn": [] }, { - "ref": "sniffio-6@1.3.1", + "ref": "urllib3-4@2.2.0", "dependsOn": [] } ] } ----- stderr ----- - Resolved 6 packages in [TIME] + Resolved 4 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -8354,7 +8097,7 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { dependencies = ["typing-extensions"] [tool.uv] - dev-dependencies = ["anyio"] + dev-dependencies = ["urllib3"] [build-system] requires = ["setuptools>=42"] @@ -8393,65 +8136,40 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "anyio-2@4.3.0", - "name": "anyio", - "version": "4.3.0", - "purl": "pkg:pypi/anyio@4.3.0" - }, - { - "type": "library", - "bom-ref": "idna-3@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6" - }, - { - "type": "library", - "bom-ref": "sniffio-4@1.3.1", - "name": "sniffio", - "version": "1.3.1", - "purl": "pkg:pypi/sniffio@1.3.1" - }, - { - "type": "library", - "bom-ref": "typing-extensions-5@4.10.0", + "bom-ref": "typing-extensions-2@4.10.0", "name": "typing-extensions", "version": "4.10.0", "purl": "pkg:pypi/typing-extensions@4.10.0" + }, + { + "type": "library", + "bom-ref": "urllib3-3@2.2.1", + "name": "urllib3", + "version": "2.2.1", + "purl": "pkg:pypi/urllib3@2.2.1" } ], "dependencies": [ - { - "ref": "anyio-2@4.3.0", - "dependsOn": [ - "idna-3@3.6", - "sniffio-4@1.3.1" - ] - }, - { - "ref": "idna-3@3.6", - "dependsOn": [] - }, { "ref": "project-1@0.1.0", "dependsOn": [ - "anyio-2@4.3.0", - "typing-extensions-5@4.10.0" + "typing-extensions-2@4.10.0", + "urllib3-3@2.2.1" ] }, { - "ref": "sniffio-4@1.3.1", + "ref": "typing-extensions-2@4.10.0", "dependsOn": [] }, { - "ref": "typing-extensions-5@4.10.0", + "ref": "urllib3-3@2.2.1", "dependsOn": [] } ] } ----- stderr ----- warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead - Resolved 5 packages in [TIME] + Resolved 3 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -8505,7 +8223,7 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { } ----- stderr ----- warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead - Resolved 5 packages in [TIME] + Resolved 3 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#); @@ -8538,47 +8256,22 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { "components": [ { "type": "library", - "bom-ref": "anyio-2@4.3.0", - "name": "anyio", - "version": "4.3.0", - "purl": "pkg:pypi/anyio@4.3.0" - }, - { - "type": "library", - "bom-ref": "idna-3@3.6", - "name": "idna", - "version": "3.6", - "purl": "pkg:pypi/idna@3.6" - }, - { - "type": "library", - "bom-ref": "sniffio-4@1.3.1", - "name": "sniffio", - "version": "1.3.1", - "purl": "pkg:pypi/sniffio@1.3.1" + "bom-ref": "urllib3-2@2.2.1", + "name": "urllib3", + "version": "2.2.1", + "purl": "pkg:pypi/urllib3@2.2.1" } ], "dependencies": [ { - "ref": "anyio-2@4.3.0", - "dependsOn": [ - "idna-3@3.6", - "sniffio-4@1.3.1" - ] - }, - { - "ref": "idna-3@3.6", - "dependsOn": [] - }, - { - "ref": "sniffio-4@1.3.1", + "ref": "urllib3-2@2.2.1", "dependsOn": [] } ] } ----- stderr ----- warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead - Resolved 5 packages in [TIME] + Resolved 3 packages in [TIME] warning: `uv export --format=cyclonedx1.5` is experimental and may change without warning. Pass `--preview-features sbom-export` to disable this warning. "#);