| Index: tools/create_windows_installer.py
|
| ===================================================================
|
| --- tools/create_windows_installer.py (revision 0)
|
| +++ tools/create_windows_installer.py (revision 0)
|
| @@ -0,0 +1,365 @@
|
| +#!/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>]
|
| +# [--print_wxs]
|
| +#
|
| +# This script assumes that wix is either in path or passed in as --wix_bin.
|
| +# 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 GetMicrosoftProductVersion(version):
|
| + version_parts = version.split('.')
|
| + if len(version_parts) is not 4:
|
| + raise Exception(
|
| + "Version string (%s) does not follow specification" % version)
|
| + (major, minor, build, patch) = map(int, version_parts)
|
| +
|
| + 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):
|
| + str = ((' ' * current_indentation) +
|
| + data +
|
| + ('\n' if new_line else ''))
|
| + xml_content.append(str)
|
| +
|
| +# Append without any indentation at the current position
|
| +def AppendRaw(data, new_line=True):
|
| + xml_content.append(data + ('\n' if new_line else ''))
|
| +
|
| +def AppendComment(comment):
|
| + Append('<!--%s-->' % comment)
|
| +
|
| +def AppendBlankLine():
|
| + Append('')
|
| +
|
| +def GetContent():
|
| + return ''.join(xml_content)
|
| +
|
| +def XmlHeader():
|
| + Append('<?xml version="1.0" encoding="UTF-8"?>')
|
| +
|
| +def TagIndent(str, indentation_string):
|
| + return ' ' * len(indentation_string) + str
|
| +
|
| +def IncreaseIndentation():
|
| + global current_indentation
|
| + current_indentation += 1
|
| +
|
| +def DecreaseIndentation():
|
| + global current_indentation
|
| + current_indentation -= 1
|
| +
|
| +class WixAndProduct(object):
|
| + def __init__(self, version):
|
| + self.version = version
|
| + self.product_name = 'Dart Editor'
|
| + self.manufacturer = 'Google Inc.'
|
| + self.upgrade_code = UPGRADE_CODE
|
| +
|
| + def __enter__(self):
|
| + self.start_wix()
|
| + self.start_product()
|
| +
|
| + def __exit__(self, *_):
|
| + self.close_product()
|
| + self.close_wix()
|
| +
|
| + 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 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.product_name, product))
|
| + Append(TagIndent('UpgradeCode="%s"' % self.upgrade_code,
|
| + product))
|
| + Append(TagIndent('Language="1033"', product))
|
| + Append(TagIndent('Manufacturer="%s"' % self.manufacturer,
|
| + product),
|
| + 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()
|
| +
|
| + def close_wix(self):
|
| + DecreaseIndentation()
|
| + Append('</Wix>')
|
| +
|
| +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 __enter__(self):
|
| + component = '<Component '
|
| + Append(component, new_line=False)
|
| + AppendRaw('Id="%s"' % self.id)
|
| + Append(TagIndent('Guid="*">', component))
|
| + IncreaseIndentation()
|
| +
|
| + def __exit__(self, *_):
|
| + DecreaseIndentation()
|
| + Append('</Component>')
|
| + feature_components.append(self.id)
|
| +
|
| +class Feature(object):
|
| + 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 __exit__(self, *_):
|
| + DecreaseIndentation()
|
| + Append('</Feature>')
|
| +
|
| +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))
|
| + 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 ListFiles(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):
|
| + ListFiles(full_path)
|
| + elif os.path.isfile(full_path):
|
| + # We assume 1 file per component, a File is always a KeyPath.
|
| + # A KeyPath on a file makes sure that we can always repair and
|
| + # remove that file in a consistent manner. A component
|
| + # can only have one child with a KeyPath.
|
| + 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'
|
| + 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')
|
| + wixobj_file = os.path.join(temp_dir, 'installer.wixobj')
|
| + print 'Saving wxs output to: %s' % wxs_file
|
| + 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.check_call('%s %s -o %s' % (candle_bin, wxs_file,
|
| + wixobj_file))
|
| + print 'Calling light on %s' % wixobj_file
|
| + subprocess.check_call('%s %s -o %s' % (light_bin, wixobj_file,
|
| + options.msi_location))
|
| + print 'Created msi file to %s' % options.msi_location
|
| +
|
| +def Main(argv):
|
| + if sys.platform != 'win32':
|
| + raise Exception("This script can only be run on windows")
|
| + (options, args) = GetOptions()
|
| + if not options.version:
|
| + raise Exception('You must supply a version')
|
| + if not os.path.isfile(options.zip_file_location):
|
| + raise Exception('You must pass in a valid zip file')
|
| +
|
| + version = GetMicrosoftProductVersion(options.version)
|
| + with utils.TempDir('installer') as temp_dir:
|
| + ExtractZipFile(options.zip_file_location, temp_dir)
|
| + print "Generating wix XML"
|
| + XmlHeader()
|
| + with WixAndProduct(version):
|
| + AppendBlankLine()
|
| + 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('RootInstallDir', 'Dart Editor'):
|
| + AppendComment("Add all files and directories")
|
| + print 'Installing files and directories in xml'
|
| + ListFiles(os.path.join(temp_dir, 'dart'))
|
| + AppendBlankLine()
|
| + AppendComment("Create shortcuts")
|
| + with Directory('ProgramMenuFolder'):
|
| + with Directory('ShortcutFolder', 'Dart Editor'):
|
| + with Component('shortcut'):
|
| + # When generating a shortcut we need an entry with
|
| + # a KeyPath (RegistryEntry) below - to be able to remove
|
| + # the shortcut again. The RemoveFolder tag is needed
|
| + # to clean up everything
|
| + Shortcut('editor_shortcut', 'Dart Editor',
|
| + '[RootInstallDir]DartEditor.exe')
|
| + RemoveFolder('RemoveShortcuts')
|
| + RegistryEntry('DartEditor')
|
| + with Feature():
|
| + # We have only one feature and that consist of all the
|
| + # files=components we have listed above"
|
| + ComponentRefs()
|
| + xml = GetContent()
|
| + if options.print_wxs:
|
| + print xml
|
| + GenerateInstaller(xml, options, temp_dir)
|
| +
|
| +if __name__ == '__main__':
|
| + sys.exit(Main(sys.argv))
|
|
|