build_py.py 12.9 KB
Newer Older
1 2 3 4 5 6
"""distutils.command.build_py

Implements the Distutils 'build_py' command."""

# created 1999/03/08, Greg Ward

7
__revision__ = "$Id$"
8

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

13 14 15 16
from distutils.core import Command
from distutils.errors import *


17
class build_py (Command):
18

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

21
    user_options = [
22
        ('build-lib=', 'd', "directory to \"build\" (copy) to"),
Greg Ward's avatar
Greg Ward committed
23
        ('force', 'f', "forcibly build everything (ignore file timestamps)"),
24
        ]
25

26 27
    boolean_options = ['force']

28

29
    def initialize_options (self):
30
        self.build_lib = None
31
        self.py_modules = None
32
        self.package = None
33
        self.package_dir = None
34
        self.force = None
35

36
    def finalize_options (self):
37 38 39
        self.set_undefined_options('build',
                                   ('build_lib', 'build_lib'),
                                   ('force', 'force'))
40 41 42 43

        # Get the distribution options that are aliases for build_py
        # options -- list of packages and list of modules.
        self.packages = self.distribution.packages
44
        self.py_modules = self.distribution.py_modules
45
        self.package_dir = self.distribution.package_dir
46 47 48 49


    def run (self):

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
        # 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).
65

66
        # Two options control which modules will be installed: 'packages'
67
        # and 'py_modules'.  The former lets us work with whole packages, not
68 69 70 71
        # specifying individual modules at all; the latter is for
        # specifying modules one-at-a-time.  Currently they are mutually
        # exclusive: you can define one or the other (or neither), but not
        # both.  It remains to be seen how limiting this is.
72

73 74
        # Dispose of the two "unusual" cases first: no pure Python modules
        # at all (no problem, just return silently), and over-specified
75
        # 'packages' and 'py_modules' options.
76

77
        if not self.py_modules and not self.packages:
78
            return
79
        if self.py_modules and self.packages:
80
            raise DistutilsOptionError, \
81
                  "build_py: supplying both 'packages' and 'py_modules' " + \
82
                  "options is not allowed"
83

84 85
        # Now we're down to two cases: 'py_modules' only and 'packages' only.
        if self.py_modules:
86
            self.build_modules()
87
        else:
88
            self.build_packages()
89 90

    # run ()
91
        
92 93 94 95 96 97

    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)."""

98
        path = string.split(package, '.')
99 100

        if not self.package_dir:
Greg Ward's avatar
Greg Ward committed
101
            if path:
102
                return apply(os.path.join, path)
Greg Ward's avatar
Greg Ward committed
103 104
            else:
                return ''
105 106 107 108
        else:
            tail = []
            while path:
                try:
109
                    pdir = self.package_dir[string.join(path, '.')]
110
                except KeyError:
111
                    tail.insert(0, path[-1])
112 113
                    del path[-1]
                else:
114 115
                    tail.insert(0, pdir)
                    return apply(os.path.join, tail)
116
            else:
117 118 119 120 121 122 123 124 125 126 127
                # 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
128
                if tail:
129
                    return apply(os.path.join, tail)
Greg Ward's avatar
Greg Ward committed
130 131
                else:
                    return ''
132 133 134 135 136 137 138 139 140 141 142

    # 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 != "":
143
            if not os.path.exists(package_dir):
144 145
                raise DistutilsFileError, \
                      "package directory '%s' does not exist" % package_dir
146
            if not os.path.isdir(package_dir):
Greg Ward's avatar
Greg Ward committed
147
                raise DistutilsFileError, \
148 149 150 151
                      ("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
152
        if package:
153 154
            init_py = os.path.join(package_dir, "__init__.py")
            if os.path.isfile(init_py):
155 156
                return init_py
            else:
157 158
                self.warn(("package init file '%s' not found " +
                           "(or not a regular file)") % init_py)
159 160 161 162 163

        # Either not in a package at all (__init__.py not expected), or
        # __init__.py doesn't exist -- so don't return the filename.
        return
                
164 165 166 167
    # check_package ()


    def check_module (self, module, module_file):
168 169 170
        if not os.path.isfile(module_file):
            self.warn("file %s (for module %s) not found" % 
                      (module_file, module))
171 172 173 174 175 176 177
            return 0
        else:
            return 1

    # check_module ()


178
    def find_package_modules (self, package, package_dir):
179 180
        self.check_package(package, package_dir)
        module_files = glob(os.path.join(package_dir, "*.py"))
181
        modules = []
182
        setup_script = os.path.abspath(self.distribution.script_name)
183

184
        for f in module_files:
185
            abs_f = os.path.abspath(f)
186
            if abs_f != setup_script:
187 188
                module = os.path.splitext(os.path.basename(f))[0]
                modules.append((package, module, f))
189 190
            else:
                self.debug_print("excluding %s" % setup_script)
191
        return modules
192 193


194
    def find_modules (self):
195
        """Finds individually-specified Python modules, ie. those listed by
