bdist_wininst.py 15.1 KB
Newer Older
1 2 3 4 5
"""distutils.command.bdist_wininst

Implements the Distutils 'bdist_wininst' command: create a windows installer
exe-program."""

6
import sys, os
7
from distutils.core import Command
8
from distutils.util import get_platform
9 10 11 12
from distutils.dir_util import create_tree, remove_tree
from distutils.errors import *
from distutils.sysconfig import get_python_version
from distutils import log
13

14
class bdist_wininst(Command):
15

16
    description = "create an executable installer for MS Windows"
17

18
    user_options = [('bdist-dir=', None,
19
                     "temporary directory for creating the distribution"),
Christian Heimes's avatar
Christian Heimes committed
20 21 22
                    ('plat-name=', 'p',
                     "platform name to embed in generated filenames "
                     "(default: %s)" % get_platform()),
23
                    ('keep-temp', 'k',
24 25
                     "keep the pseudo-installation tree around after " +
                     "creating the distribution archive"),
Thomas Heller's avatar
Thomas Heller committed
26
                    ('target-version=', None,
27
                     "require a specific python version" +
28
                     " on the target system"),
Thomas Heller's avatar
Thomas Heller committed
29 30 31
                    ('no-target-compile', 'c',
                     "do not compile .py to .pyc on the target system"),
                    ('no-target-optimize', 'o',
32 33
                     "do not compile .py to .pyo (optimized)"
                     "on the target system"),
34 35
                    ('dist-dir=', 'd',
                     "directory to put final built distributions in"),
36 37 38 39
                    ('bitmap=', 'b',
                     "bitmap to use for the installer instead of python-powered logo"),
                    ('title=', 't',
                     "title to display on the installer background instead of default"),
40 41
                    ('skip-build', None,
                     "skip rebuilding everything (for testing/debugging)"),
42
                    ('install-script=', None,
43 44
                     "basename of installation script to be run after"
                     "installation or before deinstallation"),
45 46 47 48
                    ('pre-install-script=', None,
                     "Fully qualified filename of a script to be run before "
                     "any files are installed.  This script need not be in the "
                     "distribution"),
Christian Heimes's avatar
Christian Heimes committed
49 50 51 52
                    ('user-access-control=', None,
                     "specify Vista's UAC handling - 'none'/default=no "
                     "handling, 'auto'=use UAC if target Python installed for "
                     "all users, 'force'=always use UAC"),
53 54
                   ]

55 56
    boolean_options = ['keep-temp', 'no-target-compile', 'no-target-optimize',
                       'skip-build']
57

58
    def initialize_options(self):
59
        self.bdist_dir = None
Christian Heimes's avatar
Christian Heimes committed
60
        self.plat_name = None
61
        self.keep_temp = 0
Thomas Heller's avatar
Thomas Heller committed
62 63
        self.no_target_compile = 0
        self.no_target_optimize = 0
64
        self.target_version = None
65
        self.dist_dir = None
66 67
        self.bitmap = None
        self.title = None
68
        self.skip_build = None
69
        self.install_script = None
70
        self.pre_install_script = None
Christian Heimes's avatar
Christian Heimes committed
71
        self.user_access_control = None
72 73


74
    def finalize_options(self):
75 76
        self.set_undefined_options('bdist', ('skip_build', 'skip_build'))

77
        if self.bdist_dir is None:
Georg Brandl's avatar
Georg Brandl committed
78 79 80 81 82 83
            if self.skip_build and self.plat_name:
                # If build is skipped and plat_name is overridden, bdist will
                # not see the correct 'plat_name' - so set that up manually.
                bdist = self.distribution.get_command_obj('bdist')
                bdist.plat_name = self.plat_name
                # next the command will be initialized using that name
84 85
            bdist_base = self.get_finalized_command('bdist').bdist_base
            self.bdist_dir = os.path.join(bdist_base, 'wininst')
86

