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