1"""distutils.command.bdist_wininst 2 3Implements the Distutils 'bdist_wininst' command: create a windows installer 4exe-program.""" 5 6import os 7import sys 8import warnings 9from distutils.core import Command 10from distutils.util import get_platform 11from distutils.dir_util import remove_tree 12from distutils.errors import * 13from distutils.sysconfig import get_python_version 14from distutils import log 15 16class bdist_wininst(Command): 17 18 description = "create an executable installer for MS Windows" 19 20 user_options = [('bdist-dir=', None, 21 "temporary directory for creating the distribution"), 22 ('plat-name=', 'p', 23 "platform name to embed in generated filenames " 24 "(default: %s)" % get_platform()), 25 ('keep-temp', 'k', 26 "keep the pseudo-installation tree around after " + 27 "creating the distribution archive"), 28 ('target-version=', None, 29 "require a specific python version" + 30 " on the target system"), 31 ('no-target-compile', 'c', 32 "do not compile .py to .pyc on the target system"), 33 ('no-target-optimize', 'o', 34 "do not compile .py to .pyo (optimized) " 35 "on the target system"), 36 ('dist-dir=', 'd', 37 "directory to put final built distributions in"), 38 ('bitmap=', 'b', 39 "bitmap to use for the installer instead of python-powered logo"), 40 ('title=', 't', 41 "title to display on the installer background instead of default"), 42 ('skip-build', None, 43 "skip rebuilding everything (for testing/debugging)"), 44 ('install-script=', None, 45 "basename of installation script to be run after " 46 "installation or before deinstallation"), 47 ('pre-install-script=', None, 48 "Fully qualified filename of a script to be run before " 49 "any files are installed. This script need not be in the " 50 "distribution"), 51 ('user-access-control=', None, 52 "specify Vista's UAC handling - 'none'/default=no " 53 "handling, 'auto'=use UAC if target Python installed for " 54 "all users, 'force'=always use UAC"), 55 ] 56 57 boolean_options = ['keep-temp', 'no-target-compile', 'no-target-optimize', 58 'skip-build'] 59 60 # bpo-10945: bdist_wininst requires mbcs encoding only available on Windows 61 _unsupported = (sys.platform != "win32") 62 63 def __init__(self, *args, **kw): 64 super().__init__(*args, **kw) 65 warnings.warn("bdist_wininst command is deprecated since Python 3.8, " 66 "use bdist_wheel (wheel packages) instead", 67 DeprecationWarning, 2) 68 69 def initialize_options(self): 70 self.bdist_dir = None 71 self.plat_name = None 72 self.keep_temp = 0 73 self.no_target_compile = 0 74 self.no_target_optimize = 0 75 self.target_version = None 76 self.dist_dir = None 77 self.bitmap = None 78 self.title = None 79 self.skip_build = None 80 self.install_script = None 81 self.pre_install_script = None 82 self.user_access_control = None 83 84 85 def finalize_options(self): 86 self.set_undefined_options('bdist', ('skip_build', 'skip_build')) 87 88 if self.bdist_dir is None: 89 if self.skip_build and self.plat_name: 90 # If build is skipped and plat_name is overridden, bdist will 91 # not see the correct 'plat_name' - so set that up manually. 92 bdist = self.distribution.get_command_obj('bdist') 93 bdist.plat_name = self.plat_name 94 # next the command will be initialized using that name 95 bdist_base = self.get_finalized_command('bdist').bdist_base 96 self.bdist_dir = os.path.join(bdist_base, 'wininst') 97 98 if not self.target_version: 99 self.target_version = "" 100 101 if not self.skip_build and self.distribution.has_ext_modules(): 102 short_version = get_python_version() 103 if self.target_version and self.target_version != short_version: 104 raise DistutilsOptionError( 105 "target version can only be %s, or the '--skip-build'" \ 106 " option must be specified" % (short_version,)) 107 self.target_version = short_version 108 109 self.set_undefined_options('bdist', 110 ('dist_dir', 'dist_dir'), 111 ('plat_name', 'plat_name'), 112 ) 113 114 if self.install_script: 115 for script in self.distribution.scripts: 116 if self.install_script == os.path.basename(script): 117 break 118 else: 119 raise DistutilsOptionError( 120 "install_script '%s' not found in scripts" 121 % self.install_script) 122 123 def run(self): 124 if (sys.platform != "win32" and 125 (self.distribution.has_ext_modules() or 126 self.distribution.has_c_libraries())): 127 raise DistutilsPlatformError \ 128 ("distribution contains extensions and/or C libraries; " 129 "must be compiled on a Windows 32 platform") 130 131 if not self.skip_build: 132 self.run_command('build') 133 134 install = self.reinitialize_command('install', reinit_subcommands=1) 135 install.root = self.bdist_dir 136 install.skip_build = self.skip_build 137 install.warn_dir = 0 138 install.plat_name = self.plat_name 139 140 install_lib = self.reinitialize_command('install_lib') 141 # we do not want to include pyc or pyo files 142 install_lib.compile = 0 143 install_lib.optimize = 0 144 145 if self.distribution.has_ext_modules(): 146 # If we are building an installer for a Python version other 147 # than the one we are currently running, then we need to ensure 148 # our build_lib reflects the other Python version rather than ours. 149 # Note that for target_version!=sys.version, we must have skipped the 150 # build step, so there is no issue with enforcing the build of this 151 # version. 152 target_version = self.target_version 153 if not target_version: 154 assert self.skip_build, "Should have already checked this" 155 target_version = '%d.%d' % sys.version_info[:2] 156 plat_specifier = ".%s-%s" % (self.plat_name, target_version) 157 build = self.get_finalized_command('build') 158 build.build_lib = os.path.join(build.build_base, 159 'lib' + plat_specifier) 160 161 # Use a custom scheme for the zip-file, because we have to decide 162 # at installation time which scheme to use. 163 for key in ('purelib', 'platlib', 'headers', 'scripts', 'data'): 164 value = key.upper() 165 if key == 'headers': 166 value = value + '/Include/$dist_name' 167 setattr(install, 168 'install_' + key, 169 value) 170 171 log.info("installing to %s", self.bdist_dir) 172 install.ensure_finalized() 173 174 # avoid warning of 'install_lib' about installing 175 # into a directory not in sys.path 176 sys.path.insert(0, os.path.join(self.bdist_dir, 'PURELIB')) 177 178 install.run() 179 180 del sys.path[0] 181 182 # And make an archive relative to the root of the 183 # pseudo-installation tree. 184 from tempfile import mktemp 185 archive_basename = mktemp() 186 fullname = self.distribution.get_fullname() 187 arcname = self.make_archive(archive_basename, "zip", 188 root_dir=self.bdist_dir) 189 # create an exe containing the zip-file 190 self.create_exe(arcname, fullname, self.bitmap) 191 if self.distribution.has_ext_modules(): 192 pyversion = get_python_version() 193 else: 194 pyversion = 'any' 195 self.distribution.dist_files.append(('bdist_wininst', pyversion, 196 self.get_installer_filename(fullname))) 197 # remove the zip-file again 198 log.debug("removing temporary file '%s'", arcname) 199 os.remove(arcname) 200 201 if not self.keep_temp: 202 remove_tree(self.bdist_dir, dry_run=self.dry_run) 203 204 def get_inidata(self): 205 # Return data describing the installation. 206 lines = [] 207 metadata = self.distribution.metadata 208 209 # Write the [metadata] section. 210 lines.append("[metadata]") 211 212 # 'info' will be displayed in the installer's dialog box, 213 # describing the items to be installed. 214 info = (metadata.long_description or '') + '\n' 215 216 # Escape newline characters 217 def escape(s): 218 return s.replace("\n", "\\n") 219 220 for name in ["author", "author_email", "description", "maintainer", 221 "maintainer_email", "name", "url", "version"]: 222 data = getattr(metadata, name, "") 223 if data: 224 info = info + ("\n %s: %s" % \ 225 (name.capitalize(), escape(data))) 226 lines.append("%s=%s" % (name, escape(data))) 227 228 # The [setup] section contains entries controlling 229 # the installer runtime. 230 lines.append("\n[Setup]") 231 if self.install_script: 232 lines.append("install_script=%s" % self.install_script) 233 lines.append("info=%s" % escape(info)) 234 lines.append("target_compile=%d" % (not self.no_target_compile)) 235 lines.append("target_optimize=%d" % (not self.no_target_optimize)) 236 if self.target_version: 237 lines.append("target_version=%s" % self.target_version) 238 if self.user_access_control: 239 lines.append("user_access_control=%s" % self.user_access_control) 240 241 title = self.title or self.distribution.get_fullname() 242 lines.append("title=%s" % escape(title)) 243 import time 244 import distutils 245 build_info = "Built %s with distutils-%s" % \ 246 (time.ctime(time.time()), distutils.__version__) 247 lines.append("build_info=%s" % build_info) 248 return "\n".join(lines) 249 250 def create_exe(self, arcname, fullname, bitmap=None): 251 import struct 252 253 self.mkpath(self.dist_dir) 254 255 cfgdata = self.get_inidata() 256 257 installer_name = self.get_installer_filename(fullname) 258 self.announce("creating %s" % installer_name) 259 260 if bitmap: 261 with open(bitmap, "rb") as f: 262 bitmapdata = f.read() 263 bitmaplen = len(bitmapdata) 264 else: 265 bitmaplen = 0 266 267 with open(installer_name, "wb") as file: 268 file.write(self.get_exe_bytes()) 269 if bitmap: 270 file.write(bitmapdata) 271 272 # Convert cfgdata from unicode to ascii, mbcs encoded 273 if isinstance(cfgdata, str): 274 cfgdata = cfgdata.encode("mbcs") 275 276 # Append the pre-install script 277 cfgdata = cfgdata + b"\0" 278 if self.pre_install_script: 279 # We need to normalize newlines, so we open in text mode and 280 # convert back to bytes. "latin-1" simply avoids any possible 281 # failures. 282 with open(self.pre_install_script, "r", 283 encoding="latin-1") as script: 284 script_data = script.read().encode("latin-1") 285 cfgdata = cfgdata + script_data + b"\n\0" 286 else: 287 # empty pre-install script 288 cfgdata = cfgdata + b"\0" 289 file.write(cfgdata) 290 291 # The 'magic number' 0x1234567B is used to make sure that the 292 # binary layout of 'cfgdata' is what the wininst.exe binary 293 # expects. If the layout changes, increment that number, make 294 # the corresponding changes to the wininst.exe sources, and 295 # recompile them. 296 header = struct.pack("<iii", 297 0x1234567B, # tag 298 len(cfgdata), # length 299 bitmaplen, # number of bytes in bitmap 300 ) 301 file.write(header) 302 with open(arcname, "rb") as f: 303 file.write(f.read()) 304 305 def get_installer_filename(self, fullname): 306 # Factored out to allow overriding in subclasses 307 if self.target_version: 308 # if we create an installer for a specific python version, 309 # it's better to include this in the name 310 installer_name = os.path.join(self.dist_dir, 311 "%s.%s-py%s.exe" % 312 (fullname, self.plat_name, self.target_version)) 313 else: 314 installer_name = os.path.join(self.dist_dir, 315 "%s.%s.exe" % (fullname, self.plat_name)) 316 return installer_name 317 318 def get_exe_bytes(self): 319 # If a target-version other than the current version has been 320 # specified, then using the MSVC version from *this* build is no good. 321 # Without actually finding and executing the target version and parsing 322 # its sys.version, we just hard-code our knowledge of old versions. 323 # NOTE: Possible alternative is to allow "--target-version" to 324 # specify a Python executable rather than a simple version string. 325 # We can then execute this program to obtain any info we need, such 326 # as the real sys.version string for the build. 327 cur_version = get_python_version() 328 329 # If the target version is *later* than us, then we assume they 330 # use what we use 331 # string compares seem wrong, but are what sysconfig.py itself uses 332 if self.target_version and self.target_version < cur_version: 333 if self.target_version < "2.4": 334 bv = '6.0' 335 elif self.target_version == "2.4": 336 bv = '7.1' 337 elif self.target_version == "2.5": 338 bv = '8.0' 339 elif self.target_version <= "3.2": 340 bv = '9.0' 341 elif self.target_version <= "3.4": 342 bv = '10.0' 343 else: 344 bv = '14.0' 345 else: 346 # for current version - use authoritative check. 347 try: 348 from msvcrt import CRT_ASSEMBLY_VERSION 349 except ImportError: 350 # cross-building, so assume the latest version 351 bv = '14.0' 352 else: 353 # as far as we know, CRT is binary compatible based on 354 # the first field, so assume 'x.0' until proven otherwise 355 major = CRT_ASSEMBLY_VERSION.partition('.')[0] 356 bv = major + '.0' 357 358 359 # wininst-x.y.exe is in the same directory as this file 360 directory = os.path.dirname(__file__) 361 # we must use a wininst-x.y.exe built with the same C compiler 362 # used for python. XXX What about mingw, borland, and so on? 363 364 # if plat_name starts with "win" but is not "win32" 365 # we want to strip "win" and leave the rest (e.g. -amd64) 366 # for all other cases, we don't want any suffix 367 if self.plat_name != 'win32' and self.plat_name[:3] == 'win': 368 sfix = self.plat_name[3:] 369 else: 370 sfix = '' 371 372 filename = os.path.join(directory, "wininst-%s%s.exe" % (bv, sfix)) 373 f = open(filename, "rb") 374 try: 375 return f.read() 376 finally: 377 f.close() 378