From 267c6f2ac71f92999e969232431ba04678e7437e Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 07:54:39 +0200 Subject: Adding upstream version 4:24.2.0. Signed-off-by: Daniel Baumann --- msicreator/createmsi.py | 606 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 606 insertions(+) create mode 100644 msicreator/createmsi.py (limited to 'msicreator/createmsi.py') diff --git a/msicreator/createmsi.py b/msicreator/createmsi.py new file mode 100644 index 0000000000..d728e64520 --- /dev/null +++ b/msicreator/createmsi.py @@ -0,0 +1,606 @@ +#!/usr/bin/env python3 + +# Copyright 2017-2018 Jussi Pakkanen et al +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys, os, subprocess, shutil, uuid, json, re +from glob import glob +import platform +import xml.etree.ElementTree as ET + +sys.path.append(os.getcwd()) + +def gen_guid(): + return str(uuid.uuid4()).upper() + +class Node: + def __init__(self, dirs, files): + assert(isinstance(dirs, list)) + assert(isinstance(files, list)) + self.dirs = dirs + self.files = files + +class UIGraphics: + def __init__(self): + self.banner = None + self.background = None + +class PackageGenerator: + + def __init__(self, jsonfile): + with open(jsonfile, 'rb') as f: + jsondata = json.load(f) + self.product_name = jsondata['product_name'] + self.manufacturer = jsondata['manufacturer'] + self.version = jsondata['version'] + self.comments = jsondata['comments'] + self.installdir = jsondata['installdir'] + self.license_file = jsondata.get('license_file', None) + self.name = jsondata['name'] + self.guid = jsondata.get('product_guid', '*') + self.upgrade_guid = jsondata['upgrade_guid'] + self.basename = jsondata['name_base'] + self.need_msvcrt = jsondata.get('need_msvcrt', False) + self.addremove_icon = jsondata.get('addremove_icon', None) + self.startmenu_shortcut = jsondata.get('startmenu_shortcut', None) + self.desktop_shortcut = jsondata.get('desktop_shortcut', None) + self.main_xml = self.basename + '.wxs' + self.main_o = self.basename + '.wixobj' + self.idnum = 0 + self.graphics = UIGraphics() + if 'graphics' in jsondata: + if 'banner' in jsondata['graphics']: + self.graphics.banner = jsondata['graphics']['banner'] + if 'background' in jsondata['graphics']: + self.graphics.background = jsondata['graphics']['background'] + if 'arch' in jsondata: + self.arch = jsondata['arch'] + else: + # rely on the environment variable since python architecture may not be the same as system architecture + if 'PROGRAMFILES(X86)' in os.environ: + self.arch = 64 + else: + self.arch = 32 if '32' in platform.architecture()[0] else 64 + self.final_output = '%s-%s-%d.msi' % (self.basename, self.version, self.arch) + if self.arch == 64: + self.progfile_dir = 'ProgramFiles64Folder' + if platform.system() == "Windows": + redist_glob = 'C:\\Program Files\\Microsoft Visual Studio\\*\\*\\VC\\Redist\\MSVC\\v*\\MergeModules\\Microsoft_VC*_CRT_x64.msm' + else: + redist_glob = '/usr/share/msicreator/Microsoft_VC141_CRT_x64.msm' + else: + self.progfile_dir = 'ProgramFilesFolder' + if platform.system() == "Windows": + redist_glob = 'C:\\Program Files\\Microsoft Visual Studio\\*\\Community\\VC\\Redist\\MSVC\\*\\MergeModules\\Microsoft_VC*_CRT_x86.msm' + else: + redist_glob = '/usr/share/msicreator/Microsoft_VC141_CRT_x86.msm' + trials = glob(redist_glob) + if self.need_msvcrt: + if len(trials) > 1: + sys.exit('There are more than one redist dirs: ' + + ', '.join(trials)) + if len(trials) == 0: + sys.exit('No redist dirs were detected, install MSM redistributables with VS installer.') + self.redist_path = trials[0] + self.component_num = 0 + self.registry_entries = jsondata.get('registry_entries', None) + self.major_upgrade = jsondata.get('major_upgrade', None) + self.parts = jsondata['parts'] + self.feature_components = {} + self.feature_properties = {} + + def generate_files(self): + self.root = ET.Element('Wix', {'xmlns': 'http://schemas.microsoft.com/wix/2006/wi'}) + product = ET.SubElement(self.root, 'Product', { + 'Name': self.product_name, + 'Manufacturer': self.manufacturer, + 'Id': self.guid, + 'UpgradeCode': self.upgrade_guid, + 'Language': '1033', + 'Codepage': '1252', + 'Version': self.version, + }) + + package = ET.SubElement(product, 'Package', { + 'Id': '*', + 'Keywords': 'Installer', + 'Description': '%s %s installer' % (self.name, self.version), + 'Comments': self.comments, + 'Manufacturer': self.manufacturer, + 'InstallerVersion': '500', + 'Languages': '1033', + 'Compressed': 'yes', + 'SummaryCodepage': '1252', + }) + + if self.major_upgrade is not None: + majorupgrade = ET.SubElement(product, 'MajorUpgrade', {}) + for mkey in self.major_upgrade.keys(): + majorupgrade.set(mkey, self.major_upgrade[mkey]) + else: + ET.SubElement(product, 'MajorUpgrade', {'DowngradeErrorMessage': 'A newer version of %s is already installed.' % self.name}) + if self.arch == 64: + package.set('Platform', 'x64') + ET.SubElement(product, 'Media', { + 'Id': '1', + 'Cabinet': self.basename + '.cab', + 'EmbedCab': 'yes', + }) + targetdir = ET.SubElement(product, 'Directory', { + 'Id': 'TARGETDIR', + 'Name': 'SourceDir', + }) + progfiledir = ET.SubElement(targetdir, 'Directory', { + 'Id': self.progfile_dir, + }) + pmf = ET.SubElement(targetdir, 'Directory', {'Id': 'ProgramMenuFolder'},) + if self.startmenu_shortcut is not None: + ET.SubElement(pmf, 'Directory', { + 'Id': 'ApplicationProgramsFolder', + 'Name': self.product_name, + }) + if self.desktop_shortcut is not None: + ET.SubElement(pmf, 'Directory', {'Id': 'DesktopFolder', + 'Name': 'Desktop', + }) + installdir = ET.SubElement(progfiledir, 'Directory', { + 'Id': 'INSTALLDIR', + 'Name': self.installdir, + }) + if self.need_msvcrt: + ET.SubElement(installdir, 'Merge', { + 'Id': 'VCRedist', + 'SourceFile': self.redist_path, + 'DiskId': '1', + 'Language': '0', + }) + + if self.startmenu_shortcut is not None: + ap = ET.SubElement(product, 'DirectoryRef', {'Id': 'ApplicationProgramsFolder'}) + comp = ET.SubElement(ap, 'Component', {'Id': 'ApplicationShortcut', + 'Guid': gen_guid(), + }) + ET.SubElement(comp, 'Shortcut', {'Id': 'ApplicationStartMenuShortcut', + 'Name': self.product_name, + 'Description': self.comments, + 'Target': '[INSTALLDIR]' + self.startmenu_shortcut, + 'WorkingDirectory': 'INSTALLDIR', + }) + ET.SubElement(comp, 'RemoveFolder', {'Id': 'RemoveApplicationProgramsFolder', + 'Directory': 'ApplicationProgramsFolder', + 'On': 'uninstall', + }) + ET.SubElement(comp, 'RegistryValue', {'Root': 'HKCU', + 'Key': 'Software\\Microsoft\\' + self.name, + 'Name': 'Installed', + 'Type': 'integer', + 'Value': '1', + 'KeyPath': 'yes', + }) + if self.desktop_shortcut is not None: + desk = ET.SubElement(product, 'DirectoryRef', {'Id': 'DesktopFolder'}) + comp = ET.SubElement(desk, 'Component', {'Id':'ApplicationShortcutDesktop', + 'Guid': gen_guid(), + }) + ET.SubElement(comp, 'Shortcut', {'Id': 'ApplicationDesktopShortcut', + 'Name': self.product_name, + 'Description': self.comments, + 'Target': '[INSTALLDIR]' + self.desktop_shortcut, + 'WorkingDirectory': 'INSTALLDIR', + }) + ET.SubElement(comp, 'RemoveFolder', {'Id': 'RemoveDesktopFolder', + 'Directory': 'DesktopFolder', + 'On': 'uninstall', + }) + ET.SubElement(comp, 'RegistryValue', {'Root': 'HKCU', + 'Key': 'Software\\Microsoft\\' + self.name, + 'Name': 'Installed', + 'Type': 'integer', + 'Value': '1', + 'KeyPath': 'yes', + }) + + ET.SubElement(product, 'Property', { + 'Id': 'WIXUI_INSTALLDIR', + 'Value': 'INSTALLDIR', + }) + if platform.system() == "Windows": + if self.license_file: + ET.SubElement(product, 'UIRef', { + 'Id': 'WixUI_FeatureTree', + }) + else: + self.create_licenseless_dialog_entries(product) + + if self.graphics.banner is not None: + ET.SubElement(product, 'WixVariable', { + 'Id': 'WixUIBannerBmp', + 'Value': self.graphics.banner, + }) + if self.graphics.background is not None: + ET.SubElement(product, 'WixVariable', { + 'Id': 'WixUIDialogBmp', + 'Value': self.graphics.background, + }) + + top_feature = ET.SubElement(product, 'Feature', { + 'Id': 'Complete', + 'Title': self.name + ' ' + self.version, + 'Description': 'The complete package', + 'Display': 'expand', + 'Level': '1', + 'ConfigurableDirectory': 'INSTALLDIR', + }) + + for f in self.parts: + self.scan_feature(top_feature, installdir, 1, f) + + if self.need_msvcrt: + vcredist_feature = ET.SubElement(top_feature, 'Feature', { + 'Id': 'VCRedist', + 'Title': 'Visual C++ runtime', + 'AllowAdvertise': 'no', + 'Display': 'hidden', + 'Level': '1', + }) + ET.SubElement(vcredist_feature, 'MergeRef', {'Id': 'VCRedist'}) + if self.startmenu_shortcut is not None: + ET.SubElement(top_feature, 'ComponentRef', {'Id': 'ApplicationShortcut'}) + if self.desktop_shortcut is not None: + ET.SubElement(top_feature, 'ComponentRef', {'Id': 'ApplicationShortcutDesktop'}) + if self.addremove_icon is not None: + icoid = 'addremoveicon.ico' + ET.SubElement(product, 'Icon', {'Id': icoid, + 'SourceFile': self.addremove_icon, + }) + ET.SubElement(product, 'Property', {'Id': 'ARPPRODUCTICON', + 'Value': icoid, + }) + + if self.registry_entries is not None: + registry_entries_directory = ET.SubElement(product, 'DirectoryRef', {'Id': 'TARGETDIR'}) + registry_entries_component = ET.SubElement(registry_entries_directory, 'Component', {'Id': 'RegistryEntries', 'Guid': gen_guid()}) + if self.arch == 64: + registry_entries_component.set('Win64', 'yes') + ET.SubElement(top_feature, 'ComponentRef', {'Id': 'RegistryEntries'}) + for r in self.registry_entries: + self.create_registry_entries(registry_entries_component, r) + + ET.ElementTree(self.root).write(self.main_xml, encoding='utf-8', xml_declaration=True) + # ElementTree can not do prettyprinting so do it manually + import xml.dom.minidom + doc = xml.dom.minidom.parse(self.main_xml) + with open(self.main_xml, 'w') as of: + of.write(doc.toprettyxml(indent=' ')) + + def create_registry_entries(self, comp, reg): + reg_key = ET.SubElement(comp, 'RegistryKey', { + 'Root': reg['root'], + 'Key': reg['key'], + 'Action': reg['action'], + }) + ET.SubElement(reg_key, 'RegistryValue', { + 'Name': reg['name'], + 'Type': reg['type'], + 'Value': reg['value'], + 'KeyPath': reg['key_path'], + }) + + def scan_feature(self, top_feature, installdir, depth, feature): + for sd in [feature['staged_dir']]: + if '/' in sd or '\\' in sd: + sys.exit('Staged_dir %s must not have a path segment.' % sd) + nodes = {} + for root, dirs, files in os.walk(sd): + cur_node = Node(dirs, files) + nodes[root] = cur_node + fdict = { + 'Id': feature['id'], + 'Title': feature['title'], + 'Description': feature['description'], + 'Level': '1' + } + if feature.get('absent', 'ab') == 'disallow': + fdict['Absent'] = 'disallow' + self.feature_properties[sd] = fdict + + self.feature_components[sd] = [] + self.create_xml(nodes, sd, installdir, sd) + self.build_features(nodes, top_feature, sd) + + def build_features(self, nodes, top_feature, staging_dir): + feature = ET.SubElement(top_feature, 'Feature', self.feature_properties[staging_dir]) + for component_id in self.feature_components[staging_dir]: + ET.SubElement(feature, 'ComponentRef', { + 'Id': component_id, + }) + + def path_to_id(self, pathname): + #return re.sub(r'[^a-zA-Z0-9_.]', '_', str(pathname))[-72:] + idstr = f'pathid{self.idnum}' + self.idnum += 1 + return idstr + + def create_xml(self, nodes, current_dir, parent_xml_node, staging_dir): + cur_node = nodes[current_dir] + if cur_node.files: + component_id = 'ApplicationFiles%d' % self.component_num + comp_xml_node = ET.SubElement(parent_xml_node, 'Component', { + 'Id': component_id, + 'Guid': gen_guid(), + }) + self.feature_components[staging_dir].append(component_id) + if self.arch == 64: + comp_xml_node.set('Win64', 'yes') + if platform.system() == "Windows" and self.component_num == 0: + ET.SubElement(comp_xml_node, 'Environment', { + 'Id': 'Environment', + 'Name': 'PATH', + 'Part': 'last', + 'System': 'yes', + 'Action': 'set', + 'Value': '[INSTALLDIR]', + }) + self.component_num += 1 + for f in cur_node.files: + file_id = self.path_to_id(os.path.join(current_dir, f)) + ET.SubElement(comp_xml_node, 'File', { + 'Id': file_id, + 'Name': f, + 'Source': os.path.join(current_dir, f), + }) + + for dirname in cur_node.dirs: + dir_id = self.path_to_id(os.path.join(current_dir, dirname)) + dir_node = ET.SubElement(parent_xml_node, 'Directory', { + 'Id': dir_id, + 'Name': dirname, + }) + self.create_xml(nodes, os.path.join(current_dir, dirname), dir_node, staging_dir) + + def create_licenseless_dialog_entries(self, product_element): + ui = ET.SubElement(product_element, 'UI', { + 'Id': 'WixUI_FeatureTree' + }) + + ET.SubElement(ui, 'TextStyle', { + 'Id': 'WixUI_Font_Normal', + 'FaceName': 'Tahoma', + 'Size': '8' + }) + + ET.SubElement(ui, 'TextStyle', { + 'Id': 'WixUI_Font_Bigger', + 'FaceName': 'Tahoma', + 'Size': '12' + }) + + ET.SubElement(ui, 'TextStyle', { + 'Id': 'WixUI_Font_Title', + 'FaceName': 'Tahoma', + 'Size': '9', + 'Bold': 'yes' + }) + + ET.SubElement(ui, 'Property', { + 'Id': 'DefaultUIFont', + 'Value': 'WixUI_Font_Normal' + }) + + ET.SubElement(ui, 'Property', { + 'Id': 'WixUI_Mode', + 'Value': 'FeatureTree' + }) + + ET.SubElement(ui, 'DialogRef', { + 'Id': 'ErrorDlg' + }) + + ET.SubElement(ui, 'DialogRef', { + 'Id': 'FatalError' + }) + + ET.SubElement(ui, 'DialogRef', { + 'Id': 'FilesInUse' + }) + + ET.SubElement(ui, 'DialogRef', { + 'Id': 'MsiRMFilesInUse' + }) + + ET.SubElement(ui, 'DialogRef', { + 'Id': 'PrepareDlg' + }) + + ET.SubElement(ui, 'DialogRef', { + 'Id': 'ProgressDlg' + }) + + ET.SubElement(ui, 'DialogRef', { + 'Id': 'ResumeDlg' + }) + + ET.SubElement(ui, 'DialogRef', { + 'Id': 'UserExit' + }) + + pub_exit = ET.SubElement(ui, 'Publish', { + 'Dialog': 'ExitDialog', + 'Control': 'Finish', + 'Event': 'EndDialog', + 'Value': 'Return', + 'Order': '999' + }) + + pub_exit.text = '1' + + pub_welcome_next = ET.SubElement(ui, 'Publish', { + 'Dialog': 'WelcomeDlg', + 'Control': 'Next', + 'Event': 'NewDialog', + 'Value': 'CustomizeDlg' + }) + + pub_welcome_next.text = 'NOT Installed' + + pub_welcome_maint_next = ET.SubElement(ui, 'Publish', { + 'Dialog': 'WelcomeDlg', + 'Control': 'Next', + 'Event': 'NewDialog', + 'Value': 'VerifyReadyDlg' + }) + + pub_welcome_maint_next.text = 'Installed AND PATCH' + + pub_customize_back_maint = ET.SubElement(ui, 'Publish', { + 'Dialog': 'CustomizeDlg', + 'Control': 'Back', + 'Event': 'NewDialog', + 'Value': 'MaintenanceTypeDlg', + 'Order': '1' + }) + + pub_customize_back_maint.text = 'Installed' + + pub_customize_back_welcome = ET.SubElement(ui, 'Publish', { + 'Dialog': 'CustomizeDlg', + 'Control': 'Back', + 'Event': 'NewDialog', + 'Value': 'WelcomeDlg', + 'Order': '2' + }) + + pub_customize_back_welcome.text = 'Not Installed' + + pub_customize_next = ET.SubElement(ui, 'Publish', { + 'Dialog': 'CustomizeDlg', + 'Control': 'Next', + 'Event': 'NewDialog', + 'Value': 'VerifyReadyDlg' + }) + + pub_customize_next.text = '1' + + pub_verify_customize_back = ET.SubElement(ui, 'Publish', { + 'Dialog': 'VerifyReadyDlg', + 'Control': 'Back', + 'Event': 'NewDialog', + 'Value': 'CustomizeDlg', + 'Order': '1' + }) + + pub_verify_customize_back.text = 'NOT Installed OR WixUI_InstallMode = "Change"' + + pub_verify_maint_back = ET.SubElement(ui, 'Publish', { + 'Dialog': 'VerifyReadyDlg', + 'Control': 'Back', + 'Event': 'NewDialog', + 'Value': 'MaintenanceTypeDlg', + 'Order': '2' + }) + + pub_verify_maint_back.text = 'Installed AND NOT PATCH' + + pub_verify_welcome_back = ET.SubElement(ui, 'Publish', { + 'Dialog': 'VerifyReadyDlg', + 'Control': 'Back', + 'Event': 'NewDialog', + 'Value': 'WelcomeDlg', + 'Order': '3' + }) + + pub_verify_welcome_back.text = 'Installed AND PATCH' + + pub_maint_welcome_next = ET.SubElement(ui, 'Publish', { + 'Dialog': 'MaintenanceWelcomeDlg', + 'Control': 'Next', + 'Event': 'NewDialog', + 'Value': 'MaintenanceTypeDlg' + }) + + pub_maint_welcome_next.text = '1' + + pub_maint_type_change = ET.SubElement(ui, 'Publish', { + 'Dialog': 'MaintenanceTypeDlg', + 'Control': 'ChangeButton', + 'Event': 'NewDialog', + 'Value': 'CustomizeDlg' + }) + + pub_maint_type_change.text = '1' + + pub_maint_type_repair = ET.SubElement(ui, 'Publish', { + 'Dialog': 'MaintenanceTypeDlg', + 'Control': 'RepairButton', + 'Event': 'NewDialog', + 'Value': 'VerifyReadyDlg' + }) + + pub_maint_type_repair.text = '1' + + pub_maint_type_remove = ET.SubElement(ui, 'Publish', { + 'Dialog': 'MaintenanceTypeDlg', + 'Control': 'RemoveButton', + 'Event': 'NewDialog', + 'Value': 'VerifyReadyDlg' + }) + + pub_maint_type_remove.text = '1' + + pub_maint_type_back = ET.SubElement(ui, 'Publish', { + 'Dialog': 'MaintenanceTypeDlg', + 'Control': 'Back', + 'Event': 'NewDialog', + 'Value': 'MaintenanceWelcomeDlg' + }) + + pub_maint_type_back.text = '1' + + ET.SubElement(product_element, 'UIRef', { + 'Id': 'WixUI_Common', + }) + + def build_package(self): + wixdir = 'c:\\Program Files (x86)\\Wix Toolset v3.11\\bin' + if platform.system() != "Windows": + wixdir = '/usr/bin' + if not os.path.isdir(wixdir): + wixdir = 'c:\\Program Files (x86)\\Wix Toolset v3.11\\bin' + if not os.path.isdir(wixdir): + print("ERROR: This script requires WIX") + sys.exit(1) + if platform.system() == "Windows": + subprocess.check_call([os.path.join(wixdir, 'candle'), self.main_xml]) + subprocess.check_call([os.path.join(wixdir, 'light'), + '-ext', 'WixUIExtension', + '-cultures:en-us', + '-dWixUILicenseRtf=' + self.license_file if self.license_file else '', + '-dcl:high', + '-out', self.final_output, + self.main_o]) + else: + subprocess.check_call([os.path.join(wixdir, 'wixl'), '-o', self.final_output, self.main_xml]) + +def run(args): + if len(args) != 1: + sys.exit('createmsi.py ') + jsonfile = args[0] + if '/' in jsonfile or '\\' in jsonfile: + sys.exit('Input file %s must not contain a path segment.' % jsonfile) + p = PackageGenerator(jsonfile) + p.generate_files() + p.build_package() + +if __name__ == '__main__': + run(sys.argv[1:]) -- cgit v1.2.3