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