1 """setuptools.command.bdist_egg
3 Build .egg distributions"""
5 # This module should be kept compatible with Python 2.3
6 import sys, os, marshal
7 from setuptools import Command
8 from distutils.dir_util import remove_tree, mkpath
9 from distutils.sysconfig import get_python_version, get_python_lib
10 from distutils import log
11 from distutils.errors import DistutilsSetupError
12 from pkg_resources import get_build_platform, Distribution, ensure_directory
13 from pkg_resources import EntryPoint
14 from types import CodeType
15 from setuptools.extension import Library
17 def strip_module(filename):
19 filename = os.path.splitext(filename)[0]
20 if filename.endswith('module'):
21 filename = filename[:-6]
24 def write_stub(resource, pyfile):
27 "def __bootstrap__():",
28 " global __bootstrap__, __loader__, __file__",
29 " import sys, pkg_resources, imp",
30 " __file__ = pkg_resources.resource_filename(__name__,%r)"
32 " __loader__ = None; del __bootstrap__, __loader__",
33 " imp.load_dynamic(__name__,__file__)",
39 # stub __init__.py for packages distributed without one
40 NS_PKG_STUB = '__import__("pkg_resources").declare_namespace(__name__)'
42 class bdist_egg(Command):
44 description = "create an \"egg\" distribution"
48 "temporary directory for creating the distribution"),
50 "platform name to embed in generated filenames "
51 "(default: %s)" % get_build_platform()),
52 ('exclude-source-files', None,
53 "remove all .py files from the generated egg"),
55 "keep the pseudo-installation tree around after " +
56 "creating the distribution archive"),
58 "directory to put final built distributions in"),
60 "skip rebuilding everything (for testing/debugging)"),
64 'keep-temp', 'skip-build', 'exclude-source-files'
83 def initialize_options (self):
89 self.egg_output = None
90 self.exclude_source_files = None
93 def finalize_options(self):
94 ei_cmd = self.ei_cmd = self.get_finalized_command("egg_info")
95 self.egg_info = ei_cmd.egg_info
97 if self.bdist_dir is None:
98 bdist_base = self.get_finalized_command('bdist').bdist_base
99 self.bdist_dir = os.path.join(bdist_base, 'egg')
101 if self.plat_name is None:
102 self.plat_name = get_build_platform()
104 self.set_undefined_options('bdist',('dist_dir', 'dist_dir'))
106 if self.egg_output is None:
108 # Compute filename of the output egg
109 basename = Distribution(
110 None, None, ei_cmd.egg_name, ei_cmd.egg_version,
111 get_python_version(),
112 self.distribution.has_ext_modules() and self.plat_name
115 self.egg_output = os.path.join(self.dist_dir, basename+'.egg')
124 def do_install_data(self):
125 # Hack for packages that install data to install's --install-lib
126 self.get_finalized_command('install').install_lib = self.bdist_dir
128 site_packages = os.path.normcase(os.path.realpath(get_python_lib()))
129 old, self.distribution.data_files = self.distribution.data_files,[]
132 if isinstance(item,tuple) and len(item)==2:
133 if os.path.isabs(item[0]):
134 realpath = os.path.realpath(item[0])
135 normalized = os.path.normcase(realpath)
136 if normalized==site_packages or normalized.startswith(
139 item = realpath[len(site_packages)+1:], item[1]
140 # XXX else: raise ???
141 self.distribution.data_files.append(item)
144 log.info("installing package data to %s" % self.bdist_dir)
145 self.call_command('install_data', force=0, root=None)
147 self.distribution.data_files = old
150 def get_outputs(self):
151 return [self.egg_output]
154 def call_command(self,cmdname,**kw):
155 """Invoke reinitialized command `cmdname` with keyword args"""
156 for dirname in INSTALL_DIRECTORY_ATTRS:
157 kw.setdefault(dirname,self.bdist_dir)
158 kw.setdefault('skip_build',self.skip_build)
159 kw.setdefault('dry_run', self.dry_run)
160 cmd = self.reinitialize_command(cmdname, **kw)
161 self.run_command(cmdname)
166 # Generate metadata first
167 self.run_command("egg_info")
168 # We run install_lib before install_data, because some data hacks
169 # pull their data path from the install_lib command.
170 log.info("installing library code to %s" % self.bdist_dir)
171 instcmd = self.get_finalized_command('install')
172 old_root = instcmd.root; instcmd.root = None
173 if self.distribution.has_c_libraries() and not self.skip_build:
174 self.run_command('build_clib')
175 cmd = self.call_command('install_lib', warn_dir=0)
176 instcmd.root = old_root
178 all_outputs, ext_outputs = self.get_ext_outputs()
181 for (p,ext_name) in enumerate(ext_outputs):
182 filename,ext = os.path.splitext(ext_name)
183 pyfile = os.path.join(self.bdist_dir, strip_module(filename)+'.py')
184 self.stubs.append(pyfile)
185 log.info("creating stub loader for %s" % ext_name)
187 write_stub(os.path.basename(ext_name), pyfile)
188 to_compile.append(pyfile)
189 ext_outputs[p] = ext_name.replace(os.sep,'/')
191 to_compile.extend(self.make_init_files())
193 cmd.byte_compile(to_compile)
194 if self.distribution.data_files:
195 self.do_install_data()
197 # Make the EGG-INFO directory
198 archive_root = self.bdist_dir
199 egg_info = os.path.join(archive_root,'EGG-INFO')
200 self.mkpath(egg_info)
201 if self.distribution.scripts:
202 script_dir = os.path.join(egg_info, 'scripts')
203 log.info("installing scripts to %s" % script_dir)
204 self.call_command('install_scripts',install_dir=script_dir,no_ep=1)
206 self.copy_metadata_to(egg_info)
207 native_libs = os.path.join(egg_info, "native_libs.txt")
209 log.info("writing %s" % native_libs)
211 ensure_directory(native_libs)
212 libs_file = open(native_libs, 'wt')
213 libs_file.write('\n'.join(all_outputs))
214 libs_file.write('\n')
216 elif os.path.isfile(native_libs):
217 log.info("removing %s" % native_libs)
219 os.unlink(native_libs)
222 os.path.join(archive_root,'EGG-INFO'), self.zip_safe()
225 if os.path.exists(os.path.join(self.egg_info,'depends.txt')):
227 "WARNING: 'depends.txt' will not be used by setuptools 0.6!\n"
228 "Use the install_requires/extras_require setup() args instead."
231 if self.exclude_source_files:
235 make_zipfile(self.egg_output, archive_root, verbose=self.verbose,
236 dry_run=self.dry_run, mode=self.gen_header())
237 if not self.keep_temp:
238 remove_tree(self.bdist_dir, dry_run=self.dry_run)
240 # Add to 'Distribution.dist_files' so that the "upload" command works
241 getattr(self.distribution,'dist_files',[]).append(
242 ('bdist_egg',get_python_version(),self.egg_output))
247 def zap_pyfiles(self):
248 log.info("Removing .py files from temporary directory")
249 for base,dirs,files in walk_egg(self.bdist_dir):
251 if name.endswith('.py'):
252 path = os.path.join(base,name)
253 log.debug("Deleting %s", path)
257 safe = getattr(self.distribution,'zip_safe',None)
260 log.warn("zip_safe flag not set; analyzing archive contents...")
261 return analyze_egg(self.bdist_dir, self.stubs)
263 def make_init_files(self):
264 """Create missing package __init__ files"""
266 for base,dirs,files in walk_egg(self.bdist_dir):
267 if base==self.bdist_dir:
268 # don't put an __init__ in the root
271 if name.endswith('.py'):
272 if '__init__.py' not in files:
273 pkg = base[len(self.bdist_dir)+1:].replace(os.sep,'.')
274 if self.distribution.has_contents_for(pkg):
275 log.warn("Creating missing __init__.py for %s",pkg)
276 filename = os.path.join(base,'__init__.py')
278 f = open(filename,'w'); f.write(NS_PKG_STUB)
280 init_files.append(filename)
283 # not a package, don't traverse to subdirectories
288 def gen_header(self):
289 epm = EntryPoint.parse_map(self.distribution.entry_points or '')
290 ep = epm.get('setuptools.installation',{}).get('eggsecutable')
292 return 'w' # not an eggsecutable, do it the usual way.
294 if not ep.attrs or ep.extras:
295 raise DistutilsSetupError(
296 "eggsecutable entry point (%r) cannot have 'extras' "
297 "or refer to a module" % (ep,)
300 pyver = sys.version[:3]
302 full = '.'.join(ep.attrs)
304 basename = os.path.basename(self.egg_output)
308 'if [ `basename $0` = "%(basename)s" ]\n'
309 'then exec python%(pyver)s -c "'
310 "import sys, os; sys.path.insert(0, os.path.abspath('$0')); "
311 "from %(pkg)s import %(base)s; sys.exit(%(full)s())"
314 ' echo $0 is not the correct name for this egg file.\n'
315 ' echo Please rename it back to %(basename)s and try again.\n'
322 mkpath(os.path.dirname(self.egg_output), dry_run=self.dry_run)
323 f = open(self.egg_output, 'w')
329 def copy_metadata_to(self, target_dir):
330 prefix = os.path.join(self.egg_info,'')
331 for path in self.ei_cmd.filelist.files:
332 if path.startswith(prefix):
333 target = os.path.join(target_dir, path[len(prefix):])
334 ensure_directory(target)
335 self.copy_file(path, target)
337 def get_ext_outputs(self):
338 """Get a list of relative paths to C extensions in the output distro"""
343 paths = {self.bdist_dir:''}
344 for base, dirs, files in os.walk(self.bdist_dir):
345 for filename in files:
346 if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS:
347 all_outputs.append(paths[base]+filename)
348 for filename in dirs:
349 paths[os.path.join(base,filename)] = paths[base]+filename+'/'
351 if self.distribution.has_ext_modules():
352 build_cmd = self.get_finalized_command('build_ext')
353 for ext in build_cmd.extensions:
354 if isinstance(ext,Library):
356 fullname = build_cmd.get_ext_fullname(ext.name)
357 filename = build_cmd.get_ext_filename(fullname)
358 if not os.path.basename(filename).startswith('dl-'):
359 if os.path.exists(os.path.join(self.bdist_dir,filename)):
360 ext_outputs.append(filename)
362 return all_outputs, ext_outputs
365 NATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split())
370 def walk_egg(egg_dir):
371 """Walk an unpacked egg's contents, skipping the metadata directory"""
372 walker = os.walk(egg_dir)
373 base,dirs,files = walker.next()
374 if 'EGG-INFO' in dirs:
375 dirs.remove('EGG-INFO')
376 yield base,dirs,files
380 def analyze_egg(egg_dir, stubs):
381 # check for existing flag in EGG-INFO
382 for flag,fn in safety_flags.items():
383 if os.path.exists(os.path.join(egg_dir,'EGG-INFO',fn)):
385 if not can_scan(): return False
387 for base, dirs, files in walk_egg(egg_dir):
389 if name.endswith('.py') or name.endswith('.pyw'):
391 elif name.endswith('.pyc') or name.endswith('.pyo'):
392 # always scan, even if we already know we're not safe
393 safe = scan_module(egg_dir, base, name, stubs) and safe
396 def write_safety_flag(egg_dir, safe):
397 # Write or remove zip safety flag file(s)
398 for flag,fn in safety_flags.items():
399 fn = os.path.join(egg_dir, fn)
400 if os.path.exists(fn):
401 if safe is None or bool(safe)!=flag:
403 elif safe is not None and bool(safe)==flag:
404 f=open(fn,'wb'); f.write('\n'); f.close()
408 False: 'not-zip-safe',
411 def scan_module(egg_dir, base, name, stubs):
412 """Check whether module possibly uses unsafe-for-zipfile stuff"""
414 filename = os.path.join(base,name)
415 if filename[:-1] in stubs:
416 return True # Extension module
417 pkg = base[len(egg_dir)+1:].replace(os.sep,'.')
418 module = pkg+(pkg and '.' or '')+os.path.splitext(name)[0]
419 f = open(filename,'rb'); f.read(8) # skip magic & date
420 code = marshal.load(f); f.close()
422 symbols = dict.fromkeys(iter_symbols(code))
423 for bad in ['__file__', '__path__']:
425 log.warn("%s: module references %s", module, bad)
427 if 'inspect' in symbols:
429 'getsource', 'getabsfile', 'getsourcefile', 'getfile'
430 'getsourcelines', 'findsource', 'getcomments', 'getframeinfo',
431 'getinnerframes', 'getouterframes', 'stack', 'trace'
434 log.warn("%s: module MAY be using inspect.%s", module, bad)
436 if '__name__' in symbols and '__main__' in symbols and '.' not in module:
437 if sys.version[:3]=="2.4": # -m works w/zipfiles in 2.5
438 log.warn("%s: top-level module may be 'python -m' script", module)
442 def iter_symbols(code):
443 """Yield names and strings used by `code` and its nested code objects"""
444 for name in code.co_names: yield name
445 for const in code.co_consts:
446 if isinstance(const,basestring):
448 elif isinstance(const,CodeType):
449 for name in iter_symbols(const):
453 if not sys.platform.startswith('java') and sys.platform != 'cli':
454 # CPython, PyPy, etc.
456 log.warn("Unable to analyze compiled code on this platform.")
457 log.warn("Please ask the author to include a 'zip_safe'"
458 " setting (either True or False) in the package's setup.py")
493 # Attribute names of options for commands that might need to be convinced to
494 # install to the egg build directory
496 INSTALL_DIRECTORY_ATTRS = [
497 'install_lib', 'install_dir', 'install_data', 'install_base'
500 def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=None,
503 """Create a zip file from all the files under 'base_dir'. The output
504 zip file will be named 'base_dir' + ".zip". Uses either the "zipfile"
505 Python module (if available) or the InfoZIP "zip" utility (if installed
506 and found on the default search path). If neither tool is available,
507 raises DistutilsExecError. Returns the name of the output zip file.
510 mkpath(os.path.dirname(zip_filename), dry_run=dry_run)
511 log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir)
513 def visit(z, dirname, names):
515 path = os.path.normpath(os.path.join(dirname, name))
516 if os.path.isfile(path):
517 p = path[len(base_dir)+1:]
520 log.debug("adding '%s'" % p)
523 compress = (sys.version>="2.4") # avoid 2.3 zipimport bug when 64 bits
525 compression = [zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED][bool(compress)]
527 z = zipfile.ZipFile(zip_filename, mode, compression=compression)
528 os.path.walk(base_dir, visit, z)
531 os.path.walk(base_dir, visit, None)