| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # | |
| 3 # Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
| 4 # for details. All rights reserved. Use of this source code is governed by a | |
| 5 # BSD-style license that can be found in the LICENSE file. | |
| 6 | |
| 7 # A script to generate a windows installer for the editor bundle. | |
| 8 # As input the script takes a zip file, a version and the location | |
| 9 # to store the resulting msi file in. | |
| 10 # | |
| 11 # Usage: ./tools/create_windows_installer.py --version <version> | |
| 12 # --zip_file_location <zip_file> --msi_location <output> | |
| 13 # [--wix_bin <wix_bin_location>] | |
| 14 # [--print_wxs] | |
| 15 # | |
| 16 # This script assumes that wix is either in path or passed in as --wix_bin. | |
| 17 # You can get wix from http://wixtoolset.org/. | |
| 18 | |
| 19 import optparse | |
| 20 import os | |
| 21 import subprocess | |
| 22 import sys | |
| 23 import utils | |
| 24 import zipfile | |
| 25 | |
| 26 # This should _never_ change, please don't change this value. | |
| 27 UPGRADE_CODE = '7bacdc33-2e76-4f36-a206-ea58220c0b44' | |
| 28 | |
| 29 # The content of the xml | |
| 30 xml_content = [] | |
| 31 | |
| 32 # The components we want to add to our feature. | |
| 33 feature_components = [] | |
| 34 | |
| 35 # Indentation level, each level is indented 2 spaces | |
| 36 current_indentation = 0 | |
| 37 | |
| 38 def GetOptions(): | |
| 39 options = optparse.OptionParser(usage='usage: %prog [options]') | |
| 40 options.add_option("--zip_file_location", | |
| 41 help='Where the zip file including the editor is located.') | |
| 42 options.add_option("--input_directory", | |
| 43 help='Directory where all the files needed is located.') | |
| 44 options.add_option("--msi_location", | |
| 45 help='Where to store the resulting msi.') | |
| 46 options.add_option("--version", | |
| 47 help='The version specified as Major.Minor.Build.Patch.') | |
| 48 options.add_option("--wix_bin", | |
| 49 help='The location of the wix binary files.') | |
| 50 options.add_option("--print_wxs", action="store_true", dest="print_wxs", | |
| 51 default=False, | |
| 52 help="Prints the generated wxs to stdout.") | |
| 53 (options, args) = options.parse_args() | |
| 54 if len(args) > 0: | |
| 55 raise Exception("This script takes no arguments, only options") | |
| 56 ValidateOptions(options) | |
| 57 return options | |
| 58 | |
| 59 def ValidateOptions(options): | |
| 60 if not options.version: | |
| 61 raise Exception('You must supply a version') | |
| 62 if options.zip_file_location and options.input_directory: | |
| 63 raise Exception('Please pass either zip_file_location or input_directory') | |
| 64 if not options.zip_file_location and not options.input_directory: | |
| 65 raise Exception('Please pass either zip_file_location or input_directory') | |
| 66 if (options.zip_file_location and | |
| 67 not os.path.isfile(options.zip_file_location)): | |
| 68 raise Exception('Passed in zip file not found') | |
| 69 if (options.input_directory and | |
| 70 not os.path.isdir(options.input_directory)): | |
| 71 raise Exception('Passed in directory not found') | |
| 72 | |
| 73 def GetInputDirectory(options, temp_dir): | |
| 74 if options.zip_file_location: | |
| 75 ExtractZipFile(options.zip_file_location, temp_dir) | |
| 76 return os.path.join(temp_dir, 'dart') | |
| 77 return options.input_directory | |
| 78 | |
| 79 # We combine the build and patch into a single entry since | |
| 80 # the windows installer does _not_ consider a change in Patch | |
| 81 # to require a new install. | |
| 82 # In addition to that, the limits on the size are: | |
| 83 # Major: 256 | |
| 84 # Minor: 256 | |
| 85 # Patch: 65536 | |
| 86 # To circumvent this we create the version like this: | |
| 87 # Major.Minor.X | |
| 88 # from "major.minor.patch-prerelease.prerelease_patch" | |
| 89 # where X is "patch<<10 + prerelease<<5 + prerelease_patch" | |
| 90 # Example version 1.2.4-dev.2.3 will go to 1.2.4163 | |
| 91 def GetMicrosoftProductVersion(version): | |
| 92 version_parts = version.split('.') | |
| 93 if len(version_parts) is not 5: | |
| 94 raise Exception( | |
| 95 "Version string (%s) does not follow specification" % version) | |
| 96 (major, minor, patch, prerelease, prerelease_patch) = map(int, version_parts) | |
| 97 | |
| 98 if major > 255 or minor > 255: | |
| 99 raise Exception('Major/Minor can not be above 256') | |
| 100 if patch > 63: | |
| 101 raise Exception('Patch can not be above 63') | |
| 102 if prerelease > 31: | |
| 103 raise Exception('Prerelease can not be above 31') | |
| 104 if prerelease_patch > 31: | |
| 105 raise Exception('PrereleasePatch can not be above 31') | |
| 106 | |
| 107 combined = (patch << 10) + (prerelease << 5) + prerelease_patch | |
| 108 return '%s.%s.%s' % (major, minor, combined) | |
| 109 | |
| 110 # Append using the current indentation level | |
| 111 def Append(data, new_line=True): | |
| 112 str = ((' ' * current_indentation) + | |
| 113 data + | |
| 114 ('\n' if new_line else '')) | |
| 115 xml_content.append(str) | |
| 116 | |
| 117 # Append without any indentation at the current position | |
| 118 def AppendRaw(data, new_line=True): | |
| 119 xml_content.append(data + ('\n' if new_line else '')) | |
| 120 | |
| 121 def AppendComment(comment): | |
| 122 Append('<!--%s-->' % comment) | |
| 123 | |
| 124 def AppendBlankLine(): | |
| 125 Append('') | |
| 126 | |
| 127 def GetContent(): | |
| 128 return ''.join(xml_content) | |
| 129 | |
| 130 def XmlHeader(): | |
| 131 Append('<?xml version="1.0" encoding="UTF-8"?>') | |
| 132 | |
| 133 def TagIndent(str, indentation_string): | |
| 134 return ' ' * len(indentation_string) + str | |
| 135 | |
| 136 def IncreaseIndentation(): | |
| 137 global current_indentation | |
| 138 current_indentation += 1 | |
| 139 | |
| 140 def DecreaseIndentation(): | |
| 141 global current_indentation | |
| 142 current_indentation -= 1 | |
| 143 | |
| 144 class WixAndProduct(object): | |
| 145 def __init__(self, version): | |
| 146 self.version = version | |
| 147 self.product_name = 'Dart Editor' | |
| 148 self.manufacturer = 'Google Inc.' | |
| 149 self.upgrade_code = UPGRADE_CODE | |
| 150 | |
| 151 def __enter__(self): | |
| 152 self.start_wix() | |
| 153 self.start_product() | |
| 154 | |
| 155 def __exit__(self, *_): | |
| 156 self.close_product() | |
| 157 self.close_wix() | |
| 158 | |
| 159 def get_product_id(self): | |
| 160 # This needs to change on every install to guarantee that | |
| 161 # we get a full uninstall + reinstall | |
| 162 # We let wix choose. If we need to do patch releases later on | |
| 163 # we need to retain the value over several installs. | |
| 164 return '*' | |
| 165 | |
| 166 def start_product(self): | |
| 167 product = '<Product ' | |
| 168 Append(product, new_line=False) | |
| 169 AppendRaw('Id="%s"' % self.get_product_id()) | |
| 170 Append(TagIndent('Version="%s"' % self.version, product)) | |
| 171 Append(TagIndent('Name="%s"' % self.product_name, product)) | |
| 172 Append(TagIndent('UpgradeCode="%s"' % self.upgrade_code, | |
| 173 product)) | |
| 174 Append(TagIndent('Language="1033"', product)) | |
| 175 Append(TagIndent('Manufacturer="%s"' % self.manufacturer, | |
| 176 product), | |
| 177 new_line=False) | |
| 178 AppendRaw('>') | |
| 179 IncreaseIndentation() | |
| 180 | |
| 181 def close_product(self): | |
| 182 DecreaseIndentation() | |
| 183 Append('</Product>') | |
| 184 | |
| 185 def start_wix(self): | |
| 186 Append('<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">') | |
| 187 IncreaseIndentation() | |
| 188 | |
| 189 def close_wix(self): | |
| 190 DecreaseIndentation() | |
| 191 Append('</Wix>') | |
| 192 | |
| 193 class Directory(object): | |
| 194 def __init__(self, id, name=None): | |
| 195 self.id = id | |
| 196 self.name = name | |
| 197 | |
| 198 def __enter__(self): | |
| 199 directory = '<Directory ' | |
| 200 Append(directory, new_line=False) | |
| 201 AppendRaw('Id="%s"' % self.id, new_line=self.name is not None) | |
| 202 if self.name: | |
| 203 Append(TagIndent('Name="%s"' % self.name, directory), new_line=False) | |
| 204 AppendRaw('>') | |
| 205 IncreaseIndentation() | |
| 206 | |
| 207 def __exit__(self, *_): | |
| 208 DecreaseIndentation() | |
| 209 Append('</Directory>') | |
| 210 | |
| 211 class Component(object): | |
| 212 def __init__(self, id): | |
| 213 self.id = 'CMP_%s' % id | |
| 214 | |
| 215 def __enter__(self): | |
| 216 component = '<Component ' | |
| 217 Append(component, new_line=False) | |
| 218 AppendRaw('Id="%s"' % self.id) | |
| 219 Append(TagIndent('Guid="*">', component)) | |
| 220 IncreaseIndentation() | |
| 221 | |
| 222 def __exit__(self, *_): | |
| 223 DecreaseIndentation() | |
| 224 Append('</Component>') | |
| 225 feature_components.append(self.id) | |
| 226 | |
| 227 class Feature(object): | |
| 228 def __enter__(self): | |
| 229 feature = '<Feature ' | |
| 230 Append(feature, new_line=False) | |
| 231 AppendRaw('Id="MainFeature"') | |
| 232 Append(TagIndent('Title="Dart Editor"', feature)) | |
| 233 # Install by default | |
| 234 Append(TagIndent('Level="1">', feature)) | |
| 235 IncreaseIndentation() | |
| 236 | |
| 237 def __exit__(self, *_): | |
| 238 DecreaseIndentation() | |
| 239 Append('</Feature>') | |
| 240 | |
| 241 def Package(): | |
| 242 package = '<Package ' | |
| 243 Append(package, new_line=False) | |
| 244 AppendRaw('InstallerVersion="301"') | |
| 245 Append(TagIndent('Compressed="yes" />', package)) | |
| 246 | |
| 247 def MediaTemplate(): | |
| 248 Append('<MediaTemplate EmbedCab="yes" />') | |
| 249 | |
| 250 def File(name, id): | |
| 251 file = '<File ' | |
| 252 Append(file, new_line=False) | |
| 253 AppendRaw('Id="FILE_%s"' % id) | |
| 254 Append(TagIndent('Source="%s"' % name, file)) | |
| 255 Append(TagIndent('KeyPath="yes" />', file)) | |
| 256 | |
| 257 def Shortcut(id, name, ref): | |
| 258 shortcut = '<Shortcut ' | |
| 259 Append(shortcut, new_line=False) | |
| 260 AppendRaw('Id="%s"' % id) | |
| 261 Append(TagIndent('Name="%s"' % name, shortcut)) | |
| 262 Append(TagIndent('Target="%s" />' % ref, shortcut)) | |
| 263 | |
| 264 def RemoveFolder(id): | |
| 265 remove = '<RemoveFolder ' | |
| 266 Append(remove, new_line=False) | |
| 267 AppendRaw('Id="%s"' % id) | |
| 268 Append(TagIndent('On="uninstall" />', remove)) | |
| 269 | |
| 270 def RegistryEntry(location): | |
| 271 registry = '<RegistryValue ' | |
| 272 Append(registry, new_line=False) | |
| 273 AppendRaw('Root="HKCU"') | |
| 274 Append(TagIndent('Key="Software\\Microsoft\\%s"' % location, registry)) | |
| 275 Append(TagIndent('Name="installed"', registry)) | |
| 276 Append(TagIndent('Type="integer"', registry)) | |
| 277 Append(TagIndent('Value="1"', registry)) | |
| 278 Append(TagIndent('KeyPath="yes" />', registry)) | |
| 279 | |
| 280 | |
| 281 def MajorUpgrade(): | |
| 282 upgrade = '<MajorUpgrade ' | |
| 283 Append(upgrade, new_line=False) | |
| 284 down_message = 'You already have a never version installed.' | |
| 285 AppendRaw('DowngradeErrorMessage="%s" />' % down_message) | |
| 286 | |
| 287 | |
| 288 # This is a very simplistic id generation. | |
| 289 # Unfortunately there is no easy way to generate good names, | |
| 290 # since there is a 72 character limit, and we have way longer | |
| 291 # paths. We don't really have an issue with files and ids across | |
| 292 # releases since we do full installs. | |
| 293 counter = 0 | |
| 294 def FileToId(name): | |
| 295 global counter | |
| 296 counter += 1 | |
| 297 return '%s' % counter | |
| 298 | |
| 299 def ListFiles(path): | |
| 300 for entry in os.listdir(path): | |
| 301 full_path = os.path.join(path, entry) | |
| 302 id = FileToId(full_path) | |
| 303 if os.path.isdir(full_path): | |
| 304 with Directory('DIR_%s' % id, entry): | |
| 305 ListFiles(full_path) | |
| 306 elif os.path.isfile(full_path): | |
| 307 # We assume 1 file per component, a File is always a KeyPath. | |
| 308 # A KeyPath on a file makes sure that we can always repair and | |
| 309 # remove that file in a consistent manner. A component | |
| 310 # can only have one child with a KeyPath. | |
| 311 with Component(id): | |
| 312 File(full_path, id) | |
| 313 | |
| 314 def ComponentRefs(): | |
| 315 for component in feature_components: | |
| 316 Append('<ComponentRef Id="%s" />' % component) | |
| 317 | |
| 318 def ExtractZipFile(zip, temp_dir): | |
| 319 print 'Extracting files' | |
| 320 f = zipfile.ZipFile(zip) | |
| 321 f.extractall(temp_dir) | |
| 322 f.close() | |
| 323 | |
| 324 def GenerateInstaller(wxs_content, options, temp_dir): | |
| 325 wxs_file = os.path.join(temp_dir, 'installer.wxs') | |
| 326 wixobj_file = os.path.join(temp_dir, 'installer.wixobj') | |
| 327 print 'Saving wxs output to: %s' % wxs_file | |
| 328 with open(wxs_file, 'w') as f: | |
| 329 f.write(wxs_content) | |
| 330 | |
| 331 candle_bin = 'candle.exe' | |
| 332 light_bin = 'light.exe' | |
| 333 if options.wix_bin: | |
| 334 candle_bin = os.path.join(options.wix_bin, 'candle.exe') | |
| 335 light_bin = os.path.join(options.wix_bin, 'light.exe') | |
| 336 print 'Calling candle on %s' % wxs_file | |
| 337 subprocess.check_call('%s %s -o %s' % (candle_bin, wxs_file, | |
| 338 wixobj_file)) | |
| 339 print 'Calling light on %s' % wixobj_file | |
| 340 subprocess.check_call('%s %s -o %s' % (light_bin, wixobj_file, | |
| 341 options.msi_location)) | |
| 342 print 'Created msi file to %s' % options.msi_location | |
| 343 | |
| 344 | |
| 345 def Main(argv): | |
| 346 if sys.platform != 'win32': | |
| 347 raise Exception("This script can only be run on windows") | |
| 348 options = GetOptions() | |
| 349 version = GetMicrosoftProductVersion(options.version) | |
| 350 with utils.TempDir('installer') as temp_dir: | |
| 351 input_location = GetInputDirectory(options, temp_dir) | |
| 352 print "Generating wix XML" | |
| 353 XmlHeader() | |
| 354 with WixAndProduct(version): | |
| 355 AppendBlankLine() | |
| 356 Package() | |
| 357 MediaTemplate() | |
| 358 AppendComment('We always do a major upgrade, at least for now') | |
| 359 MajorUpgrade() | |
| 360 | |
| 361 AppendComment('Directory structure') | |
| 362 with Directory('TARGETDIR', 'SourceDir'): | |
| 363 with Directory('ProgramFilesFolder'): | |
| 364 with Directory('RootInstallDir', 'Dart Editor'): | |
| 365 AppendComment("Add all files and directories") | |
| 366 print 'Installing files and directories in xml' | |
| 367 ListFiles(input_location) | |
| 368 AppendBlankLine() | |
| 369 AppendComment("Create shortcuts") | |
| 370 with Directory('ProgramMenuFolder'): | |
| 371 with Directory('ShortcutFolder', 'Dart Editor'): | |
| 372 with Component('shortcut'): | |
| 373 # When generating a shortcut we need an entry with | |
| 374 # a KeyPath (RegistryEntry) below - to be able to remove | |
| 375 # the shortcut again. The RemoveFolder tag is needed | |
| 376 # to clean up everything | |
| 377 Shortcut('editor_shortcut', 'Dart Editor', | |
| 378 '[RootInstallDir]DartEditor.exe') | |
| 379 RemoveFolder('RemoveShortcuts') | |
| 380 RegistryEntry('DartEditor') | |
| 381 with Feature(): | |
| 382 # We have only one feature, and it consists of all the | |
| 383 # files=components we have listed above" | |
| 384 ComponentRefs() | |
| 385 xml = GetContent() | |
| 386 if options.print_wxs: | |
| 387 print xml | |
| 388 GenerateInstaller(xml, options, temp_dir) | |
| 389 | |
| 390 if __name__ == '__main__': | |
| 391 sys.exit(Main(sys.argv)) | |
| OLD | NEW |