Avoid potential negative array index access to cached text.
[LibreOffice.git] / msicreator / createmsi.py
blobd728e64520d21c68309cf21a18020945bcb96134
1 #!/usr/bin/env python3
3 # Copyright 2017-2018 Jussi Pakkanen et al
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 import sys, os, subprocess, shutil, uuid, json, re
18 from glob import glob
19 import platform
20 import xml.etree.ElementTree as ET
22 sys.path.append(os.getcwd())
24 def gen_guid():
25 return str(uuid.uuid4()).upper()
27 class Node:
28 def __init__(self, dirs, files):
29 assert(isinstance(dirs, list))
30 assert(isinstance(files, list))
31 self.dirs = dirs
32 self.files = files
34 class UIGraphics:
35 def __init__(self):
36 self.banner = None
37 self.background = None
39 class PackageGenerator:
41 def __init__(self, jsonfile):
42 with open(jsonfile, 'rb') as f:
43 jsondata = json.load(f)
44 self.product_name = jsondata['product_name']
45 self.manufacturer = jsondata['manufacturer']
46 self.version = jsondata['version']
47 self.comments = jsondata['comments']
48 self.installdir = jsondata['installdir']
49 self.license_file = jsondata.get('license_file', None)
50 self.name = jsondata['name']
51 self.guid = jsondata.get('product_guid', '*')
52 self.upgrade_guid = jsondata['upgrade_guid']
53 self.basename = jsondata['name_base']
54 self.need_msvcrt = jsondata.get('need_msvcrt', False)
55 self.addremove_icon = jsondata.get('addremove_icon', None)
56 self.startmenu_shortcut = jsondata.get('startmenu_shortcut', None)
57 self.desktop_shortcut = jsondata.get('desktop_shortcut', None)
58 self.main_xml = self.basename + '.wxs'
59 self.main_o = self.basename + '.wixobj'
60 self.idnum = 0
61 self.graphics = UIGraphics()
62 if 'graphics' in jsondata:
63 if 'banner' in jsondata['graphics']:
64 self.graphics.banner = jsondata['graphics']['banner']
65 if 'background' in jsondata['graphics']:
66 self.graphics.background = jsondata['graphics']['background']
67 if 'arch' in jsondata:
68 self.arch = jsondata['arch']
69 else:
70 # rely on the environment variable since python architecture may not be the same as system architecture
71 if 'PROGRAMFILES(X86)' in os.environ:
72 self.arch = 64
73 else:
74 self.arch = 32 if '32' in platform.architecture()[0] else 64
75 self.final_output = '%s-%s-%d.msi' % (self.basename, self.version, self.arch)
76 if self.arch == 64:
77 self.progfile_dir = 'ProgramFiles64Folder'
78 if platform.system() == "Windows":
79 redist_glob = 'C:\\Program Files\\Microsoft Visual Studio\\*\\*\\VC\\Redist\\MSVC\\v*\\MergeModules\\Microsoft_VC*_CRT_x64.msm'
80 else:
81 redist_glob = '/usr/share/msicreator/Microsoft_VC141_CRT_x64.msm'
82 else:
83 self.progfile_dir = 'ProgramFilesFolder'
84 if platform.system() == "Windows":
85 redist_glob = 'C:\\Program Files\\Microsoft Visual Studio\\*\\Community\\VC\\Redist\\MSVC\\*\\MergeModules\\Microsoft_VC*_CRT_x86.msm'
86 else:
87 redist_glob = '/usr/share/msicreator/Microsoft_VC141_CRT_x86.msm'
88 trials = glob(redist_glob)
89 if self.need_msvcrt:
90 if len(trials) > 1:
91 sys.exit('There are more than one redist dirs: ' +
92 ', '.join(trials))
93 if len(trials) == 0:
94 sys.exit('No redist dirs were detected, install MSM redistributables with VS installer.')
95 self.redist_path = trials[0]
96 self.component_num = 0
97 self.registry_entries = jsondata.get('registry_entries', None)
98 self.major_upgrade = jsondata.get('major_upgrade', None)
99 self.parts = jsondata['parts']
100 self.feature_components = {}
101 self.feature_properties = {}
103 def generate_files(self):
104 self.root = ET.Element('Wix', {'xmlns': 'http://schemas.microsoft.com/wix/2006/wi'})
105 product = ET.SubElement(self.root, 'Product', {
106 'Name': self.product_name,
107 'Manufacturer': self.manufacturer,
108 'Id': self.guid,
109 'UpgradeCode': self.upgrade_guid,
110 'Language': '1033',
111 'Codepage': '1252',
112 'Version': self.version,
115 package = ET.SubElement(product, 'Package', {
116 'Id': '*',
117 'Keywords': 'Installer',
118 'Description': '%s %s installer' % (self.name, self.version),
119 'Comments': self.comments,
120 'Manufacturer': self.manufacturer,
121 'InstallerVersion': '500',
122 'Languages': '1033',
123 'Compressed': 'yes',
124 'SummaryCodepage': '1252',
127 if self.major_upgrade is not None:
128 majorupgrade = ET.SubElement(product, 'MajorUpgrade', {})
129 for mkey in self.major_upgrade.keys():
130 majorupgrade.set(mkey, self.major_upgrade[mkey])
131 else:
132 ET.SubElement(product, 'MajorUpgrade', {'DowngradeErrorMessage': 'A newer version of %s is already installed.' % self.name})
133 if self.arch == 64:
134 package.set('Platform', 'x64')
135 ET.SubElement(product, 'Media', {
136 'Id': '1',
137 'Cabinet': self.basename + '.cab',
138 'EmbedCab': 'yes',
140 targetdir = ET.SubElement(product, 'Directory', {
141 'Id': 'TARGETDIR',
142 'Name': 'SourceDir',
144 progfiledir = ET.SubElement(targetdir, 'Directory', {
145 'Id': self.progfile_dir,
147 pmf = ET.SubElement(targetdir, 'Directory', {'Id': 'ProgramMenuFolder'},)
148 if self.startmenu_shortcut is not None:
149 ET.SubElement(pmf, 'Directory', {
150 'Id': 'ApplicationProgramsFolder',
151 'Name': self.product_name,
153 if self.desktop_shortcut is not None:
154 ET.SubElement(pmf, 'Directory', {'Id': 'DesktopFolder',
155 'Name': 'Desktop',
157 installdir = ET.SubElement(progfiledir, 'Directory', {
158 'Id': 'INSTALLDIR',
159 'Name': self.installdir,
161 if self.need_msvcrt:
162 ET.SubElement(installdir, 'Merge', {
163 'Id': 'VCRedist',
164 'SourceFile': self.redist_path,
165 'DiskId': '1',
166 'Language': '0',
169 if self.startmenu_shortcut is not None:
170 ap = ET.SubElement(product, 'DirectoryRef', {'Id': 'ApplicationProgramsFolder'})
171 comp = ET.SubElement(ap, 'Component', {'Id': 'ApplicationShortcut',
172 'Guid': gen_guid(),
174 ET.SubElement(comp, 'Shortcut', {'Id': 'ApplicationStartMenuShortcut',
175 'Name': self.product_name,
176 'Description': self.comments,
177 'Target': '[INSTALLDIR]' + self.startmenu_shortcut,
178 'WorkingDirectory': 'INSTALLDIR',
180 ET.SubElement(comp, 'RemoveFolder', {'Id': 'RemoveApplicationProgramsFolder',
181 'Directory': 'ApplicationProgramsFolder',
182 'On': 'uninstall',
184 ET.SubElement(comp, 'RegistryValue', {'Root': 'HKCU',
185 'Key': 'Software\\Microsoft\\' + self.name,
186 'Name': 'Installed',
187 'Type': 'integer',
188 'Value': '1',
189 'KeyPath': 'yes',
191 if self.desktop_shortcut is not None:
192 desk = ET.SubElement(product, 'DirectoryRef', {'Id': 'DesktopFolder'})
193 comp = ET.SubElement(desk, 'Component', {'Id':'ApplicationShortcutDesktop',
194 'Guid': gen_guid(),
196 ET.SubElement(comp, 'Shortcut', {'Id': 'ApplicationDesktopShortcut',
197 'Name': self.product_name,
198 'Description': self.comments,
199 'Target': '[INSTALLDIR]' + self.desktop_shortcut,
200 'WorkingDirectory': 'INSTALLDIR',
202 ET.SubElement(comp, 'RemoveFolder', {'Id': 'RemoveDesktopFolder',
203 'Directory': 'DesktopFolder',
204 'On': 'uninstall',
206 ET.SubElement(comp, 'RegistryValue', {'Root': 'HKCU',
207 'Key': 'Software\\Microsoft\\' + self.name,
208 'Name': 'Installed',
209 'Type': 'integer',
210 'Value': '1',
211 'KeyPath': 'yes',
214 ET.SubElement(product, 'Property', {
215 'Id': 'WIXUI_INSTALLDIR',
216 'Value': 'INSTALLDIR',
218 if platform.system() == "Windows":
219 if self.license_file:
220 ET.SubElement(product, 'UIRef', {
221 'Id': 'WixUI_FeatureTree',
223 else:
224 self.create_licenseless_dialog_entries(product)
226 if self.graphics.banner is not None:
227 ET.SubElement(product, 'WixVariable', {
228 'Id': 'WixUIBannerBmp',
229 'Value': self.graphics.banner,
231 if self.graphics.background is not None:
232 ET.SubElement(product, 'WixVariable', {
233 'Id': 'WixUIDialogBmp',
234 'Value': self.graphics.background,
237 top_feature = ET.SubElement(product, 'Feature', {
238 'Id': 'Complete',
239 'Title': self.name + ' ' + self.version,
240 'Description': 'The complete package',
241 'Display': 'expand',
242 'Level': '1',
243 'ConfigurableDirectory': 'INSTALLDIR',
246 for f in self.parts:
247 self.scan_feature(top_feature, installdir, 1, f)
249 if self.need_msvcrt:
250 vcredist_feature = ET.SubElement(top_feature, 'Feature', {
251 'Id': 'VCRedist',
252 'Title': 'Visual C++ runtime',
253 'AllowAdvertise': 'no',
254 'Display': 'hidden',
255 'Level': '1',
257 ET.SubElement(vcredist_feature, 'MergeRef', {'Id': 'VCRedist'})
258 if self.startmenu_shortcut is not None:
259 ET.SubElement(top_feature, 'ComponentRef', {'Id': 'ApplicationShortcut'})
260 if self.desktop_shortcut is not None:
261 ET.SubElement(top_feature, 'ComponentRef', {'Id': 'ApplicationShortcutDesktop'})
262 if self.addremove_icon is not None:
263 icoid = 'addremoveicon.ico'
264 ET.SubElement(product, 'Icon', {'Id': icoid,
265 'SourceFile': self.addremove_icon,
267 ET.SubElement(product, 'Property', {'Id': 'ARPPRODUCTICON',
268 'Value': icoid,
271 if self.registry_entries is not None:
272 registry_entries_directory = ET.SubElement(product, 'DirectoryRef', {'Id': 'TARGETDIR'})
273 registry_entries_component = ET.SubElement(registry_entries_directory, 'Component', {'Id': 'RegistryEntries', 'Guid': gen_guid()})
274 if self.arch == 64:
275 registry_entries_component.set('Win64', 'yes')
276 ET.SubElement(top_feature, 'ComponentRef', {'Id': 'RegistryEntries'})
277 for r in self.registry_entries:
278 self.create_registry_entries(registry_entries_component, r)
280 ET.ElementTree(self.root).write(self.main_xml, encoding='utf-8', xml_declaration=True)
281 # ElementTree can not do prettyprinting so do it manually
282 import xml.dom.minidom
283 doc = xml.dom.minidom.parse(self.main_xml)
284 with open(self.main_xml, 'w') as of:
285 of.write(doc.toprettyxml(indent=' '))
287 def create_registry_entries(self, comp, reg):
288 reg_key = ET.SubElement(comp, 'RegistryKey', {
289 'Root': reg['root'],
290 'Key': reg['key'],
291 'Action': reg['action'],
293 ET.SubElement(reg_key, 'RegistryValue', {
294 'Name': reg['name'],
295 'Type': reg['type'],
296 'Value': reg['value'],
297 'KeyPath': reg['key_path'],
300 def scan_feature(self, top_feature, installdir, depth, feature):
301 for sd in [feature['staged_dir']]:
302 if '/' in sd or '\\' in sd:
303 sys.exit('Staged_dir %s must not have a path segment.' % sd)
304 nodes = {}
305 for root, dirs, files in os.walk(sd):
306 cur_node = Node(dirs, files)
307 nodes[root] = cur_node
308 fdict = {
309 'Id': feature['id'],
310 'Title': feature['title'],
311 'Description': feature['description'],
312 'Level': '1'
314 if feature.get('absent', 'ab') == 'disallow':
315 fdict['Absent'] = 'disallow'
316 self.feature_properties[sd] = fdict
318 self.feature_components[sd] = []
319 self.create_xml(nodes, sd, installdir, sd)
320 self.build_features(nodes, top_feature, sd)
322 def build_features(self, nodes, top_feature, staging_dir):
323 feature = ET.SubElement(top_feature, 'Feature', self.feature_properties[staging_dir])
324 for component_id in self.feature_components[staging_dir]:
325 ET.SubElement(feature, 'ComponentRef', {
326 'Id': component_id,
329 def path_to_id(self, pathname):
330 #return re.sub(r'[^a-zA-Z0-9_.]', '_', str(pathname))[-72:]
331 idstr = f'pathid{self.idnum}'
332 self.idnum += 1
333 return idstr
335 def create_xml(self, nodes, current_dir, parent_xml_node, staging_dir):
336 cur_node = nodes[current_dir]
337 if cur_node.files:
338 component_id = 'ApplicationFiles%d' % self.component_num
339 comp_xml_node = ET.SubElement(parent_xml_node, 'Component', {
340 'Id': component_id,
341 'Guid': gen_guid(),
343 self.feature_components[staging_dir].append(component_id)
344 if self.arch == 64:
345 comp_xml_node.set('Win64', 'yes')
346 if platform.system() == "Windows" and self.component_num == 0:
347 ET.SubElement(comp_xml_node, 'Environment', {
348 'Id': 'Environment',
349 'Name': 'PATH',
350 'Part': 'last',
351 'System': 'yes',
352 'Action': 'set',
353 'Value': '[INSTALLDIR]',
355 self.component_num += 1
356 for f in cur_node.files:
357 file_id = self.path_to_id(os.path.join(current_dir, f))
358 ET.SubElement(comp_xml_node, 'File', {
359 'Id': file_id,
360 'Name': f,
361 'Source': os.path.join(current_dir, f),
364 for dirname in cur_node.dirs:
365 dir_id = self.path_to_id(os.path.join(current_dir, dirname))
366 dir_node = ET.SubElement(parent_xml_node, 'Directory', {
367 'Id': dir_id,
368 'Name': dirname,
370 self.create_xml(nodes, os.path.join(current_dir, dirname), dir_node, staging_dir)
372 def create_licenseless_dialog_entries(self, product_element):
373 ui = ET.SubElement(product_element, 'UI', {
374 'Id': 'WixUI_FeatureTree'
377 ET.SubElement(ui, 'TextStyle', {
378 'Id': 'WixUI_Font_Normal',
379 'FaceName': 'Tahoma',
380 'Size': '8'
383 ET.SubElement(ui, 'TextStyle', {
384 'Id': 'WixUI_Font_Bigger',
385 'FaceName': 'Tahoma',
386 'Size': '12'
389 ET.SubElement(ui, 'TextStyle', {
390 'Id': 'WixUI_Font_Title',
391 'FaceName': 'Tahoma',
392 'Size': '9',
393 'Bold': 'yes'
396 ET.SubElement(ui, 'Property', {
397 'Id': 'DefaultUIFont',
398 'Value': 'WixUI_Font_Normal'
401 ET.SubElement(ui, 'Property', {
402 'Id': 'WixUI_Mode',
403 'Value': 'FeatureTree'
406 ET.SubElement(ui, 'DialogRef', {
407 'Id': 'ErrorDlg'
410 ET.SubElement(ui, 'DialogRef', {
411 'Id': 'FatalError'
414 ET.SubElement(ui, 'DialogRef', {
415 'Id': 'FilesInUse'
418 ET.SubElement(ui, 'DialogRef', {
419 'Id': 'MsiRMFilesInUse'
422 ET.SubElement(ui, 'DialogRef', {
423 'Id': 'PrepareDlg'
426 ET.SubElement(ui, 'DialogRef', {
427 'Id': 'ProgressDlg'
430 ET.SubElement(ui, 'DialogRef', {
431 'Id': 'ResumeDlg'
434 ET.SubElement(ui, 'DialogRef', {
435 'Id': 'UserExit'
438 pub_exit = ET.SubElement(ui, 'Publish', {
439 'Dialog': 'ExitDialog',
440 'Control': 'Finish',
441 'Event': 'EndDialog',
442 'Value': 'Return',
443 'Order': '999'
446 pub_exit.text = '1'
448 pub_welcome_next = ET.SubElement(ui, 'Publish', {
449 'Dialog': 'WelcomeDlg',
450 'Control': 'Next',
451 'Event': 'NewDialog',
452 'Value': 'CustomizeDlg'
455 pub_welcome_next.text = 'NOT Installed'
457 pub_welcome_maint_next = ET.SubElement(ui, 'Publish', {
458 'Dialog': 'WelcomeDlg',
459 'Control': 'Next',
460 'Event': 'NewDialog',
461 'Value': 'VerifyReadyDlg'
464 pub_welcome_maint_next.text = 'Installed AND PATCH'
466 pub_customize_back_maint = ET.SubElement(ui, 'Publish', {
467 'Dialog': 'CustomizeDlg',
468 'Control': 'Back',
469 'Event': 'NewDialog',
470 'Value': 'MaintenanceTypeDlg',
471 'Order': '1'
474 pub_customize_back_maint.text = 'Installed'
476 pub_customize_back_welcome = ET.SubElement(ui, 'Publish', {
477 'Dialog': 'CustomizeDlg',
478 'Control': 'Back',
479 'Event': 'NewDialog',
480 'Value': 'WelcomeDlg',
481 'Order': '2'
484 pub_customize_back_welcome.text = 'Not Installed'
486 pub_customize_next = ET.SubElement(ui, 'Publish', {
487 'Dialog': 'CustomizeDlg',
488 'Control': 'Next',
489 'Event': 'NewDialog',
490 'Value': 'VerifyReadyDlg'
493 pub_customize_next.text = '1'
495 pub_verify_customize_back = ET.SubElement(ui, 'Publish', {
496 'Dialog': 'VerifyReadyDlg',
497 'Control': 'Back',
498 'Event': 'NewDialog',
499 'Value': 'CustomizeDlg',
500 'Order': '1'
503 pub_verify_customize_back.text = 'NOT Installed OR WixUI_InstallMode = "Change"'
505 pub_verify_maint_back = ET.SubElement(ui, 'Publish', {
506 'Dialog': 'VerifyReadyDlg',
507 'Control': 'Back',
508 'Event': 'NewDialog',
509 'Value': 'MaintenanceTypeDlg',
510 'Order': '2'
513 pub_verify_maint_back.text = 'Installed AND NOT PATCH'
515 pub_verify_welcome_back = ET.SubElement(ui, 'Publish', {
516 'Dialog': 'VerifyReadyDlg',
517 'Control': 'Back',
518 'Event': 'NewDialog',
519 'Value': 'WelcomeDlg',
520 'Order': '3'
523 pub_verify_welcome_back.text = 'Installed AND PATCH'
525 pub_maint_welcome_next = ET.SubElement(ui, 'Publish', {
526 'Dialog': 'MaintenanceWelcomeDlg',
527 'Control': 'Next',
528 'Event': 'NewDialog',
529 'Value': 'MaintenanceTypeDlg'
532 pub_maint_welcome_next.text = '1'
534 pub_maint_type_change = ET.SubElement(ui, 'Publish', {
535 'Dialog': 'MaintenanceTypeDlg',
536 'Control': 'ChangeButton',
537 'Event': 'NewDialog',
538 'Value': 'CustomizeDlg'
541 pub_maint_type_change.text = '1'
543 pub_maint_type_repair = ET.SubElement(ui, 'Publish', {
544 'Dialog': 'MaintenanceTypeDlg',
545 'Control': 'RepairButton',
546 'Event': 'NewDialog',
547 'Value': 'VerifyReadyDlg'
550 pub_maint_type_repair.text = '1'
552 pub_maint_type_remove = ET.SubElement(ui, 'Publish', {
553 'Dialog': 'MaintenanceTypeDlg',
554 'Control': 'RemoveButton',
555 'Event': 'NewDialog',
556 'Value': 'VerifyReadyDlg'
559 pub_maint_type_remove.text = '1'
561 pub_maint_type_back = ET.SubElement(ui, 'Publish', {
562 'Dialog': 'MaintenanceTypeDlg',
563 'Control': 'Back',
564 'Event': 'NewDialog',
565 'Value': 'MaintenanceWelcomeDlg'
568 pub_maint_type_back.text = '1'
570 ET.SubElement(product_element, 'UIRef', {
571 'Id': 'WixUI_Common',
574 def build_package(self):
575 wixdir = 'c:\\Program Files (x86)\\Wix Toolset v3.11\\bin'
576 if platform.system() != "Windows":
577 wixdir = '/usr/bin'
578 if not os.path.isdir(wixdir):
579 wixdir = 'c:\\Program Files (x86)\\Wix Toolset v3.11\\bin'
580 if not os.path.isdir(wixdir):
581 print("ERROR: This script requires WIX")
582 sys.exit(1)
583 if platform.system() == "Windows":
584 subprocess.check_call([os.path.join(wixdir, 'candle'), self.main_xml])
585 subprocess.check_call([os.path.join(wixdir, 'light'),
586 '-ext', 'WixUIExtension',
587 '-cultures:en-us',
588 '-dWixUILicenseRtf=' + self.license_file if self.license_file else '',
589 '-dcl:high',
590 '-out', self.final_output,
591 self.main_o])
592 else:
593 subprocess.check_call([os.path.join(wixdir, 'wixl'), '-o', self.final_output, self.main_xml])
595 def run(args):
596 if len(args) != 1:
597 sys.exit('createmsi.py <msi definition json>')
598 jsonfile = args[0]
599 if '/' in jsonfile or '\\' in jsonfile:
600 sys.exit('Input file %s must not contain a path segment.' % jsonfile)
601 p = PackageGenerator(jsonfile)
602 p.generate_files()
603 p.build_package()
605 if __name__ == '__main__':
606 run(sys.argv[1:])