build_py.py 16.1 KB
Newer Older
1 2 3 4
"""distutils.command.build_py

Implements the Distutils 'build_py' command."""

5
# This module should be kept compatible with Python 2.1.
6

7
__revision__ = "$Id$"
8

9
import sys, os
10 11 12
from types import *
from glob import glob

13 14
from distutils.core import Command
from distutils.errors import *
15
from distutils.util import convert_path
16
from distutils import log
17

18
class build_py (Command):
19

20 21
    description = "\"build\" pure Python modules (copy to build directory)"

22
    user_options = [
23
        ('build-lib=', 'd', "directory to \"build\" (copy) to"),
24 25 26 27 28
        ('compile', 'c', "compile .py to .pyc"),
        ('no-compile', None, "don't compile .py files [default]"),
        ('optimize=', 'O',
         "also compile with optimization: -O1 for \"python -O\", "
         "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"),
Greg Ward's avatar
Greg Ward committed
29
        ('force', 'f', "forcibly build everything (ignore file timestamps)"),
30
        ]
31

32 33
    boolean_options = ['compile', 'force']
    negative_opt = {'no-compile' : 'compile'}
34

35

36
    def initialize_options (self):
37
        self.build_lib = None
38
        self.py_modules = None
39
        self.package = None
40
        self.package_data = None
41
        self.package_dir = None
42 43
        self.compile = 0
        self.optimize = 0
44
        self.force = None
45

46
    def finalize_options (self):
47 48 49
        self.set_undefined_options('build',
                                   ('build_lib', 'build_lib'),
                                   ('force', 'force'))
50 51 52 53

        # Get the distribution options that are aliases for build_py
        # options -- list of packages and list of modules.
        self.packages = self.distribution.packages
54
        self.py_modules = self.distribution.py_modules
55
        self.package_data = self.distribution.package_data
56 57 58 59
        self.package_dir = {}
        if self.distribution.package_dir:
            for name, path in self.distribution.package_dir.items():
                self.package_dir[name] = convert_path(path)
60
        self.data_files = self.get_data_files()
61

62 63 64 65 66 67 68 69
        # Ick, copied straight from install_lib.py (fancy_getopt needs a
        # type system!  Hell, *everything* needs a type system!!!)
        if type(self.optimize) is not IntType:
            try:
                self.optimize = int(self.optimize)
                assert 0 <= self.optimize <= 2
            except (ValueError, AssertionError):
                raise DistutilsOptionError, "optimize must be 0, 1, or 2"
70 71 72

    def run (self):

73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
        # XXX copy_file by default preserves atime and mtime.  IMHO this is
        # the right thing to do, but perhaps it should be an option -- in
        # particular, a site administrator might want installed files to
        # reflect the time of installation rather than the last
        # modification time before the installed release.

        # XXX copy_file by default preserves mode, which appears to be the
        # wrong thing to do: if a file is read-only in the working
        # directory, we want it to be installed read/write so that the next
        # installation of the same module distribution can overwrite it
        # without problems.  (This might be a Unix-specific issue.)  Thus
        # we turn off 'preserve_mode' when copying to the build directory,
        # since the build directory is supposed to be exactly what the
        # installation will look like (ie. we preserve mode when
        # installing).
88

89
        # Two options control which modules will be installed: 'packages'
90
        # and 'py_modules'.  The former lets us work with whole packages, not
91
        # specifying individual modules at all; the latter is for
92 93
        # specifying modules one-at-a-time.

94
        if self.py_modules:
95
            self.build_modules()
96
        if self.packages:
97
            self.build_packages()
98
            self.build_package_data()
99

100 101
        self.byte_compile(self.get_outputs(include_bytecode=0))

102
    # run ()
Fred Drake's avatar
Fred Drake committed
103

