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