77# See https://aboutcode.org for more information about nexB OSS projects.
88#
99
10+ import os
1011import re
1112
1213import 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
100176class 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
203281person_parser = re .compile (
0 commit comments