104 105 106
    def get_data_files (self):
        """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
        data = []
107 108
        if not self.packages:
            return data
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
        for package in self.packages:
            # Locate package source directory
            src_dir = self.get_package_dir(package)

            # Compute package build directory
            build_dir = os.path.join(*([self.build_lib] + package.split('.')))

            # Length of path to strip from found files
            plen = len(src_dir)+1

            # Strip directory from globbed filenames
            filenames = [
                file[plen:] for file in self.find_data_files(package, src_dir)
                ]
            data.append((package, src_dir, build_dir, filenames))
        return data

    def find_data_files (self, package, src_dir):
        """Return filenames for package's data files in 'src_dir'"""
        globs = (self.package_data.get('', [])
                 + self.package_data.get(package, []))
        files = []
        for pattern in globs:
            # Each pattern has to be converted to a platform-specific path
            filelist = glob(os.path.join(src_dir, convert_path(pattern)))
            # Files that match more than one pattern are only added once
            files.extend([fn for fn in filelist if fn not in files])
        return files

    def build_package_data (self):
        """Copy data files into build directory"""
        lastdir = None
        for package, src_dir, build_dir, filenames in self.data_files:
            for filename in filenames:
                target = os.path.join(build_dir, filename)
                self.mkpath(os.path.dirname(target))
                self.copy_file(os.path.join(src_dir, filename), target,
                               preserve_mode=False)
147 148 149 150 151 152

    def get_package_dir (self, package):
        """Return the directory, relative to the top of the source
           distribution, where package 'package' should be found
           (at least according to the 'package_dir' option, if any)."""

153
        path = package.split('.')
154 155

        if not self.package_dir:
Greg Ward's avatar
Greg Ward committed
156
            if path:
Neal Norwitz's avatar
Neal Norwitz committed
157
                return os.path.join(*path)
Greg Ward's avatar
Greg Ward committed
158 159
            else:
                return ''
160 161 162 163
        else:
            tail = []
            while path:
                try:
164
                    pdir = self.package_dir['.'.join(path)]
165
                except KeyError:
166
                    tail.insert(0, path[-1])
167 168
                    del path[-1]
                else:
169
                    tail.insert(0, pdir)
Neal Norwitz's avatar
Neal Norwitz committed
170
                    return os.path.join(*tail)
171
            else:
172 173 174 175 176 177 178 179 180 181 182
                # Oops, got all the way through 'path' without finding a
                # match in package_dir.  If package_dir defines a directory
                # for the root (nameless) package, then fallback on it;
                # otherwise, we might as well have not consulted
                # package_dir at all, as we just use the directory implied
                # by 'tail' (which should be the same as the original value
                # of 'path' at this point).
                pdir = self.package_dir.get('')
                if pdir is not None:
                    tail.insert(0, pdir)

Greg Ward's avatar
Greg Ward committed
183
                if tail:
Neal Norwitz's avatar
Neal Norwitz committed
184
                    return os.path.join(*tail)
Greg Ward's avatar
Greg Ward committed
185 186
                else:
                    return ''
187 188 189 190 191 192 193 194 195 196 197

    # get_package_dir ()


    def check_package (self, package, package_dir):

        # Empty dir name means current directory, which we can probably
        # assume exists.  Also, os.path.exists and isdir don't know about
        # my "empty string means current dir" convention, so we have to
        # circumvent them.
        if package_dir != "":
198
            if not os.path.exists(package_dir):
199 200
                raise DistutilsFileError, \
                      "package directory '%s' does not exist" % package_dir
201
            if not os.path.isdir(package_dir):
Greg Ward's avatar
Greg Ward committed
202
                raise DistutilsFileError, \
203 204 205 206
                      ("supposed package directory '%s' exists, " +
                       "but is not a directory") % package_dir

        # Require __init__.py for all but the "root package"
Greg Ward's avatar
Greg Ward committed
207
        if package:
208 209
            init_py = os.path.join(package_dir, "__init__.py")
            if os.path.isfile(init_py):
