summaryrefslogtreecommitdiffstats
path: root/scripts/lib/recipetool/create_buildsys_python.py
diff options
context:
space:
mode:
authorChristopher Larson <kergoth@gmail.com>2015-01-19 11:52:31 -0700
committerRichard Purdie <richard.purdie@linuxfoundation.org>2015-01-23 11:36:28 +0000
commit1b7b58ac97aafc2c88038bfb1f2301c4013f6761 (patch)
tree1e8a8ca8b148832c912643cb3a49fb9c0127c9c5 /scripts/lib/recipetool/create_buildsys_python.py
parente490d79fb740319b35227eb66968be852a781036 (diff)
downloadpoky-1b7b58ac97aafc2c88038bfb1f2301c4013f6761.tar.gz
recipetool: add python dependency scanning support
This uses a standalone python script named `pythondeps` which now lives in scripts. It supports scanning for provided packages and imported modules/packages, the latter via the python ast. It's not perfect, and obviously conditional imports and try/except import blocks are handled naively, listing all the imports even if they aren't all used at once, but it gives the user a solid starting point for the recipe. Currently `python_dir` from setup.py isn't being handled in an ideal way. This is easily seen when testing the python-async package. There, the root of the project is the async package, so the root has __init__.py and friends, and the python provides scanning currently just assumes the basedir of that dir is the package name in this case, which is not correct. Forthcoming patches will resolve this. (From OE-Core rev: cb093aca3b78f130dc7da820a8710342a12d1231) Signed-off-by: Christopher Larson <kergoth@gmail.com> Signed-off-by: Ross Burton <ross.burton@intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'scripts/lib/recipetool/create_buildsys_python.py')
-rw-r--r--scripts/lib/recipetool/create_buildsys_python.py160
1 files changed, 160 insertions, 0 deletions
diff --git a/scripts/lib/recipetool/create_buildsys_python.py b/scripts/lib/recipetool/create_buildsys_python.py
index 9e4e1ebd4c..f4f4212559 100644
--- a/scripts/lib/recipetool/create_buildsys_python.py
+++ b/scripts/lib/recipetool/create_buildsys_python.py
@@ -41,6 +41,13 @@ def tinfoil_init(instance):
41 41
42 42
43class PythonRecipeHandler(RecipeHandler): 43class PythonRecipeHandler(RecipeHandler):
44 base_pkgdeps = ['python-core']
45 excluded_pkgdeps = ['python-dbg']
46 # os.path is provided by python-core
47 assume_provided = ['builtins', 'os.path']
48 # Assumes that the host python builtin_module_names is sane for target too
49 assume_provided = assume_provided + list(sys.builtin_module_names)
50
44 bbvar_map = { 51 bbvar_map = {
45 'Name': 'PN', 52 'Name': 'PN',
46 'Version': 'PV', 53 'Version': 'PV',
@@ -273,6 +280,8 @@ class PythonRecipeHandler(RecipeHandler):
273 mdinfo.append('{} = "{}"'.format(k, v)) 280 mdinfo.append('{} = "{}"'.format(k, v))
274 lines_before[src_uri_line-1:src_uri_line-1] = mdinfo 281 lines_before[src_uri_line-1:src_uri_line-1] = mdinfo
275 282
283 mapped_deps, unmapped_deps = self.scan_setup_python_deps(srctree, setup_info, setup_non_literals)
284
276 extras_req = set() 285 extras_req = set()
277 if 'Extras-require' in info: 286 if 'Extras-require' in info:
278 extras_req = info['Extras-require'] 287 extras_req = info['Extras-require']
@@ -284,6 +293,8 @@ class PythonRecipeHandler(RecipeHandler):
284 lines_after.append('# Uncomment this line to enable all the optional features.') 293 lines_after.append('# Uncomment this line to enable all the optional features.')
285 lines_after.append('#PACKAGECONFIG ?= "{}"'.format(' '.join(k.lower() for k in extras_req.iterkeys()))) 294 lines_after.append('#PACKAGECONFIG ?= "{}"'.format(' '.join(k.lower() for k in extras_req.iterkeys())))
286 for feature, feature_reqs in extras_req.iteritems(): 295 for feature, feature_reqs in extras_req.iteritems():
296 unmapped_deps.difference_update(feature_reqs)
297
287 feature_req_deps = ('python-' + r.replace('.', '-').lower() for r in sorted(feature_reqs)) 298 feature_req_deps = ('python-' + r.replace('.', '-').lower() for r in sorted(feature_reqs))
288 lines_after.append('PACKAGECONFIG[{}] = ",,,{}"'.format(feature.lower(), ' '.join(feature_req_deps))) 299 lines_after.append('PACKAGECONFIG[{}] = ",,,{}"'.format(feature.lower(), ' '.join(feature_req_deps)))
289 300
@@ -293,11 +304,34 @@ class PythonRecipeHandler(RecipeHandler):
293 lines_after.append('') 304 lines_after.append('')
294 inst_reqs = info['Install-requires'] 305 inst_reqs = info['Install-requires']
295 if inst_reqs: 306 if inst_reqs:
307 unmapped_deps.difference_update(inst_reqs)
308
296 inst_req_deps = ('python-' + r.replace('.', '-').lower() for r in sorted(inst_reqs)) 309 inst_req_deps = ('python-' + r.replace('.', '-').lower() for r in sorted(inst_reqs))
297 lines_after.append('# WARNING: the following rdepends are from setuptools install_requires. These') 310 lines_after.append('# WARNING: the following rdepends are from setuptools install_requires. These')
298 lines_after.append('# upstream names may not correspond exactly to bitbake package names.') 311 lines_after.append('# upstream names may not correspond exactly to bitbake package names.')
299 lines_after.append('RDEPENDS_${{PN}} += "{}"'.format(' '.join(inst_req_deps))) 312 lines_after.append('RDEPENDS_${{PN}} += "{}"'.format(' '.join(inst_req_deps)))
300 313
314 if mapped_deps:
315 name = info.get('Name')
316 if name and name[0] in mapped_deps:
317 # Attempt to avoid self-reference
318 mapped_deps.remove(name[0])
319 mapped_deps -= set(self.excluded_pkgdeps)
320 if inst_reqs or extras_req:
321 lines_after.append('')
322 lines_after.append('# WARNING: the following rdepends are determined through basic analysis of the')
323 lines_after.append('# python sources, and might not be 100% accurate.')
324 lines_after.append('RDEPENDS_${{PN}} += "{}"'.format(' '.join(sorted(mapped_deps))))
325
326 unmapped_deps -= set(extensions)
327 unmapped_deps -= set(self.assume_provided)
328 if unmapped_deps:
329 if mapped_deps:
330 lines_after.append('')
331 lines_after.append('# WARNING: We were unable to map the following python package/module')
332 lines_after.append('# dependencies to the bitbake packages which include them:')
333 lines_after.extend('# {}'.format(d) for d in sorted(unmapped_deps))
334
301 handled.append('buildsystem') 335 handled.append('buildsystem')
302 336
303 def get_pkginfo(self, pkginfo_fn): 337 def get_pkginfo(self, pkginfo_fn):
@@ -425,6 +459,132 @@ class PythonRecipeHandler(RecipeHandler):
425 if value != new_list: 459 if value != new_list:
426 info[variable] = new_list 460 info[variable] = new_list
427 461
462 def scan_setup_python_deps(self, srctree, setup_info, setup_non_literals):
463 if 'Package-dir' in setup_info:
464 package_dir = setup_info['Package-dir']
465 else:
466 package_dir = {}
467
468 class PackageDir(distutils.command.build_py.build_py):
469 def __init__(self, package_dir):
470 self.package_dir = package_dir
471
472 pd = PackageDir(package_dir)
473 to_scan = []
474 if not any(v in setup_non_literals for v in ['Py-modules', 'Scripts', 'Packages']):
475 if 'Py-modules' in setup_info:
476 for module in setup_info['Py-modules']:
477 try:
478 package, module = module.rsplit('.', 1)
479 except ValueError:
480 package, module = '.', module
481 module_path = os.path.join(pd.get_package_dir(package), module + '.py')
482 to_scan.append(module_path)
483
484 if 'Packages' in setup_info:
485 for package in setup_info['Packages']:
486 to_scan.append(pd.get_package_dir(package))
487
488 if 'Scripts' in setup_info:
489 to_scan.extend(setup_info['Scripts'])
490 else:
491 logger.info("Scanning the entire source tree, as one or more of the following setup keywords are non-literal: py_modules, scripts, packages.")
492
493 if not to_scan:
494 to_scan = ['.']
495
496 logger.info("Scanning paths for packages & dependencies: %s", ', '.join(to_scan))
497
498 provided_packages = self.parse_pkgdata_for_python_packages()
499 scanned_deps = self.scan_python_dependencies([os.path.join(srctree, p) for p in to_scan])
500 mapped_deps, unmapped_deps = set(self.base_pkgdeps), set()
501 for dep in scanned_deps:
502 mapped = provided_packages.get(dep)
503 if mapped:
504 mapped_deps.add(mapped)
505 else:
506 unmapped_deps.add(dep)
507 return mapped_deps, unmapped_deps
508
509 def scan_python_dependencies(self, paths):
510 deps = set()
511 try:
512 dep_output = self.run_command(['pythondeps', '-d'] + paths)
513 except (OSError, subprocess.CalledProcessError):
514 pass
515 else:
516 for line in dep_output.splitlines():
517 line = line.rstrip()
518 dep, filename = line.split('\t', 1)
519 if filename.endswith('/setup.py'):
520 continue
521 deps.add(dep)
522
523 try:
524 provides_output = self.run_command(['pythondeps', '-p'] + paths)
525 except (OSError, subprocess.CalledProcessError):
526 pass
527 else:
528 provides_lines = (l.rstrip() for l in provides_output.splitlines())
529 provides = set(l for l in provides_lines if l and l != 'setup')
530 deps -= provides
531
532 return deps
533
534 def parse_pkgdata_for_python_packages(self):
535 suffixes = [t[0] for t in imp.get_suffixes()]
536 pkgdata_dir = tinfoil.config_data.getVar('PKGDATA_DIR', True)
537
538 ldata = tinfoil.config_data.createCopy()
539 bb.parse.handle('classes/python-dir.bbclass', ldata, True)
540 python_sitedir = ldata.getVar('PYTHON_SITEPACKAGES_DIR', True)
541
542 dynload_dir = os.path.join(os.path.dirname(python_sitedir), 'lib-dynload')
543 python_dirs = [python_sitedir + os.sep,
544 os.path.join(os.path.dirname(python_sitedir), 'dist-packages') + os.sep,
545 os.path.dirname(python_sitedir) + os.sep]
546 packages = {}
547 for pkgdatafile in glob.glob('{}/runtime/*'.format(pkgdata_dir)):
548 files_info = None
549 with open(pkgdatafile, 'r') as f:
550 for line in f.readlines():
551 field, value = line.split(': ', 1)
552 if field == 'FILES_INFO':
553 files_info = ast.literal_eval(value)
554 break
555 else:
556 continue
557
558 for fn in files_info.iterkeys():
559 for suffix in suffixes:
560 if fn.endswith(suffix):
561 break
562 else:
563 continue
564
565 if fn.startswith(dynload_dir + os.sep):
566 base = os.path.basename(fn)
567 provided = base.split('.', 1)[0]
568 packages[provided] = os.path.basename(pkgdatafile)
569 continue
570
571 for python_dir in python_dirs:
572 if fn.startswith(python_dir):
573 relpath = fn[len(python_dir):]
574 relstart, _, relremaining = relpath.partition(os.sep)
575 if relstart.endswith('.egg'):
576 relpath = relremaining
577 base, _ = os.path.splitext(relpath)
578
579 if '/.debug/' in base:
580 continue
581 if os.path.basename(base) == '__init__':
582 base = os.path.dirname(base)
583 base = base.replace(os.sep + os.sep, os.sep)
584 provided = base.replace(os.sep, '.')
585 packages[provided] = os.path.basename(pkgdatafile)
586 return packages
587
428 @classmethod 588 @classmethod
429 def run_command(cls, cmd, **popenargs): 589 def run_command(cls, cmd, **popenargs):
430 if 'stderr' not in popenargs: 590 if 'stderr' not in popenargs: