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