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