87 88
        if not self.target_version:
            self.target_version = ""
89

90
        if not self.skip_build and self.distribution.has_ext_modules():
91
            short_version = get_python_version()
92
            if self.target_version and self.target_version != short_version:
93
                raise DistutilsOptionError(
94
                      "target version can only be %s, or the '--skip-build'" \
95
                      " option must be specified" % (short_version,))
96 97
            self.target_version = short_version

Christian Heimes's avatar
Christian Heimes committed
98 99 100 101
        self.set_undefined_options('bdist',
                                   ('dist_dir', 'dist_dir'),
                                   ('plat_name', 'plat_name'),
                                  )
102

103 104 105 106 107
        if self.install_script:
            for script in self.distribution.scripts:
                if self.install_script == os.path.basename(script):
                    break
            else:
108 109 110
                raise DistutilsOptionError(
                      "install_script '%s' not found in scripts"
                      % self.install_script)
111

112
    def run(self):
113 114 115
        if (sys.platform != "win32" and
            (self.distribution.has_ext_modules() or
             self.distribution.has_c_libraries())):
Thomas Heller's avatar
Thomas Heller committed
116
            raise DistutilsPlatformError \
117 118
                  ("distribution contains extensions and/or C libraries; "
                   "must be compiled on a Windows 32 platform")
119

120 121
        if not self.skip_build:
            self.run_command('build')
122

123
        install = self.reinitialize_command('install', reinit_subcommands=1)
124
        install.root = self.bdist_dir
125
        install.skip_build = self.skip_build
126
        install.warn_dir = 0
Christian Heimes's avatar
Christian Heimes committed
127
        install.plat_name = self.plat_name
128

129
        install_lib = self.reinitialize_command('install_lib')
130
        # we do not want to include pyc or pyo files
131 132
        install_lib.compile = 0
        install_lib.optimize = 0
133

134 135 136 137 138 139 140 141 142 143
        if self.distribution.has_ext_modules():
            # If we are building an installer for a Python version other
            # than the one we are currently running, then we need to ensure
            # our build_lib reflects the other Python version rather than ours.
            # Note that for target_version!=sys.version, we must have skipped the
            # build step, so there is no issue with enforcing the build of this
            # version.
            target_version = self.target_version
            if not target_version:
                assert self.skip_build, "Should have already checked this"
144
                target_version = '%d.%d' % sys.version_info[:2]
Christian Heimes's avatar
Christian Heimes committed
145
            plat_specifier = ".%s-%s" % (self.plat_name, target_version)
146 147 148
            build = self.get_finalized_command('build')
            build.build_lib = os.path.join(build.build_base,
                                           'lib' + plat_specifier)
149

150 151 152
        # Use a custom scheme for the zip-file, because we have to decide
        # at installation time which scheme to use.
        for key in ('purelib', 'platlib', 'headers', 'scripts', 'data'):
153
            value = key.upper()
154 155 156 157 158
            if key == 'headers':
                value = value + '/Include/$dist_name'
            setattr(install,
                    'install_' + key,
                    value)
159

160
        log.info("installing to %s", self.bdist_dir)
161
        install.ensure_finalized()
162 163 164 165 166

        # avoid warning of 'install_lib' about installing
        # into a directory not in sys.path
        sys.path.insert(0, os.path.join(self.bdist_dir, 'PURELIB'))

167 168
        install.run()

169 170
        del sys.path[0]

171 172
        # And make an archive relative to the root of the
        # pseudo-installation tree.
173 174
        from tempfile import mktemp
        archive_basename = mktemp()
175
        fullname = self.distribution.get_fullname()
176
        arcname = self.make_archive(archive_basename, "zip",
177
                                    root_dir=self.bdist_dir)
178
        # create an exe containing the zip-file
179
        self.create_exe(arcname, fullname, self.bitmap)
