Chromium Code Reviews| 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>] | |
|
kustermann
2013/10/25 09:39:07
Add "--print_wxs" as well here.
ricow1
2013/10/28 08:57:23
Done.
| |
| 14 # | |
| 15 # This script assumes that wix is either in path or passed in as --wis_bin. | |
|
kustermann
2013/10/25 09:39:07
wis_bin -> wix_bin
ricow1
2013/10/28 08:57:23
Done.
| |
| 16 # You can get wix from http://wixtoolset.org/. | |
| 17 | |
| 18 import optparse | |
| 19 import os | |
| 20 import shutil | |
| 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("--msi_location", | |
| 43 help='Where to store the resulting msi.') | |
| 44 options.add_option("--version", | |
| 45 help='The version specified as Major.Minor.Build.Patch.') | |
| 46 options.add_option("--wix_bin", | |
| 47 help='The location of the wix binary files.') | |
| 48 options.add_option("--print_wxs", action="store_true", dest="print_wxs", | |
| 49 default=False, | |
| 50 help="Prints the generated wxs to stdout.") | |
| 51 return options.parse_args() | |
| 52 | |
| 53 # We combine the build and patch into a single entry since | |
| 54 # the windows installer does _not_ consider a change in Patch | |
| 55 # to require a new install. | |
| 56 # In addition to that, the limits on the size are: | |
| 57 # Major: 256 | |
| 58 # Minor: 256 | |
| 59 # Build: 65536 | |
| 60 # To circumvent this we create the version like this: | |
| 61 # Major.Minor.X | |
| 62 # where X is Build<<9 + Patch | |
| 63 # Example version 1.2.4.14 will go to 1.2.2062 | |
| 64 def GetVersion(version): | |
|
kustermann
2013/10/25 09:39:07
GetVersion -> GetMicrosoftProductVersion
ricow1
2013/10/28 08:57:23
Done.
| |
| 65 split_string = version.split('.') | |
|
kustermann
2013/10/25 09:39:07
split_string -> version_parts
ricow1
2013/10/28 08:57:23
Done.
| |
| 66 if len(split_string) is not 4: | |
| 67 raise Exception( | |
| 68 "Version string (%s) does not follow specification" % version) | |
| 69 # Convert all to int to check that they are integers | |
|
kustermann
2013/10/25 09:39:07
Remove comment, that's obvious.
ricow1
2013/10/28 08:57:23
Done.
| |
| 70 major = int(split_string[0]) | |
| 71 minor = int(split_string[1]) | |
| 72 build = int(split_string[2]) | |
| 73 patch = int(split_string[3]) | |
|
kustermann
2013/10/25 09:39:07
(major, minor, build, patch) = map(int, version_pa
ricow1
2013/10/28 08:57:23
Done.
| |
| 74 if build > 127 or patch > 511: | |
| 75 raise Exception('Build/Patch can not be above 127/511') | |
| 76 if major > 255 or minor > 255: | |
| 77 raise Exception('Major/Minor can not be above 256') | |
| 78 | |
| 79 combined = (build << 9) + patch | |
| 80 return '%s.%s.%s' % (major, minor, combined) | |
| 81 | |
| 82 # Append using the current indentation level | |
| 83 def Append(data, new_line=True): | |
| 84 to_append = [] | |
| 85 for i in xrange(current_indentation): | |
| 86 to_append.append(' ') | |
| 87 to_append.append(data) | |
| 88 if new_line: | |
| 89 to_append.append('\n') | |
| 90 xml_content.append(''.join(to_append)) | |
|
kustermann
2013/10/25 09:39:07
Make it simpler, something like:
def Append(data,
ricow1
2013/10/28 08:57:23
Done.
| |
| 91 | |
| 92 # Append without any indentation at the current position | |
| 93 def AppendRaw(data, new_line=True): | |
| 94 if new_line: | |
| 95 xml_content.append(data + '\n') | |
| 96 else: | |
| 97 xml_content.append(data) | |
|
kustermann
2013/10/25 09:39:07
Maybe:
xml_content.append(data + ('\n' if new_line
ricow1
2013/10/28 08:57:23
Done.
| |
| 98 | |
| 99 def AppendComment(comment): | |
| 100 Append('<!--%s-->' % comment) | |
|
kustermann
2013/10/25 09:39:07
Indentation
ricow1
2013/10/28 08:57:23
Done.
| |
| 101 | |
| 102 def AppendBlankLine(): | |
| 103 Append('') | |
| 104 | |
| 105 def GetContent(): | |
| 106 return ''.join(xml_content) | |
| 107 | |
| 108 def XmlHeader(): | |
| 109 Append('<?xml version="1.0" encoding="UTF-8"?>') | |
| 110 | |
| 111 def TagIndent(str, indentation_string): | |
| 112 to_append = [] | |
| 113 for x in indentation_string: | |
| 114 to_append.append(' ') | |
| 115 to_append.append(str) | |
| 116 return ''.join(to_append) | |
|
kustermann
2013/10/25 09:39:07
return ' ' * len(indentation_string) + str
ricow1
2013/10/28 08:57:23
Done.
| |
| 117 | |
| 118 def IncreaseIndentation(): | |
| 119 global current_indentation | |
| 120 current_indentation += 1 | |
| 121 | |
| 122 def DecreaseIndentation(): | |
| 123 global current_indentation | |
| 124 current_indentation -= 1 | |
| 125 | |
| 126 | |
| 127 GUID_PREFETCH_SIZE = 200 | |
|
kustermann
2013/10/25 09:39:07
remove this GUID code, since you don't use it.
ricow1
2013/10/28 08:57:23
Done.
| |
| 128 GUID_CACHE = [] | |
| 129 def FillGuidCache(): | |
| 130 p = subprocess.Popen("uuidgen -n%s" % GUID_PREFETCH_SIZE, | |
| 131 stdout=subprocess.PIPE, | |
| 132 stderr=subprocess.PIPE, | |
| 133 shell=True) | |
| 134 output, stderr = p.communicate() | |
| 135 if p.returncode != 0: | |
| 136 raise Exception( | |
| 137 "Could not get GUIDs, make sure uuidgen is installed") | |
| 138 | |
| 139 for guid in output.splitlines(): | |
| 140 GUID_CACHE.append(guid) | |
| 141 | |
| 142 def GetGuid(): | |
| 143 if len(GUID_CACHE) == 0: | |
| 144 FillGuidCache() | |
| 145 return GUID_CACHE.pop() | |
| 146 | |
| 147 | |
| 148 class WixAndProduct(object): | |
| 149 def get_product_id(self): | |
| 150 # This needs to change on every install to guarantee that | |
| 151 # we get a full uninstall + reinstall | |
| 152 # We let wix choose. If we need to do patch releases later on | |
| 153 # we need to retain the value over several installs. | |
| 154 return '*' | |
| 155 | |
| 156 def get_product_name(self): | |
| 157 return 'Dart Editor' | |
| 158 | |
| 159 def get_manufacturer(self): | |
| 160 return "Google" | |
| 161 | |
| 162 def get_upgrade_code(self): | |
| 163 return UPGRADE_CODE | |
|
kustermann
2013/10/25 09:39:07
You could remove these two methods and use instanc
ricow1
2013/10/28 08:57:23
Done.
| |
| 164 | |
| 165 def start_product(self): | |
| 166 product = '<Product ' | |
| 167 Append(product, new_line=False) | |
| 168 AppendRaw('Id="%s"' % self.get_product_id()) | |
| 169 Append(TagIndent('Version="%s"' % self.version, product)) | |
| 170 Append(TagIndent('Name="%s"' % self.get_product_name(), product)) | |
| 171 Append(TagIndent('UpgradeCode="%s"' % self.get_upgrade_code(), | |
| 172 product)) | |
| 173 Append(TagIndent('Language="1033"', product)) | |
| 174 Append(TagIndent('Manufacturer="%s"' % self.get_manufacturer(), | |
| 175 product), | |
|
kustermann
2013/10/25 09:39:07
You always create these things manually, you could
ricow1
2013/10/28 08:57:23
Thought about it but decided against it to make th
| |
| 176 new_line=False) | |
| 177 AppendRaw('>') | |
| 178 IncreaseIndentation() | |
| 179 | |
| 180 def close_product(self): | |
| 181 DecreaseIndentation() | |
| 182 Append('</Product>') | |
| 183 | |
| 184 def start_wix(self): | |
| 185 Append('<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">') | |
| 186 IncreaseIndentation() | |
|
kustermann
2013/10/25 09:39:07
You could extract the <Wix ...></Wix> to a differe
ricow1
2013/10/28 08:57:23
That would be 2 extra line, I really can't do that
| |
| 187 | |
| 188 def close_wix(self): | |
| 189 DecreaseIndentation() | |
| 190 Append('</Wix>') | |
| 191 | |
| 192 def __init__(self, version): | |
| 193 self.version = version | |
| 194 | |
| 195 def __enter__(self): | |
| 196 self.start_wix() | |
| 197 self.start_product() | |
| 198 | |
| 199 def __exit__(self, *_): | |
| 200 self.close_product() | |
| 201 self.close_wix() | |
|
kustermann
2013/10/25 09:39:07
Move these three methods up (constructors should a
ricow1
2013/10/28 08:57:23
Done.
| |
| 202 | |
| 203 class Directory(object): | |
| 204 def __init__(self, id, name=None): | |
| 205 self.id = id | |
| 206 self.name = name | |
| 207 | |
| 208 def __enter__(self): | |
| 209 directory = '<Directory ' | |
| 210 Append(directory, new_line=False) | |
| 211 AppendRaw('Id="%s"' % self.id, new_line=self.name is not None) | |
| 212 if self.name: | |
| 213 Append(TagIndent('Name="%s"' % self.name, directory), new_line=False) | |
| 214 AppendRaw('>') | |
| 215 IncreaseIndentation() | |
| 216 | |
| 217 def __exit__(self, *_): | |
| 218 DecreaseIndentation() | |
| 219 Append('</Directory>') | |
| 220 | |
| 221 class Component(object): | |
| 222 def __init__(self, id): | |
| 223 self.id = 'CMP_%s' % id | |
| 224 | |
| 225 def __exit__(self, *_): | |
| 226 DecreaseIndentation() | |
| 227 Append('</Component>') | |
| 228 feature_components.append(self.id) | |
| 229 | |
| 230 def __enter__(self): | |
| 231 component = '<Component ' | |
| 232 Append(component, new_line=False) | |
| 233 AppendRaw('Id="%s"' % self.id) | |
| 234 Append(TagIndent('Guid="*">', component)) | |
| 235 IncreaseIndentation() | |
| 236 | |
| 237 class Feature(object): | |
| 238 def __exit__(self, *_): | |
|
kustermann
2013/10/25 09:39:07
Sometimes you have "__enter__" before "__exit__" a
ricow1
2013/10/28 08:57:23
Done.
| |
| 239 DecreaseIndentation() | |
| 240 Append('</Feature>') | |
| 241 | |
| 242 def __enter__(self): | |
| 243 feature = '<Feature ' | |
| 244 Append(feature, new_line=False) | |
| 245 AppendRaw('Id="MainFeature"') | |
| 246 Append(TagIndent('Title="Dart Editor"', feature)) | |
| 247 # Install by default | |
| 248 Append(TagIndent('Level="1">', feature)) | |
| 249 IncreaseIndentation() | |
| 250 | |
| 251 def Package(): | |
| 252 package = '<Package ' | |
| 253 Append(package, new_line=False) | |
| 254 AppendRaw('InstallerVersion="301"') | |
| 255 Append(TagIndent('Compressed="yes" />', package)) | |
| 256 | |
| 257 def MediaTemplate(): | |
| 258 Append('<MediaTemplate EmbedCab="yes" />') | |
| 259 | |
| 260 def File(name, id): | |
| 261 file = '<File ' | |
| 262 Append(file, new_line=False) | |
| 263 AppendRaw('Id="FILE_%s"' % id) | |
| 264 Append(TagIndent('Source="%s"' % name, file)) | |
| 265 Append(TagIndent('KeyPath="yes" />', file)) | |
| 266 | |
| 267 def Shortcut(id, name, ref): | |
| 268 shortcut = '<Shortcut ' | |
| 269 Append(shortcut, new_line=False) | |
| 270 AppendRaw('Id="%s"' % id) | |
| 271 Append(TagIndent('Name="%s"' % name, shortcut)) | |
| 272 Append(TagIndent('Target="%s" />' % ref, shortcut)) | |
| 273 | |
| 274 def RemoveFolder(id): | |
| 275 remove = '<RemoveFolder ' | |
| 276 Append(remove, new_line=False) | |
| 277 AppendRaw('Id="%s"' % id) | |
| 278 Append(TagIndent('On="uninstall" />', remove)) | |
| 279 | |
| 280 def RegistryEntry(location): | |
| 281 registry = '<RegistryValue ' | |
| 282 Append(registry, new_line=False) | |
| 283 AppendRaw('Root="HKCU"') | |
| 284 Append(TagIndent('Key="Software\Microsoft\%s"' % location, registry)) | |
|
kustermann
2013/10/25 09:39:07
Make it a raw string or two \\.
ricow1
2013/10/28 08:57:23
Done.
| |
| 285 Append(TagIndent('Name="installed"', registry)) | |
| 286 Append(TagIndent('Type="integer"', registry)) | |
| 287 Append(TagIndent('Value="1"', registry)) | |
| 288 Append(TagIndent('KeyPath="yes" />', registry)) | |
| 289 | |
| 290 | |
| 291 def MajorUpgrade(): | |
| 292 upgrade = '<MajorUpgrade ' | |
| 293 Append(upgrade, new_line=False) | |
| 294 down_message = 'You already have a never version installed.' | |
| 295 AppendRaw('DowngradeErrorMessage="%s" />' % down_message) | |
| 296 | |
| 297 | |
| 298 # This is a very simplistic id generation. | |
| 299 # Unfortunately there is no easy way to generate good names, | |
| 300 # since there is a 72 character limit, and we have way longer | |
| 301 # paths. We don't really have an issue with files and ids across | |
| 302 # releases since we do full installs. | |
| 303 counter = 0 | |
| 304 def FileToId(name): | |
| 305 global counter | |
| 306 counter += 1 | |
| 307 return '%s' % counter | |
| 308 | |
| 309 def InstallFiles(path): | |
| 310 for entry in os.listdir(path): | |
| 311 full_path = os.path.join(path, entry) | |
| 312 id = FileToId(full_path) | |
| 313 if os.path.isdir(full_path): | |
| 314 with Directory('DIR_%s' % id, entry): | |
| 315 InstallFiles(full_path) | |
| 316 elif os.path.isfile(full_path): | |
| 317 # We assume 1 file per component, a File is always a KeyPath | |
|
kustermann
2013/10/25 09:39:07
Nobody knows what a 'KeyPath' is, you could add a
ricow1
2013/10/28 08:57:23
Done.
| |
| 318 with Component(id): | |
| 319 File(full_path, id) | |
| 320 | |
| 321 def ComponentRefs(): | |
| 322 for component in feature_components: | |
| 323 Append('<ComponentRef Id="%s" />' % component) | |
| 324 | |
| 325 def ExtractZipFile(zip, temp_dir): | |
| 326 print 'Extracting files' | |
| 327 if utils.IsWindows(): | |
|
kustermann
2013/10/25 09:39:07
That is strange, I'd remove this and put an
if s
ricow1
2013/10/28 08:57:23
Done.
| |
| 328 f = zipfile.ZipFile(zip) | |
| 329 f.extractall(temp_dir) | |
| 330 f.close() | |
| 331 | |
| 332 def GenerateInstaller(wxs_content, options, temp_dir): | |
| 333 wxs_file = os.path.join(temp_dir, 'installer.wxs') | |
|
kustermann
2013/10/25 09:39:07
Indentation
ricow1
2013/10/28 08:57:23
Done.
| |
| 334 wixobj_file = os.path.join(temp_dir, 'installer.wixobj') | |
| 335 msi_file = os.path.join(temp_dir, 'installer.msi') | |
| 336 print 'Saving wxs output to: %s' % wxs_file | |
|
kustermann
2013/10/25 09:39:07
This comment is useless, since after running this
ricow1
2013/10/28 08:57:23
I disagree, it make you be able to see the progres
| |
| 337 with open(wxs_file, 'w') as f: | |
| 338 f.write(wxs_content) | |
| 339 | |
| 340 candle_bin = 'candle.exe' | |
| 341 light_bin = 'light.exe' | |
| 342 if options.wix_bin: | |
| 343 candle_bin = os.path.join(options.wix_bin, 'candle.exe') | |
| 344 light_bin = os.path.join(options.wix_bin, 'light.exe') | |
| 345 print 'Calling candle on %s' % wxs_file | |
| 346 subprocess.call('%s %s -o %s' % (candle_bin, wxs_file, wixobj_file)) | |
|
kustermann
2013/10/25 09:39:07
Please use 'subprocess.check_call' or veryfiy that
ricow1
2013/10/28 08:57:23
Done.
| |
| 347 print 'Calling light on %s' % wixobj_file | |
| 348 subprocess.call('%s %s -o %s' % (light_bin, wixobj_file, msi_file)) | |
|
kustermann
2013/10/25 09:39:07
msi_file -> options.msi_location
+ remove msi_file
ricow1
2013/10/28 08:57:23
Done.
| |
| 349 print 'Copying msi file to ' % options.msi_location | |
| 350 shutil.copyfile(msi_file, options.msi_location) | |
| 351 | |
| 352 def Main(argv): | |
| 353 (options, args) = GetOptions() | |
| 354 if not options.version: | |
| 355 raise Exception('You must supply a version') | |
| 356 version = GetVersion(options.version) | |
|
kustermann
2013/10/25 09:39:07
You could validate here that the zip file exists.
ricow1
2013/10/28 08:57:23
Done.
| |
| 357 | |
| 358 with utils.TempDir('installer') as temp_dir: | |
| 359 ExtractZipFile(options.zip_file_location, temp_dir) | |
| 360 print "Generating wix XML" | |
| 361 XmlHeader() | |
| 362 with WixAndProduct(version): | |
| 363 AppendBlankLine() | |
| 364 root_dir = 'RootInstallDir' | |
|
kustermann
2013/10/25 09:39:07
You could actually remove this variable, and repla
ricow1
2013/10/28 08:57:23
ok
| |
| 365 Package() | |
| 366 MediaTemplate() | |
| 367 AppendComment('We always do a major upgrade, at least for now') | |
| 368 MajorUpgrade() | |
| 369 | |
| 370 AppendComment('Directory structure') | |
| 371 with Directory('TARGETDIR', 'SourceDir'): | |
| 372 with Directory('ProgramFilesFolder'): | |
| 373 with Directory(root_dir, 'Dart Editor'): | |
| 374 AppendComment("Add all files and directories") | |
| 375 print 'Installing files and directories in xml' | |
| 376 InstallFiles(os.path.join(temp_dir, 'dart')) | |
|
kustermann
2013/10/25 09:39:07
Maybe rename to ListFiles?
ricow1
2013/10/28 08:57:23
Done.
| |
| 377 AppendBlankLine() | |
| 378 AppendComment("Create shortcuts") | |
| 379 with Directory('ProgramMenuFolder'): | |
| 380 with Directory('ShortcutFolder', 'Dart Editor'): | |
| 381 with Component('shortcut'): | |
| 382 Shortcut('editor_shortcut', 'Dart Editor', | |
| 383 '[%s]DartEditor.exe' % root_dir) | |
| 384 RemoveFolder('RemoveShortcuts') | |
| 385 RegistryEntry('DartEditor') | |
|
kustermann
2013/10/25 09:39:07
Add a comment explaining how these three calls fit
ricow1
2013/10/28 08:57:23
Done.
| |
| 386 | |
| 387 with Feature(): | |
|
kustermann
2013/10/25 09:39:07
Add a comment, like "We have only one feature and
ricow1
2013/10/28 08:57:23
Done.
| |
| 388 ComponentRefs() | |
| 389 xml = GetContent() | |
| 390 if options.print_wxs: | |
| 391 print xml | |
| 392 GenerateInstaller(xml, options, temp_dir) | |
| 393 | |
| 394 if __name__ == '__main__': | |
| 395 sys.exit(Main(sys.argv)) | |
| OLD | NEW |