8
8
#
9
9
10
10
import io
11
+ import json
11
12
12
13
import saneyaml
13
14
from packageurl import PackageURL
23
24
See https://repo.continuum.io/pkgs/free for examples.
24
25
"""
25
26
26
- # TODO: there are likely other package data files for Conda
27
+
28
+ class CondaBaseHandler (models .DatafileHandler ):
29
+ """
30
+ Assemble package data and files present in conda manifests present in the
31
+ usual structure of a conda installation. Here the manifests which are
32
+ assembled together are:
33
+ - Conda metadata JSON (CondaMetaJsonHandler)
34
+ - Conda meta.yaml recipe (CondaMetaYamlHandler)
35
+
36
+ Example paths for these manifests:
37
+ /opt/conda/conda-meta/requests-2.32.3-py312h06a4308_1.json
38
+ /opt/conda/pkgs/requests-2.32.3-py312h06a4308_1/info/recipe/meta.yaml
39
+ """
40
+
41
+ @classmethod
42
+ def assemble (cls , package_data , resource , codebase , package_adder = models .add_to_package ):
43
+
44
+ if codebase .has_single_resource :
45
+ yield from models .DatafileHandler .assemble (package_data , resource , codebase , package_adder )
46
+ return
47
+
48
+ # We do not have any package data detected here
49
+ if not resource .package_data :
50
+ return
51
+
52
+ # If this is a Conda meta.yaml, try to find the corresponding metadata JSON
53
+ # and if present, run assembly on the metadata resource
54
+ if CondaMetaYamlHandler .is_datafile (resource .location ):
55
+ conda_meta_json = cls .find_conda_meta_json_resource (resource , codebase )
56
+ if conda_meta_json :
57
+ package_data_meta_json , = conda_meta_json .package_data
58
+ yield from cls .assemble (
59
+ package_data = package_data_meta_json ,
60
+ resource = conda_meta_json ,
61
+ codebase = codebase ,
62
+ package_adder = package_adder ,
63
+ )
64
+
65
+ # corresponding metadata JSON does not exist, so handle this meta.yaml
66
+ else :
67
+ yield from cls .assemble_from_meta_yaml_only (
68
+ package_data = package_data ,
69
+ resource = resource ,
70
+ codebase = codebase ,
71
+ package_adder = package_adder ,
72
+ )
73
+
74
+ return
75
+
76
+ if not package_data .purl :
77
+ yield resource
78
+ return
79
+
80
+ package = models .Package .from_package_data (
81
+ package_data = package_data ,
82
+ datafile_path = resource .path ,
83
+ )
84
+ yield from cls .get_and_assmeble_from_meta_yaml (
85
+ package = package ,
86
+ resource = resource ,
87
+ codebase = codebase ,
88
+ package_adder = package_adder ,
89
+ )
90
+
91
+ package .populate_license_fields ()
92
+ yield package
93
+
94
+ cls .assign_package_to_resources (
95
+ package = package ,
96
+ resource = resource ,
97
+ codebase = codebase ,
98
+ package_adder = package_adder ,
99
+ )
100
+
101
+ # we yield this as we do not want this further processed
102
+ yield resource
103
+
104
+ cls .assign_packages_to_resources_from_metadata_json (
105
+ package = package ,
106
+ package_data = package_data ,
107
+ resource = resource ,
108
+ codebase = codebase ,
109
+ package_adder = package_adder ,
110
+ )
111
+
112
+ @classmethod
113
+ def assign_packages_to_resources_from_metadata_json (
114
+ cls ,
115
+ package ,
116
+ package_data ,
117
+ resource ,
118
+ codebase ,
119
+ package_adder = models .add_to_package ,
120
+ ):
121
+ """
122
+ Get the file paths present in the `package_data` of a metadata JSON `resource`
123
+ and assign them to the `package` created from the manifest.
124
+ """
125
+ extracted_package_dir = package_data .extra_data .get ('extracted_package_dir' )
126
+ files = package_data .extra_data .get ('files' )
127
+
128
+ if not extracted_package_dir or not files :
129
+ return
130
+
131
+ conda_metadata_dir = resource .parent (codebase )
132
+ if not conda_metadata_dir :
133
+ return
134
+
135
+ conda_root_dir = conda_metadata_dir .parent (codebase )
136
+ if not conda_root_dir :
137
+ return
138
+
139
+ root_path_segment , _ , package_dir = extracted_package_dir .rpartition ("/pkgs/" )
140
+ if not conda_root_dir .path .endswith (root_path_segment ):
141
+ return
142
+
143
+ package_dir_path = f"{ conda_root_dir .path } /pkgs/{ package_dir } "
144
+ package_dir_resource = codebase .get_resource (path = package_dir_path )
145
+ if package_dir_resource :
146
+ cls .assign_package_to_resources (
147
+ package = package ,
148
+ resource = package_dir_resource ,
149
+ codebase = codebase ,
150
+ package_adder = package_adder ,
151
+ )
152
+
153
+ conda_package_path = f"{ conda_root_dir .path } /pkgs/{ package_dir } .conda"
154
+ conda_package_resource = codebase .get_resource (path = conda_package_path )
155
+ if conda_package_resource :
156
+ cls .assign_package_to_resources (
157
+ package = package ,
158
+ resource = conda_package_resource ,
159
+ codebase = codebase ,
160
+ package_adder = package_adder ,
161
+ )
162
+
163
+ for file_path in files :
164
+ full_file_path = f"{ conda_root_dir .path } /{ file_path } "
165
+ file_resource = codebase .get_resource (path = full_file_path )
166
+ if file_resource :
167
+ cls .assign_package_to_resources (
168
+ package = package ,
169
+ resource = file_resource ,
170
+ codebase = codebase ,
171
+ package_adder = package_adder ,
172
+ )
173
+
174
+ @classmethod
175
+ def get_and_assmeble_from_meta_yaml (cls , package , resource , codebase , package_adder = models .add_to_package ):
176
+ """
177
+ For a conda metadata JSON `resource`, try to find the corresponding meta.yaml and
178
+ update the `package` from it. Also yield dependencies present in the meta.yaml,
179
+ and the `resource` to complete assembling from this manifest.
180
+ """
181
+ conda_meta_yaml = cls .find_conda_meta_yaml_resource (resource , codebase )
182
+
183
+ if conda_meta_yaml :
184
+ conda_meta_yaml_package_data , = conda_meta_yaml .package_data
185
+ package .update (
186
+ package_data = conda_meta_yaml_package_data ,
187
+ datafile_path = conda_meta_yaml .path ,
188
+ )
189
+ cls .assign_package_to_resources (
190
+ package = package ,
191
+ resource = conda_meta_yaml ,
192
+ codebase = codebase ,
193
+ package_adder = package_adder ,
194
+ )
195
+ meta_yaml_package_data = models .PackageData .from_dict (conda_meta_yaml_package_data )
196
+ if meta_yaml_package_data .dependencies :
197
+ yield from models .Dependency .from_dependent_packages (
198
+ dependent_packages = meta_yaml_package_data .dependencies ,
199
+ datafile_path = conda_meta_yaml .path ,
200
+ datasource_id = meta_yaml_package_data .datasource_id ,
201
+ package_uid = package .package_uid ,
202
+ )
203
+
204
+ yield conda_meta_yaml
205
+
206
+ @classmethod
207
+ def assemble_from_meta_yaml_only (cls , package_data , resource , codebase , package_adder = models .add_to_package ):
208
+ """
209
+ Assemble and yeild package, dependencies and the meta YAML `resource` from
210
+ it's `package_data`, and also assign resources to the package.
211
+ """
212
+ if not package_data .purl :
213
+ return
214
+
215
+ package = models .Package .from_package_data (
216
+ package_data = package_data ,
217
+ datafile_path = resource .path ,
218
+ )
219
+ package .populate_license_fields ()
220
+ yield package
221
+
222
+ dependent_packages = package_data .dependencies
223
+ if dependent_packages :
224
+ yield from models .Dependency .from_dependent_packages (
225
+ dependent_packages = dependent_packages ,
226
+ datafile_path = resource .path ,
227
+ datasource_id = package_data .datasource_id ,
228
+ package_uid = package .package_uid ,
229
+ )
230
+
231
+ CondaMetaYamlHandler .assign_package_to_resources (
232
+ package = package ,
233
+ resource = resource ,
234
+ codebase = codebase ,
235
+ package_adder = package_adder ,
236
+ )
237
+ yield resource
238
+
239
+ @classmethod
240
+ def check_valid_packages_dir_name (cls , package_dir_resource , resource , codebase ):
241
+ """
242
+ Return the name of the `package_dir_resource`, if it is valid, i.e.
243
+ the package (name, version) data present in `resource` matches the
244
+ directory name, and the package directory is present in it's usual
245
+ location in a conda installation.
246
+ """
247
+ package_dir_parent = package_dir_resource .parent (codebase )
248
+
249
+ meta_yaml_package_data , = resource .package_data
250
+ name = meta_yaml_package_data .get ("name" )
251
+ version = meta_yaml_package_data .get ("version" )
252
+ if f"{ name } -{ version } " in package_dir_resource .name and (
253
+ package_dir_parent and "pkgs" in package_dir_parent .name
254
+ ):
255
+ return package_dir_resource .name
256
+
257
+ @classmethod
258
+ def find_conda_meta_json_resource (cls , resource , codebase ):
259
+ """
260
+ Given a resource for a conda meta.yaml resource, find if it has any
261
+ corresponding metadata JSON located inside the conda-meta/ directory,
262
+ and return the resource if they exist, else return None.
263
+ """
264
+ package_dir_resource = CondaMetaYamlHandler .get_conda_root (resource , codebase )
265
+ if not package_dir_resource or not resource .package_data :
266
+ return
267
+
268
+ package_dir_name = cls .check_valid_packages_dir_name (
269
+ package_dir_resource = package_dir_resource ,
270
+ resource = resource ,
271
+ codebase = codebase ,
272
+ )
273
+ if not package_dir_name :
274
+ return
275
+
276
+ root_resource = package_dir_resource .parent (codebase ).parent (codebase )
277
+ if not root_resource :
278
+ return
279
+
280
+ root_resource_path = root_resource .path
281
+ conda_meta_path = f"{ root_resource_path } /conda-meta/{ package_dir_name } .json"
282
+ conda_meta_resource = codebase .get_resource (path = conda_meta_path )
283
+
284
+ if conda_meta_resource and conda_meta_resource .package_data :
285
+ return conda_meta_resource
286
+
287
+ @classmethod
288
+ def find_conda_meta_yaml_resource (cls , resource , codebase ):
289
+ """
290
+ Given a resource for a metadata JSON located inside the conda-meta/
291
+ directory, find if it has any corresponding conda meta.yaml, and return
292
+ the resource if they exist, else return None.
293
+ """
294
+ package_dir_name , _json , _ = resource .name .rpartition (".json" )
295
+ parent_resource = resource .parent (codebase )
296
+ if not parent_resource and not parent_resource .name == "conda-meta" :
297
+ return
298
+
299
+ root_resource = parent_resource .parent (codebase )
300
+ if not root_resource :
301
+ return
302
+
303
+ root_resource_path = root_resource .path
304
+ package_dir_path = f"{ root_resource_path } /pkgs/{ package_dir_name } /"
305
+ package_dir_resource = codebase .get_resource (path = package_dir_path )
306
+ if not package_dir_resource :
307
+ return
308
+
309
+ meta_yaml_path = f"{ package_dir_path } info/recipe/meta.yaml"
310
+ meta_yaml_resource = codebase .get_resource (path = meta_yaml_path )
311
+ if meta_yaml_resource and meta_yaml_resource .package_data :
312
+ return meta_yaml_resource
313
+
314
+
315
+ class CondaMetaJsonHandler (CondaBaseHandler ):
316
+ datasource_id = 'conda_meta_json'
317
+ path_patterns = ('*conda-meta/*.json' ,)
318
+ default_package_type = 'conda'
319
+ default_primary_language = 'Python'
320
+ description = 'Conda metadata JSON in rootfs'
321
+ documentation_url = 'https://docs.conda.io/'
322
+
323
+ @classmethod
324
+ def parse (cls , location , package_only = False ):
325
+ with io .open (location , encoding = 'utf-8' ) as loc :
326
+ conda_metadata = json .load (loc )
327
+
328
+ name = conda_metadata .get ('name' )
329
+ version = conda_metadata .get ('version' )
330
+ extracted_license_statement = conda_metadata .get ('license' )
331
+ download_url = conda_metadata .get ('url' )
332
+
333
+ extra_data_fields = ['requested_spec' , 'channel' ]
334
+ package_file_fields = ['extracted_package_dir' , 'files' , 'package_tarball_full_path' ]
335
+ other_package_fields = ['size' , 'md5' , 'sha256' ]
336
+
337
+ extra_data = {}
338
+ for metadata_field in extra_data_fields + package_file_fields :
339
+ extra_data [metadata_field ] = conda_metadata .get (metadata_field )
340
+
341
+ package_data = dict (
342
+ datasource_id = cls .datasource_id ,
343
+ type = cls .default_package_type ,
344
+ name = name ,
345
+ version = version ,
346
+ extracted_license_statement = extracted_license_statement ,
347
+ download_url = download_url ,
348
+ extra_data = extra_data ,
349
+ )
350
+ for package_field in other_package_fields :
351
+ package_data [package_field ] = conda_metadata .get (package_field )
352
+ yield models .PackageData .from_data (package_data , package_only )
353
+
27
354
28
355
class CondaYamlHandler (BaseDependencyFileHandler ):
29
356
datasource_id = 'conda_yaml'
@@ -55,7 +382,7 @@ def parse(cls, location, package_only=False):
55
382
yield models .PackageData .from_data (package_data , package_only )
56
383
57
384
58
- class CondaMetaYamlHandler (models . DatafileHandler ):
385
+ class CondaMetaYamlHandler (CondaBaseHandler ):
59
386
datasource_id = 'conda_meta_yaml'
60
387
default_package_type = 'conda'
61
388
path_patterns = ('*/meta.yaml' ,)
@@ -67,13 +394,17 @@ def get_conda_root(cls, resource, codebase):
67
394
"""
68
395
Return a root Resource given a meta.yaml ``resource``.
69
396
"""
397
+ if not resource :
398
+ return
399
+
70
400
# the root is either the parent or further up for yaml stored under
71
401
# an "info" dir. We support extractcode extraction.
72
402
# in a source repo it would be in <repo>/conda.recipe/meta.yaml
73
403
paths = (
74
404
'info/recipe.tar-extract/recipe/meta.yaml' ,
75
405
'info/recipe/recipe/meta.yaml' ,
76
406
'conda.recipe/meta.yaml' ,
407
+ 'info/recipe/meta.yaml' ,
77
408
)
78
409
res = resource
79
410
for pth in paths :
0 commit comments