180 181 182 183 184
        if self.distribution.has_ext_modules():
            pyversion = get_python_version()
        else:
            pyversion = 'any'
        self.distribution.dist_files.append(('bdist_wininst', pyversion,
185
                                             self.get_installer_filename(fullname)))
186
        # remove the zip-file again
187
        log.debug("removing temporary file '%s'", arcname)
188
        os.remove(arcname)
189

190
        if not self.keep_temp:
191
            remove_tree(self.bdist_dir, dry_run=self.dry_run)
192

193
    def get_inidata(self):
194 195
        # Return data describing the installation.
        lines = []
196 197
        metadata = self.distribution.metadata

198
        # Write the [metadata] section.
199
        lines.append("[metadata]")
200

201 202
        # 'info' will be displayed in the installer's dialog box,
        # describing the items to be installed.
Greg Ward's avatar
Greg Ward committed
203
        info = (metadata.long_description or '') + '\n'
204

205 206
        # Escape newline characters
        def escape(s):
207
            return s.replace("\n", "\\n")
208

209 210 211 212 213
        for name in ["author", "author_email", "description", "maintainer",
                     "maintainer_email", "name", "url", "version"]:
            data = getattr(metadata, name, "")
            if data:
                info = info + ("\n    %s: %s" % \
214
                               (name.capitalize(), escape(data)))
215
                lines.append("%s=%s" % (name, escape(data)))
216 217 218

        # The [setup] section contains entries controlling
        # the installer runtime.
219
        lines.append("\n[Setup]")
220 221
        if self.install_script:
            lines.append("install_script=%s" % self.install_script)
222
        lines.append("info=%s" % escape(info))
223 224
        lines.append("target_compile=%d" % (not self.no_target_compile))
        lines.append("target_optimize=%d" % (not self.no_target_optimize))
225
        if self.target_version:
226
            lines.append("target_version=%s" % self.target_version)
Christian Heimes's avatar
Christian Heimes committed
227 228
        if self.user_access_control:
            lines.append("user_access_control=%s" % self.user_access_control)
229

230
        title = self.title or self.distribution.get_fullname()
231
        lines.append("title=%s" % escape(title))
232 233
        import time
        import distutils
234
        build_info = "Built %s with distutils-%s" % \
235 236
                     (time.ctime(time.time()), distutils.__version__)
        lines.append("build_info=%s" % build_info)
237
        return "\n".join(lines)
238

239
    def create_exe(self, arcname, fullname, bitmap=None):
240 241 242
        import struct

        self.mkpath(self.dist_dir)
243

244
        cfgdata = self.get_inidata()
245

246
        installer_name = self.get_installer_filename(fullname)
247
        self.announce("creating %s" % installer_name)
248

249 250 251 252 253 254
        if bitmap:
            bitmapdata = open(bitmap, "rb").read()
            bitmaplen = len(bitmapdata)
        else:
            bitmaplen = 0

255 256
        file = open(installer_name, "wb")
        file.write(self.get_exe_bytes())
257 258
        if bitmap:
            file.write(bitmapdata)
Fred Drake's avatar
Fred Drake committed
259

260
        # Convert cfgdata from unicode to ascii, mbcs encoded
261 262
        if isinstance(cfgdata, str):
            cfgdata = cfgdata.encode("mbcs")
263

264
        # Append the pre-install script
265
        cfgdata = cfgdata + b"\0"
266
        if self.pre_install_script:
267
            # We need to normalize newlines, so we open in text mode and
268
            # convert back to bytes. "latin-1" simply avoids any possible
269 270
            # failures.
            with open(self.pre_install_script, "r",
271 272
                encoding="latin-1") as script:
                script_data = script.read().encode("latin-1")
273
            cfgdata = cfgdata + script_data + b"\n\0"
274 275
        else:
            # empty pre-install script
276
            cfgdata = cfgdata + b"\0"
277
        file.write(cfgdata)
