Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions crates/uv-resolver/src/lock/export/cyclonedx_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,27 +349,37 @@ pub fn from_lock<'lock>(

let mut dependencies = create_dependencies(&nodes, &component_builder);

// With `--all-packages`, use synthetic root which depends on workspace root and all workspace members.
// With `--all-packages`, use synthetic root which depends on 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(root);
let synthetic_root_bom_ref = synthetic_root
.bom_ref
.clone()
.expect("bom-ref should always exist");
let workspace_root = metadata.component.replace(synthetic_root);
let root = metadata.component.replace(synthetic_root);

if let Some(workspace_root) = workspace_root {
let mut synthetic_root_deps = workspace_member_ids
.iter()
.filter_map(|c| component_builder.get_component(c))
.map(|c| c.bom_ref.clone().expect("bom-ref should always exist"))
.collect::<Vec<_>>();
if let Some(ref root_component) = root
&& let Some(ref root_bom_ref) = root_component.bom_ref
{
synthetic_root_deps.push(root_bom_ref.clone());
}

if let Some(workspace_root) = root {
components.push(workspace_root);
}

dependencies.push(Dependency {
dependency_ref: synthetic_root_bom_ref,
dependencies: workspace_member_ids
.iter()
.filter_map(|c| component_builder.get_component(c))
.map(|c| c.bom_ref.clone().expect("bom-ref should always exist"))
dependencies: synthetic_root_deps
.into_iter()
.sorted_unstable()
.unique()
.collect(),
});
}
Expand Down
87 changes: 87 additions & 0 deletions crates/uv/tests/it/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6257,6 +6257,93 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> {
Ok(())
}

#[test]
fn cyclonedx_export_all_packages_non_workspace_root_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 = "my-project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["urllib3==2.2.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").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": "library",
"bom-ref": "my-project-3",
"name": "my-project"
}
},
"components": [
{
"type": "library",
"bom-ref": "[email protected]",
"name": "urllib3",
"version": "2.2.0",
"purl": "pkg:pypi/[email protected]"
},
{
"type": "library",
"bom-ref": "[email protected]",
"name": "my-project",
"version": "0.1.0"
}
],
"dependencies": [
{
"ref": "[email protected]",
"dependsOn": [
"[email protected]"
]
},
{
"ref": "[email protected]",
"dependsOn": []
},
{
"ref": "my-project-3",
"dependsOn": [
"[email protected]"
]
}
]
}
----- 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(())
}

// 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_mixed_dependencies() -> Result<()> {
Expand Down
Loading