mirror of
https://github.com/dart-lang/sdk
synced 2024-09-19 21:21:41 +00:00
fe745b78c9
R=kasperl@google.com Review URL: https://codereview.chromium.org//103233006 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@30952 260f80e4-7a28-3924-810f-c04153c831b5
393 lines
13 KiB
Python
393 lines
13 KiB
Python
#!/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))
|