278 279 280 281 282 283

        # The 'magic number' 0x1234567B is used to make sure that the
        # binary layout of 'cfgdata' is what the wininst.exe binary
        # expects.  If the layout changes, increment that number, make
        # the corresponding changes to the wininst.exe sources, and
        # recompile them.
284
        header = struct.pack("<iii",
285
                             0x1234567B,       # tag
286 287 288
                             len(cfgdata),     # length
                             bitmaplen,        # number of bytes in bitmap
                             )
289 290
        file.write(header)
        file.write(open(arcname, "rb").read())
291

292 293 294 295 296 297
    def get_installer_filename(self, fullname):
        # Factored out to allow overriding in subclasses
        if self.target_version:
            # if we create an installer for a specific python version,
            # it's better to include this in the name
            installer_name = os.path.join(self.dist_dir,
Christian Heimes's avatar
Christian Heimes committed
298 299
                                          "%s.%s-py%s.exe" %
                                           (fullname, self.plat_name, self.target_version))
300 301
        else:
            installer_name = os.path.join(self.dist_dir,
Christian Heimes's avatar
Christian Heimes committed
302
                                          "%s.%s.exe" % (fullname, self.plat_name))
303
        return installer_name
304

305
    def get_exe_bytes(self):
Thomas Heller's avatar
Thomas Heller committed
306 307 308 309 310 311 312 313 314
        # If a target-version other than the current version has been
        # specified, then using the MSVC version from *this* build is no good.
        # Without actually finding and executing the target version and parsing
        # its sys.version, we just hard-code our knowledge of old versions.
        # NOTE: Possible alternative is to allow "--target-version" to
        # specify a Python executable rather than a simple version string.
        # We can then execute this program to obtain any info we need, such
        # as the real sys.version string for the build.
        cur_version = get_python_version()
315 316 317 318 319 320

        # If the target version is *later* than us, then we assume they
        # use what we use
        # string compares seem wrong, but are what sysconfig.py itself uses
        if self.target_version and self.target_version < cur_version:
            if self.target_version < "2.4":
321
                bv = '6.0'
322
            elif self.target_version == "2.4":
323
                bv = '7.1'
324
            elif self.target_version == "2.5":
325
                bv = '8.0'
326
            elif self.target_version <= "3.2":
327
                bv = '9.0'
328
            elif self.target_version <= "3.4":
329
                bv = '10.0'
Thomas Heller's avatar
Thomas Heller committed
330
            else:
331
                bv = '14.0'
Thomas Heller's avatar
Thomas Heller committed
332 333
        else:
            # for current version - use authoritative check.
334 335 336 337
            try:
                from msvcrt import CRT_ASSEMBLY_VERSION
            except ImportError:
                # cross-building, so assume the latest version
338
                bv = '14.0'
339
            else:
340 341 342 343
                # as far as we know, CRT is binary compatible based on
                # the first field, so assume 'x.0' until proven otherwise
                major = CRT_ASSEMBLY_VERSION.partition('.')[0]
                bv = major + '.0'
344

Thomas Heller's avatar
Thomas Heller committed
345

346
        # wininst-x.y.exe is in the same directory as this file
347
        directory = os.path.dirname(__file__)
348 349
        # we must use a wininst-x.y.exe built with the same C compiler
        # used for python.  XXX What about mingw, borland, and so on?
350 351 352 353 354 355

        # if plat_name starts with "win" but is not "win32"
        # we want to strip "win" and leave the rest (e.g. -amd64)
        # for all other cases, we don't want any suffix
        if self.plat_name != 'win32' and self.plat_name[:3] == 'win':
            sfix = self.plat_name[3:]
Christian Heimes's avatar
Christian Heimes committed
356
        else:
357 358
            sfix = ''

359
        filename = os.path.join(directory, "wininst-%s%s.exe" % (bv, sfix))
360 361 362 363 364
        f = open(filename, "rb")
        try:
            return f.read()
        finally:
            f.close()