Mercurial > repos > shellac > guppy_basecaller
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) |
