Skip to content

Commit 45ef995

Browse files
authored
Add ORT package-list.yml output format (#1852)
Signed-off-by: tdruez <[email protected]>
1 parent 434c673 commit 45ef995

File tree

11 files changed

+730
-2
lines changed

11 files changed

+730
-2
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ v35.4.0 (unreleased)
3535
``scanpipe.pipes.federatedcode.push_changes``. Add
3636
``scanpipe.pipes.federatedcode.write_data_as_yaml``.
3737

38+
- Add ORT ``package-list.yml`` as new downloadable output format.
39+
https://github.com/aboutcode-org/scancode.io/pull/1852
3840

3941
v35.3.0 (2025-08-20)
4042
--------------------

scanpipe/api/views.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ def results_download(self, request, *args, **kwargs):
168168
output_file = output.to_cyclonedx(project, **output_kwargs)
169169
elif format == "attribution":
170170
output_file = output.to_attribution(project)
171+
elif format == "ort-package-list":
172+
output_file = output.to_ort_package_list_yml(project)
171173
else:
172174
message = {"status": f"Format {format} not supported."}
173175
return Response(message, status=status.HTTP_400_BAD_REQUEST)

scanpipe/management/commands/output.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,15 @@
2525
from scanpipe.management.commands import ProjectCommand
2626
from scanpipe.pipes import output
2727

28-
SUPPORTED_FORMATS = ["json", "csv", "xlsx", "attribution", "spdx", "cyclonedx"]
28+
SUPPORTED_FORMATS = [
29+
"json",
30+
"csv",
31+
"xlsx",
32+
"attribution",
33+
"spdx",
34+
"cyclonedx",
35+
"ort-package-list",
36+
]
2937

3038

3139
class Command(ProjectCommand):
@@ -84,6 +92,7 @@ def handle_output(self, output_format):
8492
"spdx": output.to_spdx,
8593
"cyclonedx": output.to_cyclonedx,
8694
"attribution": output.to_attribution,
95+
"ort-package-list": output.to_ort_package_list_yml,
8796
}.get(output_format)
8897

8998
if not output_function:

scanpipe/pipes/ort.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# http://nexb.com and https://github.com/aboutcode-org/scancode.io
4+
# The ScanCode.io software is licensed under the Apache License version 2.0.
5+
# Data generated with ScanCode.io is provided as-is without warranties.
6+
# ScanCode is a trademark of nexB Inc.
7+
#
8+
# You may not use this software except in compliance with the License.
9+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
10+
# Unless required by applicable law or agreed to in writing, software distributed
11+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
12+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
13+
# specific language governing permissions and limitations under the License.
14+
#
15+
# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES
16+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
17+
# ScanCode.io should be considered or used as legal advice. Consult an Attorney
18+
# for any legal advice.
19+
#
20+
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/aboutcode-org/scancode.io for support and download.
22+
23+
24+
from dataclasses import asdict
25+
from dataclasses import dataclass
26+
from dataclasses import field
27+
from pathlib import Path
28+
29+
import saneyaml
30+
31+
"""
32+
This module provides Python dataclass models for representing a package list
33+
in a format compatible with the OSS Review Toolkit (ORT)
34+
`CreateAnalyzerResultFromPackageListCommand`.
35+
36+
The models are simplified adaptations of the Kotlin classes from:
37+
https://github.com/oss-review-toolkit/ort/blob/main/cli-helper/src/main/kotlin/commands/CreateAnalyzerResultFromPackageListCommand.kt
38+
39+
This module is intended for generating ORT-compatible YAML package lists from Python
40+
objects, allowing integration with ORT's analyzer workflows or manual creation of
41+
package metadata.
42+
"""
43+
44+
45+
# private data class SourceArtifact(
46+
# val url: String,
47+
# val hash: Hash? = null
48+
# )
49+
@dataclass
50+
class SourceArtifact:
51+
url: str
52+
# Cannot coerce empty String ("") to `org.ossreviewtoolkit.model.Hash` value
53+
# hash: str = None
54+
55+
56+
# private data class Vcs(
57+
# val type: String? = null,
58+
# val url: String? = null,
59+
# val revision: String? = null,
60+
# val path: String? = null
61+
# )
62+
@dataclass
63+
class Vcs:
64+
type: str = None
65+
url: str = None
66+
revision: str = None
67+
path: str = None
68+
69+
70+
# private data class Dependency(
71+
# val id: Identifier,
72+
# val purl: String? = null,
73+
# val vcs: Vcs? = null,
74+
# val sourceArtifact: SourceArtifact? = null,
75+
# val declaredLicenses: Set<String> = emptySet(),
76+
# val concludedLicense: SpdxExpression? = null,
77+
# val description: String? = null,
78+
# val homepageUrl: String? = null,
79+
# val isExcluded: Boolean = false,
80+
# val isDynamicallyLinked: Boolean = false,
81+
# val labels: Map<String, String> = emptyMap()
82+
# )
83+
@dataclass
84+
class Dependency:
85+
id: str
86+
purl: str = None
87+
vcs: Vcs = None
88+
sourceArtifact: SourceArtifact = None
89+
declaredLicenses: list = field(default_factory=set)
90+
# concludedLicense: str = None
91+
description: str = None
92+
homepageUrl: str = None
93+
# isExcluded: bool = False
94+
# isDynamicallyLinked: bool = False
95+
# labels: dict = field(default_factory=dict)
96+
97+
98+
# private data class PackageList(
99+
# val projectName: String? = null,
100+
# val projectVcs: Vcs? = null,
101+
# val dependencies: List<Dependency> = emptyList()
102+
# )
103+
@dataclass
104+
class PackageList:
105+
projectName: str
106+
projectVcs: Vcs = field(default_factory=Vcs)
107+
dependencies: list = field(default_factory=list)
108+
109+
def to_yaml(self):
110+
"""Dump the Project object back to a YAML string."""
111+
return saneyaml.dump(asdict(self))
112+
113+
def to_file(self, filepath):
114+
"""Write the Project object to a YAML file."""
115+
Path(filepath).write_text(self.to_yaml(), encoding="utf-8")
116+
117+
118+
def get_ort_project_type(project):
119+
"""
120+
Determine the ORT project type based on the project's input sources.
121+
122+
Currently, this function checks whether any of the project's
123+
input download URLs start with "docker://".
124+
If at least one Docker URL is found, it returns "docker".
125+
"""
126+
inputs_url = project.inputsources.values_list("download_url", flat=True)
127+
if any(url.startswith("docker://") for url in inputs_url):
128+
return "docker"
129+
130+
131+
def to_ort_package_list_yml(project):
132+
"""Convert a project object into a YAML string in the ORT package list format."""
133+
project_type = get_ort_project_type(project)
134+
135+
dependencies = []
136+
for package in project.discoveredpackages.all():
137+
dependency = Dependency(
138+
id=f"{project_type or package.type}::{package.name}:{package.version}",
139+
purl=package.purl,
140+
sourceArtifact=SourceArtifact(url=package.download_url),
141+
declaredLicenses=[package.get_declared_license_expression_spdx()],
142+
vcs=Vcs(url=package.vcs_url),
143+
description=package.description,
144+
homepageUrl=package.homepage_url,
145+
)
146+
dependencies.append(dependency)
147+
148+
package_list = PackageList(
149+
projectName=project.name,
150+
dependencies=dependencies,
151+
)
152+
153+
return package_list.to_yaml()

