import os, re, sys, zipfile, imp from distutils.command import build, install from distutils.core import Command from distutils.dep_util import newer from distutils.dir_util import remove_tree from distutils.errors import * from distutils.sysconfig import get_python_version from distutils.util import byte_compile from Ft.Lib import ImportUtil from Ft.Lib.DistExt import Structures, Util __zipsafe__ = True EXTENSION_MODULE_STUB = """# dynamic module loader stub for .egg zipfiles def __bootstrap__(): global __bootstrap__, __loader__, __file__ import imp, pkg_resources __file__ = pkg_resources.resource_filename(__name__, %(file)r) del __bootstrap__, __loader__ imp.load_dynamic(__name__, __file__) __bootstrap__() """ NAMESPACE_PACKAGE_STUB = """# loader stub for .egg namespace packages __import__("pkg_resources").declare_namespace(__name__) """ class BDistEgg(Command): command_name = 'bdist_egg' description = "create a Python Egg (.egg) built distribution" user_options = [ ('bdist-dir=', None, "temporary directory for creating the distribution"), ('keep-temp', 'k', "keep the pseudo-installation tree around after " + "creating the distribution archive"), ('plat-name=', 'p', "platform name to embed in generated filenames"), ('dist-dir=', 'd', "directory to put final built distributions in"), ('skip-build', None, "skip rebuilding everything (for testing/debugging)"), ] boolean_options = ['keep-temp', 'skip-build'] build_commands = ['build_py', 'build_clib', 'build_ext', 'build_scripts'] install_commands = ['install_lib', 'install_data', 'install_l10n', 'install_config'] def initialize_options(self): self.bdist_dir = None self.plat_name = None self.keep_temp = None self.dist_dir = None self.skip_build = None return def finalize_options(self): if self.bdist_dir is None: bdist_base = self.get_finalized_command('bdist').bdist_base egg_dir = self.distribution.get_name() + '.egg' self.bdist_dir = os.path.join(bdist_base, egg_dir) self.set_undefined_options('bdist', ('keep_temp', 'keep_temp'), ('plat_name', 'plat_name'), ('dist_dir', 'dist_dir'), ('skip_build', 'skip_build')) return def run(self): if sys.version < '2.3': raise DistutilsPlatformError('Python Eggs require Python 2.3+') if not self.skip_build: # Run any build commands self.run_command_family('build', self.build_commands) self.mkpath(self.bdist_dir) # Set the options for the installation commands install = self.reinitialize_command('install') # We need a "prestine" command (no options from anywhere else) install.initialize_options() for cmd_name, predicate in install.sub_commands: self.reinitialize_command(cmd_name).initialize_options() install.skip_build = self.skip_build install.compile = True install.install_base = install.install_platbase = self.bdist_dir install.select_scheme('zip') install.ensure_finalized() # Make the install appear to be a "chroot" install install.root = self.bdist_dir install.install_base = install.install_platbase = None # Run the commands that install "resources" commands = self.run_command_family('install', self.install_commands) outputs = [] for command in commands: cmd = self.get_finalized_command(command) outputs.extend(cmd.get_outputs()) # Create stub loaders for any extension modules py_files = self.write_extension_module_stubs() # Create namespace package loaders py_files.extend(self.write_namespace_package_stubs()) # Byte-compile any additional Python source files if py_files: outputs.extend(py_files) # Byte-compile the stubs and record the compiled filenames install_lib = self.get_finalized_command('install_lib') install_lib.byte_compile(py_files) for py_file in py_files: if install_lib.compile: outputs.append(py_file + 'c') if install_lib.optimize > 0: outputs.append(py_file + 'o') # Add the standard metadata (EGG-INFO directory) outputs.extend(self.write_metadata()) # Make the egg distribution dist_filename = self.make_distribution(outputs) # Add to 'Distribution.dist_files' so that the "upload" command works if hasattr(self.distribution, 'dist_files'): spec = ('bdist_egg', get_python_version(), dist_filename) self.distribution.dist_files.append(spec) if not self.keep_temp: remove_tree(self.bdist_dir, self.verbose, self.dry_run) return def run_command_family(self, family, commands): command = self.get_finalized_command(family) sub_commands = command.get_sub_commands() have_run = [] for cmd_name in commands: if cmd_name in sub_commands: self.run_command(cmd_name) have_run.append(cmd_name) return have_run def write_extension_module_stubs(self): outputs = [] build_ext = self.get_finalized_command('build_ext') for extension in build_ext.extensions: fullname = build_ext.get_ext_fullname(extension.name) filename = build_ext.get_ext_filename(fullname) contents = EXTENSION_MODULE_STUB % { 'file' : os.path.basename(filename)} barename, ext = os.path.splitext(filename) if barename.endswith('module'): barename = barename[:-6] filename = os.path.join(self.bdist_dir, barename + '.py') description = "extension module stub for %r" % extension.name if self.force or self.newer(description, filename): self.write_file(description, contents, filename) outputs.append(filename) return outputs def write_namespace_package_stubs(self): outputs = [] for package in self.distribution.namespace_packages: dirname = package.replace('.', os.sep) filename = os.path.join(self.bdist_dir, dirname, '__init__.py') description = "namespace package stub for %r" % package if self.force or self.newer(description, filename): self.write_file(description, NAMESPACE_PACKAGE_STUB, filename) outputs.append(filename) return outputs def write_metadata(self): outputs = [] metadata_dir = os.path.join(self.bdist_dir, 'EGG-INFO') self.mkpath(metadata_dir) filename = os.path.join(metadata_dir, 'PKG-INFO') description = "package metadata" if self.force or self.newer(description, filename): self.announce("writing %s to %s" % (description, filename), 2) if not self.dry_run: f = open(filename, 'w') try: self.distribution.metadata.write_pkg_file(f) finally: f.close() outputs.append(filename) entry_points = self.get_entry_points() if entry_points: filename = os.path.join(metadata_dir, 'entry_points.txt') description = "entry points" if self.force or self.newer(description, filename): self.write_file_sections(description, entry_points, filename) outputs.append(filename) eager_resources = self.get_eager_resources() if eager_resources: prefix = self.bdist_dir + os.sep prefix_len = len(prefix) lines = [] for resource in eager_resources: if resource.startswith(prefix): resource = resource[prefix_len:] lines.append(resource.replace(os.sep, '/')) description = "eager resources" if self.force or self.newer(description, filename): self.write_file_lines(description, lines, filename) outputs.append(filename) # Create the ZIP safety flag file if self.check_zip_safe(): filename = 'zip-safe' else: filename = 'not-zip-safe' filename = os.path.join(metadata_dir, filename) self.write_file_lines('ZIP safety flag', (), filename) outputs.append(filename) return outputs def get_entry_points(self): entry_points = {} # Get the scripts which will be turned into console_script entries if self.distribution.has_scripts(): entry_points['console_scripts'] = console_scripts = [] for script in self.distribution.scripts: if isinstance(script, Structures.Script): entry_point = '%s = %s:%s' % (script.name, script.module, script.function) console_scripts.append(entry_point) else: self.announce("WARNING: script '%s' skipped" " (unsupported format)" % script.name, 3) return entry_points def get_eager_resources(self): eager_resources = [] # Python's gettext requires true filesystem paths, so add the # localization catalogs to the list of "eager resources". if self.distribution.has_l10n(): install_l10n = self.get_finalized_command('install_l10n') eager_resources.append(install_l10n.install_dir) eager_resources.extend(install_l10n.get_outputs()) return eager_resources def check_zip_safe(self): safe = True build_py = self.get_finalized_command('build_py') for package, module, filename in build_py.find_all_modules(): if module == '__init__': fullname = package elif package: fullname = package + '.' + module # Get the code object for the module. f = open(filename, 'rU') try: code = compile(f.read(), filename, 'exec') finally: f.close() # If the module attribute '__zipsafe__' is defined, use its # value to determine zip-safety. Otherwise check the symbols # used for "unsafe" names. if '__zipsafe__' in code.co_names: constants = get_constants(code) try: safe = safe and constants['__zipsafe__'] except KeyError: # If defined, __zipsafe__ *MUST* be constant. raise ValueError("%s.__zipsafe__ not constant" % fullname) else: symbols = get_symbols(code) unsafe = [] for bad in ('__file__', '__path__'): if bad in symbols: unsafe.append(bad) if 'inspect' in symbols: for bad in ('getsource', 'getabsfile', 'getsourcefile', 'getfile', 'getsourcelines', 'findsource', 'getcomments', 'getframeinfo', 'getinnerframes', 'getouterframes', 'stack', 'trace'): if bad in symbols: unsafe.append('inspect.' + bad) for symbol in unsafe: self.announce("%s references %s" % (fullname, symbol), 2) safe = False return safe def make_distribution(self, files): self.mkpath(self.dist_dir) egg_filename = os.path.join(self.dist_dir, self.get_egg_filename()) self.announce("creating %s" % egg_filename, 2) if not self.dry_run: if sys.version < '2.4': # avoid 2.3 zipimport bug when 64 bits compression = zipfile.ZIP_STORED else: compression = zipfile.ZIP_DEFLATED eggfile = zipfile.ZipFile(egg_filename, "w", compression) else: eggfile = None prefix_len = len(self.bdist_dir) + len(os.sep) for filename in files: arcname = filename[prefix_len:] self.announce("adding %s" % arcname, 1) if eggfile is not None: eggfile.write(filename, arcname) if eggfile is not None: eggfile.close() return egg_filename def get_egg_filename(self): def make_egg_safe(name): return re.sub('[^a-zA-Z0-9.]+', '_', name) name = make_egg_safe(self.distribution.get_name()) version = make_egg_safe(self.distribution.get_version()) platform = get_python_version() if self.distribution.has_ext_modules(): platform += '-' + self.plat_name return '%s-%s-py%s.egg' % (name, version, platform) def newer(self, description, filename): try: target_mtime = os.stat(filename).st_mtime except OSError: return True if self.distribution.package_file: source_mtime = os.stat(self.distribution.package_file).st_mtime if source_mtime > target_mtime: return True command_mtime = ImportUtil.GetLastModified(__name__) if command_mtime > target_mtime: return True self.announce("skipping %s (up-to-date)" % description, 1) return False def write_file_sections(self, description, sections, filename): self.announce("writing %s to %s" % (description, filename), 2) if not self.dry_run: f = open(filename, 'w') try: for section in sections: f.write('[%s]\n' % section) for line in sections[section]: f.write(line) f.write('\n') f.write('\n') finally: f.close() return def write_file_lines(self, description, lines, filename): self.announce("writing %s to %s" % (description, filename), 2) if not self.dry_run: f = open(filename, 'w') try: for line in lines: f.write(line) f.write('\n') finally: f.close() return def write_file(self, description, contents, filename): self.announce("writing %s to %s" % (description, filename), 2) if not self.dry_run: f = open(filename, 'w') try: f.write(contents) finally: f.close() return def get_constants(code): """Returns the mapping of top-level "constants" defined by 'code'. """ # perform a "mini-eval" of the global assignments import dis LOAD_CONST = dis.opmap['LOAD_CONST'] LOAD_NAME = dis.opmap['LOAD_NAME'] STORE_NAME = dis.opmap['STORE_NAME'] EXTENDED_ARG = dis.opmap['EXTENDED_ARG'] # convert the bytecode string to a series of integers opcodes = iter(map(ord, code.co_code)) next_instr = opcodes.next constants = {} value = undefined = object() for opcode in opcodes: if opcode >= dis.HAVE_ARGUMENT: oparg = next_instr() + next_instr()*256 while opcode == EXTENDED_ARG: opcode = next_instr() oparg = oparg << 16 + next_instr() + next_instr()*256 if opcode == LOAD_CONST: value = code.co_consts[oparg] elif opcode == LOAD_NAME: name = code.co_names[oparg] if name in constants: value = constants[name] elif name in __builtins__: value = __builtins__[name] else: # not a constant delaration value = undefined elif opcode == STORE_NAME: name = code.co_names[oparg] if value is not undefined: constants[name] = value value = undefined else: # either not a variable binding opcode or not a LOAD/STORE pairing value = undefined return constants def get_symbols(code, _symbols=None): """Returns the set of names and string constants used by 'code' and any nested code objects.""" if _symbols is None: _symbols = {} for name in code.co_names: _symbols[name] = code for const in code.co_consts: if isinstance(const, (str, unicode)): _symbols[const] = code elif isinstance(const, type(code)): get_symbols(const, _symbols) return _symbols