blob: 04df5753a8bd48f78a462aaf1ea68101362ce55f [file] [log] [blame]
#!/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))