210 211
                return init_py
            else:
212 213
                log.warn(("package init file '%s' not found " +
                          "(or not a regular file)"), init_py)
214 215 216

        # Either not in a package at all (__init__.py not expected), or
        # __init__.py doesn't exist -- so don't return the filename.
217
        return None
Fred Drake's avatar
Fred Drake committed
218

219 220 221 222
    # check_package ()


    def check_module (self, module, module_file):
223
        if not os.path.isfile(module_file):
224
            log.warn("file %s (for module %s) not found", module_file, module)
225
            return 0
226
        else:
227
            return 1
228 229 230 231

    # check_module ()


232
    def find_package_modules (self, package, package_dir):
233 234
        self.check_package(package, package_dir)
        module_files = glob(os.path.join(package_dir, "*.py"))
235
        modules = []
236
        setup_script = os.path.abspath(self.distribution.script_name)
237

238
        for f in module_files:
239
            abs_f = os.path.abspath(f)
240
            if abs_f != setup_script:
241 242
                module = os.path.splitext(os.path.basename(f))[0]
                modules.append((package, module, f))
243 244
            else:
                self.debug_print("excluding %s" % setup_script)
245
        return modules
246 247


248
    def find_modules (self):
249
        """Finds individually-specified Python modules, ie. those listed by
250
        module name in 'self.py_modules'.  Returns a list of tuples (package,
251 252 253 254 255 256 257
        module_base, filename): 'package' is a tuple of the path through
        package-space to the module; 'module_base' is the bare (no
        packages, no dots) module name, and 'filename' is the path to the
        ".py" file (relative to the distribution root) that implements the
        module.
        """

258 259 260 261 262 263 264 265
        # Map package names to tuples of useful info about the package:
        #    (package_dir, checked)
        # package_dir - the directory where we'll find source files for
        #   this package
        # checked - true if we have checked that the package directory
        #   is valid (exists, contains __init__.py, ... ?)
        packages = {}

266
        # List of (package, module, filename) tuples to return
267 268
        modules = []

269 270 271 272 273
        # We treat modules-in-packages almost the same as toplevel modules,
        # just the "package" for a toplevel is empty (either an empty
        # string or empty list, depending on context).  Differences:
        #   - don't check for __init__.py in directory for empty package

274
        for module in self.py_modules:
275 276
            path = module.split('.')
            package = '.'.join(path[0:-1])
277
            module_base = path[-1]
278 279 280 281

            try:
                (package_dir, checked) = packages[package]
            except KeyError:
282
                package_dir = self.get_package_dir(package)
283 284 285
                checked = 0

            if not checked:
286
                init_py = self.check_package(package, package_dir)
287
                packages[package] = (package_dir, 1)
288 289
                if init_py:
                    modules.append((package, "__init__", init_py))
290 291 292 293

            # XXX perhaps we should also check for just .pyc files
            # (so greedy closed-source bastards can distribute Python
            # modules too)
294 295
            module_file = os.path.join(package_dir, module_base + ".py")
            if not self.check_module(module, module_file):
296 297
                continue

298
            modules.append((package, module_base, module_file))
299 300 301 302 303 304

        return modules

    # find_modules ()


305 306
    def find_all_modules (self):
        """Compute the list of all modules that will be built, whether
307
        they are specified one-module-at-a-time ('self.py_modules') or
308 309 310
        by whole packages ('self.packages').  Return a list of tuples
        (package, module, module_file), just like 'find_modules()' and
        'find_package_modules()' do."""
311

312
        modules = []
313
        if self.py_modules:
314 315
            modules.extend(self.find_modules())
        if self.packages:
316
            for package in self.packages:
317 318 319
                package_dir = self.get_package_dir(package)
                m = self.find_package_modules(package, package_dir)
                modules.extend(m)
320

321 322 323 324 325 326 327
        return modules

    # find_all_modules ()


    def get_source_files (self):

