""" Distutils convenience functionality. """ import sys, os from distutils import sysconfig from distutils.command import build_scripts, install_data, build_ext, build_py from distutils.errors import CompileError from distutils import core twisted_subprojects = ["conch", "flow", "lore", "mail", "names", "news", "pair", "runner", "web", "web2", "words"] def setup(**kw): """ An alternative to distutils' setup() which is specially designed for Twisted subprojects. Pass twisted_subproject=projname if you want package and data files to automatically be found for you. Pass detectExtensions=detecterFunction if your project has extension modules. detecterFunction will be called called with an instance of build_ext_twisted and should return a list of distutils Extensions. """ if 'twisted_subproject' in kw: if 'twisted' not in os.listdir('.'): raise RuntimeError("Sorry, you need to run setup.py from the " "toplevel source directory.") projname = kw['twisted_subproject'] projdir = os.path.join('twisted', projname) kw['packages'] = getPackages(projdir, parent='twisted') plugin = "twisted/plugins/twisted_" + projname + ".py" if os.path.exists(plugin): kw.setdefault('py_modules', []).append(plugin.replace("/", ".")[:-3]) kw['data_files'] = getDataFiles(projdir, parent='twisted') del kw['twisted_subproject'] else: if 'plugins' in kw: py_modules = [] for plg in kw['plugins']: py_modules.append("twisted.plugins." + plg) kw.setdefault('py_modules', []).extend(py_modules) del kw['plugins'] if 'cmdclass' not in kw: kw['cmdclass'] = { 'install_data': install_data_twisted, 'build_scripts': build_scripts_twisted} if sys.version_info[:3] < (2, 3, 0): kw['cmdclass']['build_py'] = build_py_twisted if 'detectExtensions' in kw: if 'ext_modules' not in kw: kw['ext_modules'] = [True] # distutils is so lame dE = kw['detectExtensions'] del kw['detectExtensions'] class my_build_ext(build_ext_twisted): detectExtensions = dE kw.setdefault('cmdclass', {})['build_ext'] = my_build_ext return core.setup(**kw) # Names that are exluded from globbing results: EXCLUDE_NAMES = ["{arch}", "CVS", ".cvsignore", "_darcs", "RCS", "SCCS", ".svn"] EXCLUDE_PATTERNS = ["*.py[cdo]", "*.s[ol]", ".#*", "*~", "*.py"] import fnmatch def _filterNames(names): """Given a list of file names, return those names that should be copied. """ names = [n for n in names if n not in EXCLUDE_NAMES] # This is needed when building a distro from a working # copy (likely a checkout) rather than a pristine export: for pattern in EXCLUDE_PATTERNS: names = [n for n in names if (not fnmatch.fnmatch(n, pattern)) and (not n.endswith('.py'))] return names def relativeTo(base, relativee): """ Gets 'relativee' relative to 'basepath'. i.e., >>> relativeTo('/home/', '/home/radix/') 'radix' >>> relativeTo('.', '/home/radix/Projects/Twisted') # curdir is /home/radix 'Projects/Twisted' The 'relativee' must be a child of 'basepath'. """ basepath = os.path.abspath(base) relativee = os.path.abspath(relativee) if relativee.startswith(basepath): relative = relativee[len(basepath):] if relative.startswith(os.sep): relative = relative[1:] return os.path.join(base, relative) raise ValueError("%s is not a subpath of %s" % (relativee, basepath)) def getDataFiles(dname, ignore=None, parent=None): """ Get all the data files that should be included in this distutils Project. 'dname' should be the path to the package that you're distributing. 'ignore' is a list of sub-packages to ignore. This facilitates disparate package hierarchies. That's a fancy way of saying that the 'twisted' package doesn't want to include the 'twisted.conch' package, so it will pass ['conch'] as the value. 'parent' is necessary if you're distributing a subpackage like twisted.conch. 'dname' should point to 'twisted/conch' and 'parent' should point to 'twisted'. This ensures that your data_files are generated correctly, only using relative paths for the first element of the tuple ('twisted/conch/*'). The default 'parent' is the current working directory. """ parent = parent or "." ignore = ignore or [] result = [] for directory, subdirectories, filenames in os.walk(dname): resultfiles = [] for exname in EXCLUDE_NAMES: if exname in subdirectories: subdirectories.remove(exname) for ig in ignore: if ig in subdirectories: subdirectories.remove(ig) for filename in _filterNames(filenames): resultfiles.append(filename) if resultfiles: result.append((relativeTo(parent, directory), [relativeTo(parent, os.path.join(directory, filename)) for filename in resultfiles])) return result def getPackages(dname, pkgname=None, results=None, ignore=None, parent=None): """ Get all packages which are under dname. This is necessary for Python 2.2's distutils. Pretty similar arguments to getDataFiles, including 'parent'. """ parent = parent or "" prefix = [] if parent: prefix = [parent] bname = os.path.basename(dname) ignore = ignore or [] if bname in ignore: return [] if results is None: results = [] if pkgname is None: pkgname = [] subfiles = os.listdir(dname) abssubfiles = [os.path.join(dname, x) for x in subfiles] if '__init__.py' in subfiles: results.append(prefix + pkgname + [bname]) for subdir in filter(os.path.isdir, abssubfiles): getPackages(subdir, pkgname=pkgname + [bname], results=results, ignore=ignore, parent=parent) res = ['.'.join(result) for result in results] return res def getScripts(projname): """ Returns a list of scripts for a Twisted subproject; this works in any of an SVN checkout, a project-specific tarball, or the Sumo tarball. """ scriptdir = os.path.join('bin', projname) if not os.path.isdir(scriptdir): # Probably a project-specific tarball, in which case only this # project's bins are included in 'bin' scriptdir = 'bin' thingies = os.listdir(scriptdir) if '.svn' in thingies: thingies.remove('.svn') return [os.path.join(scriptdir, x) for x in thingies] ## Helpers and distutil tweaks class build_py_twisted(build_py.build_py): """ Changes behavior in Python 2.2 to support simultaneous specification of `packages' and `py_modules'. """ def run(self): if self.py_modules: self.build_modules() if self.packages: self.build_packages() self.byte_compile(self.get_outputs(include_bytecode=0)) class build_scripts_twisted(build_scripts.build_scripts): """Renames scripts so they end with '.py' on Windows.""" def run(self): build_scripts.build_scripts.run(self) if not os.name == "nt": return for f in os.listdir(self.build_dir): fpath=os.path.join(self.build_dir, f) if not fpath.endswith(".py"): try: os.unlink(fpath + ".py") except EnvironmentError, e: if e.args[1]=='No such file or directory': pass os.rename(fpath, fpath + ".py") class install_data_twisted(install_data.install_data): """I make sure data files are installed in the package directory.""" def finalize_options(self): self.set_undefined_options('install', ('install_lib', 'install_dir') ) install_data.install_data.finalize_options(self) class build_ext_twisted(build_ext.build_ext): """ Allow subclasses to easily detect and customize Extensions to build at install-time. """ def build_extensions(self): """ Override the build_ext build_extensions method to call our module detection function before it tries to build the extensions. """ # always define WIN32 under Windows if os.name == 'nt': self.define_macros = [("WIN32", 1)] else: self.define_macros = [] self.extensions = self.detectExtensions() or [] build_ext.build_ext.build_extensions(self) def _remove_conftest(self): for filename in ("conftest.c", "conftest.o", "conftest.obj"): try: os.unlink(filename) except EnvironmentError: pass def _compile_helper(self, content): conftest = open("conftest.c", "w") try: conftest.write(content) conftest.close() try: self.compiler.compile(["conftest.c"], output_dir='') except CompileError: return False return True finally: self._remove_conftest() def _check_header(self, header_name): """ Check if the given header can be included by trying to compile a file that contains only an #include line. """ self.compiler.announce("checking for %s ..." % header_name, 0) return self._compile_helper("#include <%s>\n" % header_name)