77# See https://aboutcode.org for more information about nexB OSS projects.
88#
99
10- import io
11- import logging
1210import re
1311
14- import attr
1512from packageurl import PackageURL
1613
17- from commoncode import filetype
1814from packagedcode import models
1915
20-
2116"""
22- Handle opam package.
17+ Handle OCaml opam package.
2318"""
2419
25- TRACE = False
26-
27- logger = logging .getLogger (__name__ )
28-
29- if TRACE :
30- import sys
31- logging .basicConfig (stream = sys .stdout )
32- logger .setLevel (logging .DEBUG )
33-
3420
35- @ attr . s ()
36- class OpamPackageData ( models . PackageData ):
37-
38- default_type = 'opam'
21+ class OpamFileHandler ( models . DatafileHandler ):
22+ datasource_id = 'opam_file'
23+ path_patterns = ( '*opam' ,)
24+ default_package_type = 'opam'
3925 default_primary_language = 'Ocaml'
40- default_web_baseurl = 'https://opam.ocaml.org/packages'
41- default_download_baseurl = None
42- default_api_baseurl = 'https://github.com/ocaml/opam-repository/blob/master/packages'
26+ description = 'Ocaml Opam file'
27+ documentation_url = 'https://opam.ocaml.org/doc/Manual.html#Common-file-format'
4328
4429 @classmethod
45- def get_package_root (cls , manifest_resource , codebase ):
46- return manifest_resource .parent (codebase )
47-
48- def repository_homepage_url (self , baseurl = default_web_baseurl ):
49- if self .name :
50- return '{}/{}' .format (baseurl , self .name )
51-
52- def api_data_url (self , baseurl = default_api_baseurl ):
53- if self .name and self .version :
54- return '{}/{}/{}.{}/opam' .format (baseurl , self .name , self .name , self .version )
55-
56-
57- @attr .s ()
58- class OpamFile (OpamPackageData , models .PackageDataFile ):
59-
60- file_patterns = ('*opam' ,)
61- extensions = ('.opam' ,)
30+ def get_package_root (cls , resource , codebase ):
31+ return resource .parent (codebase )
6232
6333 @classmethod
64- def is_package_data_file (cls , location ):
65- """
66- Return True if the file at ``location`` is likely a manifest of this type.
67- """
68- return filetype .is_file (location ) and location .endswith ('opam' )
69-
70- @classmethod
71- def recognize (cls , location ):
72- """
73- Yield one or more Package manifest objects given a file ``location`` pointing to a
74- package archive, manifest or similar.
75- """
34+ def parse (cls , location ):
7635 opams = parse_opam (location )
7736
7837 package_dependencies = []
7938 deps = opams .get ('depends' ) or []
8039 for dep in deps :
8140 package_dependencies .append (
8241 models .DependentPackage (
83- purl = dep . purl ,
84- extracted_requirement = dep . version ,
42+ purl = dep [ " purl" ] ,
43+ extracted_requirement = dep [ " version" ] ,
8544 scope = 'dependency' ,
8645 is_runtime = True ,
8746 is_optional = False ,
@@ -91,6 +50,7 @@ def recognize(cls, location):
9150
9251 name = opams .get ('name' )
9352 version = opams .get ('version' )
53+
9454 homepage_url = opams .get ('homepage' )
9555 download_url = opams .get ('src' )
9656 vcs_url = opams .get ('dev-repo' )
@@ -100,6 +60,8 @@ def recognize(cls, location):
10060 md5 = opams .get ('md5' )
10161 sha256 = opams .get ('sha256' )
10262 sha512 = opams .get ('sha512' )
63+ repository_homepage_url = get_repository_homepage_url (name )
64+ api_data_url = get_api_data_url (name , version )
10365
10466 short_desc = opams .get ('synopsis' ) or ''
10567 long_desc = opams .get ('description' ) or ''
@@ -128,7 +90,9 @@ def recognize(cls, location):
12890 )
12991 )
13092
131- package = cls (
93+ package_data = models .PackageData (
94+ datasource_id = cls .datasource_id ,
95+ type = cls .default_package_type ,
13296 name = name ,
13397 version = version ,
13498 vcs_url = vcs_url ,
@@ -142,86 +106,29 @@ def recognize(cls, location):
142106 declared_license = declared_license ,
143107 description = description ,
144108 parties = parties ,
145- dependencies = package_dependencies
109+ dependencies = package_dependencies ,
110+ api_data_url = api_data_url ,
111+ repository_homepage_url = repository_homepage_url ,
112+ primary_language = cls .default_primary_language
146113 )
147114
148- yield package
115+ if not package_data .license_expression and package_data .declared_license :
116+ package_data .license_expression = models .compute_normalized_license (package_data .declared_license )
149117
118+ yield package_data
150119
151- @attr .s ()
152- class OpamPackage (OpamPackageData , models .Package ):
153- """
154- A Opam Package that is created out of one/multiple opam package
155- manifests and package-like data, with it's files.
156- """
120+ @classmethod
121+ def assign_package_to_resources (cls , package , resource , codebase ):
122+ return super ().assign_package_to_parent_tree (package , resource , codebase )
157123
158- @property
159- def manifests (self ):
160- return [
161- OpamFile
162- ]
163124
125+ def get_repository_homepage_url (name ):
126+ return name and '{https://opam.ocaml.org/packages}/{name}'
164127
165- """
166- Example:-
167-
168- Sample opam file(sample3.opam):
169- opam-version: "2.0"
170- version: "4.11.0+trunk"
171- synopsis: "OCaml development version"
172- depends: [
173- "ocaml" {= "4.11.0" & post}
174- "base-unix" {post}
175- ]
176- conflict-class: "ocaml-core-compiler"
177- flags: compiler
178- setenv: CAML_LD_LIBRARY_PATH = "%{lib}%/stublibs"
179- build: [
180- ["./configure" "--prefix=%{prefix}%"]
181- [make "-j%{jobs}%"]
182- ]
183- install: [make "install"]
184- 185- homepage: "https://github.com/ocaml/ocaml/"
186- bug-reports: "https://github.com/ocaml/ocaml/issues"
187- authors: [
188- "Xavier Leroy"
189- "Damien Doligez"
190- "Alain Frisch"
191- "Jacques Garrigue"
192- ]
193-
194- >>> p = parse_opam('sample3.opam')
195- >>> for k, v in p.items():
196- >>> print(k, v)
197-
198- Output:
199- opam-version 2.0
200- version 4.11.0+trunk
201- synopsis OCaml development version
202- depends [Opam(name='ocaml', version='= 4.11.0 & post'), Opam(name='base-unix', version='post')]
203- conflict-class ocaml-core-compiler
204- flags compiler
205- setenv CAML_LD_LIBRARY_PATH = %{lib}%/stublibs
206- build
207- install make install
208- 209- homepage https://github.com/ocaml/ocaml/
210- bug-reports https://github.com/ocaml/ocaml/issues
211- authors ['Xavier Leroy', 'Damien Doligez', 'Alain Frisch', 'Jacques Garrigue']
212- """
213-
214- @attr .s ()
215- class Opam (object ):
216- name = attr .ib (default = None )
217- version = attr .ib (default = None )
218128
219- @property
220- def purl (self ):
221- return PackageURL (
222- type = 'opam' ,
223- name = self .name
224- ).to_string ()
129+ def get_api_data_url (name , version ):
130+ if name and version :
131+ return f'https://github.com/ocaml/opam-repository/blob/master/packages/{ name } /{ name } .{ version } /opam'
225132
226133
227134# Regex expressions to parse file lines
@@ -259,78 +166,92 @@ def purl(self):
259166>>> assert p.group('version') == ('{= "1.0.0"}')
260167"""
261168
169+
262170def parse_opam (location ):
263171 """
264- Return a mapping of package data collected from the opam OCaml package manifest file at `location`.
172+ Return a mapping of package data collected from the opam OCaml package
173+ manifest file at ``location``.
174+ """
175+ with open (location ) as od :
176+ text = od .read ()
177+ return parse_opam_from_text (text )
178+
179+
180+ def parse_opam_from_text (text ):
181+ """
182+ Return a mapping of package data collected from the opam OCaml package
183+ manifest ``text``.
265184 """
266- with io .open (location , encoding = 'utf-8' ) as data :
267- lines = data .readlines ()
268185
269186 opam_data = {}
270187
188+ lines = text .splitlines ()
271189 for i , line in enumerate (lines ):
272190 parsed_line = parse_file_line (line )
273- if parsed_line :
274- key = parsed_line .group ('key' ).strip ()
275- value = parsed_line .group ('value' ).strip ()
276- if key == 'description' : # Get multiline description
277- value = ''
278- for cont in lines [i + 1 :]:
279- value += ' ' + cont .strip ()
280- if '"""' in cont :
191+ if not parsed_line :
192+ continue
193+ key = parsed_line .group ('key' ).strip ()
194+ value = parsed_line .group ('value' ).strip ()
195+ if key == 'description' : # Get multiline description
196+ value = ''
197+ for cont in lines [i + 1 :]:
198+ value += ' ' + cont .strip ()
199+ if '"""' in cont :
200+ break
201+
202+ opam_data [key ] = clean_data (value )
203+
204+ if key == 'maintainer' :
205+ stripped_val = value .strip ('["] ' )
206+ stripped_val = stripped_val .split ('" "' )
207+ opam_data [key ] = stripped_val
208+ elif key == 'authors' :
209+ if '[' in line : # If authors are present in multiple lines
210+ for authors in lines [i + 1 :]:
211+ value += ' ' + authors .strip ()
212+ if ']' in authors :
281213 break
282-
214+ value = value .strip ('["] ' )
215+ else :
216+ value = clean_data (value )
217+ value = value .split ('" "' )
218+ opam_data [key ] = value
219+ elif key == 'depends' : # Get multiline dependencies
220+ value = []
221+ for dep in lines [i + 1 :]:
222+ if ']' in dep :
223+ break
224+ parsed_dep = parse_dep (dep )
225+ if parsed_dep :
226+ version = parsed_dep .group ('version' ).strip ('{ } ' ).replace ('"' , '' )
227+ name = parsed_dep .group ('name' ).strip ()
228+ value .append (dict (
229+ purl = PackageURL (type = 'opam' , name = name ).to_string (),
230+ version = version ,
231+ ))
232+ opam_data [key ] = value
233+
234+ elif key == 'src' : # Get multiline src
235+ if not value :
236+ value = lines [i + 1 ].strip ()
283237 opam_data [key ] = clean_data (value )
284-
285- if key == 'maintainer' :
286- stripped_val = value .strip ('["] ' )
287- stripped_val = stripped_val .split ('" "' )
288- opam_data [key ] = stripped_val
289- elif key == 'authors' :
290- if '[' in line : # If authors are present in multiple lines
291- for authors in lines [i + 1 :]:
292- value += ' ' + authors .strip ()
293- if ']' in authors :
294- break
295- value = value .strip ('["] ' )
296- else :
297- value = clean_data (value )
298- value = value .split ('" "' )
299- opam_data [key ] = value
300- elif key == 'depends' : # Get multiline dependencies
301- value = []
302- for dep in lines [i + 1 :]:
303- if ']' in dep :
238+ elif key == 'checksum' : # Get checksums
239+ if '[' in line :
240+ for checksum in lines [i + 1 :]:
241+ checksum = checksum .strip ('" ' )
242+ if ']' in checksum :
304243 break
305- parsed_dep = parse_dep (dep )
306- if parsed_dep :
307- value .append (Opam (
308- name = parsed_dep .group ('name' ).strip (),
309- version = parsed_dep .group ('version' ).strip ('{ } ' ).replace ('"' , '' )
310- )
311- )
312- opam_data [key ] = value
313- elif key == 'src' : # Get multiline src
314- if not value :
315- value = lines [i + 1 ].strip ()
316- opam_data [key ] = clean_data (value )
317- elif key == 'checksum' : # Get checksums
318- if '[' in line :
319- for checksum in lines [i + 1 :]:
320- checksum = checksum .strip ('" ' )
321- if ']' in checksum :
322- break
323- parsed_checksum = parse_checksum (checksum )
324- key = clean_data (parsed_checksum .group ('key' ).strip ())
325- value = clean_data (parsed_checksum .group ('value' ).strip ())
326- opam_data [key ] = value
327- else :
328- value = value .strip ('" ' )
329- parsed_checksum = parse_checksum (value )
330- if parsed_checksum :
331- key = clean_data (parsed_checksum .group ('key' ).strip ())
332- value = clean_data (parsed_checksum .group ('value' ).strip ())
333- opam_data [key ] = value
244+ parsed_checksum = parse_checksum (checksum )
245+ key = clean_data (parsed_checksum .group ('key' ).strip ())
246+ value = clean_data (parsed_checksum .group ('value' ).strip ())
247+ opam_data [key ] = value
248+ else :
249+ value = value .strip ('" ' )
250+ parsed_checksum = parse_checksum (value )
251+ if parsed_checksum :
252+ key = clean_data (parsed_checksum .group ('key' ).strip ())
253+ value = clean_data (parsed_checksum .group ('value' ).strip ())
254+ opam_data [key ] = value
334255
335256 return opam_data
336257
0 commit comments