• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                and os.path.isfile(fn)])
133        return files
134
135    def build_package_data(self):
136        """Copy data files into build directory"""
137        for package, src_dir, build_dir, filenames in self.data_files:
138            for filename in filenames:
139                target = os.path.join(build_dir, filename)
140                self.mkpath(os.path.dirname(target))
141                self.copy_file(os.path.join(src_dir, filename), target,
142                               preserve_mode=False)
143
144    def get_package_dir(self, package):
145        """Return the directory, relative to the top of the source
146           distribution, where package 'package' should be found
147           (at least according to the 'package_dir' option, if any)."""
148
149        path = package.split('.')
150
151        if not self.package_dir:
152            if path:
153                return os.path.join(*path)
154            else:
155                return ''
156        else:
157            tail = []
158            while path:
159                try:
160                    pdir = self.package_dir['.'.join(path)]
161                except KeyError:
162                    tail.insert(0, path[-1])
163                    del path[-1]
164                else:
165                    tail.insert(0, pdir)
166                    return os.path.join(*tail)
167            else:
168                # Oops, got all the way through 'path' without finding a
169                # match in package_dir.  If package_dir defines a directory
170                # for the root (nameless) package, then fallback on it;
171                # otherwise, we might as well have not consulted
172                # package_dir at all, as we just use the directory implied
173                # by 'tail' (which should be the same as the original value
174                # of 'path' at this point).
175                pdir = self.package_dir.get('')
176                if pdir is not None:
177                    tail.insert(0, pdir)
178
179                if tail:
180                    return os.path.join(*tail)
181                else:
182                    return ''
183
184    def check_package(self, package, package_dir):
185        # Empty dir name means current directory, which we can probably
186        # assume exists.  Also, os.path.exists and isdir don't know about
187        # my "empty string means current dir" convention, so we have to
188        # circumvent them.
189        if package_dir != "":
190            if not os.path.exists(package_dir):
191                raise DistutilsFileError(
192                      "package directory '%s' does not exist" % package_dir)
193            if not os.path.isdir(package_dir):
194                raise DistutilsFileError(
195                       "supposed package directory '%s' exists, "
196                       "but is not a directory" % package_dir)
197
198        # Require __init__.py for all but the "root package"
199        if package:
200            init_py = os.path.join(package_dir, "__init__.py")
201            if os.path.isfile(init_py):
202                return init_py
203            else:
204                log.warn(("package init file '%s' not found " +
205                          "(or not a regular file)"), init_py)
206
207        # Either not in a package at all (__init__.py not expected), or
208        # __init__.py doesn't exist -- so don't return the filename.
209        return None
210
211    def check_module(self, module, module_file):
212        if not os.path.isfile(module_file):
213            log.warn("file %s (for module %s) not found", module_file, module)
214            return False
215        else:
216            return True
217
218    def find_package_modules(self, package, package_dir):
219        self.check_package(package, package_dir)
220        module_files = glob(os.path.join(package_dir, "*.py"))
221        modules = []
222        setup_script = os.path.abspath(self.distribution.script_name)
223
224        for f in module_files:
225            abs_f = os.path.abspath(f)
226            if abs_f != setup_script:
227                module = os.path.splitext(os.path.basename(f))[0]
228                modules.append((package, module, f))
229            else:
230                self.debug_print("excluding %s" % setup_script)
231        return modules
232
233    def find_modules(self):
234        """Finds individually-specified Python modules, ie. those listed by
235        module name in 'self.py_modules'.  Returns a list of tuples (package,
236        module_base, filename): 'package' is a tuple of the path through
237        package-space to the module; 'module_base' is the bare (no
238        packages, no dots) module name, and 'filename' is the path to the
239        ".py" file (relative to the distribution root) that implements the
240        module.
241        """
242        # Map package names to tuples of useful info about the package:
243        #    (package_dir, checked)
244        # package_dir - the directory where we'll find source files for
245        #   this package
246        # checked - true if we have checked that the package directory
247        #   is valid (exists, contains __init__.py, ... ?)
248        packages = {}
249
250        # List of (package, module, filename) tuples to return
251        modules = []
252
253        # We treat modules-in-packages almost the same as toplevel modules,
254        # just the "package" for a toplevel is empty (either an empty
255        # string or empty list, depending on context).  Differences:
256        #   - don't check for __init__.py in directory for empty package
257        for module in self.py_modules:
258            path = module.split('.')
259            package = '.'.join(path[0:-1])
260            module_base = path[-1]
261
262            try:
263                (package_dir, checked) = packages[package]
264            except KeyError:
265                package_dir = self.get_package_dir(package)
266                checked = 0
267
268            if not checked:
269                init_py = self.check_package(package, package_dir)
270                packages[package] = (package_dir, 1)
271                if init_py:
272                    modules.append((package, "__init__", init_py))
273
274            # XXX perhaps we should also check for just .pyc files
275            # (so greedy closed-source bastards can distribute Python
276            # modules too)
277            module_file = os.path.join(package_dir, module_base + ".py")
278            if not self.check_module(module, module_file):
279                continue
280
281            modules.append((package, module_base, module_file))
282
283        return modules
284
285    def find_all_modules(self):
286        """Compute the list of all modules that will be built, whether
287        they are specified one-module-at-a-time ('self.py_modules') or
288        by whole packages ('self.packages').  Return a list of tuples
289        (package, module, module_file), just like 'find_modules()' and
290        'find_package_modules()' do."""
291        modules = []
292        if self.py_modules:
293            modules.extend(self.find_modules())
294        if self.packages:
295            for package in self.packages:
296                package_dir = self.get_package_dir(package)
297                m = self.find_package_modules(package, package_dir)
298                modules.extend(m)
299        return modules
300
301    def get_source_files(self):
302        return [module[-1] for module in self.find_all_modules()]
303
304    def get_module_outfile(self, build_dir, package, module):
305        outfile_path = [build_dir] + list(package) + [module + ".py"]
306        return os.path.join(*outfile_path)
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 = package.split('.')
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        outputs += [
322            os.path.join(build_dir, filename)
323            for package, src_dir, build_dir, filenames in self.data_files
324            for filename in filenames
325            ]
326
327        return outputs
328
329    def build_module(self, module, module_file, package):
330        if isinstance(package, str):
331            package = package.split('.')
332        elif not isinstance(package, (list, tuple)):
333            raise TypeError(
334                  "'package' must be a string (dot-separated), list, or tuple")
335
336        # Now put the module source file into the "build" area -- this is
337        # easy, we just copy it somewhere under self.build_lib (the build
338        # directory for Python source).
339        outfile = self.get_module_outfile(self.build_lib, package, module)
340        dir = os.path.dirname(outfile)
341        self.mkpath(dir)
342        return self.copy_file(module_file, outfile, preserve_mode=0)
343
344    def build_modules(self):
345        modules = self.find_modules()
346        for (package, module, module_file) in modules:
347
348            # Now "build" the module -- ie. copy the source file to
349            # self.build_lib (the build directory for Python source).
350            # (Actually, it gets copied to the directory for this package
351            # under self.build_lib.)
352            self.build_module(module, module_file, package)
353
354    def build_packages(self):
355        for package in self.packages:
356
357            # Get list of (package, module, module_file) tuples based on
358            # scanning the package directory.  'package' is only included
359            # in the tuple so that 'find_modules()' and
360            # 'find_package_tuples()' have a consistent interface; it's
361            # ignored here (apart from a sanity check).  Also, 'module' is
362            # the *unqualified* module name (ie. no dots, no package -- we
363            # already know its package!), and 'module_file' is the path to
364            # the .py file, relative to the current directory
365            # (ie. including 'package_dir').
366            package_dir = self.get_package_dir(package)
367            modules = self.find_package_modules(package, package_dir)
368
369            # Now loop over the modules we found, "building" each one (just
370            # copy it to self.build_lib).
371            for (package_, module, module_file) in modules:
372                assert package == package_
373                self.build_module(module, module_file, package)
374
375    def byte_compile(self, files):
376        if sys.dont_write_bytecode:
377            self.warn('byte-compiling is disabled, skipping.')
378            return
379
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