196
        module name in 'self.py_modules'.  Returns a list of tuples (package,
197 198 199 200 201 202 203
        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.
        """

204 205 206 207 208 209 210 211
        # 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 = {}

212
        # List of (package, module, filename) tuples to return
213 214
        modules = []

215 216 217 218 219
        # 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

220
        for module in self.py_modules:
221
            path = string.split(module, '.')
222
            package = string.join(path[0:-1], '.')
223
            module_base = path[-1]
224 225 226 227

            try:
                (package_dir, checked) = packages[package]
            except KeyError:
228
                package_dir = self.get_package_dir(package)
229 230 231
                checked = 0

            if not checked:
232
                init_py = self.check_package(package, package_dir)
233
                packages[package] = (package_dir, 1)
234 235
                if init_py:
                    modules.append((package, "__init__", init_py))
236 237 238 239

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

244
            modules.append((package, module_base, module_file))
245 246 247 248 249 250

        return modules

    # find_modules ()


251 252
    def find_all_modules (self):
        """Compute the list of all modules that will be built, whether
253
        they are specified one-module-at-a-time ('self.py_modules') or
254 255 256
        by whole packages ('self.packages').  Return a list of tuples
        (package, module, module_file), just like 'find_modules()' and
        'find_package_modules()' do."""
257

258
        if self.py_modules:
259
            modules = self.find_modules()
260 261 262
        else:
            modules = []
            for package in self.packages:
263 264 265
                package_dir = self.get_package_dir(package)
                m = self.find_package_modules(package, package_dir)
                modules.extend(m)
266

267 268 269 270 271 272 273
        return modules

    # find_all_modules ()


    def get_source_files (self):

274
        modules = self.find_all_modules()
275 276
        filenames = []
        for module in modules:
277
            filenames.append(module[-1])
278

279
        return filenames
280 281


282 283
    def get_module_outfile (self, build_dir, package, module):
        outfile_path = [build_dir] + list(package) + [module + ".py"]
284
        return apply(os.path.join, outfile_path)
285 286 287


    def get_outputs (self):
288
        modules = self.find_all_modules()
289 290
        outputs = []
        for (package, module, module_file) in modules:
291 292 293
            package = string.split(package, '.')
            outputs.append(self.get_module_outfile(self.build_lib,
                                                   package, module))
294
        return outputs
295

296 297

    def build_module (self, module, module_file, package):
298 299 300
        if type(package) is StringType:
            package = string.split(package, '.')
        elif type(package) not in (ListType, TupleType):
Greg Ward's avatar
Greg Ward committed
301 302
            raise TypeError, \
                  "'package' must be a string (dot-separated), list, or tuple"
303 304

        # Now put the module source file into the "build" area -- this is
305
        # easy, we just copy it somewhere under self.build_lib (the build
306
        # directory for Python source).
307 308 309 310
        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)
311 312 313 314 315


    def build_modules (self):

        modules = self.find_modules()
316
        for (package, module, module_file) in modules:
317

318
            # Now "build" the module -- ie. copy the source file to
319
            # self.build_lib (the build directory for Python source).
320
            # (Actually, it gets copied to the directory for this package
321
            # under self.build_lib.)
322
            self.build_module(module, module_file, package)
323 324 325 326 327 328 329

    # build_modules ()


    def build_packages (self):

        for package in self.packages:
330 331 332 333 334 335 336 337 338 339

            # 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').
340 341
            package_dir = self.get_package_dir(package)
            modules = self.find_package_modules(package, package_dir)
342 343

            # Now loop over the modules we found, "building" each one (just
344
            # copy it to self.build_lib).
345 346
            for (package_, module, module_file) in modules:
                assert package == package_
347
                self.build_module(module, module_file, package)
348 349

    # build_packages ()
350
                       
351
# class build_py