1
1
# SPDX-License-Identifier: GPL-3.0-or-later
2
+ from pathlib import Path
3
+ from textwrap import dedent
4
+ from typing import Any
2
5
import hashlib
3
6
import json
4
7
import logging
5
8
import tarfile
6
- from pathlib import Path
7
- from textwrap import dedent
9
+ import urllib
8
10
11
+ from packageurl import PackageURL
9
12
import tomli
10
13
11
14
from cachi2 .core .checksum import ChecksumInfo , must_match_any_checksum
12
- from cachi2 .core .models .input import Request
15
+ from cachi2 .core .models .input import CargoPackageInput , Request
13
16
from cachi2 .core .models .output import Component , ProjectFile , RequestOutput
14
17
from cachi2 .core .package_managers .general import download_binary_file
15
18
from cachi2 .core .rooted_path import RootedPath
19
+ from cachi2 .core .scm import get_repo_id
16
20
17
21
log = logging .getLogger (__name__ )
18
22
@@ -24,20 +28,20 @@ def fetch_cargo_source(request: Request) -> RequestOutput:
24
28
"""Resolve and fetch cargo dependencies for the given request."""
25
29
components : list [Component ] = []
26
30
for package in request .cargo_packages :
27
- info = _resolve_cargo (
28
- request .source_dir / package .path ,
29
- request .output_dir ,
30
- package .lock_file ,
31
- package .pkg_name ,
32
- package .pkg_version ,
33
- )
31
+ info = _resolve_cargo (request .source_dir , request .output_dir , package )
34
32
components .append (Component .from_package_dict (info ["package" ]))
35
-
36
33
for dependency in info ["dependencies" ]:
37
- components .append (Component .from_package_dict (dependency ))
34
+ dep_purl = _generate_purl_dependency (dependency )
35
+ components .append (
36
+ Component (
37
+ name = dependency ["name" ],
38
+ version = dependency ["version" ],
39
+ purl = dep_purl ,
40
+ )
41
+ )
38
42
39
43
cargo_config = ProjectFile (
40
- abspath = request .source_dir .join_within_root (".cargo/config.toml" ),
44
+ abspath = Path ( request .source_dir .join_within_root (".cargo/config.toml" ) ),
41
45
template = dedent (
42
46
"""
43
47
[source.crates-io]
@@ -56,18 +60,21 @@ def fetch_cargo_source(request: Request) -> RequestOutput:
56
60
)
57
61
58
62
59
- def _resolve_cargo (
60
- app_path : Path , output_dir : Path , lock_file = None , pkg_name = None , pkg_version = None
61
- ):
63
+ def _resolve_cargo (source_dir : Path , output_dir : Path , package : CargoPackageInput ):
62
64
"""
63
65
Resolve and fetch cargo dependencies for the given cargo application.
64
66
"""
67
+ app_path = source_dir / package .path
68
+ pkg_name = package .pkg_name
69
+ pkg_version = package .pkg_version
65
70
if pkg_name is None and pkg_version is None :
66
71
pkg_name , pkg_version = _get_cargo_metadata (app_path )
67
72
assert pkg_name and pkg_version , "INVALID PACKAGE"
68
73
74
+ purl = _generate_purl_main_package (pkg_name , pkg_version , source_dir )
75
+
69
76
dependencies = []
70
- lock_file = app_path / (lock_file or DEFAULT_LOCK_FILE )
77
+ lock_file = app_path / (package . lock_file or DEFAULT_LOCK_FILE )
71
78
72
79
cargo_lock_dict = tomli .load (lock_file .open ("rb" ))
73
80
for dependency in cargo_lock_dict ["package" ]:
@@ -78,7 +85,7 @@ def _resolve_cargo(
78
85
79
86
dependencies = _download_cargo_dependencies (output_dir , dependencies )
80
87
return {
81
- "package" : {"name" : pkg_name , "version" : pkg_version , "type" : "cargo" },
88
+ "package" : {"name" : pkg_name , "version" : pkg_version , "type" : "cargo" , "purl" : purl },
82
89
"dependencies" : dependencies ,
83
90
"lock_file" : lock_file ,
84
91
}
@@ -90,7 +97,9 @@ def _get_cargo_metadata(package_dir: Path):
90
97
return metadata ["package" ]["name" ], metadata ["package" ]["version" ]
91
98
92
99
93
- def _download_cargo_dependencies (output_path : RootedPath , cargo_dependencies : list [dict ]):
100
+ def _download_cargo_dependencies (
101
+ output_path : RootedPath , cargo_dependencies : list [dict ]
102
+ ) -> list [dict [str , Any ]]:
94
103
downloads = []
95
104
for dep in cargo_dependencies :
96
105
checksum_info = ChecksumInfo (algorithm = "sha256" , hexdigest = dep ["checksum" ])
@@ -112,13 +121,16 @@ def _download_cargo_dependencies(output_path: RootedPath, cargo_dependencies: li
112
121
"path" : vendored_dep ,
113
122
"type" : "cargo" ,
114
123
"dev" : False ,
124
+ "kind" : "cratesio" ,
115
125
}
116
126
)
117
127
return downloads
118
128
129
+
119
130
def _calc_sha256 (content : bytes ):
120
131
return hashlib .sha256 (content ).hexdigest ()
121
132
133
+
122
134
def generate_cargo_checksum (crate_path : Path ):
123
135
"""Generate Cargo checksums
124
136
@@ -156,3 +168,55 @@ def prepare_crate_as_vendored_dep(crate_path: Path) -> Path:
156
168
cargo_checksum = crate_path .parent / folder_name / ".cargo-checksum.json"
157
169
json .dump (checksums , cargo_checksum .open ("w" ))
158
170
return crate_path .parent / folder_name
171
+
172
+
173
+ def _generate_purl_main_package (name : str , version : str , package_path : RootedPath ) -> str :
174
+ """Get the purl for this package."""
175
+ type = "cargo"
176
+ url = get_repo_id (package_path .root ).as_vcs_url_qualifier ()
177
+ qualifiers = {"vcs_url" : url }
178
+ if package_path .subpath_from_root != Path ("." ):
179
+ subpath = package_path .subpath_from_root .as_posix ()
180
+ else :
181
+ subpath = None
182
+
183
+ purl = PackageURL (
184
+ type = type ,
185
+ name = name ,
186
+ version = version ,
187
+ qualifiers = qualifiers ,
188
+ subpath = subpath ,
189
+ )
190
+
191
+ return purl .to_string ()
192
+
193
+
194
+ def _generate_purl_dependency (package : dict [str , Any ]) -> str :
195
+ """Get the purl for this dependency."""
196
+ type = "cargo"
197
+ name = package ["name" ]
198
+ dependency_kind = package .get ("kind" , None )
199
+ version = None
200
+ qualifiers = None
201
+
202
+ if dependency_kind == "cratesio" :
203
+ version = package ["version" ]
204
+ elif dependency_kind == "vcs" :
205
+ qualifiers = {"vcs_url" : package ["version" ]}
206
+ elif dependency_kind == "url" :
207
+ parsed_url = urllib .parse .urldefrag (package ["version" ])
208
+ fragments = urllib .parse .parse_qs (parsed_url .fragment )
209
+ checksum = fragments ["cachito_hash" ][0 ]
210
+ qualifiers = {"download_url" : parsed_url .url , "checksum" : checksum }
211
+ else :
212
+ # Should not happen
213
+ raise RuntimeError (f"Unexpected requirement kind: { dependency_kind } " )
214
+
215
+ purl = PackageURL (
216
+ type = type ,
217
+ name = name ,
218
+ version = version ,
219
+ qualifiers = qualifiers ,
220
+ )
221
+
222
+ return purl .to_string ()
0 commit comments