create_buildsys_python.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. # Recipe creation tool - create build system handler for python
  2. #
  3. # Copyright (C) 2015 Mentor Graphics Corporation
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License version 2 as
  7. # published by the Free Software Foundation.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License along
  15. # with this program; if not, write to the Free Software Foundation, Inc.,
  16. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  17. import ast
  18. import codecs
  19. import collections
  20. import distutils.command.build_py
  21. import email
  22. import imp
  23. import glob
  24. import itertools
  25. import logging
  26. import os
  27. import re
  28. import sys
  29. import subprocess
  30. from recipetool.create import RecipeHandler
  31. logger = logging.getLogger('recipetool')
  32. tinfoil = None
  33. def tinfoil_init(instance):
  34. global tinfoil
  35. tinfoil = instance
  36. class PythonRecipeHandler(RecipeHandler):
  37. base_pkgdeps = ['python-core']
  38. excluded_pkgdeps = ['python-dbg']
  39. # os.path is provided by python-core
  40. assume_provided = ['builtins', 'os.path']
  41. # Assumes that the host python builtin_module_names is sane for target too
  42. assume_provided = assume_provided + list(sys.builtin_module_names)
  43. bbvar_map = {
  44. 'Name': 'PN',
  45. 'Version': 'PV',
  46. 'Home-page': 'HOMEPAGE',
  47. 'Summary': 'SUMMARY',
  48. 'Description': 'DESCRIPTION',
  49. 'License': 'LICENSE',
  50. 'Requires': 'RDEPENDS_${PN}',
  51. 'Provides': 'RPROVIDES_${PN}',
  52. 'Obsoletes': 'RREPLACES_${PN}',
  53. }
  54. # PN/PV are already set by recipetool core & desc can be extremely long
  55. excluded_fields = [
  56. 'Description',
  57. ]
  58. setup_parse_map = {
  59. 'Url': 'Home-page',
  60. 'Classifiers': 'Classifier',
  61. 'Description': 'Summary',
  62. }
  63. setuparg_map = {
  64. 'Home-page': 'url',
  65. 'Classifier': 'classifiers',
  66. 'Summary': 'description',
  67. 'Description': 'long-description',
  68. }
  69. # Values which are lists, used by the setup.py argument based metadata
  70. # extraction method, to determine how to process the setup.py output.
  71. setuparg_list_fields = [
  72. 'Classifier',
  73. 'Requires',
  74. 'Provides',
  75. 'Obsoletes',
  76. 'Platform',
  77. 'Supported-Platform',
  78. ]
  79. setuparg_multi_line_values = ['Description']
  80. replacements = [
  81. ('License', r' +$', ''),
  82. ('License', r'^ +', ''),
  83. ('License', r' ', '-'),
  84. ('License', r'^GNU-', ''),
  85. ('License', r'-[Ll]icen[cs]e(,?-[Vv]ersion)?', ''),
  86. ('License', r'^UNKNOWN$', ''),
  87. # Remove currently unhandled version numbers from these variables
  88. ('Requires', r' *\([^)]*\)', ''),
  89. ('Provides', r' *\([^)]*\)', ''),
  90. ('Obsoletes', r' *\([^)]*\)', ''),
  91. ('Install-requires', r'^([^><= ]+).*', r'\1'),
  92. ('Extras-require', r'^([^><= ]+).*', r'\1'),
  93. ('Tests-require', r'^([^><= ]+).*', r'\1'),
  94. # Remove unhandled dependency on particular features (e.g. foo[PDF])
  95. ('Install-requires', r'\[[^\]]+\]$', ''),
  96. ]
  97. classifier_license_map = {
  98. 'License :: OSI Approved :: Academic Free License (AFL)': 'AFL',
  99. 'License :: OSI Approved :: Apache Software License': 'Apache',
  100. 'License :: OSI Approved :: Apple Public Source License': 'APSL',
  101. 'License :: OSI Approved :: Artistic License': 'Artistic',
  102. 'License :: OSI Approved :: Attribution Assurance License': 'AAL',
  103. 'License :: OSI Approved :: BSD License': 'BSD',
  104. 'License :: OSI Approved :: Common Public License': 'CPL',
  105. 'License :: OSI Approved :: Eiffel Forum License': 'EFL',
  106. 'License :: OSI Approved :: European Union Public Licence 1.0 (EUPL 1.0)': 'EUPL-1.0',
  107. 'License :: OSI Approved :: European Union Public Licence 1.1 (EUPL 1.1)': 'EUPL-1.1',
  108. 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)': 'AGPL-3.0+',
  109. 'License :: OSI Approved :: GNU Affero General Public License v3': 'AGPL-3.0',
  110. 'License :: OSI Approved :: GNU Free Documentation License (FDL)': 'GFDL',
  111. 'License :: OSI Approved :: GNU General Public License (GPL)': 'GPL',
  112. 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)': 'GPL-2.0',
  113. 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)': 'GPL-2.0+',
  114. 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)': 'GPL-3.0',
  115. 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)': 'GPL-3.0+',
  116. 'License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)': 'LGPL-2.0',
  117. 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)': 'LGPL-2.0+',
  118. 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)': 'LGPL-3.0',
  119. 'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)': 'LGPL-3.0+',
  120. 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)': 'LGPL',
  121. 'License :: OSI Approved :: IBM Public License': 'IPL',
  122. 'License :: OSI Approved :: ISC License (ISCL)': 'ISC',
  123. 'License :: OSI Approved :: Intel Open Source License': 'Intel',
  124. 'License :: OSI Approved :: Jabber Open Source License': 'Jabber',
  125. 'License :: OSI Approved :: MIT License': 'MIT',
  126. 'License :: OSI Approved :: MITRE Collaborative Virtual Workspace License (CVW)': 'CVWL',
  127. 'License :: OSI Approved :: Motosoto License': 'Motosoto',
  128. 'License :: OSI Approved :: Mozilla Public License 1.0 (MPL)': 'MPL-1.0',
  129. 'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)': 'MPL-1.1',
  130. 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)': 'MPL-2.0',
  131. 'License :: OSI Approved :: Nethack General Public License': 'NGPL',
  132. 'License :: OSI Approved :: Nokia Open Source License': 'Nokia',
  133. 'License :: OSI Approved :: Open Group Test Suite License': 'OGTSL',
  134. 'License :: OSI Approved :: Python License (CNRI Python License)': 'CNRI-Python',
  135. 'License :: OSI Approved :: Python Software Foundation License': 'PSF',
  136. 'License :: OSI Approved :: Qt Public License (QPL)': 'QPL',
  137. 'License :: OSI Approved :: Ricoh Source Code Public License': 'RSCPL',
  138. 'License :: OSI Approved :: Sleepycat License': 'Sleepycat',
  139. 'License :: OSI Approved :: Sun Industry Standards Source License (SISSL)': '-- Sun Industry Standards Source License (SISSL)',
  140. 'License :: OSI Approved :: Sun Public License': 'SPL',
  141. 'License :: OSI Approved :: University of Illinois/NCSA Open Source License': 'NCSA',
  142. 'License :: OSI Approved :: Vovida Software License 1.0': 'VSL-1.0',
  143. 'License :: OSI Approved :: W3C License': 'W3C',
  144. 'License :: OSI Approved :: X.Net License': 'Xnet',
  145. 'License :: OSI Approved :: Zope Public License': 'ZPL',
  146. 'License :: OSI Approved :: zlib/libpng License': 'Zlib',
  147. }
  148. def __init__(self):
  149. pass
  150. def process(self, srctree, classes, lines_before, lines_after, handled, extravalues):
  151. if 'buildsystem' in handled:
  152. return False
  153. if not RecipeHandler.checkfiles(srctree, ['setup.py']):
  154. return
  155. # setup.py is always parsed to get at certain required information, such as
  156. # distutils vs setuptools
  157. #
  158. # If egg info is available, we use it for both its PKG-INFO metadata
  159. # and for its requires.txt for install_requires.
  160. # If PKG-INFO is available but no egg info is, we use that for metadata in preference to
  161. # the parsed setup.py, but use the install_requires info from the
  162. # parsed setup.py.
  163. setupscript = os.path.join(srctree, 'setup.py')
  164. try:
  165. setup_info, uses_setuptools, setup_non_literals, extensions = self.parse_setup_py(setupscript)
  166. except Exception:
  167. logger.exception("Failed to parse setup.py")
  168. setup_info, uses_setuptools, setup_non_literals, extensions = {}, True, [], []
  169. egginfo = glob.glob(os.path.join(srctree, '*.egg-info'))
  170. if egginfo:
  171. info = self.get_pkginfo(os.path.join(egginfo[0], 'PKG-INFO'))
  172. requires_txt = os.path.join(egginfo[0], 'requires.txt')
  173. if os.path.exists(requires_txt):
  174. with codecs.open(requires_txt) as f:
  175. inst_req = []
  176. extras_req = collections.defaultdict(list)
  177. current_feature = None
  178. for line in f.readlines():
  179. line = line.rstrip()
  180. if not line:
  181. continue
  182. if line.startswith('['):
  183. current_feature = line[1:-1]
  184. elif current_feature:
  185. extras_req[current_feature].append(line)
  186. else:
  187. inst_req.append(line)
  188. info['Install-requires'] = inst_req
  189. info['Extras-require'] = extras_req
  190. elif RecipeHandler.checkfiles(srctree, ['PKG-INFO']):
  191. info = self.get_pkginfo(os.path.join(srctree, 'PKG-INFO'))
  192. if setup_info:
  193. if 'Install-requires' in setup_info:
  194. info['Install-requires'] = setup_info['Install-requires']
  195. if 'Extras-require' in setup_info:
  196. info['Extras-require'] = setup_info['Extras-require']
  197. else:
  198. if setup_info:
  199. info = setup_info
  200. else:
  201. info = self.get_setup_args_info(setupscript)
  202. # Grab the license value before applying replacements
  203. license_str = info.get('License', '').strip()
  204. self.apply_info_replacements(info)
  205. if uses_setuptools:
  206. classes.append('setuptools')
  207. else:
  208. classes.append('distutils')
  209. if license_str:
  210. for i, line in enumerate(lines_before):
  211. if line.startswith('LICENSE = '):
  212. lines_before.insert(i, '# NOTE: License in setup.py/PKGINFO is: %s' % license_str)
  213. break
  214. if 'Classifier' in info:
  215. existing_licenses = info.get('License', '')
  216. licenses = []
  217. for classifier in info['Classifier']:
  218. if classifier in self.classifier_license_map:
  219. license = self.classifier_license_map[classifier]
  220. if license == 'Apache' and 'Apache-2.0' in existing_licenses:
  221. license = 'Apache-2.0'
  222. elif license == 'GPL':
  223. if 'GPL-2.0' in existing_licenses or 'GPLv2' in existing_licenses:
  224. license = 'GPL-2.0'
  225. elif 'GPL-3.0' in existing_licenses or 'GPLv3' in existing_licenses:
  226. license = 'GPL-3.0'
  227. elif license == 'LGPL':
  228. if 'LGPL-2.1' in existing_licenses or 'LGPLv2.1' in existing_licenses:
  229. license = 'LGPL-2.1'
  230. elif 'LGPL-2.0' in existing_licenses or 'LGPLv2' in existing_licenses:
  231. license = 'LGPL-2.0'
  232. elif 'LGPL-3.0' in existing_licenses or 'LGPLv3' in existing_licenses:
  233. license = 'LGPL-3.0'
  234. licenses.append(license)
  235. if licenses:
  236. info['License'] = ' & '.join(licenses)
  237. # Map PKG-INFO & setup.py fields to bitbake variables
  238. for field, values in info.items():
  239. if field in self.excluded_fields:
  240. continue
  241. if field not in self.bbvar_map:
  242. continue
  243. if isinstance(values, str):
  244. value = values
  245. else:
  246. value = ' '.join(str(v) for v in values if v)
  247. bbvar = self.bbvar_map[field]
  248. if bbvar not in extravalues and value:
  249. extravalues[bbvar] = value
  250. mapped_deps, unmapped_deps = self.scan_setup_python_deps(srctree, setup_info, setup_non_literals)
  251. extras_req = set()
  252. if 'Extras-require' in info:
  253. extras_req = info['Extras-require']
  254. if extras_req:
  255. lines_after.append('# The following configs & dependencies are from setuptools extras_require.')
  256. lines_after.append('# These dependencies are optional, hence can be controlled via PACKAGECONFIG.')
  257. lines_after.append('# The upstream names may not correspond exactly to bitbake package names.')
  258. lines_after.append('#')
  259. lines_after.append('# Uncomment this line to enable all the optional features.')
  260. lines_after.append('#PACKAGECONFIG ?= "{}"'.format(' '.join(k.lower() for k in extras_req)))
  261. for feature, feature_reqs in extras_req.items():
  262. unmapped_deps.difference_update(feature_reqs)
  263. feature_req_deps = ('python-' + r.replace('.', '-').lower() for r in sorted(feature_reqs))
  264. lines_after.append('PACKAGECONFIG[{}] = ",,,{}"'.format(feature.lower(), ' '.join(feature_req_deps)))
  265. inst_reqs = set()
  266. if 'Install-requires' in info:
  267. if extras_req:
  268. lines_after.append('')
  269. inst_reqs = info['Install-requires']
  270. if inst_reqs:
  271. unmapped_deps.difference_update(inst_reqs)
  272. inst_req_deps = ('python-' + r.replace('.', '-').lower() for r in sorted(inst_reqs))
  273. lines_after.append('# WARNING: the following rdepends are from setuptools install_requires. These')
  274. lines_after.append('# upstream names may not correspond exactly to bitbake package names.')
  275. lines_after.append('RDEPENDS_${{PN}} += "{}"'.format(' '.join(inst_req_deps)))
  276. if mapped_deps:
  277. name = info.get('Name')
  278. if name and name[0] in mapped_deps:
  279. # Attempt to avoid self-reference
  280. mapped_deps.remove(name[0])
  281. mapped_deps -= set(self.excluded_pkgdeps)
  282. if inst_reqs or extras_req:
  283. lines_after.append('')
  284. lines_after.append('# WARNING: the following rdepends are determined through basic analysis of the')
  285. lines_after.append('# python sources, and might not be 100% accurate.')
  286. lines_after.append('RDEPENDS_${{PN}} += "{}"'.format(' '.join(sorted(mapped_deps))))
  287. unmapped_deps -= set(extensions)
  288. unmapped_deps -= set(self.assume_provided)
  289. if unmapped_deps:
  290. if mapped_deps:
  291. lines_after.append('')
  292. lines_after.append('# WARNING: We were unable to map the following python package/module')
  293. lines_after.append('# dependencies to the bitbake packages which include them:')
  294. lines_after.extend('# {}'.format(d) for d in sorted(unmapped_deps))
  295. handled.append('buildsystem')
  296. def get_pkginfo(self, pkginfo_fn):
  297. msg = email.message_from_file(open(pkginfo_fn, 'r'))
  298. msginfo = {}
  299. for field in msg.keys():
  300. values = msg.get_all(field)
  301. if len(values) == 1:
  302. msginfo[field] = values[0]
  303. else:
  304. msginfo[field] = values
  305. return msginfo
  306. def parse_setup_py(self, setupscript='./setup.py'):
  307. with codecs.open(setupscript) as f:
  308. info, imported_modules, non_literals, extensions = gather_setup_info(f)
  309. def _map(key):
  310. key = key.replace('_', '-')
  311. key = key[0].upper() + key[1:]
  312. if key in self.setup_parse_map:
  313. key = self.setup_parse_map[key]
  314. return key
  315. # Naive mapping of setup() arguments to PKG-INFO field names
  316. for d in [info, non_literals]:
  317. for key, value in list(d.items()):
  318. if key is None:
  319. continue
  320. new_key = _map(key)
  321. if new_key != key:
  322. del d[key]
  323. d[new_key] = value
  324. return info, 'setuptools' in imported_modules, non_literals, extensions
  325. def get_setup_args_info(self, setupscript='./setup.py'):
  326. cmd = ['python', setupscript]
  327. info = {}
  328. keys = set(self.bbvar_map.keys())
  329. keys |= set(self.setuparg_list_fields)
  330. keys |= set(self.setuparg_multi_line_values)
  331. grouped_keys = itertools.groupby(keys, lambda k: (k in self.setuparg_list_fields, k in self.setuparg_multi_line_values))
  332. for index, keys in grouped_keys:
  333. if index == (True, False):
  334. # Splitlines output for each arg as a list value
  335. for key in keys:
  336. arg = self.setuparg_map.get(key, key.lower())
  337. try:
  338. arg_info = self.run_command(cmd + ['--' + arg], cwd=os.path.dirname(setupscript))
  339. except (OSError, subprocess.CalledProcessError):
  340. pass
  341. else:
  342. info[key] = [l.rstrip() for l in arg_info.splitlines()]
  343. elif index == (False, True):
  344. # Entire output for each arg
  345. for key in keys:
  346. arg = self.setuparg_map.get(key, key.lower())
  347. try:
  348. arg_info = self.run_command(cmd + ['--' + arg], cwd=os.path.dirname(setupscript))
  349. except (OSError, subprocess.CalledProcessError):
  350. pass
  351. else:
  352. info[key] = arg_info
  353. else:
  354. info.update(self.get_setup_byline(list(keys), setupscript))
  355. return info
  356. def get_setup_byline(self, fields, setupscript='./setup.py'):
  357. info = {}
  358. cmd = ['python', setupscript]
  359. cmd.extend('--' + self.setuparg_map.get(f, f.lower()) for f in fields)
  360. try:
  361. info_lines = self.run_command(cmd, cwd=os.path.dirname(setupscript)).splitlines()
  362. except (OSError, subprocess.CalledProcessError):
  363. pass
  364. else:
  365. if len(fields) != len(info_lines):
  366. logger.error('Mismatch between setup.py output lines and number of fields')
  367. sys.exit(1)
  368. for lineno, line in enumerate(info_lines):
  369. line = line.rstrip()
  370. info[fields[lineno]] = line
  371. return info
  372. def apply_info_replacements(self, info):
  373. for variable, search, replace in self.replacements:
  374. if variable not in info:
  375. continue
  376. def replace_value(search, replace, value):
  377. if replace is None:
  378. if re.search(search, value):
  379. return None
  380. else:
  381. new_value = re.sub(search, replace, value)
  382. if value != new_value:
  383. return new_value
  384. return value
  385. value = info[variable]
  386. if isinstance(value, str):
  387. new_value = replace_value(search, replace, value)
  388. if new_value is None:
  389. del info[variable]
  390. elif new_value != value:
  391. info[variable] = new_value
  392. elif hasattr(value, 'items'):
  393. for dkey, dvalue in list(value.items()):
  394. new_list = []
  395. for pos, a_value in enumerate(dvalue):
  396. new_value = replace_value(search, replace, a_value)
  397. if new_value is not None and new_value != value:
  398. new_list.append(new_value)
  399. if value != new_list:
  400. value[dkey] = new_list
  401. else:
  402. new_list = []
  403. for pos, a_value in enumerate(value):
  404. new_value = replace_value(search, replace, a_value)
  405. if new_value is not None and new_value != value:
  406. new_list.append(new_value)
  407. if value != new_list:
  408. info[variable] = new_list
  409. def scan_setup_python_deps(self, srctree, setup_info, setup_non_literals):
  410. if 'Package-dir' in setup_info:
  411. package_dir = setup_info['Package-dir']
  412. else:
  413. package_dir = {}
  414. class PackageDir(distutils.command.build_py.build_py):
  415. def __init__(self, package_dir):
  416. self.package_dir = package_dir
  417. pd = PackageDir(package_dir)
  418. to_scan = []
  419. if not any(v in setup_non_literals for v in ['Py-modules', 'Scripts', 'Packages']):
  420. if 'Py-modules' in setup_info:
  421. for module in setup_info['Py-modules']:
  422. try:
  423. package, module = module.rsplit('.', 1)
  424. except ValueError:
  425. package, module = '.', module
  426. module_path = os.path.join(pd.get_package_dir(package), module + '.py')
  427. to_scan.append(module_path)
  428. if 'Packages' in setup_info:
  429. for package in setup_info['Packages']:
  430. to_scan.append(pd.get_package_dir(package))
  431. if 'Scripts' in setup_info:
  432. to_scan.extend(setup_info['Scripts'])
  433. else:
  434. logger.info("Scanning the entire source tree, as one or more of the following setup keywords are non-literal: py_modules, scripts, packages.")
  435. if not to_scan:
  436. to_scan = ['.']
  437. logger.info("Scanning paths for packages & dependencies: %s", ', '.join(to_scan))
  438. provided_packages = self.parse_pkgdata_for_python_packages()
  439. scanned_deps = self.scan_python_dependencies([os.path.join(srctree, p) for p in to_scan])
  440. mapped_deps, unmapped_deps = set(self.base_pkgdeps), set()
  441. for dep in scanned_deps:
  442. mapped = provided_packages.get(dep)
  443. if mapped:
  444. logger.debug('Mapped %s to %s' % (dep, mapped))
  445. mapped_deps.add(mapped)
  446. else:
  447. logger.debug('Could not map %s' % dep)
  448. unmapped_deps.add(dep)
  449. return mapped_deps, unmapped_deps
  450. def scan_python_dependencies(self, paths):
  451. deps = set()
  452. try:
  453. dep_output = self.run_command(['pythondeps', '-d'] + paths)
  454. except (OSError, subprocess.CalledProcessError):
  455. pass
  456. else:
  457. for line in dep_output.splitlines():
  458. line = line.rstrip()
  459. dep, filename = line.split('\t', 1)
  460. if filename.endswith('/setup.py'):
  461. continue
  462. deps.add(dep)
  463. try:
  464. provides_output = self.run_command(['pythondeps', '-p'] + paths)
  465. except (OSError, subprocess.CalledProcessError):
  466. pass
  467. else:
  468. provides_lines = (l.rstrip() for l in provides_output.splitlines())
  469. provides = set(l for l in provides_lines if l and l != 'setup')
  470. deps -= provides
  471. return deps
  472. def parse_pkgdata_for_python_packages(self):
  473. suffixes = [t[0] for t in imp.get_suffixes()]
  474. pkgdata_dir = tinfoil.config_data.getVar('PKGDATA_DIR')
  475. ldata = tinfoil.config_data.createCopy()
  476. bb.parse.handle('classes/python-dir.bbclass', ldata, True)
  477. python_sitedir = ldata.getVar('PYTHON_SITEPACKAGES_DIR')
  478. dynload_dir = os.path.join(os.path.dirname(python_sitedir), 'lib-dynload')
  479. python_dirs = [python_sitedir + os.sep,
  480. os.path.join(os.path.dirname(python_sitedir), 'dist-packages') + os.sep,
  481. os.path.dirname(python_sitedir) + os.sep]
  482. packages = {}
  483. for pkgdatafile in glob.glob('{}/runtime/*'.format(pkgdata_dir)):
  484. files_info = None
  485. with open(pkgdatafile, 'r') as f:
  486. for line in f.readlines():
  487. field, value = line.split(': ', 1)
  488. if field == 'FILES_INFO':
  489. files_info = ast.literal_eval(value)
  490. break
  491. else:
  492. continue
  493. for fn in files_info:
  494. for suffix in suffixes:
  495. if fn.endswith(suffix):
  496. break
  497. else:
  498. continue
  499. if fn.startswith(dynload_dir + os.sep):
  500. if '/.debug/' in fn:
  501. continue
  502. base = os.path.basename(fn)
  503. provided = base.split('.', 1)[0]
  504. packages[provided] = os.path.basename(pkgdatafile)
  505. continue
  506. for python_dir in python_dirs:
  507. if fn.startswith(python_dir):
  508. relpath = fn[len(python_dir):]
  509. relstart, _, relremaining = relpath.partition(os.sep)
  510. if relstart.endswith('.egg'):
  511. relpath = relremaining
  512. base, _ = os.path.splitext(relpath)
  513. if '/.debug/' in base:
  514. continue
  515. if os.path.basename(base) == '__init__':
  516. base = os.path.dirname(base)
  517. base = base.replace(os.sep + os.sep, os.sep)
  518. provided = base.replace(os.sep, '.')
  519. packages[provided] = os.path.basename(pkgdatafile)
  520. return packages
  521. @classmethod
  522. def run_command(cls, cmd, **popenargs):
  523. if 'stderr' not in popenargs:
  524. popenargs['stderr'] = subprocess.STDOUT
  525. try:
  526. return subprocess.check_output(cmd, **popenargs).decode('utf-8')
  527. except OSError as exc:
  528. logger.error('Unable to run `{}`: {}', ' '.join(cmd), exc)
  529. raise
  530. except subprocess.CalledProcessError as exc:
  531. logger.error('Unable to run `{}`: {}', ' '.join(cmd), exc.output)
  532. raise
  533. def gather_setup_info(fileobj):
  534. parsed = ast.parse(fileobj.read(), fileobj.name)
  535. visitor = SetupScriptVisitor()
  536. visitor.visit(parsed)
  537. non_literals, extensions = {}, []
  538. for key, value in list(visitor.keywords.items()):
  539. if key == 'ext_modules':
  540. if isinstance(value, list):
  541. for ext in value:
  542. if (isinstance(ext, ast.Call) and
  543. isinstance(ext.func, ast.Name) and
  544. ext.func.id == 'Extension' and
  545. not has_non_literals(ext.args)):
  546. extensions.append(ext.args[0])
  547. elif has_non_literals(value):
  548. non_literals[key] = value
  549. del visitor.keywords[key]
  550. return visitor.keywords, visitor.imported_modules, non_literals, extensions
  551. class SetupScriptVisitor(ast.NodeVisitor):
  552. def __init__(self):
  553. ast.NodeVisitor.__init__(self)
  554. self.keywords = {}
  555. self.non_literals = []
  556. self.imported_modules = set()
  557. def visit_Expr(self, node):
  558. if isinstance(node.value, ast.Call) and \
  559. isinstance(node.value.func, ast.Name) and \
  560. node.value.func.id == 'setup':
  561. self.visit_setup(node.value)
  562. def visit_setup(self, node):
  563. call = LiteralAstTransform().visit(node)
  564. self.keywords = call.keywords
  565. for k, v in self.keywords.items():
  566. if has_non_literals(v):
  567. self.non_literals.append(k)
  568. def visit_Import(self, node):
  569. for alias in node.names:
  570. self.imported_modules.add(alias.name)
  571. def visit_ImportFrom(self, node):
  572. self.imported_modules.add(node.module)
  573. class LiteralAstTransform(ast.NodeTransformer):
  574. """Simplify the ast through evaluation of literals."""
  575. excluded_fields = ['ctx']
  576. def visit(self, node):
  577. if not isinstance(node, ast.AST):
  578. return node
  579. else:
  580. return ast.NodeTransformer.visit(self, node)
  581. def generic_visit(self, node):
  582. try:
  583. return ast.literal_eval(node)
  584. except ValueError:
  585. for field, value in ast.iter_fields(node):
  586. if field in self.excluded_fields:
  587. delattr(node, field)
  588. if value is None:
  589. continue
  590. if isinstance(value, list):
  591. if field in ('keywords', 'kwargs'):
  592. new_value = dict((kw.arg, self.visit(kw.value)) for kw in value)
  593. else:
  594. new_value = [self.visit(i) for i in value]
  595. else:
  596. new_value = self.visit(value)
  597. setattr(node, field, new_value)
  598. return node
  599. def visit_Name(self, node):
  600. if hasattr('__builtins__', node.id):
  601. return getattr(__builtins__, node.id)
  602. else:
  603. return self.generic_visit(node)
  604. def visit_Tuple(self, node):
  605. return tuple(self.visit(v) for v in node.elts)
  606. def visit_List(self, node):
  607. return [self.visit(v) for v in node.elts]
  608. def visit_Set(self, node):
  609. return set(self.visit(v) for v in node.elts)
  610. def visit_Dict(self, node):
  611. keys = (self.visit(k) for k in node.keys)
  612. values = (self.visit(v) for v in node.values)
  613. return dict(zip(keys, values))
  614. def has_non_literals(value):
  615. if isinstance(value, ast.AST):
  616. return True
  617. elif isinstance(value, str):
  618. return False
  619. elif hasattr(value, 'values'):
  620. return any(has_non_literals(v) for v in value.values())
  621. elif hasattr(value, '__iter__'):
  622. return any(has_non_literals(v) for v in value)
  623. def register_recipe_handlers(handlers):
  624. # We need to make sure this is ahead of the makefile fallback handler
  625. handlers.append((PythonRecipeHandler(), 70))