bdist_egg.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. """setuptools.command.bdist_egg
  2. Build .egg distributions"""
  3. from distutils.errors import DistutilsSetupError
  4. from distutils.dir_util import remove_tree, mkpath
  5. from distutils import log
  6. from types import CodeType
  7. import sys
  8. import os
  9. import re
  10. import textwrap
  11. import marshal
  12. import warnings
  13. from setuptools.extern import six
  14. from pkg_resources import get_build_platform, Distribution, ensure_directory
  15. from pkg_resources import EntryPoint
  16. from setuptools.extension import Library
  17. from setuptools import Command, SetuptoolsDeprecationWarning
  18. try:
  19. # Python 2.7 or >=3.2
  20. from sysconfig import get_path, get_python_version
  21. def _get_purelib():
  22. return get_path("purelib")
  23. except ImportError:
  24. from distutils.sysconfig import get_python_lib, get_python_version
  25. def _get_purelib():
  26. return get_python_lib(False)
  27. def strip_module(filename):
  28. if '.' in filename:
  29. filename = os.path.splitext(filename)[0]
  30. if filename.endswith('module'):
  31. filename = filename[:-6]
  32. return filename
  33. def sorted_walk(dir):
  34. """Do os.walk in a reproducible way,
  35. independent of indeterministic filesystem readdir order
  36. """
  37. for base, dirs, files in os.walk(dir):
  38. dirs.sort()
  39. files.sort()
  40. yield base, dirs, files
  41. def write_stub(resource, pyfile):
  42. _stub_template = textwrap.dedent("""
  43. def __bootstrap__():
  44. global __bootstrap__, __loader__, __file__
  45. import sys, pkg_resources
  46. from importlib.machinery import ExtensionFileLoader
  47. __file__ = pkg_resources.resource_filename(__name__, %r)
  48. __loader__ = None; del __bootstrap__, __loader__
  49. ExtensionFileLoader(__name__,__file__).exec_module()
  50. __bootstrap__()
  51. """).lstrip()
  52. with open(pyfile, 'w') as f:
  53. f.write(_stub_template % resource)
  54. class bdist_egg(Command):
  55. description = "create an \"egg\" distribution"
  56. user_options = [
  57. ('bdist-dir=', 'b',
  58. "temporary directory for creating the distribution"),
  59. ('plat-name=', 'p', "platform name to embed in generated filenames "
  60. "(default: %s)" % get_build_platform()),
  61. ('exclude-source-files', None,
  62. "remove all .py files from the generated egg"),
  63. ('keep-temp', 'k',
  64. "keep the pseudo-installation tree around after " +
  65. "creating the distribution archive"),
  66. ('dist-dir=', 'd',
  67. "directory to put final built distributions in"),
  68. ('skip-build', None,
  69. "skip rebuilding everything (for testing/debugging)"),
  70. ]
  71. boolean_options = [
  72. 'keep-temp', 'skip-build', 'exclude-source-files'
  73. ]
  74. def initialize_options(self):
  75. self.bdist_dir = None
  76. self.plat_name = None
  77. self.keep_temp = 0
  78. self.dist_dir = None
  79. self.skip_build = 0
  80. self.egg_output = None
  81. self.exclude_source_files = None
  82. def finalize_options(self):
  83. ei_cmd = self.ei_cmd = self.get_finalized_command("egg_info")
  84. self.egg_info = ei_cmd.egg_info
  85. if self.bdist_dir is None:
  86. bdist_base = self.get_finalized_command('bdist').bdist_base
  87. self.bdist_dir = os.path.join(bdist_base, 'egg')
  88. if self.plat_name is None:
  89. self.plat_name = get_build_platform()
  90. self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
  91. if self.egg_output is None:
  92. # Compute filename of the output egg
  93. basename = Distribution(
  94. None, None, ei_cmd.egg_name, ei_cmd.egg_version,
  95. get_python_version(),
  96. self.distribution.has_ext_modules() and self.plat_name
  97. ).egg_name()
  98. self.egg_output = os.path.join(self.dist_dir, basename + '.egg')
  99. def do_install_data(self):
  100. # Hack for packages that install data to install's --install-lib
  101. self.get_finalized_command('install').install_lib = self.bdist_dir
  102. site_packages = os.path.normcase(os.path.realpath(_get_purelib()))
  103. old, self.distribution.data_files = self.distribution.data_files, []
  104. for item in old:
  105. if isinstance(item, tuple) and len(item) == 2:
  106. if os.path.isabs(item[0]):
  107. realpath = os.path.realpath(item[0])
  108. normalized = os.path.normcase(realpath)
  109. if normalized == site_packages or normalized.startswith(
  110. site_packages + os.sep
  111. ):
  112. item = realpath[len(site_packages) + 1:], item[1]
  113. # XXX else: raise ???
  114. self.distribution.data_files.append(item)
  115. try:
  116. log.info("installing package data to %s", self.bdist_dir)
  117. self.call_command('install_data', force=0, root=None)
  118. finally:
  119. self.distribution.data_files = old
  120. def get_outputs(self):
  121. return [self.egg_output]
  122. def call_command(self, cmdname, **kw):
  123. """Invoke reinitialized command `cmdname` with keyword args"""
  124. for dirname in INSTALL_DIRECTORY_ATTRS:
  125. kw.setdefault(dirname, self.bdist_dir)
  126. kw.setdefault('skip_build', self.skip_build)
  127. kw.setdefault('dry_run', self.dry_run)
  128. cmd = self.reinitialize_command(cmdname, **kw)
  129. self.run_command(cmdname)
  130. return cmd
  131. def run(self):
  132. # Generate metadata first
  133. self.run_command("egg_info")
  134. # We run install_lib before install_data, because some data hacks
  135. # pull their data path from the install_lib command.
  136. log.info("installing library code to %s", self.bdist_dir)
  137. instcmd = self.get_finalized_command('install')
  138. old_root = instcmd.root
  139. instcmd.root = None
  140. if self.distribution.has_c_libraries() and not self.skip_build:
  141. self.run_command('build_clib')
  142. cmd = self.call_command('install_lib', warn_dir=0)
  143. instcmd.root = old_root
  144. all_outputs, ext_outputs = self.get_ext_outputs()
  145. self.stubs = []
  146. to_compile = []
  147. for (p, ext_name) in enumerate(ext_outputs):
  148. filename, ext = os.path.splitext(ext_name)
  149. pyfile = os.path.join(self.bdist_dir, strip_module(filename) +
  150. '.py')
  151. self.stubs.append(pyfile)
  152. log.info("creating stub loader for %s", ext_name)
  153. if not self.dry_run:
  154. write_stub(os.path.basename(ext_name), pyfile)
  155. to_compile.append(pyfile)
  156. ext_outputs[p] = ext_name.replace(os.sep, '/')
  157. if to_compile:
  158. cmd.byte_compile(to_compile)
  159. if self.distribution.data_files:
  160. self.do_install_data()
  161. # Make the EGG-INFO directory
  162. archive_root = self.bdist_dir
  163. egg_info = os.path.join(archive_root, 'EGG-INFO')
  164. self.mkpath(egg_info)
  165. if self.distribution.scripts:
  166. script_dir = os.path.join(egg_info, 'scripts')
  167. log.info("installing scripts to %s", script_dir)
  168. self.call_command('install_scripts', install_dir=script_dir,
  169. no_ep=1)
  170. self.copy_metadata_to(egg_info)
  171. native_libs = os.path.join(egg_info, "native_libs.txt")
  172. if all_outputs:
  173. log.info("writing %s", native_libs)
  174. if not self.dry_run:
  175. ensure_directory(native_libs)
  176. libs_file = open(native_libs, 'wt')
  177. libs_file.write('\n'.join(all_outputs))
  178. libs_file.write('\n')
  179. libs_file.close()
  180. elif os.path.isfile(native_libs):
  181. log.info("removing %s", native_libs)
  182. if not self.dry_run:
  183. os.unlink(native_libs)
  184. write_safety_flag(
  185. os.path.join(archive_root, 'EGG-INFO'), self.zip_safe()
  186. )
  187. if os.path.exists(os.path.join(self.egg_info, 'depends.txt')):
  188. log.warn(
  189. "WARNING: 'depends.txt' will not be used by setuptools 0.6!\n"
  190. "Use the install_requires/extras_require setup() args instead."
  191. )
  192. if self.exclude_source_files:
  193. self.zap_pyfiles()
  194. # Make the archive
  195. make_zipfile(self.egg_output, archive_root, verbose=self.verbose,
  196. dry_run=self.dry_run, mode=self.gen_header())
  197. if not self.keep_temp:
  198. remove_tree(self.bdist_dir, dry_run=self.dry_run)
  199. # Add to 'Distribution.dist_files' so that the "upload" command works
  200. getattr(self.distribution, 'dist_files', []).append(
  201. ('bdist_egg', get_python_version(), self.egg_output))
  202. def zap_pyfiles(self):
  203. log.info("Removing .py files from temporary directory")
  204. for base, dirs, files in walk_egg(self.bdist_dir):
  205. for name in files:
  206. path = os.path.join(base, name)
  207. if name.endswith('.py'):
  208. log.debug("Deleting %s", path)
  209. os.unlink(path)
  210. if base.endswith('__pycache__'):
  211. path_old = path
  212. pattern = r'(?P<name>.+)\.(?P<magic>[^.]+)\.pyc'
  213. m = re.match(pattern, name)
  214. path_new = os.path.join(
  215. base, os.pardir, m.group('name') + '.pyc')
  216. log.info(
  217. "Renaming file from [%s] to [%s]"
  218. % (path_old, path_new))
  219. try:
  220. os.remove(path_new)
  221. except OSError:
  222. pass
  223. os.rename(path_old, path_new)
  224. def zip_safe(self):
  225. safe = getattr(self.distribution, 'zip_safe', None)
  226. if safe is not None:
  227. return safe
  228. log.warn("zip_safe flag not set; analyzing archive contents...")
  229. return analyze_egg(self.bdist_dir, self.stubs)
  230. def gen_header(self):
  231. epm = EntryPoint.parse_map(self.distribution.entry_points or '')
  232. ep = epm.get('setuptools.installation', {}).get('eggsecutable')
  233. if ep is None:
  234. return 'w' # not an eggsecutable, do it the usual way.
  235. warnings.warn(
  236. "Eggsecutables are deprecated and will be removed in a future "
  237. "version.",
  238. SetuptoolsDeprecationWarning
  239. )
  240. if not ep.attrs or ep.extras:
  241. raise DistutilsSetupError(
  242. "eggsecutable entry point (%r) cannot have 'extras' "
  243. "or refer to a module" % (ep,)
  244. )
  245. pyver = '{}.{}'.format(*sys.version_info)
  246. pkg = ep.module_name
  247. full = '.'.join(ep.attrs)
  248. base = ep.attrs[0]
  249. basename = os.path.basename(self.egg_output)
  250. header = (
  251. "#!/bin/sh\n"
  252. 'if [ `basename $0` = "%(basename)s" ]\n'
  253. 'then exec python%(pyver)s -c "'
  254. "import sys, os; sys.path.insert(0, os.path.abspath('$0')); "
  255. "from %(pkg)s import %(base)s; sys.exit(%(full)s())"
  256. '" "$@"\n'
  257. 'else\n'
  258. ' echo $0 is not the correct name for this egg file.\n'
  259. ' echo Please rename it back to %(basename)s and try again.\n'
  260. ' exec false\n'
  261. 'fi\n'
  262. ) % locals()
  263. if not self.dry_run:
  264. mkpath(os.path.dirname(self.egg_output), dry_run=self.dry_run)
  265. f = open(self.egg_output, 'w')
  266. f.write(header)
  267. f.close()
  268. return 'a'
  269. def copy_metadata_to(self, target_dir):
  270. "Copy metadata (egg info) to the target_dir"
  271. # normalize the path (so that a forward-slash in egg_info will
  272. # match using startswith below)
  273. norm_egg_info = os.path.normpath(self.egg_info)
  274. prefix = os.path.join(norm_egg_info, '')
  275. for path in self.ei_cmd.filelist.files:
  276. if path.startswith(prefix):
  277. target = os.path.join(target_dir, path[len(prefix):])
  278. ensure_directory(target)
  279. self.copy_file(path, target)
  280. def get_ext_outputs(self):
  281. """Get a list of relative paths to C extensions in the output distro"""
  282. all_outputs = []
  283. ext_outputs = []
  284. paths = {self.bdist_dir: ''}
  285. for base, dirs, files in sorted_walk(self.bdist_dir):
  286. for filename in files:
  287. if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS:
  288. all_outputs.append(paths[base] + filename)
  289. for filename in dirs:
  290. paths[os.path.join(base, filename)] = (paths[base] +
  291. filename + '/')
  292. if self.distribution.has_ext_modules():
  293. build_cmd = self.get_finalized_command('build_ext')
  294. for ext in build_cmd.extensions:
  295. if isinstance(ext, Library):
  296. continue
  297. fullname = build_cmd.get_ext_fullname(ext.name)
  298. filename = build_cmd.get_ext_filename(fullname)
  299. if not os.path.basename(filename).startswith('dl-'):
  300. if os.path.exists(os.path.join(self.bdist_dir, filename)):
  301. ext_outputs.append(filename)
  302. return all_outputs, ext_outputs
  303. NATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split())
  304. def walk_egg(egg_dir):
  305. """Walk an unpacked egg's contents, skipping the metadata directory"""
  306. walker = sorted_walk(egg_dir)
  307. base, dirs, files = next(walker)
  308. if 'EGG-INFO' in dirs:
  309. dirs.remove('EGG-INFO')
  310. yield base, dirs, files
  311. for bdf in walker:
  312. yield bdf
  313. def analyze_egg(egg_dir, stubs):
  314. # check for existing flag in EGG-INFO
  315. for flag, fn in safety_flags.items():
  316. if os.path.exists(os.path.join(egg_dir, 'EGG-INFO', fn)):
  317. return flag
  318. if not can_scan():
  319. return False
  320. safe = True
  321. for base, dirs, files in walk_egg(egg_dir):
  322. for name in files:
  323. if name.endswith('.py') or name.endswith('.pyw'):
  324. continue
  325. elif name.endswith('.pyc') or name.endswith('.pyo'):
  326. # always scan, even if we already know we're not safe
  327. safe = scan_module(egg_dir, base, name, stubs) and safe
  328. return safe
  329. def write_safety_flag(egg_dir, safe):
  330. # Write or remove zip safety flag file(s)
  331. for flag, fn in safety_flags.items():
  332. fn = os.path.join(egg_dir, fn)
  333. if os.path.exists(fn):
  334. if safe is None or bool(safe) != flag:
  335. os.unlink(fn)
  336. elif safe is not None and bool(safe) == flag:
  337. f = open(fn, 'wt')
  338. f.write('\n')
  339. f.close()
  340. safety_flags = {
  341. True: 'zip-safe',
  342. False: 'not-zip-safe',
  343. }
  344. def scan_module(egg_dir, base, name, stubs):
  345. """Check whether module possibly uses unsafe-for-zipfile stuff"""
  346. filename = os.path.join(base, name)
  347. if filename[:-1] in stubs:
  348. return True # Extension module
  349. pkg = base[len(egg_dir) + 1:].replace(os.sep, '.')
  350. module = pkg + (pkg and '.' or '') + os.path.splitext(name)[0]
  351. if six.PY2:
  352. skip = 8 # skip magic & date
  353. elif sys.version_info < (3, 7):
  354. skip = 12 # skip magic & date & file size
  355. else:
  356. skip = 16 # skip magic & reserved? & date & file size
  357. f = open(filename, 'rb')
  358. f.read(skip)
  359. code = marshal.load(f)
  360. f.close()
  361. safe = True
  362. symbols = dict.fromkeys(iter_symbols(code))
  363. for bad in ['__file__', '__path__']:
  364. if bad in symbols:
  365. log.warn("%s: module references %s", module, bad)
  366. safe = False
  367. if 'inspect' in symbols:
  368. for bad in [
  369. 'getsource', 'getabsfile', 'getsourcefile', 'getfile'
  370. 'getsourcelines', 'findsource', 'getcomments', 'getframeinfo',
  371. 'getinnerframes', 'getouterframes', 'stack', 'trace'
  372. ]:
  373. if bad in symbols:
  374. log.warn("%s: module MAY be using inspect.%s", module, bad)
  375. safe = False
  376. return safe
  377. def iter_symbols(code):
  378. """Yield names and strings used by `code` and its nested code objects"""
  379. for name in code.co_names:
  380. yield name
  381. for const in code.co_consts:
  382. if isinstance(const, six.string_types):
  383. yield const
  384. elif isinstance(const, CodeType):
  385. for name in iter_symbols(const):
  386. yield name
  387. def can_scan():
  388. if not sys.platform.startswith('java') and sys.platform != 'cli':
  389. # CPython, PyPy, etc.
  390. return True
  391. log.warn("Unable to analyze compiled code on this platform.")
  392. log.warn("Please ask the author to include a 'zip_safe'"
  393. " setting (either True or False) in the package's setup.py")
  394. # Attribute names of options for commands that might need to be convinced to
  395. # install to the egg build directory
  396. INSTALL_DIRECTORY_ATTRS = [
  397. 'install_lib', 'install_dir', 'install_data', 'install_base'
  398. ]
  399. def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True,
  400. mode='w'):
  401. """Create a zip file from all the files under 'base_dir'. The output
  402. zip file will be named 'base_dir' + ".zip". Uses either the "zipfile"
  403. Python module (if available) or the InfoZIP "zip" utility (if installed
  404. and found on the default search path). If neither tool is available,
  405. raises DistutilsExecError. Returns the name of the output zip file.
  406. """
  407. import zipfile
  408. mkpath(os.path.dirname(zip_filename), dry_run=dry_run)
  409. log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir)
  410. def visit(z, dirname, names):
  411. for name in names:
  412. path = os.path.normpath(os.path.join(dirname, name))
  413. if os.path.isfile(path):
  414. p = path[len(base_dir) + 1:]
  415. if not dry_run:
  416. z.write(path, p)
  417. log.debug("adding '%s'", p)
  418. compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED
  419. if not dry_run:
  420. z = zipfile.ZipFile(zip_filename, mode, compression=compression)
  421. for dirname, dirs, files in sorted_walk(base_dir):
  422. visit(z, dirname, files)
  423. z.close()
  424. else:
  425. for dirname, dirs, files in sorted_walk(base_dir):
  426. visit(None, dirname, files)
  427. return zip_filename