OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # | |
3 # Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
4 # for details. All rights reserved. Use of this source code is governed by a | |
5 # BSD-style license that can be found in the LICENSE file. | |
6 | |
7 # A script to generate a windows installer for the editor bundle. | |
8 # As input the script takes a zip file, a version and the location | |
9 # to store the resulting msi file in. | |
10 # | |
11 # Usage: ./tools/create_windows_installer.py --version <version> | |
12 # --zip_file_location <zip_file> --msi_location <output> | |
13 # [--wix_bin <wix_bin_location>] | |
14 # [--print_wxs] | |
15 # | |
16 # This script assumes that wix is either in path or passed in as --wix_bin. | |
17 # You can get wix from http://wixtoolset.org/. | |
18 | |
19 import optparse | |
20 import os | |
21 import subprocess | |
22 import sys | |
23 import utils | |
24 import zipfile | |
25 | |
26 # This should _never_ change, please don't change this value. | |
27 UPGRADE_CODE = '7bacdc33-2e76-4f36-a206-ea58220c0b44' | |
28 | |
29 # The content of the xml | |
30 xml_content = [] | |
31 | |
32 # The components we want to add to our feature. | |
33 feature_components = [] | |
34 | |
35 # Indentation level, each level is indented 2 spaces | |
36 current_indentation = 0 | |
37 | |
38 def GetOptions(): | |
39 options = optparse.OptionParser(usage='usage: %prog [options]') | |
40 options.add_option("--zip_file_location", | |
41 help='Where the zip file including the editor is located.') | |
42 options.add_option("--input_directory", | |
43 help='Directory where all the files needed is located.') | |
44 options.add_option("--msi_location", | |
45 help='Where to store the resulting msi.') | |
46 options.add_option("--version", | |
47 help='The version specified as Major.Minor.Build.Patch.') | |
48 options.add_option("--wix_bin", | |
49 help='The location of the wix binary files.') | |
50 options.add_option("--print_wxs", action="store_true", dest="print_wxs", | |
51 default=False, | |
52 help="Prints the generated wxs to stdout.") | |
53 (options, args) = options.parse_args() | |
54 if len(args) > 0: | |
55 raise Exception("This script takes no arguments, only options") | |
56 ValidateOptions(options) | |
57 return options | |
58 | |
59 def ValidateOptions(options): | |
60 if not options.version: | |
61 raise Exception('You must supply a version') | |
62 if options.zip_file_location and options.input_directory: | |
63 raise Exception('Please pass either zip_file_location or input_directory') | |
64 if not options.zip_file_location and not options.input_directory: | |
65 raise Exception('Please pass either zip_file_location or input_directory') | |
66 if (options.zip_file_location and | |
67 not os.path.isfile(options.zip_file_location)): | |
68 raise Exception('Passed in zip file not found') | |
69 if (options.input_directory and | |
70 not os.path.isdir(options.input_directory)): | |
71 raise Exception('Passed in directory not found') | |
72 | |
73 def GetInputDirectory(options, temp_dir): | |
74 if options.zip_file_location: | |
75 ExtractZipFile(options.zip_file_location, temp_dir) | |
76 return os.path.join(temp_dir, 'dart') | |
77 return options.input_directory | |
78 | |
79 # We combine the build and patch into a single entry since | |
80 # the windows installer does _not_ consider a change in Patch | |
81 # to require a new install. | |
82 # In addition to that, the limits on the size are: | |
83 # Major: 256 | |
84 # Minor: 256 | |
85 # Patch: 65536 | |
86 # To circumvent this we create the version like this: | |
87 # Major.Minor.X | |
88 # from "major.minor.patch-prerelease.prerelease_patch" | |
89 # where X is "patch<<10 + prerelease<<5 + prerelease_patch" | |
90 # Example version 1.2.4-dev.2.3 will go to 1.2.4163 | |
91 def GetMicrosoftProductVersion(version): | |
92 version_parts = version.split('.') | |
93 if len(version_parts) is not 5: | |
94 raise Exception( | |
95 "Version string (%s) does not follow specification" % version) | |
96 (major, minor, patch, prerelease, prerelease_patch) = map(int, version_parts) | |
97 | |
98 if major > 255 or minor > 255: | |
99 raise Exception('Major/Minor can not be above 256') | |
100 if patch > 63: | |
101 raise Exception('Patch can not be above 63') | |
102 if prerelease > 31: | |
103 raise Exception('Prerelease can not be above 31') | |
104 if prerelease_patch > 31: | |
105 raise Exception('PrereleasePatch can not be above 31') | |
106 | |
107 combined = (patch << 10) + (prerelease << 5) + prerelease_patch | |
108 return '%s.%s.%s' % (major, minor, combined) | |
109 | |
110 # Append using the current indentation level | |
111 def Append(data, new_line=True): | |
112 str = ((' ' * current_indentation) + | |
113 data + | |
114 ('\n' if new_line else '')) | |
115 xml_content.append(str) | |
116 | |
117 # Append without any indentation at the current position | |
118 def AppendRaw(data, new_line=True): | |
119 xml_content.append(data + ('\n' if new_line else '')) | |
120 | |
121 def AppendComment(comment): | |
122 Append('<!--%s-->' % comment) | |
123 | |
124 def AppendBlankLine(): | |
125 Append('') | |
126 | |
127 def GetContent(): | |
128 return ''.join(xml_content) | |
129 | |
130 def XmlHeader(): | |
131 Append('<?xml version="1.0" encoding="UTF-8"?>') | |
132 | |
133 def TagIndent(str, indentation_string): | |
134 return ' ' * len(indentation_string) + str | |
135 | |
136 def IncreaseIndentation(): | |
137 global current_indentation | |
138 current_indentation += 1 | |
139 | |
140 def DecreaseIndentation(): | |
141 global current_indentation | |
142 current_indentation -= 1 | |
143 | |
144 class WixAndProduct(object): | |
145 def __init__(self, version): | |
146 self.version = version | |
147 self.product_name = 'Dart Editor' | |
148 self.manufacturer = 'Google Inc.' | |
149 self.upgrade_code = UPGRADE_CODE | |
150 | |
151 def __enter__(self): | |
152 self.start_wix() | |
153 self.start_product() | |
154 | |
155 def __exit__(self, *_): | |
156 self.close_product() | |
157 self.close_wix() | |
158 | |
159 def get_product_id(self): | |
160 # This needs to change on every install to guarantee that | |
161 # we get a full uninstall + reinstall | |
162 # We let wix choose. If we need to do patch releases later on | |
163 # we need to retain the value over several installs. | |
164 return '*' | |
165 | |
166 def start_product(self): | |
167 product = '<Product ' | |
168 Append(product, new_line=False) | |
169 AppendRaw('Id="%s"' % self.get_product_id()) | |
170 Append(TagIndent('Version="%s"' % self.version, product)) | |
171 Append(TagIndent('Name="%s"' % self.product_name, product)) | |
172 Append(TagIndent('UpgradeCode="%s"' % self.upgrade_code, | |
173 product)) | |
174 Append(TagIndent('Language="1033"', product)) | |
175 Append(TagIndent('Manufacturer="%s"' % self.manufacturer, | |
176 product), | |
177 new_line=False) | |
178 AppendRaw('>') | |
179 IncreaseIndentation() | |
180 | |
181 def close_product(self): | |
182 DecreaseIndentation() | |
183 Append('</Product>') | |
184 | |
185 def start_wix(self): | |
186 Append('<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">') | |
187 IncreaseIndentation() | |
188 | |
189 def close_wix(self): | |
190 DecreaseIndentation() | |
191 Append('</Wix>') | |
192 | |
193 class Directory(object): | |
194 def __init__(self, id, name=None): | |
195 self.id = id | |
196 self.name = name | |
197 | |
198 def __enter__(self): | |
199 directory = '<Directory ' | |
200 Append(directory, new_line=False) | |
201 AppendRaw('Id="%s"' % self.id, new_line=self.name is not None) | |
202 if self.name: | |
203 Append(TagIndent('Name="%s"' % self.name, directory), new_line=False) | |
204 AppendRaw('>') | |
205 IncreaseIndentation() | |
206 | |
207 def __exit__(self, *_): | |
208 DecreaseIndentation() | |
209 Append('</Directory>') | |
210 | |
211 class Component(object): | |
212 def __init__(self, id): | |
213 self.id = 'CMP_%s' % id | |
214 | |
215 def __enter__(self): | |
216 component = '<Component ' | |
217 Append(component, new_line=False) | |
218 AppendRaw('Id="%s"' % self.id) | |
219 Append(TagIndent('Guid="*">', component)) | |
220 IncreaseIndentation() | |
221 | |
222 def __exit__(self, *_): | |
223 DecreaseIndentation() | |
224 Append('</Component>') | |
225 feature_components.append(self.id) | |
226 | |
227 class Feature(object): | |
228 def __enter__(self): | |
229 feature = '<Feature ' | |
230 Append(feature, new_line=False) | |
231 AppendRaw('Id="MainFeature"') | |
232 Append(TagIndent('Title="Dart Editor"', feature)) | |
233 # Install by default | |
234 Append(TagIndent('Level="1">', feature)) | |
235 IncreaseIndentation() | |
236 | |
237 def __exit__(self, *_): | |
238 DecreaseIndentation() | |
239 Append('</Feature>') | |
240 | |
241 def Package(): | |
242 package = '<Package ' | |
243 Append(package, new_line=False) | |
244 AppendRaw('InstallerVersion="301"') | |
245 Append(TagIndent('Compressed="yes" />', package)) | |
246 | |
247 def MediaTemplate(): | |
248 Append('<MediaTemplate EmbedCab="yes" />') | |
249 | |
250 def File(name, id): | |
251 file = '<File ' | |
252 Append(file, new_line=False) | |
253 AppendRaw('Id="FILE_%s"' % id) | |
254 Append(TagIndent('Source="%s"' % name, file)) | |
255 Append(TagIndent('KeyPath="yes" />', file)) | |
256 | |
257 def Shortcut(id, name, ref): | |
258 shortcut = '<Shortcut ' | |
259 Append(shortcut, new_line=False) | |
260 AppendRaw('Id="%s"' % id) | |
261 Append(TagIndent('Name="%s"' % name, shortcut)) | |
262 Append(TagIndent('Target="%s" />' % ref, shortcut)) | |
263 | |
264 def RemoveFolder(id): | |
265 remove = '<RemoveFolder ' | |
266 Append(remove, new_line=False) | |
267 AppendRaw('Id="%s"' % id) | |
268 Append(TagIndent('On="uninstall" />', remove)) | |
269 | |
270 def RegistryEntry(location): | |
271 registry = '<RegistryValue ' | |
272 Append(registry, new_line=False) | |
273 AppendRaw('Root="HKCU"') | |
274 Append(TagIndent('Key="Software\\Microsoft\\%s"' % location, registry)) | |
275 Append(TagIndent('Name="installed"', registry)) | |
276 Append(TagIndent('Type="integer"', registry)) | |
277 Append(TagIndent('Value="1"', registry)) | |
278 Append(TagIndent('KeyPath="yes" />', registry)) | |
279 | |
280 | |
281 def MajorUpgrade(): | |
282 upgrade = '<MajorUpgrade ' | |
283 Append(upgrade, new_line=False) | |
284 down_message = 'You already have a never version installed.' | |
285 AppendRaw('DowngradeErrorMessage="%s" />' % down_message) | |
286 | |
287 | |
288 # This is a very simplistic id generation. | |
289 # Unfortunately there is no easy way to generate good names, | |
290 # since there is a 72 character limit, and we have way longer | |
291 # paths. We don't really have an issue with files and ids across | |
292 # releases since we do full installs. | |
293 counter = 0 | |
294 def FileToId(name): | |
295 global counter | |
296 counter += 1 | |
297 return '%s' % counter | |
298 | |
299 def ListFiles(path): | |
300 for entry in os.listdir(path): | |
301 full_path = os.path.join(path, entry) | |
302 id = FileToId(full_path) | |
303 if os.path.isdir(full_path): | |
304 with Directory('DIR_%s' % id, entry): | |
305 ListFiles(full_path) | |
306 elif os.path.isfile(full_path): | |
307 # We assume 1 file per component, a File is always a KeyPath. | |
308 # A KeyPath on a file makes sure that we can always repair and | |
309 # remove that file in a consistent manner. A component | |
310 # can only have one child with a KeyPath. | |
311 with Component(id): | |
312 File(full_path, id) | |
313 | |
314 def ComponentRefs(): | |
315 for component in feature_components: | |
316 Append('<ComponentRef Id="%s" />' % component) | |
317 | |
318 def ExtractZipFile(zip, temp_dir): | |
319 print 'Extracting files' | |
320 f = zipfile.ZipFile(zip) | |
321 f.extractall(temp_dir) | |
322 f.close() | |
323 | |
324 def GenerateInstaller(wxs_content, options, temp_dir): | |
325 wxs_file = os.path.join(temp_dir, 'installer.wxs') | |
326 wixobj_file = os.path.join(temp_dir, 'installer.wixobj') | |
327 print 'Saving wxs output to: %s' % wxs_file | |
328 with open(wxs_file, 'w') as f: | |
329 f.write(wxs_content) | |
330 | |
331 candle_bin = 'candle.exe' | |
332 light_bin = 'light.exe' | |
333 if options.wix_bin: | |
334 candle_bin = os.path.join(options.wix_bin, 'candle.exe') | |
335 light_bin = os.path.join(options.wix_bin, 'light.exe') | |
336 print 'Calling candle on %s' % wxs_file | |
337 subprocess.check_call('%s %s -o %s' % (candle_bin, wxs_file, | |
338 wixobj_file)) | |
339 print 'Calling light on %s' % wixobj_file | |
340 subprocess.check_call('%s %s -o %s' % (light_bin, wixobj_file, | |
341 options.msi_location)) | |
342 print 'Created msi file to %s' % options.msi_location | |
343 | |
344 | |
345 def Main(argv): | |
346 if sys.platform != 'win32': | |
347 raise Exception("This script can only be run on windows") | |
348 options = GetOptions() | |
349 version = GetMicrosoftProductVersion(options.version) | |
350 with utils.TempDir('installer') as temp_dir: | |
351 input_location = GetInputDirectory(options, temp_dir) | |
352 print "Generating wix XML" | |
353 XmlHeader() | |
354 with WixAndProduct(version): | |
355 AppendBlankLine() | |
356 Package() | |
357 MediaTemplate() | |
358 AppendComment('We always do a major upgrade, at least for now') | |
359 MajorUpgrade() | |
360 | |
361 AppendComment('Directory structure') | |
362 with Directory('TARGETDIR', 'SourceDir'): | |
363 with Directory('ProgramFilesFolder'): | |
364 with Directory('RootInstallDir', 'Dart Editor'): | |
365 AppendComment("Add all files and directories") | |
366 print 'Installing files and directories in xml' | |
367 ListFiles(input_location) | |
368 AppendBlankLine() | |
369 AppendComment("Create shortcuts") | |
370 with Directory('ProgramMenuFolder'): | |
371 with Directory('ShortcutFolder', 'Dart Editor'): | |
372 with Component('shortcut'): | |
373 # When generating a shortcut we need an entry with | |
374 # a KeyPath (RegistryEntry) below - to be able to remove | |
375 # the shortcut again. The RemoveFolder tag is needed | |
376 # to clean up everything | |
377 Shortcut('editor_shortcut', 'Dart Editor', | |
378 '[RootInstallDir]DartEditor.exe') | |
379 RemoveFolder('RemoveShortcuts') | |
380 RegistryEntry('DartEditor') | |
381 with Feature(): | |
382 # We have only one feature, and it consists of all the | |
383 # files=components we have listed above" | |
384 ComponentRefs() | |
385 xml = GetContent() | |
386 if options.print_wxs: | |
387 print xml | |
388 GenerateInstaller(xml, options, temp_dir) | |
389 | |
390 if __name__ == '__main__': | |
391 sys.exit(Main(sys.argv)) | |
OLD | NEW |