• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""distutils.command.build_py
2
3Implements the Distutils 'build_py' command."""
4
5import os
6import importlib.util
7import sys
8from glob import glob
9
10from distutils.core import Command
11from distutils.errors import *
12from distutils.util import convert_path, Mixin2to3
13from distutils import log
14
15class build_py (Command):
16
17    description = "\"build\" pure Python modules (copy to build directory)"
18
19    user_options = [
20        ('build-lib=', 'd', "directory to \"build\" (copy) to"),
21        ('compile', 'c', "compile .py to .pyc"),
22        ('no-compile', None, "don't compile .py files [default]"),
23        ('optimize=', 'O',
24         "also compile with optimization: -O1 for \"python -O\", "
25         "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"),
26        ('force', 'f', "forcibly build everything (ignore file timestamps)"),
27        ]
28
29    boolean_options = ['compile', 'force']
30    negative_opt = {'no-compile' : 'compile'}
31
32    def initialize_options(self):
33        self.build_lib = None
34        self.py_modules = None
35        self.package = None
36        self.package_data = None
37        self.package_dir = None
38        self.compile = 0
39        self.optimize = 0
40        self.force = None
41
42    def finalize_options(self):
43        self.set_undefined_options('build',
44                                   ('build_lib', 'build_lib'),
45                                   ('force', 'force'))
46
47        # Get the distribution options that are aliases for build_py
48        # options -- list of packages and list of modules.
49        self.packages = self.distribution.packages
50        self.py_modules = self.distribution.py_modules
51        self.package_data = self.distribution.package_data
52        self.package_dir = {}
53        if self.distribution.package_dir:
54            for name, path in self.distribution.package_dir.items():
55                self.package_dir[name] = convert_path(path)
56        self.data_files = self.get_data_files()
57
58        # Ick, copied straight from install_lib.py (fancy_getopt needs a
59        # type system!  Hell, *everything* needs a type system!!!)
60        if not isinstance(self.optimize, int):
61            try:
62                self.optimize = int(self.optimize)
63                assert 0 <= self.optimize <= 2
64            except (ValueError, AssertionError):
65                raise DistutilsOptionError("optimize must be 0, 1, or 2")
66
67    def run(self):
68        # XXX copy_file by default preserves atime and mtime.  IMHO this is
69        # the right thing to do, but perhaps it should be an option -- in
70        # particular, a site administrator might want installed files to
71        # reflect the time of installation rather than the last
72        # modification time before the installed release.
73
74        # XXX copy_file by default preserves mode, which appears to be the
75        # wrong thing to do: if a file is read-only in the working
76        # directory, we want it to be installed read/write so that the next
77        # installation of the same module distribution can overwrite it
78        # without problems.  (This might be a Unix-specific issue.)  Thus
79        # we turn off 'preserve_mode' when copying to the build directory,
80        # since the build directory is supposed to be exactly what the
81        # installation will look like (ie. we preserve mode when
82        # installing).
83
84        # Two options control which modules will be installed: 'packages'
85        # and 'py_modules'.  The former lets us work with whole packages, not
86        # specifying individual modules at all; the latter is for
87        # specifying modules one-at-a-time.
88
89        if self.py_modules:
90            self.build_modules()
91        if self.packages:
92            self.build_packages()
93            self.build_package_data()
94
95        self.byte_compile(self.get_outputs(include_bytecode=0))
96
97    def get_data_files(self):
98        """Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
99        data = []
100        if not self.packages:
101            return data
102        for package in self.packages:
103            # Locate package source directory
104            src_dir = self.get_package_dir(package)
105
106            # Compute package build directory
107            build_dir = os.path.join(*([self.build_lib] + package.split('.')))
108
109            # Length of path to strip from found files
110            plen = 0
111            if src_dir:
112                plen = len(src_dir)+1
113
114            # Strip directory from globbed filenames
115            filenames = [
116                file[plen:] for file in self.find_data_files(package, src_dir)
117                ]
118            data.append((package, src_dir, build_dir, filenames))
119        return data
120
121    def find_data_files(self, package, src_dir):
122        """Return filenames for package's data files in 'src_dir'"""
123        globs = (self.package_data.get('', [])
124                 + self.package_data.get(package, []))
125        files = []
126        for pattern in globs:
127            # Each pattern has to be converted to a platform-specific path
128            filelist = glob(os.path.join(src_dir, convert_path(pattern)))
129            # Files that match more than one pattern are only added once
130            files.extend([fn for fn in filelist if fn not in files
131                and os.path.isfile(fn)])
132        return files
133
134    def build_package_data(self):
135        """Copy data files into build directory"""
136        lastdir = None
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        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(importlib.util.cache_from_source(
317                        filename, optimization=''))
318                if self.optimize > 0:
319                    outputs.append(importlib.util.cache_from_source(
320                        filename, optimization=self.optimize))
321
322        outputs += [
323            os.path.join(build_dir, filename)
324            for package, src_dir, build_dir, filenames in self.data_files
325            for filename in filenames
326            ]
327
328        return outputs
329
330    def build_module(self, module, module_file, package):
331        if isinstance(package, str):
332            package = package.split('.')
333        elif not isinstance(package, (list, tuple)):
334            raise TypeError(
335                  "'package' must be a string (dot-separated), list, or tuple")
336
337        # Now put the module source file into the "build" area -- this is
338        # easy, we just copy it somewhere under self.build_lib (the build
339        # directory for Python source).
340        outfile = self.get_module_outfile(self.build_lib, package, module)
341        dir = os.path.dirname(outfile)
342        self.mkpath(dir)
343        return self.copy_file(module_file, outfile, preserve_mode=0)
344
345    def build_modules(self):
346        modules = self.find_modules()
347        for (package, module, module_file) in modules:
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            # 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        if self.compile:
388            byte_compile(files, optimize=0,
389                         force=self.force, prefix=prefix, dry_run=self.dry_run)
390        if self.optimize > 0:
391            byte_compile(files, optimize=self.optimize,
392                         force=self.force, prefix=prefix, dry_run=self.dry_run)
393
394class build_py_2to3(build_py, Mixin2to3):
395    def run(self):
396        self.updated_files = []
397
398        # Base class code
399        if self.py_modules:
400            self.build_modules()
401        if self.packages:
402            self.build_packages()
403            self.build_package_data()
404
405        # 2to3
406        self.run_2to3(self.updated_files)
407
408        # Remaining base class code
409        self.byte_compile(self.get_outputs(include_bytecode=0))
410
411    def build_module(self, module, module_file, package):
412        res = build_py.build_module(self, module, module_file, package)
413        if res[1]:
414            # file was copied
415            self.updated_files.append(res[0])
416        return res
417