#!/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)) |