interrogate_setup_dot_py.py.stamp 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. # Software License Agreement (BSD License)
  2. #
  3. # Copyright (c) 2012, Willow Garage, Inc.
  4. # All rights reserved.
  5. #
  6. # Redistribution and use in source and binary forms, with or without
  7. # modification, are permitted provided that the following conditions
  8. # are met:
  9. #
  10. # * Redistributions of source code must retain the above copyright
  11. # notice, this list of conditions and the following disclaimer.
  12. # * Redistributions in binary form must reproduce the above
  13. # copyright notice, this list of conditions and the following
  14. # disclaimer in the documentation and/or other materials provided
  15. # with the distribution.
  16. # * Neither the name of Willow Garage, Inc. nor the names of its
  17. # contributors may be used to endorse or promote products derived
  18. # from this software without specific prior written permission.
  19. #
  20. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  23. # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
  24. # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  25. # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  26. # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  27. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  28. # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  29. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  30. # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  31. # POSSIBILITY OF SUCH DAMAGE.
  32. from __future__ import print_function
  33. import os
  34. import runpy
  35. import sys
  36. from argparse import ArgumentParser
  37. setup_modules = []
  38. try:
  39. import distutils.core
  40. setup_modules.append(distutils.core)
  41. except ImportError:
  42. pass
  43. try:
  44. import setuptools
  45. setup_modules.append(setuptools)
  46. except ImportError:
  47. pass
  48. assert setup_modules, 'Must have distutils or setuptools installed'
  49. def _get_locations(pkgs, package_dir):
  50. """
  51. Based on setuptools logic and the package_dir dict, builds a dict of location roots for each pkg in pkgs.
  52. See http://docs.python.org/distutils/setupscript.html
  53. :returns: a dict {pkgname: root} for each pkgname in pkgs (and each of their parents)
  54. """
  55. # package_dir contains a dict {package_name: relativepath}
  56. # Example {'': 'src', 'foo': 'lib', 'bar': 'lib2'}
  57. #
  58. # '' means where to look for any package unless a parent package
  59. # is listed so package bar.pot is expected at lib2/bar/pot,
  60. # whereas package sup.dee is expected at src/sup/dee
  61. #
  62. # if package_dir does not state anything about a package,
  63. # setuptool expects the package folder to be in the root of the
  64. # project
  65. locations = {}
  66. allprefix = package_dir.get('', '')
  67. for pkg in pkgs:
  68. parent_location = None
  69. splits = pkg.split('.')
  70. # we iterate over compound name from parent to child
  71. # so once we found parent, children just append to their parent
  72. for key_len in range(len(splits)):
  73. key = '.'.join(splits[:key_len + 1])
  74. if key not in locations:
  75. if key in package_dir:
  76. locations[key] = package_dir[key]
  77. elif parent_location is not None:
  78. locations[key] = os.path.join(parent_location, splits[key_len])
  79. else:
  80. locations[key] = os.path.join(allprefix, key)
  81. parent_location = locations[key]
  82. return locations
  83. def generate_cmake_file(package_name, version, scripts, package_dir, pkgs, modules, setup_module=None):
  84. """
  85. Generate lines to add to a cmake file which will set variables.
  86. :param version: str, format 'int.int.int'
  87. :param scripts: [list of str]: relative paths to scripts
  88. :param package_dir: {modulename: path}
  89. :param pkgs: [list of str] python_packages declared in catkin package
  90. :param modules: [list of str] python modules
  91. :param setup_module: str, setuptools or distutils
  92. """
  93. prefix = '%s_SETUP_PY' % package_name
  94. result = []
  95. if setup_module:
  96. result.append(r'set(%s_SETUP_MODULE "%s")' % (prefix, setup_module))
  97. result.append(r'set(%s_VERSION "%s")' % (prefix, version))
  98. result.append(r'set(%s_SCRIPTS "%s")' % (prefix, ';'.join(scripts)))
  99. # Remove packages with '.' separators.
  100. #
  101. # setuptools allows specifying submodules in other folders than
  102. # their parent
  103. #
  104. # The symlink approach of catkin does not work with such submodules.
  105. # In the common case, this does not matter as the submodule is
  106. # within the containing module. We verify this assumption, and if
  107. # it passes, we remove submodule packages.
  108. locations = _get_locations(pkgs, package_dir)
  109. for pkgname, location in locations.items():
  110. if '.' not in pkgname:
  111. continue
  112. splits = pkgname.split('.')
  113. # hack: ignore write-combining setup.py files for msg and srv files
  114. if splits[1] in ['msg', 'srv']:
  115. continue
  116. # check every child has the same root folder as its parent
  117. root_name = splits[0]
  118. root_location = location
  119. for _ in range(len(splits) - 1):
  120. root_location = os.path.dirname(root_location)
  121. if root_location != locations[root_name]:
  122. raise RuntimeError(
  123. 'catkin_export_python does not support setup.py files that combine across multiple directories: %s in %s, %s in %s' % (pkgname, location, root_name, locations[root_name]))
  124. # If checks pass, remove all submodules
  125. pkgs = [p for p in pkgs if '.' not in p]
  126. resolved_pkgs = []
  127. for pkg in pkgs:
  128. resolved_pkgs += [locations[pkg]]
  129. result.append(r'set(%s_PACKAGES "%s")' % (prefix, ';'.join(pkgs)))
  130. result.append(r'set(%s_PACKAGE_DIRS "%s")' % (prefix, ';'.join(resolved_pkgs).replace('\\', '/')))
  131. # skip modules which collide with package names
  132. filtered_modules = []
  133. for modname in modules:
  134. splits = modname.split('.')
  135. # check all parents too
  136. equals_package = [('.'.join(splits[:-i]) in locations) for i in range(len(splits))]
  137. if any(equals_package):
  138. continue
  139. filtered_modules.append(modname)
  140. module_locations = _get_locations(filtered_modules, package_dir)
  141. result.append(r'set(%s_MODULES "%s")' % (prefix, ';'.join(['%s.py' % m.replace('.', '/') for m in filtered_modules])))
  142. result.append(r'set(%s_MODULE_DIRS "%s")' % (prefix, ';'.join([module_locations[m] for m in filtered_modules]).replace('\\', '/')))
  143. return result
  144. def _create_mock_setup_function(setup_module, package_name, outfile):
  145. """
  146. Create a function to call instead of distutils.core.setup or setuptools.setup.
  147. It just captures some args and writes them into a file that can be used from cmake.
  148. :param package_name: name of the package
  149. :param outfile: filename that cmake will use afterwards
  150. :returns: a function to replace disutils.core.setup and setuptools.setup
  151. """
  152. def setup(*args, **kwargs):
  153. """Check kwargs and write a scriptfile."""
  154. if 'version' not in kwargs:
  155. sys.stderr.write("\n*** Unable to find 'version' in setup.py of %s\n" % package_name)
  156. raise RuntimeError('version not found in setup.py')
  157. version = kwargs['version']
  158. package_dir = kwargs.get('package_dir', {})
  159. pkgs = kwargs.get('packages', [])
  160. scripts = kwargs.get('scripts', [])
  161. modules = kwargs.get('py_modules', [])
  162. unsupported_args = [
  163. 'entry_points',
  164. 'exclude_package_data',
  165. 'ext_modules ',
  166. 'ext_package',
  167. 'include_package_data',
  168. 'namespace_packages',
  169. 'setup_requires',
  170. 'use_2to3',
  171. 'zip_safe']
  172. used_unsupported_args = [arg for arg in unsupported_args if arg in kwargs]
  173. if used_unsupported_args:
  174. sys.stderr.write('*** Arguments %s to setup() not supported in catkin devel space in setup.py of %s\n' % (used_unsupported_args, package_name))
  175. result = generate_cmake_file(package_name=package_name,
  176. version=version,
  177. scripts=scripts,
  178. package_dir=package_dir,
  179. pkgs=pkgs,
  180. modules=modules,
  181. setup_module=setup_module)
  182. with open(outfile, 'w') as out:
  183. out.write('\n'.join(result))
  184. return setup
  185. def main():
  186. """Script main, parses arguments and invokes Dummy.setup indirectly."""
  187. parser = ArgumentParser(description='Utility to read setup.py values from cmake macros. Creates a file with CMake set commands setting variables.')
  188. parser.add_argument('package_name', help='Name of catkin package')
  189. parser.add_argument('setupfile_path', help='Full path to setup.py')
  190. parser.add_argument('outfile', help='Where to write result to')
  191. args = parser.parse_args()
  192. # print("%s" % sys.argv)
  193. # PACKAGE_NAME = sys.argv[1]
  194. # OUTFILE = sys.argv[3]
  195. # print("Interrogating setup.py for package %s into %s " % (PACKAGE_NAME, OUTFILE),
  196. # file=sys.stderr)
  197. # print("executing %s" % args.setupfile_path)
  198. # be sure you're in the directory containing
  199. # setup.py so the sys.path manipulation works,
  200. # so the import of __version__ works
  201. os.chdir(os.path.dirname(os.path.abspath(args.setupfile_path)))
  202. # patch setup() function of distutils and setuptools for the
  203. # context of evaluating setup.py
  204. backup_modules = {}
  205. try:
  206. for module in setup_modules:
  207. backup_modules[id(module)] = module.setup
  208. module.setup = _create_mock_setup_function(
  209. setup_module=module.__name__, package_name=args.package_name, outfile=args.outfile)
  210. runpy.run_path(args.setupfile_path)
  211. finally:
  212. for module in setup_modules:
  213. module.setup = backup_modules[id(module)]
  214. if __name__ == '__main__':
  215. main()