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