Chromium Code Reviews| Index: tools/create_windows_installer.py |
| =================================================================== |
| --- tools/create_windows_installer.py (revision 0) |
| +++ tools/create_windows_installer.py (revision 0) |
| @@ -0,0 +1,395 @@ |
| +#!/usr/bin/env python |
| +# |
| +# Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| +# for details. All rights reserved. Use of this source code is governed by a |
| +# BSD-style license that can be found in the LICENSE file. |
| + |
| +# A script to generate a windows installer for the editor bundle. |
| +# As input the script takes a zip file, a version and the location |
| +# to store the resulting msi file in. |
| +# |
| +# Usage: ./tools/create_windows_installer.py --version <version> |
| +# --zip_file_location <zip_file> --msi_location <output> |
| +# [--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.
|
| +# |
| +# 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.
|
| +# You can get wix from http://wixtoolset.org/. |
| + |
| +import optparse |
| +import os |
| +import shutil |
| +import subprocess |
| +import sys |
| +import utils |
| +import zipfile |
| + |
| +# This should _never_ change, please don't change this value. |
| +UPGRADE_CODE = '7bacdc33-2e76-4f36-a206-ea58220c0b44' |
| + |
| +# The content of the xml |
| +xml_content = [] |
| + |
| +# The components we want to add to our feature. |
| +feature_components = [] |
| + |
| +# Indentation level, each level is indented 2 spaces |
| +current_indentation = 0 |
| + |
| +def GetOptions(): |
| + options = optparse.OptionParser(usage='usage: %prog [options]') |
| + options.add_option("--zip_file_location", |
| + help='Where the zip file including the editor is located.') |
| + options.add_option("--msi_location", |
| + help='Where to store the resulting msi.') |
| + options.add_option("--version", |
| + help='The version specified as Major.Minor.Build.Patch.') |
| + options.add_option("--wix_bin", |
| + help='The location of the wix binary files.') |
| + options.add_option("--print_wxs", action="store_true", dest="print_wxs", |
| + default=False, |
| + help="Prints the generated wxs to stdout.") |
| + return options.parse_args() |
| + |
| +# We combine the build and patch into a single entry since |
| +# the windows installer does _not_ consider a change in Patch |
| +# to require a new install. |
| +# In addition to that, the limits on the size are: |
| +# Major: 256 |
| +# Minor: 256 |
| +# Build: 65536 |
| +# To circumvent this we create the version like this: |
| +# Major.Minor.X |
| +# where X is Build<<9 + Patch |
| +# Example version 1.2.4.14 will go to 1.2.2062 |
| +def GetVersion(version): |
|
kustermann
2013/10/25 09:39:07
GetVersion -> GetMicrosoftProductVersion
ricow1
2013/10/28 08:57:23
Done.
|
| + split_string = version.split('.') |
|
kustermann
2013/10/25 09:39:07
split_string -> version_parts
ricow1
2013/10/28 08:57:23
Done.
|
| + if len(split_string) is not 4: |
| + raise Exception( |
| + "Version string (%s) does not follow specification" % version) |
| + # 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.
|
| + major = int(split_string[0]) |
| + minor = int(split_string[1]) |
| + build = int(split_string[2]) |
| + 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.
|
| + if build > 127 or patch > 511: |
| + raise Exception('Build/Patch can not be above 127/511') |
| + if major > 255 or minor > 255: |
| + raise Exception('Major/Minor can not be above 256') |
| + |
| + combined = (build << 9) + patch |
| + return '%s.%s.%s' % (major, minor, combined) |
| + |
| +# Append using the current indentation level |
| +def Append(data, new_line=True): |
| + to_append = [] |
| + for i in xrange(current_indentation): |
| + to_append.append(' ') |
| + to_append.append(data) |
| + if new_line: |
| + to_append.append('\n') |
| + 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.
|
| + |
| +# Append without any indentation at the current position |
| +def AppendRaw(data, new_line=True): |
| + if new_line: |
| + xml_content.append(data + '\n') |
| + else: |
| + 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.
|
| + |
| +def AppendComment(comment): |
| + Append('<!--%s-->' % comment) |
|
kustermann
2013/10/25 09:39:07
Indentation
ricow1
2013/10/28 08:57:23
Done.
|
| + |
| +def AppendBlankLine(): |
| + Append('') |
| + |
| +def GetContent(): |
| + return ''.join(xml_content) |
| + |
| +def XmlHeader(): |
| + Append('<?xml version="1.0" encoding="UTF-8"?>') |
| + |
| +def TagIndent(str, indentation_string): |
| + to_append = [] |
| + for x in indentation_string: |
| + to_append.append(' ') |
| + to_append.append(str) |
| + return ''.join(to_append) |
|
kustermann
2013/10/25 09:39:07
return ' ' * len(indentation_string) + str
ricow1
2013/10/28 08:57:23
Done.
|
| + |
| +def IncreaseIndentation(): |
| + global current_indentation |
| + current_indentation += 1 |
| + |
| +def DecreaseIndentation(): |
| + global current_indentation |
| + current_indentation -= 1 |
| + |
| + |
| +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.
|
| +GUID_CACHE = [] |
| +def FillGuidCache(): |
| + p = subprocess.Popen("uuidgen -n%s" % GUID_PREFETCH_SIZE, |
| + stdout=subprocess.PIPE, |
| + stderr=subprocess.PIPE, |
| + shell=True) |
| + output, stderr = p.communicate() |
| + if p.returncode != 0: |
| + raise Exception( |
| + "Could not get GUIDs, make sure uuidgen is installed") |
| + |
| + for guid in output.splitlines(): |
| + GUID_CACHE.append(guid) |
| + |
| +def GetGuid(): |
| + if len(GUID_CACHE) == 0: |
| + FillGuidCache() |
| + return GUID_CACHE.pop() |
| + |
| + |
| +class WixAndProduct(object): |
| + def get_product_id(self): |
| + # This needs to change on every install to guarantee that |
| + # we get a full uninstall + reinstall |
| + # We let wix choose. If we need to do patch releases later on |
| + # we need to retain the value over several installs. |
| + return '*' |
| + |
| + def get_product_name(self): |
| + return 'Dart Editor' |
| + |
| + def get_manufacturer(self): |
| + return "Google" |
| + |
| + def get_upgrade_code(self): |
| + 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.
|
| + |
| + def start_product(self): |
| + product = '<Product ' |
| + Append(product, new_line=False) |
| + AppendRaw('Id="%s"' % self.get_product_id()) |
| + Append(TagIndent('Version="%s"' % self.version, product)) |
| + Append(TagIndent('Name="%s"' % self.get_product_name(), product)) |
| + Append(TagIndent('UpgradeCode="%s"' % self.get_upgrade_code(), |
| + product)) |
| + Append(TagIndent('Language="1033"', product)) |
| + Append(TagIndent('Manufacturer="%s"' % self.get_manufacturer(), |
| + 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
|
| + new_line=False) |
| + AppendRaw('>') |
| + IncreaseIndentation() |
| + |
| + def close_product(self): |
| + DecreaseIndentation() |
| + Append('</Product>') |
| + |
| + def start_wix(self): |
| + Append('<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">') |
| + 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
|
| + |
| + def close_wix(self): |
| + DecreaseIndentation() |
| + Append('</Wix>') |
| + |
| + def __init__(self, version): |
| + self.version = version |
| + |
| + def __enter__(self): |
| + self.start_wix() |
| + self.start_product() |
| + |
| + def __exit__(self, *_): |
| + self.close_product() |
| + 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.
|
| + |
| +class Directory(object): |
| + def __init__(self, id, name=None): |
| + self.id = id |
| + self.name = name |
| + |
| + def __enter__(self): |
| + directory = '<Directory ' |
| + Append(directory, new_line=False) |
| + AppendRaw('Id="%s"' % self.id, new_line=self.name is not None) |
| + if self.name: |
| + Append(TagIndent('Name="%s"' % self.name, directory), new_line=False) |
| + AppendRaw('>') |
| + IncreaseIndentation() |
| + |
| + def __exit__(self, *_): |
| + DecreaseIndentation() |
| + Append('</Directory>') |
| + |
| +class Component(object): |
| + def __init__(self, id): |
| + self.id = 'CMP_%s' % id |
| + |
| + def __exit__(self, *_): |
| + DecreaseIndentation() |
| + Append('</Component>') |
| + feature_components.append(self.id) |
| + |
| + def __enter__(self): |
| + component = '<Component ' |
| + Append(component, new_line=False) |
| + AppendRaw('Id="%s"' % self.id) |
| + Append(TagIndent('Guid="*">', component)) |
| + IncreaseIndentation() |
| + |
| +class Feature(object): |
| + 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.
|
| + DecreaseIndentation() |
| + Append('</Feature>') |
| + |
| + def __enter__(self): |
| + feature = '<Feature ' |
| + Append(feature, new_line=False) |
| + AppendRaw('Id="MainFeature"') |
| + Append(TagIndent('Title="Dart Editor"', feature)) |
| + # Install by default |
| + Append(TagIndent('Level="1">', feature)) |
| + IncreaseIndentation() |
| + |
| +def Package(): |
| + package = '<Package ' |
| + Append(package, new_line=False) |
| + AppendRaw('InstallerVersion="301"') |
| + Append(TagIndent('Compressed="yes" />', package)) |
| + |
| +def MediaTemplate(): |
| + Append('<MediaTemplate EmbedCab="yes" />') |
| + |
| +def File(name, id): |
| + file = '<File ' |
| + Append(file, new_line=False) |
| + AppendRaw('Id="FILE_%s"' % id) |
| + Append(TagIndent('Source="%s"' % name, file)) |
| + Append(TagIndent('KeyPath="yes" />', file)) |
| + |
| +def Shortcut(id, name, ref): |
| + shortcut = '<Shortcut ' |
| + Append(shortcut, new_line=False) |
| + AppendRaw('Id="%s"' % id) |
| + Append(TagIndent('Name="%s"' % name, shortcut)) |
| + Append(TagIndent('Target="%s" />' % ref, shortcut)) |
| + |
| +def RemoveFolder(id): |
| + remove = '<RemoveFolder ' |
| + Append(remove, new_line=False) |
| + AppendRaw('Id="%s"' % id) |
| + Append(TagIndent('On="uninstall" />', remove)) |
| + |
| +def RegistryEntry(location): |
| + registry = '<RegistryValue ' |
| + Append(registry, new_line=False) |
| + AppendRaw('Root="HKCU"') |
| + 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.
|
| + Append(TagIndent('Name="installed"', registry)) |
| + Append(TagIndent('Type="integer"', registry)) |
| + Append(TagIndent('Value="1"', registry)) |
| + Append(TagIndent('KeyPath="yes" />', registry)) |
| + |
| + |
| +def MajorUpgrade(): |
| + upgrade = '<MajorUpgrade ' |
| + Append(upgrade, new_line=False) |
| + down_message = 'You already have a never version installed.' |
| + AppendRaw('DowngradeErrorMessage="%s" />' % down_message) |
| + |
| + |
| +# This is a very simplistic id generation. |
| +# Unfortunately there is no easy way to generate good names, |
| +# since there is a 72 character limit, and we have way longer |
| +# paths. We don't really have an issue with files and ids across |
| +# releases since we do full installs. |
| +counter = 0 |
| +def FileToId(name): |
| + global counter |
| + counter += 1 |
| + return '%s' % counter |
| + |
| +def InstallFiles(path): |
| + for entry in os.listdir(path): |
| + full_path = os.path.join(path, entry) |
| + id = FileToId(full_path) |
| + if os.path.isdir(full_path): |
| + with Directory('DIR_%s' % id, entry): |
| + InstallFiles(full_path) |
| + elif os.path.isfile(full_path): |
| + # 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.
|
| + with Component(id): |
| + File(full_path, id) |
| + |
| +def ComponentRefs(): |
| + for component in feature_components: |
| + Append('<ComponentRef Id="%s" />' % component) |
| + |
| +def ExtractZipFile(zip, temp_dir): |
| + print 'Extracting files' |
| + 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.
|
| + f = zipfile.ZipFile(zip) |
| + f.extractall(temp_dir) |
| + f.close() |
| + |
| +def GenerateInstaller(wxs_content, options, temp_dir): |
| + 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.
|
| + wixobj_file = os.path.join(temp_dir, 'installer.wixobj') |
| + msi_file = os.path.join(temp_dir, 'installer.msi') |
| + 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
|
| + with open(wxs_file, 'w') as f: |
| + f.write(wxs_content) |
| + |
| + candle_bin = 'candle.exe' |
| + light_bin = 'light.exe' |
| + if options.wix_bin: |
| + candle_bin = os.path.join(options.wix_bin, 'candle.exe') |
| + light_bin = os.path.join(options.wix_bin, 'light.exe') |
| + print 'Calling candle on %s' % wxs_file |
| + 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.
|
| + print 'Calling light on %s' % wixobj_file |
| + 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.
|
| + print 'Copying msi file to ' % options.msi_location |
| + shutil.copyfile(msi_file, options.msi_location) |
| + |
| +def Main(argv): |
| + (options, args) = GetOptions() |
| + if not options.version: |
| + raise Exception('You must supply a version') |
| + 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.
|
| + |
| + with utils.TempDir('installer') as temp_dir: |
| + ExtractZipFile(options.zip_file_location, temp_dir) |
| + print "Generating wix XML" |
| + XmlHeader() |
| + with WixAndProduct(version): |
| + AppendBlankLine() |
| + 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
|
| + Package() |
| + MediaTemplate() |
| + AppendComment('We always do a major upgrade, at least for now') |
| + MajorUpgrade() |
| + |
| + AppendComment('Directory structure') |
| + with Directory('TARGETDIR', 'SourceDir'): |
| + with Directory('ProgramFilesFolder'): |
| + with Directory(root_dir, 'Dart Editor'): |
| + AppendComment("Add all files and directories") |
| + print 'Installing files and directories in xml' |
| + 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.
|
| + AppendBlankLine() |
| + AppendComment("Create shortcuts") |
| + with Directory('ProgramMenuFolder'): |
| + with Directory('ShortcutFolder', 'Dart Editor'): |
| + with Component('shortcut'): |
| + Shortcut('editor_shortcut', 'Dart Editor', |
| + '[%s]DartEditor.exe' % root_dir) |
| + RemoveFolder('RemoveShortcuts') |
| + 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.
|
| + |
| + 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.
|
| + ComponentRefs() |
| + xml = GetContent() |
| + if options.print_wxs: |
| + print xml |
| + GenerateInstaller(xml, options, temp_dir) |
| + |
| +if __name__ == '__main__': |
| + sys.exit(Main(sys.argv)) |