1"""distutils.command.install_lib
2
3Implements the Distutils 'install_lib' command
4(install all Python modules)."""
5
6import os
7import importlib.util
8import sys
9
10from distutils.core import Command
11from distutils.errors import DistutilsOptionError
12
13
14# Extension for Python source files.
15PYTHON_SOURCE_EXTENSION = ".py"
16
17class install_lib(Command):
18
19    description = "install all Python modules (extensions and pure Python)"
20
21    # The byte-compilation options are a tad confusing.  Here are the
22    # possible scenarios:
23    #   1) no compilation at all (--no-compile --no-optimize)
24    #   2) compile .pyc only (--compile --no-optimize; default)
25    #   3) compile .pyc and "opt-1" .pyc (--compile --optimize)
26    #   4) compile "opt-1" .pyc only (--no-compile --optimize)
27    #   5) compile .pyc and "opt-2" .pyc (--compile --optimize-more)
28    #   6) compile "opt-2" .pyc only (--no-compile --optimize-more)
29    #
30    # The UI for this is two options, 'compile' and 'optimize'.
31    # 'compile' is strictly boolean, and only decides whether to
32    # generate .pyc files.  'optimize' is three-way (0, 1, or 2), and
33    # decides both whether to generate .pyc files and what level of
34    # optimization to use.
35
36    user_options = [
37        ('install-dir=', 'd', "directory to install to"),
38        ('build-dir=','b', "build directory (where to install from)"),
39        ('force', 'f', "force installation (overwrite existing files)"),
40        ('compile', 'c', "compile .py to .pyc [default]"),
41        ('no-compile', None, "don't compile .py files"),
42        ('optimize=', 'O',
43         "also compile with optimization: -O1 for \"python -O\", "
44         "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"),
45        ('skip-build', None, "skip the build steps"),
46        ]
47
48    boolean_options = ['force', 'compile', 'skip-build']
49    negative_opt = {'no-compile' : 'compile'}
50
51    def initialize_options(self):
52        # let the 'install' command dictate our installation directory
53        self.install_dir = None
54        self.build_dir = None
55        self.force = 0
56        self.compile = None
57        self.optimize = None
58        self.skip_build = None
59
60    def finalize_options(self):
61        # Get all the information we need to install pure Python modules
62        # from the umbrella 'install' command -- build (source) directory,
63        # install (target) directory, and whether to compile .py files.
64        self.set_undefined_options('install',
65                                   ('build_lib', 'build_dir'),
66                                   ('install_lib', 'install_dir'),
67                                   ('force', 'force'),
68                                   ('compile', 'compile'),
69                                   ('optimize', 'optimize'),
70                                   ('skip_build', 'skip_build'),
71                                  )
72
73        if self.compile is None:
74            self.compile = True
75        if self.optimize is None:
76            self.optimize = False
77
78        if not isinstance(self.optimize, int):
79            try:
80                self.optimize = int(self.optimize)
81                if self.optimize not in (0, 1, 2):
82                    raise AssertionError
83            except (ValueError, AssertionError):
84                raise DistutilsOptionError("optimize must be 0, 1, or 2")
85
86    def run(self):
87        # Make sure we have built everything we need first
88        self.build()
89
90        # Install everything: simply dump the entire contents of the build
91        # directory to the installation directory (that's the beauty of
92        # having a build directory!)
93        outfiles = self.install()
94
95        # (Optionally) compile .py to .pyc
96        if outfiles is not None and self.distribution.has_pure_modules():
97            self.byte_compile(outfiles)
98
99    # -- Top-level worker functions ------------------------------------
100    # (called from 'run()')
101
102    def build(self):
103        if not self.skip_build:
104            if self.distribution.has_pure_modules():
105                self.run_command('build_py')
106            if self.distribution.has_ext_modules():
107                self.run_command('build_ext')
108
109    def install(self):
110        if os.path.isdir(self.build_dir):
111            outfiles = self.copy_tree(self.build_dir, self.install_dir)
112        else:
113            self.warn("'%s' does not exist -- no Python modules to install" %
114                      self.build_dir)
115            return
116        return outfiles
117
118    def byte_compile(self, files):
119        if sys.dont_write_bytecode:
120            self.warn('byte-compiling is disabled, skipping.')
121            return
122
123        from distutils.util import byte_compile
124
125        # Get the "--root" directory supplied to the "install" command,
126        # and use it as a prefix to strip off the purported filename
127        # encoded in bytecode files.  This is far from complete, but it
128        # should at least generate usable bytecode in RPM distributions.
129        install_root = self.get_finalized_command('install').root
130
131        if self.compile:
132            byte_compile(files, optimize=0,
133                         force=self.force, prefix=install_root,
134                         dry_run=self.dry_run)
135        if self.optimize > 0:
136            byte_compile(files, optimize=self.optimize,
137                         force=self.force, prefix=install_root,
138                         verbose=self.verbose, dry_run=self.dry_run)
139
140
141    # -- Utility methods -----------------------------------------------
142
143    def _mutate_outputs(self, has_any, build_cmd, cmd_option, output_dir):
144        if not has_any:
145            return []
146
147        build_cmd = self.get_finalized_command(build_cmd)
148        build_files = build_cmd.get_outputs()
149        build_dir = getattr(build_cmd, cmd_option)
150
151        prefix_len = len(build_dir) + len(os.sep)
152        outputs = []
153        for file in build_files:
154            outputs.append(os.path.join(output_dir, file[prefix_len:]))
155
156        return outputs
157
158    def _bytecode_filenames(self, py_filenames):
159        bytecode_files = []
160        for py_file in py_filenames:
161            # Since build_py handles package data installation, the
162            # list of outputs can contain more than just .py files.
163            # Make sure we only report bytecode for the .py files.
164            ext = os.path.splitext(os.path.normcase(py_file))[1]
165            if ext != PYTHON_SOURCE_EXTENSION:
166                continue
167            if self.compile:
168                bytecode_files.append(importlib.util.cache_from_source(
169                    py_file, optimization=''))
170            if self.optimize > 0:
171                bytecode_files.append(importlib.util.cache_from_source(
172                    py_file, optimization=self.optimize))
173
174        return bytecode_files
175
176
177    # -- External interface --------------------------------------------
178    # (called by outsiders)
179
180    def get_outputs(self):
181        """Return the list of files that would be installed if this command
182        were actually run.  Not affected by the "dry-run" flag or whether
183        modules have actually been built yet.
184        """
185        pure_outputs = \
186            self._mutate_outputs(self.distribution.has_pure_modules(),
187                                 'build_py', 'build_lib',
188                                 self.install_dir)
189        if self.compile:
190            bytecode_outputs = self._bytecode_filenames(pure_outputs)
191        else:
192            bytecode_outputs = []
193
194        ext_outputs = \
195            self._mutate_outputs(self.distribution.has_ext_modules(),
196                                 'build_ext', 'build_lib',
197                                 self.install_dir)
198
199        return pure_outputs + bytecode_outputs + ext_outputs
200
201    def get_inputs(self):
202        """Get the list of files that are input to this command, ie. the
203        files that get installed as they are named in the build tree.
204        The files in this list correspond one-to-one to the output
205        filenames returned by 'get_outputs()'.
206        """
207        inputs = []
208
209        if self.distribution.has_pure_modules():
210            build_py = self.get_finalized_command('build_py')
211            inputs.extend(build_py.get_outputs())
212
213        if self.distribution.has_ext_modules():
214            build_ext = self.get_finalized_command('build_ext')
215            inputs.extend(build_ext.get_outputs())
216
217        return inputs
218