scanpipe/pipes/output.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
from scanpipe.models import ProjectMessage
6161
from scanpipe.pipes import docker
6262
from scanpipe.pipes import flag
63+
from scanpipe.pipes import ort
6364
from scanpipe.pipes import spdx
6465

6566
scanpipe_app = apps.get_app_config("scanpipe")
@@ -1058,10 +1059,23 @@ def to_attribution(project):
10581059
return output_file
10591060

10601061

1062+
def to_ort_package_list_yml(project):
1063+
"""
1064+
Generate an ORT compatible "package-list.yml" output.
1065+
The output file is created in the ``project`` "output/" directory.
1066+
Return the path of the generated output file.
1067+
"""
1068+
output_file = project.get_output_file_path("results", "package-list.yml")
1069+
ort_yml = ort.to_ort_package_list_yml(project)
1070+
output_file.write_text(ort_yml)
1071+
return output_file
1072+
1073+
10611074
FORMAT_TO_FUNCTION_MAPPING = {
10621075
"json": to_json,
10631076
"xlsx": to_xlsx,
10641077
"spdx": to_spdx,
10651078
"cyclonedx": to_cyclonedx,
10661079
"attribution": to_attribution,
1080+
"ort-package-list": to_ort_package_list_yml,
10671081
}

scanpipe/templates/scanpipe/dropdowns/project_download_dropdown.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
<a href="{% url 'project_results' project.slug 'attribution' %}" class="dropdown-item">
3232
<strong>Attribution</strong>
3333
</a>
34+
<a href="{% url 'project_results' project.slug 'ort-package-list' %}" class="dropdown-item">
35+
<strong>ORT (package-list)</strong>
36+
</a>
3437
</div>
3538
</div>
3639
</div>

scanpipe/templates/scanpipe/includes/project_downloads.html

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
</a>
1313
<div class="dropdown is-hoverable">
1414
<div class="dropdown-trigger">
15-
<button class="button tag is-success is-medium" aria-haspopup="true" aria-controls="dropdown-menu-cyclonedx">
15+
<button class="button tag is-success is-medium has-text-weight-normal" aria-haspopup="true" aria-controls="dropdown-menu-cyclonedx">
1616
<span class="icon">
1717
<i class="fa-solid fa-download" aria-hidden="true"></i>
1818
</span>
1919
<span>CycloneDX</span>
20+
<span class="icon is-small">
21+
<i class="fa-solid fa-angle-down" aria-hidden="true"></i>
22+
</span>
2023
</button>
2124
</div>
2225
<div class="dropdown-menu" id="dropdown-menu-cyclonedx" role="menu">
@@ -36,5 +39,25 @@
3639
<a class="tag is-success is-medium" href="{% url 'project_results' project.slug 'attribution' %}">
3740
<span class="icon mr-1"><i class="fa-solid fa-download"></i></span>Attribution
3841
</a>
42+
<div class="dropdown is-hoverable">
43+
<div class="dropdown-trigger">
44+
<button class="button tag is-success is-medium has-text-weight-normal" aria-haspopup="true" aria-controls="dropdown-menu-ort">
45+
<span class="icon">
46+
<i class="fa-solid fa-download" aria-hidden="true"></i>
47+
</span>
48+
<span>Tools formats</span>
49+
<span class="icon is-small">
50+
<i class="fa-solid fa-angle-down" aria-hidden="true"></i>
51+
</span>
52+
</button>
53+
</div>
54+
<div class="dropdown-menu" id="dropdown-menu-ort" role="menu">
55+
<div class="dropdown-content">
56+
<a href="{% url 'project_results' project.slug 'ort-package-list' %}" class="dropdown-item has-text-weight-semibold">
57+
ORT package-list.yml
58+
</a>
59+
</div>
60+
</div>
61+
</div>
3962
</div>
4063
</article>

0 commit comments

Comments
 (0)