Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(99)

Unified Diff: tools/create_windows_installer.py

Issue 43273003: Add script for generating a windows installer from an archived editor bundle. (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 7 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: tools/create_windows_installer.py
===================================================================
--- tools/create_windows_installer.py (revision 0)
+++ tools/create_windows_installer.py (revision 0)
@@ -0,0 +1,395 @@
+#!/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>]
kustermann 2013/10/25 09:39:07 Add "--print_wxs" as well here.
ricow1 2013/10/28 08:57:23 Done.
+#
+# This script assumes that wix is either in path or passed in as --wis_bin.
kustermann 2013/10/25 09:39:07 wis_bin -> wix_bin
ricow1 2013/10/28 08:57:23 Done.
+# 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 GetVersion(version):
kustermann 2013/10/25 09:39:07 GetVersion -> GetMicrosoftProductVersion
ricow1 2013/10/28 08:57:23 Done.
+ split_string = version.split('.')
kustermann 2013/10/25 09:39:07 split_string -> version_parts
ricow1 2013/10/28 08:57:23 Done.
+ if len(split_string) is not 4:
+ raise Exception(
+ "Version string (%s) does not follow specification" % version)
+ # Convert all to int to check that they are integers
kustermann 2013/10/25 09:39:07 Remove comment, that's obvious.
ricow1 2013/10/28 08:57:23 Done.
+ major = int(split_string[0])
+ minor = int(split_string[1])
+ build = int(split_string[2])
+ patch = int(split_string[3])
kustermann 2013/10/25 09:39:07 (major, minor, build, patch) = map(int, version_pa
ricow1 2013/10/28 08:57:23 Done.
+ 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):
+ to_append = []
+ for i in xrange(current_indentation):
+ to_append.append(' ')
+ to_append.append(data)
+ if new_line:
+ to_append.append('\n')
+ xml_content.append(''.join(to_append))
kustermann 2013/10/25 09:39:07 Make it simpler, something like: def Append(data,
ricow1 2013/10/28 08:57:23 Done.
+
+# Append without any indentation at the current position
+def AppendRaw(data, new_line=True):
+ if new_line:
+ xml_content.append(data + '\n')
+ else:
+ xml_content.append(data)
kustermann 2013/10/25 09:39:07 Maybe: xml_content.append(data + ('\n' if new_line
ricow1 2013/10/28 08:57:23 Done.
+
+def AppendComment(comment):
+ Append('<!--%s-->' % comment)
kustermann 2013/10/25 09:39:07 Indentation
ricow1 2013/10/28 08:57:23 Done.
+
+def AppendBlankLine():
+ Append('')
+
+def GetContent():
+ return ''.join(xml_content)
+
+def XmlHeader():
+ Append('<?xml version="1.0" encoding="UTF-8"?>')
+
+def TagIndent(str, indentation_string):
+ to_append = []
+ for x in indentation_string:
+ to_append.append(' ')
+ to_append.append(str)
+ return ''.join(to_append)
kustermann 2013/10/25 09:39:07 return ' ' * len(indentation_string) + str
ricow1 2013/10/28 08:57:23 Done.
+
+def IncreaseIndentation():
+ global current_indentation
+ current_indentation += 1
+
+def DecreaseIndentation():
+ global current_indentation
+ current_indentation -= 1
+
+
+GUID_PREFETCH_SIZE = 200
kustermann 2013/10/25 09:39:07 remove this GUID code, since you don't use it.
ricow1 2013/10/28 08:57:23 Done.
+GUID_CACHE = []
+def FillGuidCache():
+ p = subprocess.Popen("uuidgen -n%s" % GUID_PREFETCH_SIZE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ shell=True)
+ output, stderr = p.communicate()
+ if p.returncode != 0:
+ raise Exception(
+ "Could not get GUIDs, make sure uuidgen is installed")
+
+ for guid in output.splitlines():
+ GUID_CACHE.append(guid)
+
+def GetGuid():
+ if len(GUID_CACHE) == 0:
+ FillGuidCache()
+ return GUID_CACHE.pop()
+
+
+class WixAndProduct(object):
+ 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 get_product_name(self):
+ return 'Dart Editor'
+
+ def get_manufacturer(self):
+ return "Google"
+
+ def get_upgrade_code(self):
+ return UPGRADE_CODE
kustermann 2013/10/25 09:39:07 You could remove these two methods and use instanc
ricow1 2013/10/28 08:57:23 Done.
+
+ 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.get_product_name(), product))
+ Append(TagIndent('UpgradeCode="%s"' % self.get_upgrade_code(),
+ product))
+ Append(TagIndent('Language="1033"', product))
+ Append(TagIndent('Manufacturer="%s"' % self.get_manufacturer(),
+ product),
kustermann 2013/10/25 09:39:07 You always create these things manually, you could
ricow1 2013/10/28 08:57:23 Thought about it but decided against it to make th
+ 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()
kustermann 2013/10/25 09:39:07 You could extract the <Wix ...></Wix> to a differe
ricow1 2013/10/28 08:57:23 That would be 2 extra line, I really can't do that
+
+ def close_wix(self):
+ DecreaseIndentation()
+ Append('</Wix>')
+
+ def __init__(self, version):
+ self.version = version
+
+ def __enter__(self):
+ self.start_wix()
+ self.start_product()
+
+ def __exit__(self, *_):
+ self.close_product()
+ self.close_wix()
kustermann 2013/10/25 09:39:07 Move these three methods up (constructors should a
ricow1 2013/10/28 08:57:23 Done.
+
+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 __exit__(self, *_):
+ DecreaseIndentation()
+ Append('</Component>')
+ feature_components.append(self.id)
+
+ def __enter__(self):
+ component = '<Component '
+ Append(component, new_line=False)
+ AppendRaw('Id="%s"' % self.id)
+ Append(TagIndent('Guid="*">', component))
+ IncreaseIndentation()
+
+class Feature(object):
+ def __exit__(self, *_):
kustermann 2013/10/25 09:39:07 Sometimes you have "__enter__" before "__exit__" a
ricow1 2013/10/28 08:57:23 Done.
+ DecreaseIndentation()
+ Append('</Feature>')
+
+ 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 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))
kustermann 2013/10/25 09:39:07 Make it a raw string or two \\.
ricow1 2013/10/28 08:57:23 Done.
+ 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 InstallFiles(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):
+ InstallFiles(full_path)
+ elif os.path.isfile(full_path):
+ # We assume 1 file per component, a File is always a KeyPath
kustermann 2013/10/25 09:39:07 Nobody knows what a 'KeyPath' is, you could add a
ricow1 2013/10/28 08:57:23 Done.
+ 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'
+ if utils.IsWindows():
kustermann 2013/10/25 09:39:07 That is strange, I'd remove this and put an if s
ricow1 2013/10/28 08:57:23 Done.
+ 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')
kustermann 2013/10/25 09:39:07 Indentation
ricow1 2013/10/28 08:57:23 Done.
+ wixobj_file = os.path.join(temp_dir, 'installer.wixobj')
+ msi_file = os.path.join(temp_dir, 'installer.msi')
+ print 'Saving wxs output to: %s' % wxs_file
kustermann 2013/10/25 09:39:07 This comment is useless, since after running this
ricow1 2013/10/28 08:57:23 I disagree, it make you be able to see the progres
+ 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.call('%s %s -o %s' % (candle_bin, wxs_file, wixobj_file))
kustermann 2013/10/25 09:39:07 Please use 'subprocess.check_call' or veryfiy that
ricow1 2013/10/28 08:57:23 Done.
+ print 'Calling light on %s' % wixobj_file
+ subprocess.call('%s %s -o %s' % (light_bin, wixobj_file, msi_file))
kustermann 2013/10/25 09:39:07 msi_file -> options.msi_location + remove msi_file
ricow1 2013/10/28 08:57:23 Done.
+ print 'Copying msi file to ' % options.msi_location
+ shutil.copyfile(msi_file, options.msi_location)
+
+def Main(argv):
+ (options, args) = GetOptions()
+ if not options.version:
+ raise Exception('You must supply a version')
+ version = GetVersion(options.version)
kustermann 2013/10/25 09:39:07 You could validate here that the zip file exists.
ricow1 2013/10/28 08:57:23 Done.
+
+ with utils.TempDir('installer') as temp_dir:
+ ExtractZipFile(options.zip_file_location, temp_dir)
+ print "Generating wix XML"
+ XmlHeader()
+ with WixAndProduct(version):
+ AppendBlankLine()
+ root_dir = 'RootInstallDir'
kustermann 2013/10/25 09:39:07 You could actually remove this variable, and repla
ricow1 2013/10/28 08:57:23 ok
+ 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(root_dir, 'Dart Editor'):
+ AppendComment("Add all files and directories")
+ print 'Installing files and directories in xml'
+ InstallFiles(os.path.join(temp_dir, 'dart'))
kustermann 2013/10/25 09:39:07 Maybe rename to ListFiles?
ricow1 2013/10/28 08:57:23 Done.
+ AppendBlankLine()
+ AppendComment("Create shortcuts")
+ with Directory('ProgramMenuFolder'):
+ with Directory('ShortcutFolder', 'Dart Editor'):
+ with Component('shortcut'):
+ Shortcut('editor_shortcut', 'Dart Editor',
+ '[%s]DartEditor.exe' % root_dir)
+ RemoveFolder('RemoveShortcuts')
+ RegistryEntry('DartEditor')
kustermann 2013/10/25 09:39:07 Add a comment explaining how these three calls fit
ricow1 2013/10/28 08:57:23 Done.
+
+ with Feature():
kustermann 2013/10/25 09:39:07 Add a comment, like "We have only one feature and
ricow1 2013/10/28 08:57:23 Done.
+ ComponentRefs()
+ xml = GetContent()
+ if options.print_wxs:
+ print xml
+ GenerateInstaller(xml, options, temp_dir)
+
+if __name__ == '__main__':
+ sys.exit(Main(sys.argv))
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698