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