7
7
# See https://aboutcode.org for more information about nexB OSS projects.
8
8
#
9
9
10
+ import logging
10
11
import os
11
12
import re
13
+ import sys
12
14
13
- import saneyaml
14
15
import toml
15
16
from packageurl import PackageURL
16
17
20
21
Handle Rust cargo crates
21
22
"""
22
23
24
+ TRACE = os .environ .get ('SCANCODE_DEBUG_PACKAGE_CARGO' , False )
25
+
26
+
27
+ def logger_debug (* args ):
28
+ pass
29
+
30
+
31
+ logger = logging .getLogger (__name__ )
32
+
33
+ if TRACE :
34
+ logging .basicConfig (stream = sys .stdout )
35
+ logger .setLevel (logging .DEBUG )
36
+
37
+ def logger_debug (* args ):
38
+ return logger .debug (' ' .join (isinstance (a , str ) and a or repr (a ) for a in args ))
39
+
23
40
24
41
class CargoBaseHandler (models .DatafileHandler ):
25
42
@classmethod
@@ -29,7 +46,7 @@ def assemble(cls, package_data, resource, codebase, package_adder):
29
46
support cargo workspaces where we have multiple packages from
30
47
a repository and some shared information present at top-level.
31
48
"""
32
- workspace = package_data .extra_data .get (" workspace" , {})
49
+ workspace = package_data .extra_data .get (' workspace' , {})
33
50
workspace_members = workspace .get ("members" , [])
34
51
workspace_package_data = workspace .get ("package" , {})
35
52
attributes_to_copy = [
@@ -39,10 +56,13 @@ def assemble(cls, package_data, resource, codebase, package_adder):
39
56
]
40
57
if "license" in workspace_package_data :
41
58
for attribute in attributes_to_copy :
59
+ package_data .extra_data [attribute ] = 'workspace'
42
60
workspace_package_data [attribute ] = getattr (package_data , attribute )
43
61
44
62
workspace_root_path = resource .parent (codebase ).path
45
63
if workspace_package_data and workspace_members :
64
+
65
+ # TODO: support glob patterns found in cargo workspaces
46
66
for workspace_member_path in workspace_members :
47
67
workspace_directory_path = os .path .join (workspace_root_path , workspace_member_path )
48
68
workspace_directory = codebase .get_resource (path = workspace_directory_path )
@@ -56,9 +76,13 @@ def assemble(cls, package_data, resource, codebase, package_adder):
56
76
if not resource .package_data :
57
77
continue
58
78
79
+ if TRACE :
80
+ logger_debug (f"Resource manifest to update: { resource .path } " )
81
+
59
82
updated_package_data = cls .update_resource_package_data (
60
- package_data = workspace_package_data ,
61
- old_package_data = resource .package_data .pop (),
83
+ workspace = workspace ,
84
+ workspace_package_data = workspace_package_data ,
85
+ resource_package_data = resource .package_data .pop (),
62
86
mapping = CARGO_ATTRIBUTE_MAPPING ,
63
87
)
64
88
resource .package_data .append (updated_package_data )
@@ -79,20 +103,61 @@ def assemble(cls, package_data, resource, codebase, package_adder):
79
103
)
80
104
81
105
@classmethod
82
- def update_resource_package_data (cls , package_data , old_package_data , mapping = None ):
106
+ def update_resource_package_data (cls , workspace , workspace_package_data , resource_package_data , mapping = None ):
83
107
84
- for attribute in old_package_data .keys ():
108
+ extra_data = resource_package_data ["extra_data" ]
109
+ for attribute in resource_package_data .keys ():
85
110
if attribute in mapping :
86
111
replace_by_attribute = mapping .get (attribute )
87
- old_package_data [attribute ] = package_data .get (replace_by_attribute )
112
+ if not replace_by_attribute in extra_data :
113
+ continue
114
+
115
+ extra_data .pop (replace_by_attribute )
116
+ replace_by_value = workspace_package_data .get (replace_by_attribute )
117
+ if replace_by_value :
118
+ resource_package_data [attribute ] = replace_by_value
88
119
elif attribute == "parties" :
89
- old_package_data [attribute ] = list (get_parties (
90
- person_names = package_data .get ("authors" ),
120
+ resource_package_data [attribute ] = list (get_parties (
121
+ person_names = workspace_package_data .get ("authors" , [] ),
91
122
party_role = 'author' ,
92
123
))
93
-
94
- return old_package_data
95
-
124
+ if "authors" in extra_data :
125
+ extra_data .pop ("authors" )
126
+
127
+ extra_data_copy = extra_data .copy ()
128
+ for key , value in extra_data_copy .items ():
129
+ if value == 'workspace' :
130
+ extra_data .pop (key )
131
+
132
+ if key in workspace_package_data :
133
+ workspace_value = workspace_package_data .get (key )
134
+ if workspace_value and key in mapping :
135
+ replace_by_attribute = mapping .get (key )
136
+ extra_data [replace_by_attribute ] = workspace_value
137
+
138
+ # refresh purl if version updated from workspace
139
+ if "version" in workspace_package_data :
140
+ resource_package_data ["purl" ] = PackageURL (
141
+ type = cls .default_package_type ,
142
+ name = resource_package_data ["name" ],
143
+ namespace = resource_package_data ["namespace" ],
144
+ version = resource_package_data ["version" ],
145
+ ).to_string ()
146
+
147
+ workspace_dependencies = dependency_mapper (dependencies = workspace .get ('dependencies' , {}))
148
+ deps_by_purl = {}
149
+ for dependency in workspace_dependencies :
150
+ deps_by_purl [dependency .purl ] = dependency
151
+
152
+ for dep_mapping in resource_package_data ['dependencies' ]:
153
+ workspace_dependency = deps_by_purl .get (dep_mapping ['purl' ], None )
154
+ if workspace_dependency and workspace_dependency .extracted_requirement :
155
+ dep_mapping ['extracted_requirement' ] = workspace_dependency .extracted_requirement
156
+
157
+ if 'workspace' in dep_mapping ["extra_data" ]:
158
+ dep_mapping ['extra_data' ].pop ('workspace' )
159
+
160
+ return resource_package_data
96
161
97
162
98
163
class CargoTomlHandler (CargoBaseHandler ):
@@ -105,16 +170,21 @@ class CargoTomlHandler(CargoBaseHandler):
105
170
106
171
@classmethod
107
172
def parse (cls , location , package_only = False ):
108
- package_data = toml .load (location , _dict = dict )
109
- core_package_data = package_data .get ('package ' , {})
110
- workspace = package_data .get ('workspace ' , {})
173
+ package_data_toml = toml .load (location , _dict = dict )
174
+ workspace = package_data_toml .get ('workspace ' , {})
175
+ core_package_data = package_data_toml .get ('package ' , {})
111
176
extra_data = {}
177
+ if workspace :
178
+ extra_data ['workspace' ] = workspace
179
+
180
+ package_data = core_package_data .copy ()
181
+ for key , value in package_data .items ():
182
+ if isinstance (value , dict ) and 'workspace' in value :
183
+ core_package_data .pop (key )
184
+ extra_data [key ] = 'workspace'
112
185
113
186
name = core_package_data .get ('name' )
114
187
version = core_package_data .get ('version' )
115
- if isinstance (version , dict ) and "workspace" in version :
116
- version = None
117
- extra_data ["version" ] = "workspace"
118
188
119
189
description = core_package_data .get ('description' ) or ''
120
190
description = description .strip ()
@@ -132,22 +202,28 @@ def parse(cls, location, package_only=False):
132
202
133
203
# cargo dependencies are complex and can be overriden at multiple levels
134
204
dependencies = []
135
- for key , value in core_package_data .items ():
205
+ for key , value in package_data_toml .items ():
136
206
if key .endswith ('dependencies' ):
137
207
dependencies .extend (dependency_mapper (dependencies = value , scope = key ))
138
208
139
209
# TODO: add file refs:
140
210
# - readme, include and exclude
141
- # TODO: other URLs
142
- # - documentation
143
211
144
212
vcs_url = core_package_data .get ('repository' )
145
213
homepage_url = core_package_data .get ('homepage' )
146
214
repository_homepage_url = name and f'https://crates.io/crates/{ name } '
147
215
repository_download_url = name and version and f'https://crates.io/api/v1/crates/{ name } /{ version } /download'
148
216
api_data_url = name and f'https://crates.io/api/v1/crates/{ name } '
149
- if workspace :
150
- extra_data ["workspace" ] = workspace
217
+
218
+ extra_data_mappings = {
219
+ "documentation" : "documentation_url" ,
220
+ "rust-version" : "rust_version" ,
221
+ "edition" : "rust_edition" ,
222
+ }
223
+ for cargo_attribute , extra_attribute in extra_data_mappings .items ():
224
+ value = core_package_data .get (cargo_attribute )
225
+ if value :
226
+ extra_data [extra_attribute ] = value
151
227
152
228
package_data = dict (
153
229
datasource_id = cls .datasource_id ,
@@ -156,6 +232,7 @@ def parse(cls, location, package_only=False):
156
232
version = version ,
157
233
primary_language = cls .default_primary_language ,
158
234
description = description ,
235
+ keywords = keywords ,
159
236
parties = parties ,
160
237
extracted_license_statement = extracted_license_statement ,
161
238
vcs_url = vcs_url ,
@@ -171,6 +248,7 @@ def parse(cls, location, package_only=False):
171
248
172
249
CARGO_ATTRIBUTE_MAPPING = {
173
250
# Fields in PackageData model: Fields in cargo
251
+ "version" : "version" ,
174
252
"homepage_url" : "homepage" ,
175
253
"vcs_url" : "repository" ,
176
254
"keywords" : "categories" ,
@@ -179,6 +257,9 @@ def parse(cls, location, package_only=False):
179
257
"license_detections" : "license_detections" ,
180
258
"declared_license_expression" : "declared_license_expression" ,
181
259
"declared_license_expression_spdx" : "declared_license_expression_spdx" ,
260
+ # extra data fields (reverse mapping)
261
+ "edition" : "rust_edition" ,
262
+ "rust-version" : "rust_version" ,
182
263
}
183
264
184
265
@@ -237,25 +318,36 @@ def dependency_mapper(dependencies, scope='dependencies'):
237
318
"""
238
319
is_runtime = not scope .endswith (('dev-dependencies' , 'build-dependencies' ))
239
320
for name , requirement in dependencies .items ():
321
+ extra_data = {}
322
+ extracted_requirement = None
240
323
if isinstance (requirement , str ):
241
324
# plain version requirement
242
325
is_optional = False
326
+ extracted_requirement = requirement
327
+
243
328
elif isinstance (requirement , dict ):
244
- # complex requirement, with more than version are harder to handle
245
- # so we just dump
329
+ # complex requirement, we extract version if available
330
+ # everything else is just dumped in extra data
331
+ # here {workspace = true} means dependency version
332
+ # should be inherited
246
333
is_optional = requirement .pop ('optional' , False )
247
- requirement = saneyaml .dump (requirement )
334
+ if 'version' in requirement :
335
+ extracted_requirement = requirement .get ('version' )
336
+
337
+ if requirement :
338
+ extra_data = requirement
248
339
249
340
yield models .DependentPackage (
250
341
purl = PackageURL (
251
342
type = 'cargo' ,
252
343
name = name ,
253
344
).to_string (),
254
- extracted_requirement = requirement ,
345
+ extracted_requirement = extracted_requirement ,
255
346
scope = scope ,
256
347
is_runtime = is_runtime ,
257
348
is_optional = is_optional ,
258
349
is_resolved = False ,
350
+ extra_data = extra_data ,
259
351
)
260
352
261
353
0 commit comments