Home | History | Annotate | Download | only in command
      1 """distutils.command.build_py
      2 
      3 Implements the Distutils 'build_py' command."""
      4 
      5 __revision__ = "$Id$"
      6 
      7 import os
      8 import sys
      9 from glob import glob
     10 
     11 from distutils.core import Command
     12 from distutils.errors import DistutilsOptionError, DistutilsFileError
     13 from distutils.util import convert_path
     14 from distutils import log
     15 
     16 class build_py(Command):
     17 
     18     description = "\"build\" pure Python modules (copy to build directory)"
     19 
     20     user_options = [
     21         ('build-lib=', 'd', "directory to \"build\" (copy) to"),
     22         ('compile', 'c', "compile .py to .pyc"),
     23         ('no-compile', None, "don't compile .py files [default]"),
     24         ('optimize=', 'O',
     25          "also compile with optimization: -O1 for \"python -O\", "
     26          "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"),
     27         ('force', 'f', "forcibly build everything (ignore file timestamps)"),
     28         ]
     29 
     30     boolean_options = ['compile', 'force']
     31     negative_opt = {'no-compile' : 'compile'}
     32 
     33     def initialize_options(self):
     34         self.build_lib = None
     35         self.py_modules = None
     36         self.package = None
     37         self.package_data = None
     38         self.package_dir = None
     39         self.compile = 0
     40         self.optimize = 0
     41         self.force = None
     42 
     43     def finalize_options(self):
     44         self.set_undefined_options('build',
     45                                    ('build_lib', 'build_lib'),
     46                                    ('force', 'force'))
     47 
     48         # Get the distribution options that are aliases for build_py
     49         # options -- list of packages and list of modules.
     50         self.packages = self.distribution.packages
     51         self.py_modules = self.distribution.py_modules
     52         self.package_data = self.distribution.package_data
     53         self.package_dir = {}
     54         if self.distribution.package_dir:
     55             for name, path in self.distribution.package_dir.items():
     56                 self.package_dir[name] = convert_path(path)
     57         self.data_files = self.get_data_files()
     58 
     59         # Ick, copied straight from install_lib.py (fancy_getopt needs a
     60         # type system!  Hell, *everything* needs a type system!!!)
     61         if not isinstance(self.optimize, int):
     62             try:
     63                 self.optimize = int(self.optimize)
     64                 assert 0 <= self.optimize <= 2
     65             except (ValueError, AssertionError):
     66                 raise DistutilsOptionError("optimize must be 0, 1, or 2")
     67 
     68     def run(self):
     69         # XXX copy_file by default preserves atime and mtime.  IMHO this is
     70         # the right thing to do, but perhaps it should be an option -- in
     71         # particular, a site administrator might want installed files to
     72         # reflect the time of installation rather than the last
     73         # modification time before the installed release.
     74 
     75         # XXX copy_file by default preserves mode, which appears to be the
     76         # wrong thing to do: if a file is read-only in the working
     77         # directory, we want it to be installed read/write so that the next
     78         # installation of the same module distribution can overwrite it
     79         # without problems.  (This might be a Unix-specific issue.)  Thus
     80         # we turn off 'preserve_mode' when copying to the build directory,
     81         # since the build directory is supposed to be exactly what the
     82         # installation will look like (ie. we preserve mode when
     83         # installing).
     84 
     85         # Two options control which modules will be installed: 'packages'
     86         # and 'py_modules'.  The former lets us work with whole packages, not
     87         # specifying individual modules at all; the latter is for
     88         # specifying modules one-at-a-time.
     89 
     90         if self.py_modules:
     91             self.build_modules()
     92         if self.packages:
     93             self.build_packages()
     94             self.build_package_data()
     95 
     96         self.byte_compile(self.get_outputs(include_bytecode=0))
     97 
     98     def get_data_files(self):
     99         """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
    100         data = []
    101         if not self.packages:
    102             return data
    103         for package in self.packages:
    104             # Locate package source directory
    105             src_dir = self.get_package_dir(package)
    106 
    107             # Compute package build directory
    108             build_dir = os.path.join(*([self.build_lib] + package.split('.')))
    109 
    110             # Length of path to strip from found files
    111             plen = 0
    112             if src_dir:
    113                 plen = len(src_dir)+1
    114 
    115             # Strip directory from globbed filenames
    116             filenames = [
    117                 file[plen:] for file in self.find_data_files(package, src_dir)
    118                 ]
    119             data.append((package, src_dir, build_dir, filenames))
    120         return data
    121 
    122     def find_data_files(self, package, src_dir):
    123         """Return filenames for package's data files in 'src_dir'"""
    124         globs = (self.package_data.get('', [])
    125                  + self.package_data.get(package, []))
    126         files = []
    127         for pattern in globs:
    128             # Each pattern has to be converted to a platform-specific path
    129             filelist = glob(os.path.join(src_dir, convert_path(pattern)))
    130             # Files that match more than one pattern are only added once
    131             files.extend([fn for fn in filelist if fn not in files])
    132         return files
    133 
    134     def build_package_data(self):
    135         """Copy data files into build directory"""
    136         for package, src_dir, build_dir, filenames in self.data_files:
    137             for filename in filenames:
    138                 target = os.path.join(build_dir, filename)
    139                 self.mkpath(os.path.dirname(target))
    140                 self.copy_file(os.path.join(src_dir, filename), target,
    141                                preserve_mode=False)
    142 
    143     def get_package_dir(self, package):
    144         """Return the directory, relative to the top of the source
    145            distribution, where package 'package' should be found
    146            (at least according to the 'package_dir' option, if any)."""
    147 
    148         path = package.split('.')
    149 
    150         if not self.package_dir:
    151             if path:
    152                 return os.path.join(*path)
    153             else:
    154                 return ''
    155         else:
    156             tail = []
    157             while path:
    158                 try:
    159                     pdir = self.package_dir['.'.join(path)]
    160                 except KeyError:
    161                     tail.insert(0, path[-1])
    162                     del path[-1]
    163                 else:
    164                     tail.insert(0, pdir)
    165                     return os.path.join(*tail)
    166             else:
    167                 # Oops, got all the way through 'path' without finding a
    168                 # match in package_dir.  If package_dir defines a directory
    169                 # for the root (nameless) package, then fallback on it;
    170                 # otherwise, we might as well have not consulted
    171                 # package_dir at all, as we just use the directory implied
    172                 # by 'tail' (which should be the same as the original value
    173                 # of 'path' at this point).
    174                 pdir = self.package_dir.get('')
    175                 if pdir is not None:
    176                     tail.insert(0, pdir)
    177 
    178                 if tail:
    179                     return os.path.join(*tail)
    180                 else:
    181                     return ''
    182 
    183     def check_package(self, package, package_dir):
    184         # Empty dir name means current directory, which we can probably
    185         # assume exists.  Also, os.path.exists and isdir don't know about
    186         # my "empty string means current dir" convention, so we have to
    187         # circumvent them.
    188         if package_dir != "":
    189             if not os.path.exists(package_dir):
    190                 raise DistutilsFileError(
    191                       "package directory '%s' does not exist" % package_dir)
    192             if not os.path.isdir(package_dir):
    193                 raise DistutilsFileError(
    194                        "supposed package directory '%s' exists, "
    195                        "but is not a directory" % package_dir)
    196 
    197         # Require __init__.py for all but the "root package"
    198         if package:
    199             init_py = os.path.join(package_dir, "__init__.py")
    200             if os.path.isfile(init_py):
    201                 return init_py
    202             else:
    203                 log.warn(("package init file '%s' not found " +
    204                           "(or not a regular file)"), init_py)
    205 
    206         # Either not in a package at all (__init__.py not expected), or
    207         # __init__.py doesn't exist -- so don't return the filename.
    208         return None
    209 
    210     def check_module(self, module, module_file):
    211         if not os.path.isfile(module_file):
    212             log.warn("file %s (for module %s) not found", module_file, module)
    213             return False
    214         else:
    215             return True
    216 
    217     def find_package_modules(self, package, package_dir):
    218         self.check_package(package, package_dir)
    219         module_files = glob(os.path.join(package_dir, "*.py"))
    220         modules = []
    221         setup_script = os.path.abspath(self.distribution.script_name)
    222 
    223         for f in module_files:
    224             abs_f = os.path.abspath(f)
    225             if abs_f != setup_script:
    226                 module = os.path.splitext(os.path.basename(f))[0]
    227                 modules.append((package, module, f))
    228             else:
    229                 self.debug_print("excluding %s" % setup_script)
    230         return modules
    231 
    232     def find_modules(self):
    233         """Finds individually-specified Python modules, ie. those listed by
    234         module name in 'self.py_modules'.  Returns a list of tuples (package,
    235         module_base, filename): 'package' is a tuple of the path through
    236         package-space to the module; 'module_base' is the bare (no
    237         packages, no dots) module name, and 'filename' is the path to the
    238         ".py" file (relative to the distribution root) that implements the
    239         module.
    240         """
    241         # Map package names to tuples of useful info about the package:
    242         #    (package_dir, checked)
    243         # package_dir - the directory where we'll find source files for
    244         #   this package
    245         # checked - true if we have checked that the package directory
    246         #   is valid (exists, contains __init__.py, ... ?)
    247         packages = {}
    248 
    249         # List of (package, module, filename) tuples to return
    250         modules = []
    251 
    252         # We treat modules-in-packages almost the same as toplevel modules,
    253         # just the "package" for a toplevel is empty (either an empty
    254         # string or empty list, depending on context).  Differences:
    255         #   - don't check for __init__.py in directory for empty package
    256         for module in self.py_modules:
    257             path = module.split('.')
    258             package = '.'.join(path[0:-1])
    259             module_base = path[-1]
    260 
    261             try:
    262                 (package_dir, checked) = packages[package]
    263             except KeyError:
    264                 package_dir = self.get_package_dir(package)
    265                 checked = 0
    266 
    267             if not checked:
    268                 init_py = self.check_package(package, package_dir)
    269                 packages[package] = (package_dir, 1)
    270                 if init_py:
    271                     modules.append((package, "__init__", init_py))
    272 
    273             # XXX perhaps we should also check for just .pyc files
    274             # (so greedy closed-source bastards can distribute Python
    275             # modules too)
    276             module_file = os.path.join(package_dir, module_base + ".py")
    277             if not self.check_module(module, module_file):
    278                 continue
    279 
    280             modules.append((package, module_base, module_file))
    281 
    282         return modules
    283 
    284     def find_all_modules(self):
    285         """Compute the list of all modules that will be built, whether
    286         they are specified one-module-at-a-time ('self.py_modules') or
    287         by whole packages ('self.packages').  Return a list of tuples
    288         (package, module, module_file), just like 'find_modules()' and
    289         'find_package_modules()' do."""
    290         modules = []
    291         if self.py_modules:
    292             modules.extend(self.find_modules())
    293         if self.packages:
    294             for package in self.packages:
    295                 package_dir = self.get_package_dir(package)
    296                 m = self.find_package_modules(package, package_dir)
    297                 modules.extend(m)
    298         return modules
    299 
    300     def get_source_files(self):
    301         return [module[-1] for module in self.find_all_modules()]
    302 
    303     def get_module_outfile(self, build_dir, package, module):
    304         outfile_path = [build_dir] + list(package) + [module + ".py"]
    305         return os.path.join(*outfile_path)
    306 
    307     def get_outputs(self, include_bytecode=1):
    308         modules = self.find_all_modules()
    309         outputs = []
    310         for (package, module, module_file) in modules:
    311             package = package.split('.')
    312             filename = self.get_module_outfile(self.build_lib, package, module)
    313             outputs.append(filename)
    314             if include_bytecode:
    315                 if self.compile:
    316                     outputs.append(filename + "c")
    317                 if self.optimize > 0:
    318                     outputs.append(filename + "o")
    319 
    320         outputs += [
    321             os.path.join(build_dir, filename)
    322             for package, src_dir, build_dir, filenames in self.data_files
    323             for filename in filenames
    324             ]
    325 
    326         return outputs
    327 
    328     def build_module(self, module, module_file, package):
    329         if isinstance(package, str):
    330             package = package.split('.')
    331         elif not isinstance(package, (list, tuple)):
    332             raise TypeError(
    333                   "'package' must be a string (dot-separated), list, or tuple")
    334 
    335         # Now put the module source file into the "build" area -- this is
    336         # easy, we just copy it somewhere under self.build_lib (the build
    337         # directory for Python source).
    338         outfile = self.get_module_outfile(self.build_lib, package, module)
    339         dir = os.path.dirname(outfile)
    340         self.mkpath(dir)
    341         return self.copy_file(module_file, outfile, preserve_mode=0)
    342 
    343     def build_modules(self):
    344         modules = self.find_modules()
    345         for (package, module, module_file) in modules:
    346 
    347             # Now "build" the module -- ie. copy the source file to
    348             # self.build_lib (the build directory for Python source).
    349             # (Actually, it gets copied to the directory for this package
    350             # under self.build_lib.)
    351             self.build_module(module, module_file, package)
    352 
    353     def build_packages(self):
    354         for package in self.packages:
    355 
    356             # Get list of (package, module, module_file) tuples based on
    357             # scanning the package directory.  'package' is only included
    358             # in the tuple so that 'find_modules()' and
    359             # 'find_package_tuples()' have a consistent interface; it's
    360             # ignored here (apart from a sanity check).  Also, 'module' is
    361             # the *unqualified* module name (ie. no dots, no package -- we
    362             # already know its package!), and 'module_file' is the path to
    363             # the .py file, relative to the current directory
    364             # (ie. including 'package_dir').
    365             package_dir = self.get_package_dir(package)
    366             modules = self.find_package_modules(package, package_dir)
    367 
    368             # Now loop over the modules we found, "building" each one (just
    369             # copy it to self.build_lib).
    370             for (package_, module, module_file) in modules:
    371                 assert package == package_
    372                 self.build_module(module, module_file, package)
    373 
    374     def byte_compile(self, files):
    375         if sys.dont_write_bytecode:
    376             self.warn('byte-compiling is disabled, skipping.')
    377             return
    378 
    379         from distutils.util import byte_compile
    380         prefix = self.build_lib
    381         if prefix[-1] != os.sep:
    382             prefix = prefix + os.sep
    383 
    384         # XXX this code is essentially the same as the 'byte_compile()
    385         # method of the "install_lib" command, except for the determination
    386         # of the 'prefix' string.  Hmmm.
    387 
    388         if self.compile:
    389             byte_compile(files, optimize=0,
    390                          force=self.force, prefix=prefix, dry_run=self.dry_run)
    391         if self.optimize > 0:
    392             byte_compile(files, optimize=self.optimize,
    393                          force=self.force, prefix=prefix, dry_run=self.dry_run)
    394