Version 7.6.3.2-android, tag libreoffice-7.6.3.2-android
[LibreOffice.git] / msicreator / createmsi.py
blob23ae885906ff080483ee758de1b4513a08191ad2
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 jsondata = json.load(open(jsonfile, 'rb'))
43 self.product_name = jsondata['product_name']
44 self.manufacturer = jsondata['manufacturer']
45 self.version = jsondata['version']
46 self.comments = jsondata['comments']
47 self.installdir = jsondata['installdir']
48 self.license_file = jsondata.get('license_file', None)
49 self.name = jsondata['name']
50 self.guid = jsondata.get('product_guid', '*')
51 self.upgrade_guid = jsondata['upgrade_guid']
52 self.basename = jsondata['name_base']
53 self.need_msvcrt = jsondata.get('need_msvcrt', False)
54 self.addremove_icon = jsondata.get('addremove_icon', None)
55 self.startmenu_shortcut = jsondata.get('startmenu_shortcut', None)
56 self.desktop_shortcut = jsondata.get('desktop_shortcut', None)
57 self.main_xml = self.basename + '.wxs'
58 self.main_o = self.basename + '.wixobj'
59 self.idnum = 0
60 self.graphics = UIGraphics()
61 if 'graphics' in jsondata:
62 if 'banner' in jsondata['graphics']:
63 self.graphics.banner = jsondata['graphics']['banner']
64 if 'background' in jsondata['graphics']:
65 self.graphics.background = jsondata['graphics']['background']
66 if 'arch' in jsondata:
67 self.arch = jsondata['arch']
68 else:
69 # rely on the environment variable since python architecture may not be the same as system architecture
70 if 'PROGRAMFILES(X86)' in os.environ:
71 self.arch = 64
72 else:
73 self.arch = 32 if '32' in platform.architecture()[0] else 64
74 self.final_output = '%s-%s-%d.msi' % (self.basename, self.version, self.arch)
75 if self.arch == 64:
76 self.progfile_dir = 'ProgramFiles64Folder'
77 if platform.system() == "Windows":
78 redist_glob = 'C:\\Program Files\\Microsoft Visual Studio\\*\\*\\VC\\Redist\\MSVC\\v*\\MergeModules\\Microsoft_VC*_CRT_x64.msm'
79 else:
80 redist_glob = '/usr/share/msicreator/Microsoft_VC141_CRT_x64.msm'
81 else:
82 self.progfile_dir = 'ProgramFilesFolder'
83 if platform.system() == "Windows":
84 redist_glob = 'C:\\Program Files\\Microsoft Visual Studio\\*\\Community\\VC\\Redist\\MSVC\\*\\MergeModules\\Microsoft_VC*_CRT_x86.msm'
85 else:
86 redist_glob = '/usr/share/msicreator/Microsoft_VC141_CRT_x86.msm'
87 trials = glob(redist_glob)
88 if self.need_msvcrt:
89 if len(trials) > 1:
90 sys.exit('There are more than one redist dirs: ' +
91 ', '.join(trials))
92 if len(trials) == 0:
93 sys.exit('No redist dirs were detected, install MSM redistributables with VS installer.')
94 self.redist_path = trials[0]
95 self.component_num = 0
96 self.registry_entries = jsondata.get('registry_entries', None)
97 self.major_upgrade = jsondata.get('major_upgrade', None)
98 self.parts = jsondata['parts']
99 self.feature_components = {}
100 self.feature_properties = {}
102 def generate_files(self):
103 self.root = ET.Element('Wix', {'xmlns': 'http://schemas.microsoft.com/wix/2006/wi'})
104 product = ET.SubElement(self.root, 'Product', {
105 'Name': self.product_name,
106 'Manufacturer': self.manufacturer,
107 'Id': self.guid,
108 'UpgradeCode': self.upgrade_guid,
109 'Language': '1033',
110 'Codepage': '1252',
111 'Version': self.version,
114 package = ET.SubElement(product, 'Package', {
115 'Id': '*',
116 'Keywords': 'Installer',
117 'Description': '%s %s installer' % (self.name, self.version),
118 'Comments': self.comments,
119 'Manufacturer': self.manufacturer,
120 'InstallerVersion': '500',
121 'Languages': '1033',
122 'Compressed': 'yes',
123 'SummaryCodepage': '1252',
126 if self.major_upgrade is not None:
127 majorupgrade = ET.SubElement(product, 'MajorUpgrade', {})
128 for mkey in self.major_upgrade.keys():
129 majorupgrade.set(mkey, self.major_upgrade[mkey])
130 else:
131 ET.SubElement(product, 'MajorUpgrade', {'DowngradeErrorMessage': 'A newer version of %s is already installed.' % self.name})
132 if self.arch == 64:
133 package.set('Platform', 'x64')
134 ET.SubElement(product, 'Media', {
135 'Id': '1',
136 'Cabinet': self.basename + '.cab',
137 'EmbedCab': 'yes',
139 targetdir = ET.SubElement(product, 'Directory', {
140 'Id': 'TARGETDIR',
141 'Name': 'SourceDir',
143 progfiledir = ET.SubElement(targetdir, 'Directory', {
144 'Id': self.progfile_dir,
146 pmf = ET.SubElement(targetdir, 'Directory', {'Id': 'ProgramMenuFolder'},)
147 if self.startmenu_shortcut is not None:
148 ET.SubElement(pmf, 'Directory', {
149 'Id': 'ApplicationProgramsFolder',
150 'Name': self.product_name,
152 if self.desktop_shortcut is not None:
153 ET.SubElement(pmf, 'Directory', {'Id': 'DesktopFolder',
154 'Name': 'Desktop',
156 installdir = ET.SubElement(progfiledir, 'Directory', {
157 'Id': 'INSTALLDIR',
158 'Name': self.installdir,
160 if self.need_msvcrt:
161 ET.SubElement(installdir, 'Merge', {
162 'Id': 'VCRedist',
163 'SourceFile': self.redist_path,
164 'DiskId': '1',
165 'Language': '0',
168 if self.startmenu_shortcut is not None:
169 ap = ET.SubElement(product, 'DirectoryRef', {'Id': 'ApplicationProgramsFolder'})
170 comp = ET.SubElement(ap, 'Component', {'Id': 'ApplicationShortcut',
171 'Guid': gen_guid(),
173 ET.SubElement(comp, 'Shortcut', {'Id': 'ApplicationStartMenuShortcut',
174 'Name': self.product_name,
175 'Description': self.comments,
176 'Target': '[INSTALLDIR]' + self.startmenu_shortcut,
177 'WorkingDirectory': 'INSTALLDIR',
179 ET.SubElement(comp, 'RemoveFolder', {'Id': 'RemoveApplicationProgramsFolder',
180 'Directory': 'ApplicationProgramsFolder',
181 'On': 'uninstall',
183 ET.SubElement(comp, 'RegistryValue', {'Root': 'HKCU',
184 'Key': 'Software\\Microsoft\\' + self.name,
185 'Name': 'Installed',
186 'Type': 'integer',
187 'Value': '1',
188 'KeyPath': 'yes',
190 if self.desktop_shortcut is not None:
191 desk = ET.SubElement(product, 'DirectoryRef', {'Id': 'DesktopFolder'})
192 comp = ET.SubElement(desk, 'Component', {'Id':'ApplicationShortcutDesktop',
193 'Guid': gen_guid(),
195 ET.SubElement(comp, 'Shortcut', {'Id': 'ApplicationDesktopShortcut',
196 'Name': self.product_name,
197 'Description': self.comments,
198 'Target': '[INSTALLDIR]' + self.desktop_shortcut,
199 'WorkingDirectory': 'INSTALLDIR',
201 ET.SubElement(comp, 'RemoveFolder', {'Id': 'RemoveDesktopFolder',
202 'Directory': 'DesktopFolder',
203 'On': 'uninstall',
205 ET.SubElement(comp, 'RegistryValue', {'Root': 'HKCU',
206 'Key': 'Software\\Microsoft\\' + self.name,
207 'Name': 'Installed',
208 'Type': 'integer',
209 'Value': '1',
210 'KeyPath': 'yes',
213 ET.SubElement(product, 'Property', {
214 'Id': 'WIXUI_INSTALLDIR',
215 'Value': 'INSTALLDIR',
217 if platform.system() == "Windows":
218 if self.license_file:
219 ET.SubElement(product, 'UIRef', {
220 'Id': 'WixUI_FeatureTree',
222 else:
223 self.create_licenseless_dialog_entries(product)
225 if self.graphics.banner is not None:
226 ET.SubElement(product, 'WixVariable', {
227 'Id': 'WixUIBannerBmp',
228 'Value': self.graphics.banner,
230 if self.graphics.background is not None:
231 ET.SubElement(product, 'WixVariable', {
232 'Id': 'WixUIDialogBmp',
233 'Value': self.graphics.background,
236 top_feature = ET.SubElement(product, 'Feature', {
237 'Id': 'Complete',
238 'Title': self.name + ' ' + self.version,
239 'Description': 'The complete package',
240 'Display': 'expand',
241 'Level': '1',
242 'ConfigurableDirectory': 'INSTALLDIR',
245 for f in self.parts:
246 self.scan_feature(top_feature, installdir, 1, f)
248 if self.need_msvcrt:
249 vcredist_feature = ET.SubElement(top_feature, 'Feature', {
250 'Id': 'VCRedist',
251 'Title': 'Visual C++ runtime',
252 'AllowAdvertise': 'no',
253 'Display': 'hidden',
254 'Level': '1',
256 ET.SubElement(vcredist_feature, 'MergeRef', {'Id': 'VCRedist'})
257 if self.startmenu_shortcut is not None:
258 ET.SubElement(top_feature, 'ComponentRef', {'Id': 'ApplicationShortcut'})
259 if self.desktop_shortcut is not None:
260 ET.SubElement(top_feature, 'ComponentRef', {'Id': 'ApplicationShortcutDesktop'})
261 if self.addremove_icon is not None:
262 icoid = 'addremoveicon.ico'
263 ET.SubElement(product, 'Icon', {'Id': icoid,
264 'SourceFile': self.addremove_icon,
266 ET.SubElement(product, 'Property', {'Id': 'ARPPRODUCTICON',
267 'Value': icoid,
270 if self.registry_entries is not None:
271 registry_entries_directory = ET.SubElement(product, 'DirectoryRef', {'Id': 'TARGETDIR'})
272 registry_entries_component = ET.SubElement(registry_entries_directory, 'Component', {'Id': 'RegistryEntries', 'Guid': gen_guid()})
273 if self.arch == 64:
274 registry_entries_component.set('Win64', 'yes')
275 ET.SubElement(top_feature, 'ComponentRef', {'Id': 'RegistryEntries'})
276 for r in self.registry_entries:
277 self.create_registry_entries(registry_entries_component, r)
279 ET.ElementTree(self.root).write(self.main_xml, encoding='utf-8', xml_declaration=True)
280 # ElementTree can not do prettyprinting so do it manually
281 import xml.dom.minidom
282 doc = xml.dom.minidom.parse(self.main_xml)
283 with open(self.main_xml, 'w') as of:
284 of.write(doc.toprettyxml(indent=' '))
286 def create_registry_entries(self, comp, reg):
287 reg_key = ET.SubElement(comp, 'RegistryKey', {
288 'Root': reg['root'],
289 'Key': reg['key'],
290 'Action': reg['action'],
292 ET.SubElement(reg_key, 'RegistryValue', {
293 'Name': reg['name'],
294 'Type': reg['type'],
295 'Value': reg['value'],
296 'KeyPath': reg['key_path'],
299 def scan_feature(self, top_feature, installdir, depth, feature):
300 for sd in [feature['staged_dir']]:
301 if '/' in sd or '\\' in sd:
302 sys.exit('Staged_dir %s must not have a path segment.' % sd)
303 nodes = {}
304 for root, dirs, files in os.walk(sd):
305 cur_node = Node(dirs, files)
306 nodes[root] = cur_node
307 fdict = {
308 'Id': feature['id'],
309 'Title': feature['title'],
310 'Description': feature['description'],
311 'Level': '1'
313 if feature.get('absent', 'ab') == 'disallow':
314 fdict['Absent'] = 'disallow'
315 self.feature_properties[sd] = fdict
317 self.feature_components[sd] = []
318 self.create_xml(nodes, sd, installdir, sd)
319 self.build_features(nodes, top_feature, sd)
321 def build_features(self, nodes, top_feature, staging_dir):
322 feature = ET.SubElement(top_feature, 'Feature', self.feature_properties[staging_dir])
323 for component_id in self.feature_components[staging_dir]:
324 ET.SubElement(feature, 'ComponentRef', {
325 'Id': component_id,
328 def path_to_id(self, pathname):
329 #return re.sub(r'[^a-zA-Z0-9_.]', '_', str(pathname))[-72:]
330 idstr = f'pathid{self.idnum}'
331 self.idnum += 1
332 return idstr
334 def create_xml(self, nodes, current_dir, parent_xml_node, staging_dir):
335 cur_node = nodes[current_dir]
336 if cur_node.files:
337 component_id = 'ApplicationFiles%d' % self.component_num
338 comp_xml_node = ET.SubElement(parent_xml_node, 'Component', {
339 'Id': component_id,
340 'Guid': gen_guid(),
342 self.feature_components[staging_dir].append(component_id)
343 if self.arch == 64:
344 comp_xml_node.set('Win64', 'yes')
345 if platform.system() == "Windows" and self.component_num == 0:
346 ET.SubElement(comp_xml_node, 'Environment', {
347 'Id': 'Environment',
348 'Name': 'PATH',
349 'Part': 'last',
350 'System': 'yes',
351 'Action': 'set',
352 'Value': '[INSTALLDIR]',
354 self.component_num += 1
355 for f in cur_node.files:
356 file_id = self.path_to_id(os.path.join(current_dir, f))
357 ET.SubElement(comp_xml_node, 'File', {
358 'Id': file_id,
359 'Name': f,
360 'Source': os.path.join(current_dir, f),
363 for dirname in cur_node.dirs:
364 dir_id = self.path_to_id(os.path.join(current_dir, dirname))
365 dir_node = ET.SubElement(parent_xml_node, 'Directory', {
366 'Id': dir_id,
367 'Name': dirname,
369 self.create_xml(nodes, os.path.join(current_dir, dirname), dir_node, staging_dir)
371 def create_licenseless_dialog_entries(self, product_element):
372 ui = ET.SubElement(product_element, 'UI', {
373 'Id': 'WixUI_FeatureTree'
376 ET.SubElement(ui, 'TextStyle', {
377 'Id': 'WixUI_Font_Normal',
378 'FaceName': 'Tahoma',
379 'Size': '8'
382 ET.SubElement(ui, 'TextStyle', {
383 'Id': 'WixUI_Font_Bigger',
384 'FaceName': 'Tahoma',
385 'Size': '12'
388 ET.SubElement(ui, 'TextStyle', {
389 'Id': 'WixUI_Font_Title',
390 'FaceName': 'Tahoma',
391 'Size': '9',
392 'Bold': 'yes'
395 ET.SubElement(ui, 'Property', {
396 'Id': 'DefaultUIFont',
397 'Value': 'WixUI_Font_Normal'
400 ET.SubElement(ui, 'Property', {
401 'Id': 'WixUI_Mode',
402 'Value': 'FeatureTree'
405 ET.SubElement(ui, 'DialogRef', {
406 'Id': 'ErrorDlg'
409 ET.SubElement(ui, 'DialogRef', {
410 'Id': 'FatalError'
413 ET.SubElement(ui, 'DialogRef', {
414 'Id': 'FilesInUse'
417 ET.SubElement(ui, 'DialogRef', {
418 'Id': 'MsiRMFilesInUse'
421 ET.SubElement(ui, 'DialogRef', {
422 'Id': 'PrepareDlg'
425 ET.SubElement(ui, 'DialogRef', {
426 'Id': 'ProgressDlg'
429 ET.SubElement(ui, 'DialogRef', {
430 'Id': 'ResumeDlg'
433 ET.SubElement(ui, 'DialogRef', {
434 'Id': 'UserExit'
437 pub_exit = ET.SubElement(ui, 'Publish', {
438 'Dialog': 'ExitDialog',
439 'Control': 'Finish',
440 'Event': 'EndDialog',
441 'Value': 'Return',
442 'Order': '999'
445 pub_exit.text = '1'
447 pub_welcome_next = ET.SubElement(ui, 'Publish', {
448 'Dialog': 'WelcomeDlg',
449 'Control': 'Next',
450 'Event': 'NewDialog',
451 'Value': 'CustomizeDlg'
454 pub_welcome_next.text = 'NOT Installed'
456 pub_welcome_maint_next = ET.SubElement(ui, 'Publish', {
457 'Dialog': 'WelcomeDlg',
458 'Control': 'Next',
459 'Event': 'NewDialog',
460 'Value': 'VerifyReadyDlg'
463 pub_welcome_maint_next.text = 'Installed AND PATCH'
465 pub_customize_back_maint = ET.SubElement(ui, 'Publish', {
466 'Dialog': 'CustomizeDlg',
467 'Control': 'Back',
468 'Event': 'NewDialog',
469 'Value': 'MaintenanceTypeDlg',
470 'Order': '1'
473 pub_customize_back_maint.text = 'Installed'
475 pub_customize_back_welcome = ET.SubElement(ui, 'Publish', {
476 'Dialog': 'CustomizeDlg',
477 'Control': 'Back',
478 'Event': 'NewDialog',
479 'Value': 'WelcomeDlg',
480 'Order': '2'
483 pub_customize_back_welcome.text = 'Not Installed'
485 pub_customize_next = ET.SubElement(ui, 'Publish', {
486 'Dialog': 'CustomizeDlg',
487 'Control': 'Next',
488 'Event': 'NewDialog',
489 'Value': 'VerifyReadyDlg'
492 pub_customize_next.text = '1'
494 pub_verify_customize_back = ET.SubElement(ui, 'Publish', {
495 'Dialog': 'VerifyReadyDlg',
496 'Control': 'Back',
497 'Event': 'NewDialog',
498 'Value': 'CustomizeDlg',
499 'Order': '1'
502 pub_verify_customize_back.text = 'NOT Installed OR WixUI_InstallMode = "Change"'
504 pub_verify_maint_back = ET.SubElement(ui, 'Publish', {
505 'Dialog': 'VerifyReadyDlg',
506 'Control': 'Back',
507 'Event': 'NewDialog',
508 'Value': 'MaintenanceTypeDlg',
509 'Order': '2'
512 pub_verify_maint_back.text = 'Installed AND NOT PATCH'
514 pub_verify_welcome_back = ET.SubElement(ui, 'Publish', {
515 'Dialog': 'VerifyReadyDlg',
516 'Control': 'Back',
517 'Event': 'NewDialog',
518 'Value': 'WelcomeDlg',
519 'Order': '3'
522 pub_verify_welcome_back.text = 'Installed AND PATCH'
524 pub_maint_welcome_next = ET.SubElement(ui, 'Publish', {
525 'Dialog': 'MaintenanceWelcomeDlg',
526 'Control': 'Next',
527 'Event': 'NewDialog',
528 'Value': 'MaintenanceTypeDlg'
531 pub_maint_welcome_next.text = '1'
533 pub_maint_type_change = ET.SubElement(ui, 'Publish', {
534 'Dialog': 'MaintenanceTypeDlg',
535 'Control': 'ChangeButton',
536 'Event': 'NewDialog',
537 'Value': 'CustomizeDlg'
540 pub_maint_type_change.text = '1'
542 pub_maint_type_repair = ET.SubElement(ui, 'Publish', {
543 'Dialog': 'MaintenanceTypeDlg',
544 'Control': 'RepairButton',
545 'Event': 'NewDialog',
546 'Value': 'VerifyReadyDlg'
549 pub_maint_type_repair.text = '1'
551 pub_maint_type_remove = ET.SubElement(ui, 'Publish', {
552 'Dialog': 'MaintenanceTypeDlg',
553 'Control': 'RemoveButton',
554 'Event': 'NewDialog',
555 'Value': 'VerifyReadyDlg'
558 pub_maint_type_remove.text = '1'
560 pub_maint_type_back = ET.SubElement(ui, 'Publish', {
561 'Dialog': 'MaintenanceTypeDlg',
562 'Control': 'Back',
563 'Event': 'NewDialog',
564 'Value': 'MaintenanceWelcomeDlg'
567 pub_maint_type_back.text = '1'
569 ET.SubElement(product_element, 'UIRef', {
570 'Id': 'WixUI_Common',
573 def build_package(self):
574 wixdir = 'c:\\Program Files (x86)\\Wix Toolset v3.11\\bin'
575 if platform.system() != "Windows":
576 wixdir = '/usr/bin'
577 if not os.path.isdir(wixdir):
578 wixdir = 'c:\\Program Files (x86)\\Wix Toolset v3.11\\bin'
579 if not os.path.isdir(wixdir):
580 print("ERROR: This script requires WIX")
581 sys.exit(1)
582 if platform.system() == "Windows":
583 subprocess.check_call([os.path.join(wixdir, 'candle'), self.main_xml])
584 subprocess.check_call([os.path.join(wixdir, 'light'),
585 '-ext', 'WixUIExtension',
586 '-cultures:en-us',
587 '-dWixUILicenseRtf=' + self.license_file if self.license_file else '',
588 '-dcl:high',
589 '-out', self.final_output,
590 self.main_o])
591 else:
592 subprocess.check_call([os.path.join(wixdir, 'wixl'), '-o', self.final_output, self.main_xml])
594 def run(args):
595 if len(args) != 1:
596 sys.exit('createmsi.py <msi definition json>')
597 jsonfile = args[0]
598 if '/' in jsonfile or '\\' in jsonfile:
599 sys.exit('Input file %s must not contain a path segment.' % jsonfile)
600 p = PackageGenerator(jsonfile)
601 p.generate_files()
602 p.build_package()
604 if __name__ == '__main__':
605 run(sys.argv[1:])