From a22d8443e826dff5dbf4720f017e7ef73bb16acb Mon Sep 17 00:00:00 2001 From: JKarasti Date: Thu, 12 Sep 2024 18:16:23 +0300 Subject: [PATCH 01/22] Fix: SyntaxWarning while generating Dangerzone.wxs --- install/windows/build-wxs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index 14aa92d..ab5397d 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -82,7 +82,7 @@ def build_dir_xml(root, data): component_el, "RegistryValue", Root="HKCU", - Key="Software\Freedom of the Press Foundation\Dangerzone", + Key="Software\\Freedom of the Press Foundation\\Dangerzone", Name="installed", Type="integer", Value="1", From 99f231b46a55ed0381a70a1c6289c49378b17996 Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 20:07:52 +0300 Subject: [PATCH 02/22] Fix: Make generated WiX authoring pass WixCop checks WixCop.exe is a built in formatting tool that comes with WiX toolset v3. This fixes `wix convert` command not beins able to run --- install/windows/build-wxs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index ab5397d..434badc 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -234,10 +234,10 @@ def main(): ET.SubElement(feature_el, "ComponentRef", Id=component_id) ET.SubElement(feature_el, "ComponentRef", Id="ApplicationShortcuts") - print('') + print('') print(f'') print('') - ET.indent(root_el) + ET.indent(root_el, space=" ") print(ET.tostring(root_el).decode()) From 6e2659bc0e3f6ed796ea2d97dded9bee892f9d45 Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 20:16:35 +0300 Subject: [PATCH 03/22] Change: Stop generating an XML declaration at the top of the WiX authoring It's not needed anymore. --- install/windows/build-wxs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index 434badc..5bd4465 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -234,7 +234,6 @@ def main(): ET.SubElement(feature_el, "ComponentRef", Id=component_id) ET.SubElement(feature_el, "ComponentRef", Id="ApplicationShortcuts") - print('') print(f'') print('') ET.indent(root_el, space=" ") From b2c085c812a46effd46fb25f83156a61d336b55d Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 20:18:58 +0300 Subject: [PATCH 04/22] Change: Update WiX schema namespace Also rename `root_el` to `wix_el`. WiX version 5 uses the same namespace. --- install/windows/build-wxs.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index 5bd4465..c96c6c5 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -158,9 +158,10 @@ def main(): ) ) - root_el = ET.Element("Wix", xmlns="http://schemas.microsoft.com/wix/2006/wi") + # Add the Wix root element + wix_el = ET.Element("Wix", xmlns="http://wixtoolset.org/schemas/v4/wxs") product_el = ET.SubElement( - root_el, + wix_el, "Product", Name="Dangerzone", Manufacturer="Freedom of the Press Foundation", @@ -236,8 +237,8 @@ def main(): print(f'') print('') - ET.indent(root_el, space=" ") - print(ET.tostring(root_el).decode()) + ET.indent(wix_el, space=" ") + print(ET.tostring(wix_el).decode()) if __name__ == "__main__": From 3357b30edb3f0991195f8c140a1a39f3f1db1b75 Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 20:21:48 +0300 Subject: [PATCH 05/22] Change: Rename `INSTALLDIR` to `INSTALLFOLDER` It's the new default name for it --- install/windows/build-wxs.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index c96c6c5..49cc83a 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -17,7 +17,7 @@ def build_data(dirname, dir_prefix, id_, name): if os.path.isfile(filename): data["files"].append(os.path.join(dir_prefix, basename)) elif os.path.isdir(filename): - if id_ == "INSTALLDIR": + if id_ == "INSTALLFOLDER": id_prefix = "Folder" else: id_prefix = id_ @@ -42,7 +42,7 @@ def build_data(dirname, dir_prefix, id_, name): ) if len(data["files"]) > 0: - if id_ == "INSTALLDIR": + if id_ == "INSTALLFOLDER": data["component_id"] = "ApplicationFiles" else: data["component_id"] = "FolderComponent" + id_[len("Folder") :] @@ -75,8 +75,8 @@ def build_dir_xml(root, data): Id="ApplicationShortcut1", Name="Dangerzone", Description="Dangerzone", - Target="[INSTALLDIR]dangerzone.exe", - WorkingDirectory="INSTALLDIR", + Target="[INSTALLFOLDER]dangerzone.exe", + WorkingDirectory="INSTALLFOLDER", ) ET.SubElement( component_el, @@ -153,7 +153,7 @@ def main(): build_data( dist_dir, "exe.win-amd64-3.12", - "INSTALLDIR", + "INSTALLFOLDER", "Dangerzone", ) ) @@ -204,7 +204,7 @@ def main(): product_el, "Property", Id="WIXUI_INSTALLDIR", - Value="INSTALLDIR", + Value="INSTALLFOLDER", ) ET.SubElement(product_el, "UIRef", Id="WixUI_InstallDir") ET.SubElement(product_el, "UIRef", Id="WixUI_ErrorProgressText") From 3df5c0d3be5fffc147dbea9d98310bbf80d76cc9 Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 20:31:58 +0300 Subject: [PATCH 06/22] Change: Merge Product into Package element - The Keywords and Description items move under a new SummaryInformation element. - Shuffle things around so that elements previously under the product element are now under the Package element. - Rename SummaryCodepage in SummaryInformation to Codepage and remove a duplicate Manufacturer item. - Remove InstallerVersion and let WiX set it to default value. (500 a.k.a Windows 7) --- install/windows/build-wxs.py | 48 +++++++++++++++++------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index 49cc83a..b494009 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -160,77 +160,75 @@ def main(): # Add the Wix root element wix_el = ET.Element("Wix", xmlns="http://wixtoolset.org/schemas/v4/wxs") - product_el = ET.SubElement( + + # Add the Package element + package_el = ET.SubElement( wix_el, - "Product", + "Package", Name="Dangerzone", Manufacturer="Freedom of the Press Foundation", - Id="*", UpgradeCode="$(var.ProductUpgradeCode)", Language="1033", + Compressed="yes", Codepage="1252", Version="$(var.ProductVersion)", ) ET.SubElement( - product_el, - "Package", - Id="*", + package_el, + "SummaryInformation", Keywords="Installer", Description="Dangerzone $(var.ProductVersion) Installer", - Manufacturer="Freedom of the Press Foundation", - InstallerVersion="100", - Languages="1033", - Compressed="yes", - SummaryCodepage="1252", + Codepage="1252", ) - ET.SubElement(product_el, "Media", Id="1", Cabinet="product.cab", EmbedCab="yes") + ET.SubElement(package_el, "Media", Id="1", Cabinet="product.cab", EmbedCab="yes") ET.SubElement( - product_el, "Icon", Id="ProductIcon", SourceFile="..\\share\\dangerzone.ico" + package_el, "Icon", Id="ProductIcon", SourceFile="..\\share\\dangerzone.ico" ) - ET.SubElement(product_el, "Property", Id="ARPPRODUCTICON", Value="ProductIcon") + ET.SubElement(package_el, "Property", Id="ARPPRODUCTICON", Value="ProductIcon") ET.SubElement( - product_el, + package_el, "Property", Id="ARPHELPLINK", Value="https://dangerzone.rocks", ) ET.SubElement( - product_el, + package_el, "Property", Id="ARPURLINFOABOUT", Value="https://freedom.press", ) ET.SubElement( - product_el, + package_el, "Property", Id="WIXUI_INSTALLDIR", Value="INSTALLFOLDER", ) - ET.SubElement(product_el, "UIRef", Id="WixUI_InstallDir") - ET.SubElement(product_el, "UIRef", Id="WixUI_ErrorProgressText") + ET.SubElement(package_el, "UIRef", Id="WixUI_InstallDir") + ET.SubElement(package_el, "UIRef", Id="WixUI_ErrorProgressText") ET.SubElement( - product_el, + package_el, "WixVariable", Id="WixUILicenseRtf", Value="..\\install\\windows\\license.rtf", ) ET.SubElement( - product_el, + package_el, "WixVariable", Id="WixUIDialogBmp", Value="..\\install\\windows\\dialog.bmp", ) ET.SubElement( - product_el, + package_el, "MajorUpgrade", AllowSameVersionUpgrades="yes", DowngradeErrorMessage="A newer version of [ProductName] is already installed. If you are sure you want to downgrade, remove the existing installation via Programs and Features.", ) - build_dir_xml(product_el, data) - component_ids = build_components_xml(product_el, data) + build_dir_xml(package_el, data) + component_ids = build_components_xml(package_el, data) - feature_el = ET.SubElement(product_el, "Feature", Id="DefaultFeature", Level="1") + # Add the Feature element + feature_el = ET.SubElement(package_el, "Feature", Id="DefaultFeature", Level="1") for component_id in component_ids: ET.SubElement(feature_el, "ComponentRef", Id=component_id) ET.SubElement(feature_el, "ComponentRef", Id="ApplicationShortcuts") From 0f2a385913accba1bc5d6056537c93d6c402b4a1 Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 20:43:31 +0300 Subject: [PATCH 07/22] Change: Wrap `ProgramMenuFolder` component with a `StandardDirectory` component --- install/windows/build-wxs.py | 65 ++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index b494009..e34c28c 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -61,34 +61,6 @@ def build_dir_xml(root, data): for subdata in data["dirs"]: build_dir_xml(el, subdata) - # If this is the ProgramMenuFolder, add the menu component - if "id" in data and data["id"] == "ProgramMenuFolder": - component_el = ET.SubElement( - el, - "Component", - Id="ApplicationShortcuts", - Guid="539e7de8-a124-4c09-aa55-0dd516aad7bc", - ) - ET.SubElement( - component_el, - "Shortcut", - Id="ApplicationShortcut1", - Name="Dangerzone", - Description="Dangerzone", - Target="[INSTALLFOLDER]dangerzone.exe", - WorkingDirectory="INSTALLFOLDER", - ) - ET.SubElement( - component_el, - "RegistryValue", - Root="HKCU", - Key="Software\\Freedom of the Press Foundation\\Dangerzone", - Name="installed", - Type="integer", - Value="1", - KeyPath="yes", - ) - def build_components_xml(root, data): component_ids = [] @@ -142,10 +114,6 @@ def main(): "id": "ProgramFilesFolder", "dirs": [], }, - { - "id": "ProgramMenuFolder", - "dirs": [], - }, ], } @@ -224,6 +192,39 @@ def main(): DowngradeErrorMessage="A newer version of [ProductName] is already installed. If you are sure you want to downgrade, remove the existing installation via Programs and Features.", ) + # Add the ProgramMenuFolder StandardDirectory + programmenufolder_el = ET.SubElement( + package_el, + "StandardDirectory", + Id="ProgramMenuFolder", + ) + # Add a shortcut for Dangerzone in the Start menu + shortcut_el = ET.SubElement( + programmenufolder_el, + "Component", + Id="ApplicationShortcuts", + Guid="539e7de8-a124-4c09-aa55-0dd516aad7bc", + ) + ET.SubElement( + shortcut_el, + "Shortcut", + Id="DangerzoneStartMenuShortcut", + Name="Dangerzone", + Description="Dangerzone", + Target="[INSTALLFOLDER]dangerzone.exe", + WorkingDirectory="INSTALLFOLDER", + ) + ET.SubElement( + shortcut_el, + "RegistryValue", + Root="HKCU", + Key="Software\\Freedom of the Press Foundation\\Dangerzone", + Name="installed", + Type="integer", + Value="1", + KeyPath="yes", + ) + build_dir_xml(package_el, data) component_ids = build_components_xml(package_el, data) From 6ca30a39b265fd6940850d69168feaa9856ca465 Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 21:17:43 +0300 Subject: [PATCH 08/22] Change: Wrap `ProgramFilesFolder` component with a `StandardDirectory` component --- install/windows/build-wxs.py | 55 ++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index e34c28c..f965d03 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -67,20 +67,19 @@ def build_components_xml(root, data): if "component_id" in data: component_ids.append(data["component_id"]) - for subdata in data["dirs"]: - if "component_guid" in subdata: - dir_ref_el = ET.SubElement(root, "DirectoryRef", Id=subdata["id"]) + if "component_guid" in data: + dir_ref_el = ET.SubElement(root, "DirectoryRef", Id=data["id"]) component_el = ET.SubElement( dir_ref_el, "Component", - Id=subdata["component_id"], - Guid=subdata["component_guid"], + Id=data["component_id"], + Guid=data["component_guid"], ) - for filename in subdata["files"]: + for filename in data["files"]: file_el = ET.SubElement( component_el, "File", Source=filename, Id="file_" + uuid.uuid4().hex ) - + for subdata in data["dirs"]: component_ids += build_components_xml(root, subdata) return component_ids @@ -106,24 +105,12 @@ def main(): print("You must build the dangerzone binary before running this") return - data = { - "id": "TARGETDIR", - "name": "SourceDir", - "dirs": [ - { - "id": "ProgramFilesFolder", - "dirs": [], - }, - ], - } - - data["dirs"][0]["dirs"].append( - build_data( - dist_dir, - "exe.win-amd64-3.12", - "INSTALLFOLDER", - "Dangerzone", - ) + # Prepare data for WiX file harvesting from the output of cx_Freeze + data = build_data( + dist_dir, + "exe.win-amd64-3.12", + "INSTALLFOLDER", + "Dangerzone", ) # Add the Wix root element @@ -225,8 +212,22 @@ def main(): KeyPath="yes", ) - build_dir_xml(package_el, data) - component_ids = build_components_xml(package_el, data) + # Add the ProgramFilesFolder StandardDirectory + programfilesfolder_el = ET.SubElement( + package_el, + "StandardDirectory", + Id="ProgramFilesFolder", + ) + + # Create the directory structure for the installed product + build_dir_xml(programfilesfolder_el, data) + + # Create a component group for application components + applicationcomponents_el = ET.SubElement( + package_el, "ComponentGroup", Id="ApplicationComponents" + ) + # Populate the application components group with components for the installed package + build_components_xml(applicationcomponents_el, data) # Add the Feature element feature_el = ET.SubElement(package_el, "Feature", Id="DefaultFeature", Level="1") From 287bd55c2e9a75ad6c98fbe062dd4510d4149826 Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 21:21:00 +0300 Subject: [PATCH 09/22] Change: Convert Wix UI extension authoring to WiX Toolset v5 Due to limitations of the xml.etree.ElementTree library, add the items in the root element as a dictionary --- install/windows/build-wxs.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index f965d03..d4372b6 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -114,7 +114,13 @@ def main(): ) # Add the Wix root element - wix_el = ET.Element("Wix", xmlns="http://wixtoolset.org/schemas/v4/wxs") + wix_el = ET.Element( + "Wix", + { + "xmlns": "http://wixtoolset.org/schemas/v4/wxs", + "xmlns:ui": "http://wixtoolset.org/schemas/v4/wxs/ui", + }, + ) # Add the Package element package_el = ET.SubElement( @@ -153,12 +159,8 @@ def main(): Value="https://freedom.press", ) ET.SubElement( - package_el, - "Property", - Id="WIXUI_INSTALLDIR", - Value="INSTALLFOLDER", + package_el, "ui:WixUI", Id="WixUI_InstallDir", InstallDirectory="INSTALLFOLDER" ) - ET.SubElement(package_el, "UIRef", Id="WixUI_InstallDir") ET.SubElement(package_el, "UIRef", Id="WixUI_ErrorProgressText") ET.SubElement( package_el, From 1a84fbe9985af8e34e61f7829c5c25ea7bb6a6dc Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 21:23:20 +0300 Subject: [PATCH 10/22] Change: Swap Media element with MediaTemplate This is a new default and makes authoring slightly simpler without any functional changes. --- install/windows/build-wxs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index d4372b6..3163591 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -141,7 +141,7 @@ def main(): Description="Dangerzone $(var.ProductVersion) Installer", Codepage="1252", ) - ET.SubElement(package_el, "Media", Id="1", Cabinet="product.cab", EmbedCab="yes") + ET.SubElement(package_el, "MediaTemplate", EmbedCab="yes") ET.SubElement( package_el, "Icon", Id="ProductIcon", SourceFile="..\\share\\dangerzone.ico" ) From e8202759f1e007f300de4064c0fbcbada9261de2 Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 21:34:05 +0300 Subject: [PATCH 11/22] Refactor: Simplify `build_data()` function - Rename variables to be more clear about what they do: - reorganise code - simplify a few checks --- install/windows/build-wxs.py | 61 ++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index 3163591..9af39bc 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -4,49 +4,50 @@ import uuid import xml.etree.ElementTree as ET -def build_data(dirname, dir_prefix, id_, name): +def build_data(base_path, path_prefix, dir_id, dir_name): data = { - "id": id_, - "name": name, + "directory_name": dir_name, + "directory_id": dir_id, "files": [], "dirs": [], } - for basename in os.listdir(dirname): - filename = os.path.join(dirname, basename) - if os.path.isfile(filename): - data["files"].append(os.path.join(dir_prefix, basename)) - elif os.path.isdir(filename): - if id_ == "INSTALLFOLDER": - id_prefix = "Folder" + if dir_id == "INSTALLFOLDER": + data["component_id"] = "ApplicationFiles" + else: + data["component_id"] = "Component" + dir_id + data["component_guid"] = str(uuid.uuid4()) + + for entry in os.listdir(base_path): + entry_path = os.path.join(base_path, entry) + if os.path.isfile(entry_path): + data["files"].append(os.path.join(path_prefix, entry)) + elif os.path.isdir(entry_path): + if dir_id == "INSTALLFOLDER": + next_dir_prefix = "Folder" else: - id_prefix = id_ + next_dir_prefix = dir_id # Skip lib/PySide6/examples folder due to ilegal file names - if "\\build\\exe.win-amd64-3.12\\lib\\PySide6\\examples" in dirname: + if "\\build\\exe.win-amd64-3.12\\lib\\PySide6\\examples" in base_path: continue # Skip lib/PySide6/qml/QtQuick folder due to ilegal file names # XXX Since we're not using Qml it should be no problem - if "\\build\\exe.win-amd64-3.12\\lib\\PySide6\\qml\\QtQuick" in dirname: + if "\\build\\exe.win-amd64-3.12\\lib\\PySide6\\qml\\QtQuick" in base_path: continue - id_value = f"{id_prefix}{basename.capitalize().replace('-', '_')}" - data["dirs"].append( - build_data( - os.path.join(dirname, basename), - os.path.join(dir_prefix, basename), - id_value, - basename, - ) + next_dir_id = next_dir_prefix + entry.capitalize().replace("-", "_") + subdata = build_data( + os.path.join(base_path, entry), + os.path.join(path_prefix, entry), + next_dir_id, + entry, ) - if len(data["files"]) > 0: - if id_ == "INSTALLFOLDER": - data["component_id"] = "ApplicationFiles" - else: - data["component_id"] = "FolderComponent" + id_[len("Folder") :] - data["component_guid"] = str(uuid.uuid4()) + # Add the subdirectory only if it contains files or subdirectories + if subdata["files"] or subdata["dirs"]: + data["dirs"].append(subdata) return data @@ -54,9 +55,9 @@ def build_data(dirname, dir_prefix, id_, name): def build_dir_xml(root, data): attrs = {} if "id" in data: - attrs["Id"] = data["id"] + attrs["Id"] = data["directory_id"] if "name" in data: - attrs["Name"] = data["name"] + attrs["Name"] = data["directory_name"] el = ET.SubElement(root, "Directory", attrs) for subdata in data["dirs"]: build_dir_xml(el, subdata) @@ -68,7 +69,7 @@ def build_components_xml(root, data): component_ids.append(data["component_id"]) if "component_guid" in data: - dir_ref_el = ET.SubElement(root, "DirectoryRef", Id=data["id"]) + dir_ref_el = ET.SubElement(root, "DirectoryRef", Id=data["directory_id"]) component_el = ET.SubElement( dir_ref_el, "Component", From cd9eafbd63a117702621773e5277285ae58efb4c Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 21:57:38 +0300 Subject: [PATCH 12/22] Change: Wrap all files to be included in the .msi in a `ComponentGroupRef` With this, all the files are organised into Components, each of which points to a Directory defined in the StandardDirectory element. This simplifies the Feature element considerable as only thing it needs to include everything in the built msi is a reference to `ApplicationComponents` --- install/windows/build-wxs.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index 9af39bc..6bbf186 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -64,26 +64,17 @@ def build_dir_xml(root, data): def build_components_xml(root, data): - component_ids = [] - if "component_id" in data: - component_ids.append(data["component_id"]) - - if "component_guid" in data: - dir_ref_el = ET.SubElement(root, "DirectoryRef", Id=data["directory_id"]) - component_el = ET.SubElement( - dir_ref_el, - "Component", - Id=data["component_id"], - Guid=data["component_guid"], - ) - for filename in data["files"]: - file_el = ET.SubElement( - component_el, "File", Source=filename, Id="file_" + uuid.uuid4().hex - ) + component_el = ET.SubElement( + root, + "Component", + Id=data["component_id"], + Guid=data["component_guid"], + Directory=data["directory_id"], + ) + for filename in data["files"]: + ET.SubElement(component_el, "File", Source=filename) for subdata in data["dirs"]: - component_ids += build_components_xml(root, subdata) - - return component_ids + build_components_xml(root, subdata) def main(): @@ -234,8 +225,7 @@ def main(): # Add the Feature element feature_el = ET.SubElement(package_el, "Feature", Id="DefaultFeature", Level="1") - for component_id in component_ids: - ET.SubElement(feature_el, "ComponentRef", Id=component_id) + ET.SubElement(feature_el, "ComponentGroupRef", Id="ApplicationComponents") ET.SubElement(feature_el, "ComponentRef", Id="ApplicationShortcuts") print(f'') From a78f0a7000a1f3d136526dac11549f41bbbc9de0 Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 22:12:40 +0300 Subject: [PATCH 13/22] Refactor: `build_dir_xml()` function - rename for clarity - remove unnecessary checks --- install/windows/build-wxs.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index 6bbf186..42a01de 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -52,15 +52,13 @@ def build_data(base_path, path_prefix, dir_id, dir_name): return data -def build_dir_xml(root, data): +def build_directory_xml(root, data): attrs = {} - if "id" in data: - attrs["Id"] = data["directory_id"] - if "name" in data: - attrs["Name"] = data["directory_name"] - el = ET.SubElement(root, "Directory", attrs) + attrs["Id"] = data["directory_id"] + attrs["Name"] = data["directory_name"] + directory_el = ET.SubElement(root, "Directory", attrs) for subdata in data["dirs"]: - build_dir_xml(el, subdata) + build_directory_xml(directory_el, subdata) def build_components_xml(root, data): @@ -214,7 +212,7 @@ def main(): ) # Create the directory structure for the installed product - build_dir_xml(programfilesfolder_el, data) + build_directory_xml(programfilesfolder_el, data) # Create a component group for application components applicationcomponents_el = ET.SubElement( From b9c9dbccae735ba08c270268794f6a7c6b2ceba4 Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 22:17:03 +0300 Subject: [PATCH 14/22] Change: Write dangerzone version and upgradecode into Package and SummaryInformation elements directly --- install/windows/build-wxs.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index 42a01de..f0f677e 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -118,17 +118,17 @@ def main(): "Package", Name="Dangerzone", Manufacturer="Freedom of the Press Foundation", - UpgradeCode="$(var.ProductUpgradeCode)", + UpgradeCode="12b9695c-965b-4be0-bc33-21274e809576", Language="1033", Compressed="yes", Codepage="1252", - Version="$(var.ProductVersion)", + Version=version, ) ET.SubElement( package_el, "SummaryInformation", Keywords="Installer", - Description="Dangerzone $(var.ProductVersion) Installer", + Description="Dangerzone " + version + " Installer", Codepage="1252", ) ET.SubElement(package_el, "MediaTemplate", EmbedCab="yes") @@ -226,8 +226,6 @@ def main(): ET.SubElement(feature_el, "ComponentGroupRef", Id="ApplicationComponents") ET.SubElement(feature_el, "ComponentRef", Id="ApplicationShortcuts") - print(f'') - print('') ET.indent(wix_el, space=" ") print(ET.tostring(wix_el).decode()) From 31e865a347d66de5bc8cca8329136d739e2d69b1 Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 22:31:16 +0300 Subject: [PATCH 15/22] Fix: Make GUIDs uppercase See [1] [1] https://learn.microsoft.com/en-us/windows/win32/msi/guid --- install/windows/build-wxs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index f0f677e..fb26205 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -16,7 +16,7 @@ def build_data(base_path, path_prefix, dir_id, dir_name): data["component_id"] = "ApplicationFiles" else: data["component_id"] = "Component" + dir_id - data["component_guid"] = str(uuid.uuid4()) + data["component_guid"] = str(uuid.uuid4()).upper() for entry in os.listdir(base_path): entry_path = os.path.join(base_path, entry) @@ -118,7 +118,7 @@ def main(): "Package", Name="Dangerzone", Manufacturer="Freedom of the Press Foundation", - UpgradeCode="12b9695c-965b-4be0-bc33-21274e809576", + UpgradeCode="12B9695C-965B-4BE0-BC33-21274E809576", Language="1033", Compressed="yes", Codepage="1252", @@ -182,7 +182,7 @@ def main(): programmenufolder_el, "Component", Id="ApplicationShortcuts", - Guid="539e7de8-a124-4c09-aa55-0dd516aad7bc", + Guid="539E7DE8-A124-4C09-AA55-0DD516AAD7BC", ) ET.SubElement( shortcut_el, From b1d9bfccab75cb666b0a0b8cfc28d712ffaecbfb Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 22:35:34 +0300 Subject: [PATCH 16/22] Change: Write Dangerzone.wxs inside the script directly Also reduce duplication slightly by definig `build_dir`, `cx_freeze_dir` and `dist_dir` --- install/windows/build-wxs.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index fb26205..424f7c2 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -86,11 +86,15 @@ def main(): # -rc markers. version = f.read().strip().split("-")[0] - dist_dir = os.path.join( + build_dir = os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "build", - "exe.win-amd64-3.12", ) + + cx_freeze_dir = "exe.win-amd64-3.12" + + dist_dir = os.path.join(build_dir, cx_freeze_dir) + if not os.path.exists(dist_dir): print("You must build the dangerzone binary before running this") return @@ -98,7 +102,7 @@ def main(): # Prepare data for WiX file harvesting from the output of cx_Freeze data = build_data( dist_dir, - "exe.win-amd64-3.12", + cx_freeze_dir, "INSTALLFOLDER", "Dangerzone", ) @@ -227,7 +231,9 @@ def main(): ET.SubElement(feature_el, "ComponentRef", Id="ApplicationShortcuts") ET.indent(wix_el, space=" ") - print(ET.tostring(wix_el).decode()) + + with open(os.path.join(build_dir, "Dangerzone.wxs"), "w") as wxs_file: + wxs_file.write(ET.tostring(wix_el).decode()) if __name__ == "__main__": From 2d95c078d3742d61cd80d157096cb31a161af20d Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 22:38:47 +0300 Subject: [PATCH 17/22] Change: Use WiX Toolset v5 to build the msi in CI --- .github/workflows/ci.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca94580..888bb58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,10 +110,14 @@ jobs: key: v1-tessdata-${{ hashFiles('./install/common/download-tessdata.py') }} - name: Run CLI tests run: poetry run make test - # Taken from: https://github.com/orgs/community/discussions/27149#discussioncomment-3254829 - - name: Set path for candle and light - run: echo "C:\Program Files (x86)\WiX Toolset v3.14\bin" >> $GITHUB_PATH - shell: bash + - name: Set up .NET CLI environment + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.x" + - name: Install WiX Toolset + run: dotnet tool install --global wix --version 5.0.1 + - name: Add WiX UI extension + run: wix extension add --global WixToolset.UI.wixext/5.0.1 - name: Build the MSI installer # NOTE: This also builds the .exe internally. run: poetry run .\install\windows\build-app.bat From 5ecfbcd323f6ed7dea79a6da906798211eef4fae Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 22:44:46 +0300 Subject: [PATCH 18/22] Change: Update the `build-app.bat` script to work with WiX Toolset v5 - WiX Toolset v3 used to validate the msi package by default. In v5 that has moved to a new command, so add a new validation step to the script. - Also emove the step that uses `insignia.exe` to sign the Dangerzone.msi with the digital signatures from its external cab archives. In WiX Toolset v4 and newer, insignia is replaced with a new command `wix msi inscribe`, but we tell wix to embed the cabinets into the .msi (That's what`EmbedCab="yes"` in the Media / MediaTemplate element does) so singning them separately is not necessary. [0] [0] https://wixtoolset.org/docs/tools/signing/ --- install/windows/build-app.bat | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/install/windows/build-app.bat b/install/windows/build-app.bat index 1d2b770..44c8e81 100644 --- a/install/windows/build-app.bat +++ b/install/windows/build-app.bat @@ -17,22 +17,23 @@ signtool.exe sign /v /d "Dangerzone" /a /n "Freedom of the Press Foundation" /fd REM verify the signature of dangerzone-cli.exe signtool.exe verify /pa build\exe.win-amd64-3.12\dangerzone-cli.exe -REM build the wix file -python install\windows\build-wxs.py > build\Dangerzone.wxs +REM build the wxs file +python install\windows\build-wxs.py REM build the msi package cd build -candle.exe Dangerzone.wxs -light.exe -ext WixUIExtension Dangerzone.wixobj +wix build -ext WixToolset.UI.wixext .\Dangerzone.wxs -out Dangerzone.msi + +REM validate Dangerzone.msi +wix msi validate Dangerzone.msi REM code sign Dangerzone.msi -insignia.exe -im Dangerzone.msi signtool.exe sign /v /d "Dangerzone" /a /n "Freedom of the Press Foundation" /fd sha256 /t http://time.certum.pl/ Dangerzone.msi REM verify the signature of Dangerzone.msi signtool.exe verify /pa Dangerzone.msi -REM moving Dangerzone.msi to dist +REM move Dangerzone.msi to dist cd .. mkdir dist move build\Dangerzone.msi dist From 096b0751263dc58a8ac908defe4b694c32a34d4e Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 22:46:36 +0300 Subject: [PATCH 19/22] Docs: Documentation for WiX Toolset 5 --- BUILD.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/BUILD.md b/BUILD.md index c395f50..b727fc3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -474,11 +474,22 @@ poetry shell .\dev_scripts\dangerzone.bat ``` -### If you want to build the installer +### If you want to build the Windows installer -* Go to https://dotnet.microsoft.com/download/dotnet-framework and download and install .NET Framework 3.5 SP1 Runtime. I downloaded `dotnetfx35.exe`. -* Go to https://wixtoolset.org/releases/ and download and install WiX toolset. I downloaded `wix314.exe`. -* Add `C:\Program Files (x86)\WiX Toolset v3.14\bin` to the path ([instructions](https://web.archive.org/web/20230221104142/https://windowsloop.com/how-to-add-to-windows-path/)). +Install [.NET SDK](https://dotnet.microsoft.com/en-us/download) version 6 or later. Then, open a terminal and install the [WiX Toolset .NET tool](https://wixtoolset.org/) v5.0.1. + +```sh +dotnet tool install --global wix --version 5.0.1 +``` + +Install the WiX UI extension **in a new terminal**, in order to use the newly installed `wix` .NET tool: + +```sh +wix extension add --global WixToolset.UI.wixext/5.0.1 +``` + +> [!IMPORTANT] +> To prevent compatibility issues, ensure that all WiX plugins you install match the version of WiX Toolset. ### If you want to sign binaries with Authenticode From 2149ad89631a7e9dd0412101cba593926bb79f20 Mon Sep 17 00:00:00 2001 From: JKarasti Date: Mon, 23 Sep 2024 23:17:02 +0300 Subject: [PATCH 20/22] Change: Build 64bit installer --- install/windows/build-app.bat | 2 +- install/windows/build-wxs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/install/windows/build-app.bat b/install/windows/build-app.bat index 44c8e81..ea74429 100644 --- a/install/windows/build-app.bat +++ b/install/windows/build-app.bat @@ -22,7 +22,7 @@ python install\windows\build-wxs.py REM build the msi package cd build -wix build -ext WixToolset.UI.wixext .\Dangerzone.wxs -out Dangerzone.msi +wix build -arch x64 -ext WixToolset.UI.wixext .\Dangerzone.wxs -out Dangerzone.msi REM validate Dangerzone.msi wix msi validate Dangerzone.msi diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index 424f7c2..f94a184 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -212,7 +212,7 @@ def main(): programfilesfolder_el = ET.SubElement( package_el, "StandardDirectory", - Id="ProgramFilesFolder", + Id="ProgramFiles64Folder", ) # Create the directory structure for the installed product From 5bb09f7c038201a68f904434c8bd90209fabb65e Mon Sep 17 00:00:00 2001 From: jkarasti Date: Tue, 29 Oct 2024 18:56:56 +0200 Subject: [PATCH 21/22] Change: Wrap installer ui related things in a `UI` element --- install/windows/build-wxs.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index f94a184..f03e1ab 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -152,10 +152,6 @@ def main(): Id="ARPURLINFOABOUT", Value="https://freedom.press", ) - ET.SubElement( - package_el, "ui:WixUI", Id="WixUI_InstallDir", InstallDirectory="INSTALLFOLDER" - ) - ET.SubElement(package_el, "UIRef", Id="WixUI_ErrorProgressText") ET.SubElement( package_el, "WixVariable", @@ -175,6 +171,13 @@ def main(): DowngradeErrorMessage="A newer version of [ProductName] is already installed. If you are sure you want to downgrade, remove the existing installation via Programs and Features.", ) + # Add The UI element + ui_el = ET.SubElement(package_el, "UI") + ET.SubElement( + ui_el, "ui:WixUI", Id="WixUI_InstallDir", InstallDirectory="INSTALLFOLDER" + ) + ET.SubElement(ui_el, "UIRef", Id="WixUI_ErrorProgressText") + # Add the ProgramMenuFolder StandardDirectory programmenufolder_el = ET.SubElement( package_el, From 5c9f5fe34c04180ee026a0ceea19fcc269c453e4 Mon Sep 17 00:00:00 2001 From: jkarasti Date: Thu, 31 Oct 2024 18:45:47 +0200 Subject: [PATCH 22/22] Fix: Dangerzone installed using an msi built with WiX Toolset v3 is not uninstalled by an msi built with WiX Toolset v5 Fix the issue by adding some extra functionality to the "Next" button on the welcome screen of the installer. When the user clicks it to proceed with the installation this: 1. Flips the install scope to "perUser" which is the default in WiX v3 2. Finds the older installation 3. And finally flips the scope back to "perMachine" which is the default in WiX v4 and newer TODO: Revert this once we are reasonably certain there are no affected Dangerzone Installations? --- install/windows/build-wxs.py | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/install/windows/build-wxs.py b/install/windows/build-wxs.py index f03e1ab..9baff94 100644 --- a/install/windows/build-wxs.py +++ b/install/windows/build-wxs.py @@ -178,6 +178,43 @@ def main(): ) ET.SubElement(ui_el, "UIRef", Id="WixUI_ErrorProgressText") + # Workaround for an issue after upgrading from WiX Toolset v3 to v5 where the older + # version of Dangerzone is not uninstalled during the upgrade + # + # Fix the issue by adding some extra functionality to the "Next" button on the welcome screen + # of the installer. When the user clicks it to proceed with the installation this: + # 1. Flips the install scope to "perUser" which is the default in WiX v3 + # 2. Finds the older installation + # 3. And finally flips the scope back to "perMachine" which is the default in WiX v4 and newer + # + # Adapted from this stack overflow answer: https://stackoverflow.com/a/35064434 + # + # TODO: Revert this once we are reasonably certain there are no affected Dangerzone Installations? + ET.SubElement( + ui_el, + "Publish", + Dialog="WelcomeDlg", + Control="Next", + Property="ALLUSERS", + Value="{}", + ) + ET.SubElement( + ui_el, + "Publish", + Dialog="WelcomeDlg", + Control="Next", + Event="DoAction", + Value="FindRelatedProducts", + ) + ET.SubElement( + ui_el, + "Publish", + Dialog="WelcomeDlg", + Control="Next", + Property="ALLUSERS", + Value="1", + ) + # Add the ProgramMenuFolder StandardDirectory programmenufolder_el = ET.SubElement( package_el,