328
        modules = self.find_all_modules()
329 330
        filenames = []
        for module in modules:
331
            filenames.append(module[-1])
332

333
        return filenames
334 335


336 337
    def get_module_outfile (self, build_dir, package, module):
        outfile_path = [build_dir] + list(package) + [module + ".py"]
Neal Norwitz's avatar
Neal Norwitz committed
338
        return os.path.join(*outfile_path)
339 340


341
    def get_outputs (self, include_bytecode=1):
342
        modules = self.find_all_modules()
343 344
        outputs = []
        for (package, module, module_file) in modules:
345
            package = package.split('.')
346 347 348 349 350 351 352 353
            filename = self.get_module_outfile(self.build_lib, package, module)
            outputs.append(filename)
            if include_bytecode:
                if self.compile:
                    outputs.append(filename + "c")
                if self.optimize > 0:
                    outputs.append(filename + "o")

354 355 356 357 358 359
        outputs += [
            os.path.join(build_dir, filename)
            for package, src_dir, build_dir, filenames in self.data_files
            for filename in filenames
            ]

360
        return outputs
361

362 363

    def build_module (self, module, module_file, package):
364
        if type(package) is StringType:
365
            package = package.split('.')
366
        elif type(package) not in (ListType, TupleType):
Greg Ward's avatar
Greg Ward committed
367 368
            raise TypeError, \
                  "'package' must be a string (dot-separated), list, or tuple"
369 370

        # Now put the module source file into the "build" area -- this is
371
        # easy, we just copy it somewhere under self.build_lib (the build
372
        # directory for Python source).
373 374 375 376
        outfile = self.get_module_outfile(self.build_lib, package, module)
        dir = os.path.dirname(outfile)
        self.mkpath(dir)
        return self.copy_file(module_file, outfile, preserve_mode=0)
377 378 379 380 381


    def build_modules (self):

        modules = self.find_modules()
382
        for (package, module, module_file) in modules:
383

384
            # Now "build" the module -- ie. copy the source file to
385
            # self.build_lib (the build directory for Python source).
386
            # (Actually, it gets copied to the directory for this package
387
            # under self.build_lib.)
388
            self.build_module(module, module_file, package)
389 390 391 392 393 394 395

    # build_modules ()


    def build_packages (self):

        for package in self.packages:
396 397 398 399 400 401 402 403 404 405

            # Get list of (package, module, module_file) tuples based on
            # scanning the package directory.  'package' is only included
            # in the tuple so that 'find_modules()' and
            # 'find_package_tuples()' have a consistent interface; it's
            # ignored here (apart from a sanity check).  Also, 'module' is
            # the *unqualified* module name (ie. no dots, no package -- we
            # already know its package!), and 'module_file' is the path to
            # the .py file, relative to the current directory
            # (ie. including 'package_dir').
406 407
            package_dir = self.get_package_dir(package)
            modules = self.find_package_modules(package, package_dir)
408 409

            # Now loop over the modules we found, "building" each one (just
410
            # copy it to self.build_lib).
411 412
            for (package_, module, module_file) in modules:
                assert package == package_
413
                self.build_module(module, module_file, package)
414 415

    # build_packages ()
416 417 418 419 420 421 422 423 424 425 426 427 428 429


    def byte_compile (self, files):
        from distutils.util import byte_compile
        prefix = self.build_lib
        if prefix[-1] != os.sep:
            prefix = prefix + os.sep

        # XXX this code is essentially the same as the 'byte_compile()
        # method of the "install_lib" command, except for the determination
        # of the 'prefix' string.  Hmmm.

        if self.compile:
            byte_compile(files, optimize=0,
430
                         force=self.force, prefix=prefix, dry_run=self.dry_run)
431 432
        if self.optimize > 0:
            byte_compile(files, optimize=self.optimize,
433
                         force=self.force, prefix=prefix, dry_run=self.dry_run)
434

435
# class build_py