Skip to content

Commit 2a2f512

Browse files
Support cargo workspaces in assembly
Reference: #3598 Signed-off-by: Ayan Sinha Mahapatra <ayansmahapatra@gmail.com>
1 parent 853cef1 commit 2a2f512

File tree

4 files changed

+551
-96
lines changed

4 files changed

+551
-96
lines changed

src/packagedcode/cargo.py

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
import os
1011
import re
1112

1213
import saneyaml
@@ -31,8 +32,8 @@ class CargoTomlHandler(models.DatafileHandler):
3132
@classmethod
3233
def parse(cls, location):
3334
package_data = toml.load(location, _dict=dict)
34-
3535
core_package_data = package_data.get('package', {})
36+
workspace = package_data.get('workspace', {})
3637

3738
name = core_package_data.get('name')
3839
version = core_package_data.get('version')
@@ -66,6 +67,9 @@ def parse(cls, location):
6667
repository_homepage_url = name and f'https://crates.io/crates/{name}'
6768
repository_download_url = name and version and f'https://crates.io/api/v1/crates/{name}/{version}/download'
6869
api_data_url = name and f'https://crates.io/api/v1/crates/{name}'
70+
extra_data = {}
71+
if workspace:
72+
extra_data["workspace"] = workspace
6973

7074
yield models.PackageData(
7175
datasource_id=cls.datasource_id,
@@ -82,20 +86,92 @@ def parse(cls, location):
8286
repository_download_url=repository_download_url,
8387
api_data_url=api_data_url,
8488
dependencies=dependencies,
89+
extra_data=extra_data,
8590
)
8691

8792
@classmethod
8893
def assemble(cls, package_data, resource, codebase, package_adder):
8994
"""
90-
Assemble Cargo.toml and possible Cargo.lock datafiles
95+
Assemble Cargo.toml and possible Cargo.lock datafiles. Also
96+
support cargo workspaces where we have multiple packages from
97+
a repository and some shared information present at top-level.
9198
"""
92-
yield from cls.assemble_from_many_datafiles(
93-
datafile_name_patterns=('Cargo.toml', 'cargo.toml', 'Cargo.lock', 'cargo.lock'),
94-
directory=resource.parent(codebase),
95-
codebase=codebase,
96-
package_adder=package_adder,
97-
)
99+
workspace = package_data.extra_data.get("workspace", {})
100+
workspace_members = workspace.get("members", [])
101+
workspace_package_data = workspace.get("package", {})
102+
attributes_to_copy = [
103+
"license_detections",
104+
"declared_license_expression",
105+
"declared_license_expression_spdx"
106+
]
107+
if "license" in workspace_package_data:
108+
for attribute in attributes_to_copy:
109+
workspace_package_data[attribute] = getattr(package_data, attribute)
110+
111+
workspace_root_path = resource.parent(codebase).path
112+
if workspace_package_data and workspace_members:
113+
for workspace_member_path in workspace_members:
114+
workspace_directory_path = os.path.join(workspace_root_path, workspace_member_path)
115+
workspace_directory = codebase.get_resource(path=workspace_directory_path)
116+
if not workspace_directory:
117+
continue
118+
119+
# Update the package data for all members with the
120+
# workspace package data
121+
for resource in workspace_directory.children(codebase):
122+
if cls.is_datafile(location=resource.location):
123+
if not resource.package_data:
124+
continue
125+
126+
updated_package_data = cls.update_resource_package_data(
127+
package_data=workspace_package_data,
128+
old_package_data=resource.package_data.pop(),
129+
mapping=CARGO_ATTRIBUTE_MAPPING,
130+
)
131+
resource.package_data.append(updated_package_data)
132+
resource.save(codebase)
133+
134+
yield from cls.assemble_from_many_datafiles(
135+
datafile_name_patterns=('Cargo.toml', 'cargo.toml', 'Cargo.lock', 'cargo.lock'),
136+
directory=workspace_directory,
137+
codebase=codebase,
138+
package_adder=package_adder,
139+
)
140+
else:
141+
yield from cls.assemble_from_many_datafiles(
142+
datafile_name_patterns=('Cargo.toml', 'cargo.toml', 'Cargo.lock', 'cargo.lock'),
143+
directory=resource.parent(codebase),
144+
codebase=codebase,
145+
package_adder=package_adder,
146+
)
98147

148+
@classmethod
149+
def update_resource_package_data(cls, package_data, old_package_data, mapping=None):
150+
151+
for attribute in old_package_data.keys():
152+
if attribute in mapping:
153+
replace_by_attribute = mapping.get(attribute)
154+
old_package_data[attribute] = package_data.get(replace_by_attribute)
155+
elif attribute == "parties":
156+
old_package_data[attribute] = list(get_parties(
157+
person_names=package_data.get("authors"),
158+
party_role='author',
159+
))
160+
161+
return old_package_data
162+
163+
164+
CARGO_ATTRIBUTE_MAPPING = {
165+
# Fields in PackageData model: Fields in cargo
166+
"homepage_url": "homepage",
167+
"vcs_url": "repository",
168+
"keywords": "categories",
169+
"extracted_license_statement": "license",
170+
# These are fields carried over to avoid re-detection of licenses
171+
"license_detections": "license_detections",
172+
"declared_license_expression": "declared_license_expression",
173+
"declared_license_expression_spdx": "declared_license_expression_spdx",
174+
}
99175

100176
class CargoLockHandler(models.DatafileHandler):
101177
datasource_id = 'cargo_lock'
@@ -185,19 +261,21 @@ def dependency_mapper(dependencies, scope='dependencies'):
185261
)
186262

187263

188-
def get_parties(person_names, party_role):
264+
def get_parties(person_names, party_role, debug=False):
189265
"""
190266
Yields Party of `party_role` given a list of ``person_names`` strings.
191267
https://doc.rust-lang.org/cargo/reference/manifest.html#the-authors-field-optional
192268
"""
269+
if debug:
270+
raise Exception(person_names)
193271
for person_name in person_names:
194272
name, email = parse_person(person_name)
195273
yield models.Party(
196274
type=models.party_person,
197275
name=name,
198276
role=party_role,
199277
email=email,
200-
)
278+
).to_dict()
201279

202280

203281
person_parser = re.compile(

0 commit comments

Comments
 (0)