util.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. # Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  12. # implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. # Copyright (C) 2013 Association of Universities for Research in Astronomy
  17. # (AURA)
  18. #
  19. # Redistribution and use in source and binary forms, with or without
  20. # modification, are permitted provided that the following conditions are met:
  21. #
  22. # 1. Redistributions of source code must retain the above copyright
  23. # notice, this list of conditions and the following disclaimer.
  24. #
  25. # 2. Redistributions in binary form must reproduce the above
  26. # copyright notice, this list of conditions and the following
  27. # disclaimer in the documentation and/or other materials provided
  28. # with the distribution.
  29. #
  30. # 3. The name of AURA and its representatives may not be used to
  31. # endorse or promote products derived from this software without
  32. # specific prior written permission.
  33. #
  34. # THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED
  35. # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  36. # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  37. # DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT,
  38. # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  39. # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
  40. # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  41. # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
  42. # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
  43. # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
  44. # DAMAGE.
  45. """The code in this module is mostly copy/pasted out of the distutils2 source
  46. code, as recommended by Tarek Ziade. As such, it may be subject to some change
  47. as distutils2 development continues, and will have to be kept up to date.
  48. I didn't want to use it directly from distutils2 itself, since I do not want it
  49. to be an installation dependency for our packages yet--it is still too unstable
  50. (the latest version on PyPI doesn't even install).
  51. """
  52. # These first two imports are not used, but are needed to get around an
  53. # irritating Python bug that can crop up when using ./setup.py test.
  54. # See: http://www.eby-sarna.com/pipermail/peak/2010-May/003355.html
  55. try:
  56. import multiprocessing # flake8: noqa
  57. except ImportError:
  58. pass
  59. import logging # flake8: noqa
  60. import os
  61. import re
  62. import sys
  63. import traceback
  64. from collections import defaultdict
  65. import distutils.ccompiler
  66. import pkg_resources
  67. from distutils import log
  68. from distutils import errors
  69. from setuptools.command.egg_info import manifest_maker
  70. from setuptools import dist as st_dist
  71. from setuptools import extension
  72. try:
  73. import ConfigParser as configparser
  74. except ImportError:
  75. import configparser
  76. from pbr import extra_files
  77. import pbr.hooks
  78. # A simplified RE for this; just checks that the line ends with version
  79. # predicates in ()
  80. _VERSION_SPEC_RE = re.compile(r'\s*(.*?)\s*\((.*)\)\s*$')
  81. # Mappings from setup() keyword arguments to setup.cfg options;
  82. # The values are (section, option) tuples, or simply (section,) tuples if
  83. # the option has the same name as the setup() argument
  84. D1_D2_SETUP_ARGS = {
  85. "name": ("metadata",),
  86. "version": ("metadata",),
  87. "author": ("metadata",),
  88. "author_email": ("metadata",),
  89. "maintainer": ("metadata",),
  90. "maintainer_email": ("metadata",),
  91. "url": ("metadata", "home_page"),
  92. "project_urls": ("metadata",),
  93. "description": ("metadata", "summary"),
  94. "keywords": ("metadata",),
  95. "long_description": ("metadata", "description"),
  96. "long_description_content_type": ("metadata", "description_content_type"),
  97. "download_url": ("metadata",),
  98. "classifiers": ("metadata", "classifier"),
  99. "platforms": ("metadata", "platform"), # **
  100. "license": ("metadata",),
  101. # Use setuptools install_requires, not
  102. # broken distutils requires
  103. "install_requires": ("metadata", "requires_dist"),
  104. "setup_requires": ("metadata", "setup_requires_dist"),
  105. "provides": ("metadata", "provides_dist"), # **
  106. "obsoletes": ("metadata", "obsoletes_dist"), # **
  107. "package_dir": ("files", 'packages_root'),
  108. "packages": ("files",),
  109. "package_data": ("files",),
  110. "namespace_packages": ("files",),
  111. "data_files": ("files",),
  112. "scripts": ("files",),
  113. "py_modules": ("files", "modules"), # **
  114. "cmdclass": ("global", "commands"),
  115. # Not supported in distutils2, but provided for
  116. # backwards compatibility with setuptools
  117. "use_2to3": ("backwards_compat", "use_2to3"),
  118. "zip_safe": ("backwards_compat", "zip_safe"),
  119. "tests_require": ("backwards_compat", "tests_require"),
  120. "dependency_links": ("backwards_compat",),
  121. "include_package_data": ("backwards_compat",),
  122. }
  123. # setup() arguments that can have multiple values in setup.cfg
  124. MULTI_FIELDS = ("classifiers",
  125. "platforms",
  126. "install_requires",
  127. "provides",
  128. "obsoletes",
  129. "namespace_packages",
  130. "packages",
  131. "package_data",
  132. "data_files",
  133. "scripts",
  134. "py_modules",
  135. "dependency_links",
  136. "setup_requires",
  137. "tests_require",
  138. "cmdclass")
  139. # setup() arguments that can have mapping values in setup.cfg
  140. MAP_FIELDS = ("project_urls",)
  141. # setup() arguments that contain boolean values
  142. BOOL_FIELDS = ("use_2to3", "zip_safe", "include_package_data")
  143. CSV_FIELDS = ("keywords",)
  144. def resolve_name(name):
  145. """Resolve a name like ``module.object`` to an object and return it.
  146. Raise ImportError if the module or name is not found.
  147. """
  148. parts = name.split('.')
  149. cursor = len(parts) - 1
  150. module_name = parts[:cursor]
  151. attr_name = parts[-1]
  152. while cursor > 0:
  153. try:
  154. ret = __import__('.'.join(module_name), fromlist=[attr_name])
  155. break
  156. except ImportError:
  157. if cursor == 0:
  158. raise
  159. cursor -= 1
  160. module_name = parts[:cursor]
  161. attr_name = parts[cursor]
  162. ret = ''
  163. for part in parts[cursor:]:
  164. try:
  165. ret = getattr(ret, part)
  166. except AttributeError:
  167. raise ImportError(name)
  168. return ret
  169. def cfg_to_args(path='setup.cfg', script_args=()):
  170. """Distutils2 to distutils1 compatibility util.
  171. This method uses an existing setup.cfg to generate a dictionary of
  172. keywords that can be used by distutils.core.setup(kwargs**).
  173. :param path:
  174. The setup.cfg path.
  175. :param script_args:
  176. List of commands setup.py was called with.
  177. :raises DistutilsFileError:
  178. When the setup.cfg file is not found.
  179. """
  180. # The method source code really starts here.
  181. if sys.version_info >= (3, 2):
  182. parser = configparser.ConfigParser()
  183. else:
  184. parser = configparser.SafeConfigParser()
  185. if not os.path.exists(path):
  186. raise errors.DistutilsFileError("file '%s' does not exist" %
  187. os.path.abspath(path))
  188. try:
  189. parser.read(path, encoding='utf-8')
  190. except TypeError:
  191. # Python 2 doesn't accept the encoding kwarg
  192. parser.read(path)
  193. config = {}
  194. for section in parser.sections():
  195. config[section] = dict()
  196. for k, value in parser.items(section):
  197. config[section][k.replace('-', '_')] = value
  198. # Run setup_hooks, if configured
  199. setup_hooks = has_get_option(config, 'global', 'setup_hooks')
  200. package_dir = has_get_option(config, 'files', 'packages_root')
  201. # Add the source package directory to sys.path in case it contains
  202. # additional hooks, and to make sure it's on the path before any existing
  203. # installations of the package
  204. if package_dir:
  205. package_dir = os.path.abspath(package_dir)
  206. sys.path.insert(0, package_dir)
  207. try:
  208. if setup_hooks:
  209. setup_hooks = [
  210. hook for hook in split_multiline(setup_hooks)
  211. if hook != 'pbr.hooks.setup_hook']
  212. for hook in setup_hooks:
  213. hook_fn = resolve_name(hook)
  214. try :
  215. hook_fn(config)
  216. except SystemExit:
  217. log.error('setup hook %s terminated the installation')
  218. except:
  219. e = sys.exc_info()[1]
  220. log.error('setup hook %s raised exception: %s\n' %
  221. (hook, e))
  222. log.error(traceback.format_exc())
  223. sys.exit(1)
  224. # Run the pbr hook
  225. pbr.hooks.setup_hook(config)
  226. kwargs = setup_cfg_to_setup_kwargs(config, script_args)
  227. # Set default config overrides
  228. kwargs['include_package_data'] = True
  229. kwargs['zip_safe'] = False
  230. register_custom_compilers(config)
  231. ext_modules = get_extension_modules(config)
  232. if ext_modules:
  233. kwargs['ext_modules'] = ext_modules
  234. entry_points = get_entry_points(config)
  235. if entry_points:
  236. kwargs['entry_points'] = entry_points
  237. # Handle the [files]/extra_files option
  238. files_extra_files = has_get_option(config, 'files', 'extra_files')
  239. if files_extra_files:
  240. extra_files.set_extra_files(split_multiline(files_extra_files))
  241. finally:
  242. # Perform cleanup if any paths were added to sys.path
  243. if package_dir:
  244. sys.path.pop(0)
  245. return kwargs
  246. def setup_cfg_to_setup_kwargs(config, script_args=()):
  247. """Processes the setup.cfg options and converts them to arguments accepted
  248. by setuptools' setup() function.
  249. """
  250. kwargs = {}
  251. # Temporarily holds install_requires and extra_requires while we
  252. # parse env_markers.
  253. all_requirements = {}
  254. for arg in D1_D2_SETUP_ARGS:
  255. if len(D1_D2_SETUP_ARGS[arg]) == 2:
  256. # The distutils field name is different than distutils2's.
  257. section, option = D1_D2_SETUP_ARGS[arg]
  258. elif len(D1_D2_SETUP_ARGS[arg]) == 1:
  259. # The distutils field name is the same thant distutils2's.
  260. section = D1_D2_SETUP_ARGS[arg][0]
  261. option = arg
  262. in_cfg_value = has_get_option(config, section, option)
  263. if not in_cfg_value:
  264. # There is no such option in the setup.cfg
  265. if arg == "long_description":
  266. in_cfg_value = has_get_option(config, section,
  267. "description_file")
  268. if in_cfg_value:
  269. in_cfg_value = split_multiline(in_cfg_value)
  270. value = ''
  271. for filename in in_cfg_value:
  272. description_file = open(filename)
  273. try:
  274. value += description_file.read().strip() + '\n\n'
  275. finally:
  276. description_file.close()
  277. in_cfg_value = value
  278. else:
  279. continue
  280. if arg in CSV_FIELDS:
  281. in_cfg_value = split_csv(in_cfg_value)
  282. if arg in MULTI_FIELDS:
  283. in_cfg_value = split_multiline(in_cfg_value)
  284. elif arg in MAP_FIELDS:
  285. in_cfg_map = {}
  286. for i in split_multiline(in_cfg_value):
  287. k, v = i.split('=')
  288. in_cfg_map[k.strip()] = v.strip()
  289. in_cfg_value = in_cfg_map
  290. elif arg in BOOL_FIELDS:
  291. # Provide some flexibility here...
  292. if in_cfg_value.lower() in ('true', 't', '1', 'yes', 'y'):
  293. in_cfg_value = True
  294. else:
  295. in_cfg_value = False
  296. if in_cfg_value:
  297. if arg in ('install_requires', 'tests_require'):
  298. # Replaces PEP345-style version specs with the sort expected by
  299. # setuptools
  300. in_cfg_value = [_VERSION_SPEC_RE.sub(r'\1\2', pred)
  301. for pred in in_cfg_value]
  302. if arg == 'install_requires':
  303. # Split install_requires into package,env_marker tuples
  304. # These will be re-assembled later
  305. install_requires = []
  306. requirement_pattern = '(?P<package>[^;]*);?(?P<env_marker>[^#]*?)(?:\s*#.*)?$'
  307. for requirement in in_cfg_value:
  308. m = re.match(requirement_pattern, requirement)
  309. requirement_package = m.group('package').strip()
  310. env_marker = m.group('env_marker').strip()
  311. install_requires.append((requirement_package,env_marker))
  312. all_requirements[''] = install_requires
  313. elif arg == 'package_dir':
  314. in_cfg_value = {'': in_cfg_value}
  315. elif arg in ('package_data', 'data_files'):
  316. data_files = {}
  317. firstline = True
  318. prev = None
  319. for line in in_cfg_value:
  320. if '=' in line:
  321. key, value = line.split('=', 1)
  322. key, value = (key.strip(), value.strip())
  323. if key in data_files:
  324. # Multiple duplicates of the same package name;
  325. # this is for backwards compatibility of the old
  326. # format prior to d2to1 0.2.6.
  327. prev = data_files[key]
  328. prev.extend(value.split())
  329. else:
  330. prev = data_files[key.strip()] = value.split()
  331. elif firstline:
  332. raise errors.DistutilsOptionError(
  333. 'malformed package_data first line %r (misses '
  334. '"=")' % line)
  335. else:
  336. prev.extend(line.strip().split())
  337. firstline = False
  338. if arg == 'data_files':
  339. # the data_files value is a pointlessly different structure
  340. # from the package_data value
  341. data_files = data_files.items()
  342. in_cfg_value = data_files
  343. elif arg == 'cmdclass':
  344. cmdclass = {}
  345. dist = st_dist.Distribution()
  346. for cls_name in in_cfg_value:
  347. cls = resolve_name(cls_name)
  348. cmd = cls(dist)
  349. cmdclass[cmd.get_command_name()] = cls
  350. in_cfg_value = cmdclass
  351. kwargs[arg] = in_cfg_value
  352. # Transform requirements with embedded environment markers to
  353. # setuptools' supported marker-per-requirement format.
  354. #
  355. # install_requires are treated as a special case of extras, before
  356. # being put back in the expected place
  357. #
  358. # fred =
  359. # foo:marker
  360. # bar
  361. # -> {'fred': ['bar'], 'fred:marker':['foo']}
  362. if 'extras' in config:
  363. requirement_pattern = '(?P<package>[^:]*):?(?P<env_marker>[^#]*?)(?:\s*#.*)?$'
  364. extras = config['extras']
  365. # Add contents of test-requirements, if any, into an extra named
  366. # 'test' if one does not already exist.
  367. if 'test' not in extras:
  368. from pbr import packaging
  369. extras['test'] = "\n".join(packaging.parse_requirements(
  370. packaging.TEST_REQUIREMENTS_FILES)).replace(';', ':')
  371. for extra in extras:
  372. extra_requirements = []
  373. requirements = split_multiline(extras[extra])
  374. for requirement in requirements:
  375. m = re.match(requirement_pattern, requirement)
  376. extras_value = m.group('package').strip()
  377. env_marker = m.group('env_marker')
  378. extra_requirements.append((extras_value,env_marker))
  379. all_requirements[extra] = extra_requirements
  380. # Transform the full list of requirements into:
  381. # - install_requires, for those that have no extra and no
  382. # env_marker
  383. # - named extras, for those with an extra name (which may include
  384. # an env_marker)
  385. # - and as a special case, install_requires with an env_marker are
  386. # treated as named extras where the name is the empty string
  387. extras_require = {}
  388. for req_group in all_requirements:
  389. for requirement, env_marker in all_requirements[req_group]:
  390. if env_marker:
  391. extras_key = '%s:(%s)' % (req_group, env_marker)
  392. # We do not want to poison wheel creation with locally
  393. # evaluated markers. sdists always re-create the egg_info
  394. # and as such do not need guarded, and pip will never call
  395. # multiple setup.py commands at once.
  396. if 'bdist_wheel' not in script_args:
  397. try:
  398. if pkg_resources.evaluate_marker('(%s)' % env_marker):
  399. extras_key = req_group
  400. except SyntaxError:
  401. log.error(
  402. "Marker evaluation failed, see the following "
  403. "error. For more information see: "
  404. "http://docs.openstack.org/"
  405. "developer/pbr/compatibility.html#evaluate-marker"
  406. )
  407. raise
  408. else:
  409. extras_key = req_group
  410. extras_require.setdefault(extras_key, []).append(requirement)
  411. kwargs['install_requires'] = extras_require.pop('', [])
  412. kwargs['extras_require'] = extras_require
  413. return kwargs
  414. def register_custom_compilers(config):
  415. """Handle custom compilers; this has no real equivalent in distutils, where
  416. additional compilers could only be added programmatically, so we have to
  417. hack it in somehow.
  418. """
  419. compilers = has_get_option(config, 'global', 'compilers')
  420. if compilers:
  421. compilers = split_multiline(compilers)
  422. for compiler in compilers:
  423. compiler = resolve_name(compiler)
  424. # In distutils2 compilers these class attributes exist; for
  425. # distutils1 we just have to make something up
  426. if hasattr(compiler, 'name'):
  427. name = compiler.name
  428. else:
  429. name = compiler.__name__
  430. if hasattr(compiler, 'description'):
  431. desc = compiler.description
  432. else:
  433. desc = 'custom compiler %s' % name
  434. module_name = compiler.__module__
  435. # Note; this *will* override built in compilers with the same name
  436. # TODO: Maybe display a warning about this?
  437. cc = distutils.ccompiler.compiler_class
  438. cc[name] = (module_name, compiler.__name__, desc)
  439. # HACK!!!! Distutils assumes all compiler modules are in the
  440. # distutils package
  441. sys.modules['distutils.' + module_name] = sys.modules[module_name]
  442. def get_extension_modules(config):
  443. """Handle extension modules"""
  444. EXTENSION_FIELDS = ("sources",
  445. "include_dirs",
  446. "define_macros",
  447. "undef_macros",
  448. "library_dirs",
  449. "libraries",
  450. "runtime_library_dirs",
  451. "extra_objects",
  452. "extra_compile_args",
  453. "extra_link_args",
  454. "export_symbols",
  455. "swig_opts",
  456. "depends")
  457. ext_modules = []
  458. for section in config:
  459. if ':' in section:
  460. labels = section.split(':', 1)
  461. else:
  462. # Backwards compatibility for old syntax; don't use this though
  463. labels = section.split('=', 1)
  464. labels = [l.strip() for l in labels]
  465. if (len(labels) == 2) and (labels[0] == 'extension'):
  466. ext_args = {}
  467. for field in EXTENSION_FIELDS:
  468. value = has_get_option(config, section, field)
  469. # All extension module options besides name can have multiple
  470. # values
  471. if not value:
  472. continue
  473. value = split_multiline(value)
  474. if field == 'define_macros':
  475. macros = []
  476. for macro in value:
  477. macro = macro.split('=', 1)
  478. if len(macro) == 1:
  479. macro = (macro[0].strip(), None)
  480. else:
  481. macro = (macro[0].strip(), macro[1].strip())
  482. macros.append(macro)
  483. value = macros
  484. ext_args[field] = value
  485. if ext_args:
  486. if 'name' not in ext_args:
  487. ext_args['name'] = labels[1]
  488. ext_modules.append(extension.Extension(ext_args.pop('name'),
  489. **ext_args))
  490. return ext_modules
  491. def get_entry_points(config):
  492. """Process the [entry_points] section of setup.cfg to handle setuptools
  493. entry points. This is, of course, not a standard feature of
  494. distutils2/packaging, but as there is not currently a standard alternative
  495. in packaging, we provide support for them.
  496. """
  497. if not 'entry_points' in config:
  498. return {}
  499. return dict((option, split_multiline(value))
  500. for option, value in config['entry_points'].items())
  501. def has_get_option(config, section, option):
  502. if section in config and option in config[section]:
  503. return config[section][option]
  504. else:
  505. return False
  506. def split_multiline(value):
  507. """Special behaviour when we have a multi line options"""
  508. value = [element for element in
  509. (line.strip() for line in value.split('\n'))
  510. if element and not element.startswith('#')]
  511. return value
  512. def split_csv(value):
  513. """Special behaviour when we have a comma separated options"""
  514. value = [element for element in
  515. (chunk.strip() for chunk in value.split(','))
  516. if element]
  517. return value
  518. # The following classes are used to hack Distribution.command_options a bit
  519. class DefaultGetDict(defaultdict):
  520. """Like defaultdict, but the get() method also sets and returns the default
  521. value.
  522. """
  523. def get(self, key, default=None):
  524. if default is None:
  525. default = self.default_factory()
  526. return super(DefaultGetDict, self).setdefault(key, default)