| #!/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("--input_directory", | |
| help='Directory where all the files needed 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.") | |
| (options, args) = options.parse_args() | |
| if len(args) > 0: | |
| raise Exception("This script takes no arguments, only options") | |
| ValidateOptions(options) | |
| return options | |
| def ValidateOptions(options): | |
| if not options.version: | |
| raise Exception('You must supply a version') | |
| if options.zip_file_location and options.input_directory: | |
| raise Exception('Please pass either zip_file_location or input_directory') | |
| if not options.zip_file_location and not options.input_directory: | |
| raise Exception('Please pass either zip_file_location or input_directory') | |
| if (options.zip_file_location and | |
| not os.path.isfile(options.zip_file_location)): | |
| raise Exception('Passed in zip file not found') | |
| if (options.input_directory and | |
| not os.path.isdir(options.input_directory)): | |
| raise Exception('Passed in directory not found') | |
| def GetInputDirectory(options, temp_dir): | |
| if options.zip_file_location: | |
| ExtractZipFile(options.zip_file_location, temp_dir) | |
| return os.path.join(temp_dir, 'dart') | |
| return options.input_directory | |
| # 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 | |
| # Patch: 65536 | |
| # To circumvent this we create the version like this: | |
| # Major.Minor.X | |
| # from "major.minor.patch-prerelease.prerelease_patch" | |
| # where X is "patch<<10 + prerelease<<5 + prerelease_patch" | |
| # Example version 1.2.4-dev.2.3 will go to 1.2.4163 | |
| def GetMicrosoftProductVersion(version): | |
| version_parts = version.split('.') | |
| if len(version_parts) is not 5: | |
| raise Exception( | |
| "Version string (%s) does not follow specification" % version) | |
| (major, minor, patch, prerelease, prerelease_patch) = map(int, version_parts) | |
| if major > 255 or minor > 255: | |
| raise Exception('Major/Minor can not be above 256') | |
| if patch > 63: | |
| raise Exception('Patch can not be above 63') | |
| if prerelease > 31: | |
| raise Exception('Prerelease can not be above 31') | |
| if prerelease_patch > 31: | |
| raise Exception('PrereleasePatch can not be above 31') | |
| combined = (patch << 10) + (prerelease << 5) + prerelease_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 = GetOptions() | |
| version = GetMicrosoftProductVersion(options.version) | |
| with utils.TempDir('installer') as temp_dir: | |
| input_location = GetInputDirectory(options, 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(input_location) | |
| 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)) |