comparison env/lib/python3.7/site-packages/planemo/conda_verify/recipe.py @ 0:26e78fe6e8c4 draft

"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author shellac
date Sat, 02 May 2020 07:14:21 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:26e78fe6e8c4
1 from __future__ import (
2 absolute_import,
3 division,
4 print_function,
5 )
6
7 import os
8 import re
9 from os.path import (
10 basename,
11 getsize,
12 isfile,
13 join,
14 )
15
16 import yaml
17
18 from planemo.conda_verify.const import (
19 FIELDS,
20 LICENSE_FAMILIES,
21 )
22 from planemo.conda_verify.utils import (
23 all_ascii,
24 get_bad_seq,
25 memoized,
26 )
27
28 PEDANTIC = True
29 sel_pat = re.compile(r'(.+?)\s*\[(.+)\]$')
30 name_pat = re.compile(r'[a-z0-9_][a-z0-9_\-\.]*$')
31 version_pat = re.compile(r'[\w\.]+$')
32 url_pat = re.compile(r'(ftp|http(s)?)://')
33
34
35 class RecipeError(Exception):
36 pass
37
38
39 def ns_cfg(cfg):
40 plat = cfg['plat']
41 py = cfg['PY']
42 np = cfg['NPY']
43 for x in py, np:
44 assert isinstance(x, int), x
45 return dict(
46 nomkl=False,
47 debug=False,
48 linux=plat.startswith('linux-'),
49 linux32=bool(plat == 'linux-32'),
50 linux64=bool(plat == 'linux-64'),
51 armv7l=False,
52 arm=False,
53 ppc64le=False,
54 osx=plat.startswith('osx-'),
55 unix=plat.startswith(('linux-', 'osx-')),
56 win=plat.startswith('win-'),
57 win32=bool(plat == 'win-32'),
58 win64=bool(plat == 'win-64'),
59 x86=plat.endswith(('-32', '-64')),
60 x86_64=plat.endswith('-64'),
61 py=py,
62 py3k=bool(30 <= py < 40),
63 py2k=bool(20 <= py < 30),
64 py26=bool(py == 26),
65 py27=bool(py == 27),
66 py33=bool(py == 33),
67 py34=bool(py == 34),
68 py35=bool(py == 35),
69 np=np,
70 )
71
72
73 def select_lines(data, namespace):
74 lines = []
75 for line in data.splitlines():
76 line = line.rstrip()
77 m = sel_pat.match(line)
78 if m:
79 if PEDANTIC:
80 x = m.group(1).strip()
81 # error on comment, unless the whole line is a comment
82 if '#' in x and not x.startswith('#'):
83 raise RecipeError("found commented selector: %s" % line)
84 cond = m.group(2)
85 if eval(cond, namespace, {}):
86 lines.append(m.group(1))
87 continue
88 lines.append(line)
89 return '\n'.join(lines) + '\n'
90
91
92 @memoized
93 def yamlize(data):
94 res = yaml.safe_load(data)
95 # ensure the result is a dict
96 if res is None:
97 res = {}
98 return res
99
100
101 def parse(data, cfg):
102 if cfg is not None:
103 data = select_lines(data, ns_cfg(cfg))
104 # ensure we create new object, because yamlize is memoized
105 return dict(yamlize(data))
106
107
108 def get_field(meta, field, default=None):
109 section, key = field.split('/')
110 submeta = meta.get(section)
111 if submeta is None:
112 submeta = {}
113 res = submeta.get(key)
114 if res is None:
115 res = default
116 return res
117
118
119 def check_name(name):
120 if name:
121 name = str(name)
122 else:
123 raise RecipeError("package name missing")
124 if not name_pat.match(name) or name.endswith(('.', '-', '_')):
125 raise RecipeError("invalid package name '%s'" % name)
126 seq = get_bad_seq(name)
127 if seq:
128 raise RecipeError("'%s' is not allowed in "
129 "package name: '%s'" % (seq, name))
130
131
132 def check_version(ver):
133 if ver:
134 ver = str(ver)
135 else:
136 raise RecipeError("package version missing")
137 if not version_pat.match(ver):
138 raise RecipeError("invalid version '%s'" % ver)
139 if ver.startswith(('_', '.')) or ver.endswith(('_', '.')):
140 raise RecipeError("version cannot start or end with '_' or '.': %s" %
141 ver)
142 seq = get_bad_seq(ver)
143 if seq:
144 raise RecipeError("'%s' not allowed in version '%s'" % (seq, ver))
145
146
147 def check_build_number(bn):
148 if not (isinstance(bn, int) and bn >= 0):
149 raise RecipeError("build/number '%s' (not a positive interger)" % bn)
150
151
152 def check_requirements(meta):
153 for req in get_field(meta, 'requirements/run', []):
154 name = req.split()[0]
155 if not name_pat.match(name):
156 raise RecipeError("invalid run requirement name '%s'" % name)
157
158
159 def check_license_family(meta):
160 if not PEDANTIC:
161 return
162 lf = get_field(meta, 'about/license_family',
163 get_field(meta, 'about/license'))
164 if lf not in LICENSE_FAMILIES:
165 print("""\
166 Error: license_family is invalid: %s
167 Note that about/license_family falls back to about/license.
168 Allowed license families are:""" % lf)
169 for x in LICENSE_FAMILIES:
170 print(" - %s" % x)
171 raise RecipeError("wrong license family")
172
173
174 def check_url(url):
175 if not url_pat.match(url):
176 raise RecipeError("not a valid URL: %s" % url)
177
178
179 def check_about(meta):
180 summary = get_field(meta, 'about/summary')
181 if summary and len(summary) > 80:
182 msg = "summary exceeds 80 characters"
183 if PEDANTIC:
184 raise RecipeError(msg)
185 else:
186 print("Warning: %s" % msg)
187
188 for field in ('about/home', 'about/dev_url', 'about/doc_url',
189 'about/license_url'):
190 url = get_field(meta, field)
191 if url:
192 check_url(url)
193
194 check_license_family(meta)
195
196
197 hash_pat = {'md5': re.compile(r'[a-f0-9]{32}$'),
198 'sha1': re.compile(r'[a-f0-9]{40}$'),
199 'sha256': re.compile(r'[a-f0-9]{64}$')}
200
201
202 def check_source(meta):
203 src = meta.get('source')
204 if not src:
205 return
206 fn = src.get('fn')
207 if fn:
208 for ht in 'md5', 'sha1', 'sha256':
209 hexgigest = src.get(ht)
210 if hexgigest and not hash_pat[ht].match(hexgigest):
211 raise RecipeError("invalid hash: %s" % hexgigest)
212 url = src.get('url')
213 if url:
214 check_url(url)
215
216 git_url = src.get('git_url')
217 if git_url and (src.get('git_tag') and src.get('git_branch')):
218 raise RecipeError("cannot specify both git_branch and git_tag")
219
220
221 def validate_meta(meta):
222 for section in meta:
223 if PEDANTIC and section not in FIELDS:
224 raise RecipeError("Unknown section: %s" % section)
225 submeta = meta.get(section)
226 if submeta is None:
227 submeta = {}
228 for key in submeta:
229 if PEDANTIC and key not in FIELDS[section]:
230 raise RecipeError("in section %r: unknown key %r" %
231 (section, key))
232
233 check_name(get_field(meta, 'package/name'))
234 check_version(get_field(meta, 'package/version'))
235 check_build_number(get_field(meta, 'build/number', 0))
236 check_requirements(meta)
237 check_about(meta)
238 check_source(meta)
239
240
241 def validate_files(recipe_dir, meta):
242 for field in 'test/files', 'source/patches':
243 flst = get_field(meta, field)
244 if not flst:
245 continue
246 for fn in flst:
247 if PEDANTIC and fn.startswith('..'):
248 raise RecipeError("path outsite recipe: %s" % fn)
249 path = join(recipe_dir, fn)
250 if isfile(path):
251 continue
252 raise RecipeError("no such file '%s'" % path)
253
254
255 def iter_cfgs():
256 for py in 27, 34, 35:
257 for plat in 'linux-64', 'linux-32', 'osx-64', 'win-32', 'win-64':
258 yield dict(plat=plat, PY=py, NPY=111)
259
260
261 def dir_size(dir_path):
262 return sum(sum(getsize(join(root, fn)) for fn in files)
263 for root, unused_dirs, files in os.walk(dir_path))
264
265
266 def check_dir_content(recipe_dir):
267 disallowed_extensions = (
268 '.tar', '.tar.gz', '.tar.bz2', '.tar.xz',
269 '.so', '.dylib', '.la', '.a', '.dll', '.pyd',
270 )
271 for root, unused_dirs, files in os.walk(recipe_dir):
272 for fn in files:
273 fn_lower = fn.lower()
274 if fn_lower.endswith(disallowed_extensions):
275 if PEDANTIC:
276 raise RecipeError("found: %s" % fn)
277 else:
278 print("Warning: found: %s" % fn)
279 path = join(root, fn)
280 # only allow small archives for testing
281 if (PEDANTIC and fn_lower.endswith(('.bz2', '.gz')) and getsize(path) > 512):
282 raise RecipeError("found: %s (too large)" % fn)
283
284 if basename(recipe_dir) == 'icu':
285 return
286
287 # check total size od recipe directory (recursively)
288 kb_size = dir_size(recipe_dir) / 1024
289 kb_limit = 512
290 if PEDANTIC and kb_size > kb_limit:
291 raise RecipeError("recipe too large: %d KB (limit %d KB)" %
292 (kb_size, kb_limit))
293
294 if PEDANTIC:
295 try:
296 with open(join(recipe_dir, 'build.sh'), 'rb') as fi:
297 data = fi.read()
298 if data and not data.decode('utf-8').startswith(('#!/bin/bash\n',
299 '#!/bin/sh\n')):
300 raise RecipeError("not a bash script: build.sh")
301 except IOError:
302 pass
303
304
305 def render_jinja2(recipe_dir):
306 import jinja2
307
308 loaders = [jinja2.FileSystemLoader(recipe_dir)]
309 env = jinja2.Environment(loader=jinja2.ChoiceLoader(loaders))
310 template = env.get_or_select_template('meta.yaml')
311 return template.render(environment=env)
312
313
314 def validate_recipe(recipe_dir, pedantic=True):
315 global PEDANTIC
316 PEDANTIC = bool(pedantic)
317
318 meta_path = join(recipe_dir, 'meta.yaml')
319 with open(meta_path, 'rb') as fi:
320 data = fi.read()
321 if PEDANTIC and not all_ascii(data):
322 raise RecipeError("non-ASCII in: %s" % meta_path)
323 if b'{{' in data:
324 if PEDANTIC:
325 raise RecipeError("found {{ in %s (Jinja templating not allowed)" %
326 meta_path)
327 else:
328 data = render_jinja2(recipe_dir)
329 else:
330 data = data.decode('utf-8')
331
332 check_dir_content(recipe_dir)
333
334 for cfg in iter_cfgs():
335 meta = parse(data, cfg)
336 validate_meta(meta)
337 validate_files(recipe_dir, meta)