build_py.py revision 9d72bb452bced3a100f07f8a9e30c4495a9ec41a
1"""distutils.command.build_py
2
3Implements the Distutils 'build_py' command."""
4
5# This module should be kept compatible with Python 2.1.
6
7__revision__ = "$Id$"
8
9import sys, 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_data = None
41        self.package_dir = None
42        self.compile = 0
43        self.optimize = 0
44        self.force = None
45
46    def finalize_options (self):
47        self.set_undefined_options('build',
48                                   ('build_lib', 'build_lib'),
49                                   ('force', 'force'))
50
51        # Get the distribution options that are aliases for build_py
52        # options -- list of packages and list of modules.
53        self.packages = self.distribution.packages
54        self.py_modules = self.distribution.py_modules
55        self.package_data = self.distribution.package_data
56        self.package_dir = {}
57        if self.distribution.package_dir:
58            for name, path in self.distribution.package_dir.items():
59                self.package_dir[name] = convert_path(path)
60        self.data_files = self.get_data_files()
61
62        # Ick, copied straight from install_lib.py (fancy_getopt needs a
63        # type system!  Hell, *everything* needs a type system!!!)
64        if type(self.optimize) is not IntType:
65            try:
66                self.optimize = int(self.optimize)
67                assert 0 <= self.optimize <= 2
68            except (ValueError, AssertionError):
69                raise DistutilsOptionError, "optimize must be 0, 1, or 2"
70
71    def run (self):
72
73        # XXX copy_file by default preserves atime and mtime.  IMHO this is
74        # the right thing to do, but perhaps it should be an option -- in
75        # particular, a site administrator might want installed files to
76        # reflect the time of installation rather than the last
77        # modification time before the installed release.
78
79        # XXX copy_file by default preserves mode, which appears to be the
80        # wrong thing to do: if a file is read-only in the working
81        # directory, we want it to be installed read/write so that the next
82        # installation of the same module distribution can overwrite it
83        # without problems.  (This might be a Unix-specific issue.)  Thus
84        # we turn off 'preserve_mode' when copying to the build directory,
85        # since the build directory is supposed to be exactly what the
86        # installation will look like (ie. we preserve mode when
87        # 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        # specifying modules one-at-a-time.
93
94        if self.py_modules:
95            self.build_modules()
96        if self.packages:
97            self.build_packages()
98            self.build_package_data()
99
100        self.byte_compile(self.get_outputs(include_bytecode=0))
101
102    # run ()
103
104    def get_data_files (self):
105        """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
106        data = []
107        if not self.packages:
108            return data
109        for package in self.packages:
110            # Locate package source directory
111            src_dir = self.get_package_dir(package)
112
113            # Compute package build directory
114            build_dir = os.path.join(*([self.build_lib] + package.split('.')))
115
116            # Length of path to strip from found files
117            plen = len(src_dir)+1
118
119            # Strip directory from globbed filenames
120            filenames = [
121                file[plen:] for file in self.find_data_files(package, src_dir)
122                ]
123            data.append((package, src_dir, build_dir, filenames))
124        return data
125
126    def find_data_files (self, package, src_dir):
127        """Return filenames for package's data files in 'src_dir'"""
128        globs = (self.package_data.get('', [])
129                 + self.package_data.get(package, []))
130        files = []
131        for pattern in globs:
132            # Each pattern has to be converted to a platform-specific path
133            filelist = glob(os.path.join(src_dir, convert_path(pattern)))
134            # Files that match more than one pattern are only added once
135            files.extend([fn for fn in filelist if fn not in files])
136        return files
137
138    def build_package_data (self):
139        """Copy data files into build directory"""
140        lastdir = None
141        for package, src_dir, build_dir, filenames in self.data_files:
142            for filename in filenames:
143                target = os.path.join(build_dir, filename)
144                self.mkpath(os.path.dirname(target))
145                self.copy_file(os.path.join(src_dir, filename), target,
146                               preserve_mode=False)
147
148    def get_package_dir (self, package):
149        """Return the directory, relative to the top of the source
150           distribution, where package 'package' should be found
151           (at least according to the 'package_dir' option, if any)."""
152
153        path = package.split('.')
154
155        if not self.package_dir:
156            if path:
157                return os.path.join(*path)
158            else:
159                return ''
160        else:
161            tail = []
162            while path:
163                try:
164                    pdir = self.package_dir['.'.join(path)]
165                except KeyError:
166                    tail.insert(0, path[-1])
167                    del path[-1]
168                else:
169                    tail.insert(0, pdir)
170                    return os.path.join(*tail)
171            else:
172                # Oops, got all the way through 'path' without finding a
173                # match in package_dir.  If package_dir defines a directory
174                # for the root (nameless) package, then fallback on it;
175                # otherwise, we might as well have not consulted
176                # package_dir at all, as we just use the directory implied
177                # by 'tail' (which should be the same as the original value
178                # of 'path' at this point).
179                pdir = self.package_dir.get('')
180                if pdir is not None:
181                    tail.insert(0, pdir)
182
183                if tail:
184                    return os.path.join(*tail)
185                else:
186                    return ''
187
188    # get_package_dir ()
189
190
191    def check_package (self, package, package_dir):
192
193        # Empty dir name means current directory, which we can probably
194        # assume exists.  Also, os.path.exists and isdir don't know about
195        # my "empty string means current dir" convention, so we have to
196        # circumvent them.
197        if package_dir != "":
198            if not os.path.exists(package_dir):
199                raise DistutilsFileError, \
200                      "package directory '%s' does not exist" % package_dir
201            if not os.path.isdir(package_dir):
202                raise DistutilsFileError, \
203                      ("supposed package directory '%s' exists, " +
204                       "but is not a directory") % package_dir
205
206        # Require __init__.py for all but the "root package"
207        if package:
208            init_py = os.path.join(package_dir, "__init__.py")
209            if os.path.isfile(init_py):
210                return init_py
211            else:
212                log.warn(("package init file '%s' not found " +
213                          "(or not a regular file)"), init_py)
214
215        # Either not in a package at all (__init__.py not expected), or
216        # __init__.py doesn't exist -- so don't return the filename.
217        return None
218
219    # check_package ()
220
221
222    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    # check_module ()
230
231
232    def find_package_modules (self, package, package_dir):
233        self.check_package(package, package_dir)
234        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                module = os.path.splitext(os.path.basename(f))[0]
242                modules.append((package, module, f))
243            else:
244                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        module_base, filename): 'package' is a tuple of the path through
252        package-space to the module; 'module_base' is the bare (no
253        packages, no dots) module name, and 'filename' is the path to the
254        ".py" file (relative to the distribution root) that implements the
255        module.
256        """
257
258        # Map package names to tuples of useful info about the package:
259        #    (package_dir, checked)
260        # package_dir - the directory where we'll find source files for
261        #   this package
262        # checked - true if we have checked that the package directory
263        #   is valid (exists, contains __init__.py, ... ?)
264        packages = {}
265
266        # List of (package, module, filename) tuples to return
267        modules = []
268
269        # We treat modules-in-packages almost the same as toplevel modules,
270        # just the "package" for a toplevel is empty (either an empty
271        # string or empty list, depending on context).  Differences:
272        #   - don't check for __init__.py in directory for empty package
273
274        for module in self.py_modules:
275            path = module.split('.')
276            package = '.'.join(path[0:-1])
277            module_base = path[-1]
278
279            try:
280                (package_dir, checked) = packages[package]
281            except KeyError:
282                package_dir = self.get_package_dir(package)
283                checked = 0
284
285            if not checked:
286                init_py = self.check_package(package, package_dir)
287                packages[package] = (package_dir, 1)
288                if init_py:
289                    modules.append((package, "__init__", init_py))
290
291            # XXX perhaps we should also check for just .pyc files
292            # (so greedy closed-source bastards can distribute Python
293            # modules too)
294            module_file = os.path.join(package_dir, module_base + ".py")
295            if not self.check_module(module, module_file):
296                continue
297
298            modules.append((package, module_base, module_file))
299
300        return modules
301
302    # find_modules ()
303
304
305    def find_all_modules (self):
306        """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        by whole packages ('self.packages').  Return a list of tuples
309        (package, module, module_file), just like 'find_modules()' and
310        'find_package_modules()' do."""
311
312        modules = []
313        if self.py_modules:
314            modules.extend(self.find_modules())
315        if self.packages:
316            for package in self.packages:
317                package_dir = self.get_package_dir(package)
318                m = self.find_package_modules(package, package_dir)
319                modules.extend(m)
320
321        return modules
322
323    # find_all_modules ()
324
325
326    def get_source_files (self):
327
328        modules = self.find_all_modules()
329        filenames = []
330        for module in modules:
331            filenames.append(module[-1])
332
333        return filenames
334
335
336    def get_module_outfile (self, build_dir, package, module):
337        outfile_path = [build_dir] + list(package) + [module + ".py"]
338        return os.path.join(*outfile_path)
339
340
341    def get_outputs (self, include_bytecode=1):
342        modules = self.find_all_modules()
343        outputs = []
344        for (package, module, module_file) in modules:
345            package = package.split('.')
346            filename = self.get_module_outfile(self.build_lib, package, module)
347            outputs.append(filename)
348            if include_bytecode:
349                if self.compile:
350                    outputs.append(filename + "c")
351                if self.optimize > 0:
352                    outputs.append(filename + "o")
353
354        outputs += [
355            os.path.join(build_dir, filename)
356            for package, src_dir, build_dir, filenames in self.data_files
357            for filename in filenames
358            ]
359
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):
367            raise TypeError, \
368                  "'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        outfile = self.get_module_outfile(self.build_lib, package, module)
374        dir = os.path.dirname(outfile)
375        self.mkpath(dir)
376        return self.copy_file(module_file, outfile, preserve_mode=0)
377
378
379    def build_modules (self):
380
381        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    # build_modules ()
391
392
393    def build_packages (self):
394
395        for package in self.packages:
396
397            # Get list of (package, module, module_file) tuples based on
398            # scanning the package directory.  'package' is only included
399            # in the tuple so that 'find_modules()' and
400            # 'find_package_tuples()' have a consistent interface; it's
401            # ignored here (apart from a sanity check).  Also, 'module' is
402            # the *unqualified* module name (ie. no dots, no package -- we
403            # already know its package!), and 'module_file' is the path to
404            # the .py file, relative to the current directory
405            # (ie. including 'package_dir').
406            package_dir = self.get_package_dir(package)
407            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            for (package_, module, module_file) in modules:
412                assert package == package_
413                self.build_module(module, module_file, package)
414
415    # build_packages ()
416
417
418    def byte_compile (self, files):
419        from distutils.util import byte_compile
420        prefix = self.build_lib
421        if prefix[-1] != os.sep:
422            prefix = prefix + os.sep
423
424        # XXX this code is essentially the same as the 'byte_compile()
425        # method of the "install_lib" command, except for the determination
426        # of the 'prefix' string.  Hmmm.
427
428        if self.compile:
429            byte_compile(files, optimize=0,
430                         force=self.force, prefix=prefix, dry_run=self.dry_run)
431        if self.optimize > 0:
432            byte_compile(files, optimize=self.optimize,
433                         force=self.force, prefix=prefix, dry_run=self.dry_run)
434
435# class build_py
436