Compare commits

...

25 commits

Author SHA1 Message Date
jkarasti
fc529860de
Merge da09566a3c into 96aa56a6dc 2024-11-11 17:13:24 +00:00
jkarasti
da09566a3c Fixup: Tweak wix install doc 2024-11-11 19:10:16 +02:00
jkarasti
180b907cff 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?
2024-11-11 19:10:16 +02:00
jkarasti
a900a7c42e Change: Wrap installer ui related things in a UI element 2024-11-11 19:10:16 +02:00
JKarasti
945f1846d1 Change: Build a 64-bit installer 2024-11-11 19:08:29 +02:00
JKarasti
3eaa89fcce Docs: Documentation for WiX Toolset 5 2024-11-11 19:08:29 +02:00
JKarasti
63dbe6a037 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/
2024-11-11 19:08:29 +02:00
JKarasti
0767e01053 CI: Use WiX Toolset v5 to build the msi 2024-11-11 19:08:29 +02:00
JKarasti
ada9f47d65 Change: Write Dangerzone.wxs inside the script directly
Also reduce duplication slightly by definig `build_dir`, `cx_freeze_dir` and `dist_dir`
2024-11-11 19:08:29 +02:00
JKarasti
39062804bf Fix: Make GUIDs uppercase
See [1]

[1] https://learn.microsoft.com/en-us/windows/win32/msi/guid
2024-11-11 19:08:29 +02:00
JKarasti
9da607093f Change: Write dangerzone version and upgradecode into Package and SummaryInformation elements directly 2024-11-11 19:08:29 +02:00
JKarasti
02c8396a9a Refactor: build_dir_xml() function
- rename for clarity
- remove unnecessary checks
2024-11-11 19:08:29 +02:00
JKarasti
2b73427e16 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`
2024-11-11 19:08:29 +02:00
JKarasti
c7e3f995c0 Refactor: Simplify build_data() function
- Rename variables to be more clear about what they do:
- reorganise code
- simplify a few checks
2024-11-11 19:08:29 +02:00
JKarasti
410beead11 Change: Swap Media element with MediaTemplate
This is a new default and makes authoring slightly simpler without any functional changes.
2024-11-11 19:08:29 +02:00
JKarasti
68cd96a7d3 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
2024-11-11 19:08:29 +02:00
JKarasti
e6f5a25818 Change: Wrap ProgramFilesFolder component with a StandardDirectory component 2024-11-11 19:08:29 +02:00
JKarasti
28b555ac6f Change: Wrap ProgramMenuFolder component with a StandardDirectory component 2024-11-11 19:08:29 +02:00
jkarasti
9a87e64e69 Change: Disable AllowSameVersionUpgrades
Since running `wix msi validate` with it set to `yes` causes an error.
2024-11-11 19:08:29 +02:00
JKarasti
3df5c0d3be 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)
2024-10-31 19:18:02 +02:00
JKarasti
3357b30edb Change: Rename INSTALLDIR to INSTALLFOLDER
It's the new default name for it
2024-10-27 17:43:55 +02:00
JKarasti
b2c085c812 Change: Update WiX schema namespace
Also rename `root_el` to `wix_el`.

WiX version 5 uses the same namespace.
2024-10-27 17:43:55 +02:00
JKarasti
6e2659bc0e Change: Stop generating an XML declaration at the top of the WiX authoring
It's not needed anymore.
2024-10-27 17:43:54 +02:00
JKarasti
99f231b46a 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
2024-10-27 17:43:54 +02:00
JKarasti
a22d8443e8 Fix: SyntaxWarning while generating Dangerzone.wxs 2024-10-27 17:43:54 +02:00
4 changed files with 217 additions and 166 deletions

View file

@ -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
- name: Add WiX UI extension
run: wix extension add --global WixToolset.UI.wixext
- name: Build the MSI installer
# NOTE: This also builds the .exe internally.
run: poetry run .\install\windows\build-app.bat

View file

@ -474,11 +474,23 @@ 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 latest version of [WiX Toolset .NET tool](https://wixtoolset.org/) **v5** with:
```sh
dotnet tool install --global wix --version 5.*
```
Install the WiX UI extension. You may need to open a new terminal in order to use the newly installed `wix` .NET tool:
```sh
wix extension add --global WixToolset.UI.wixext/5.x.y
```
> [!IMPORTANT]
> To avoid compatibility issues, ensure the WiX UI extension version matches the version of the WiX Toolset.
> Run `wix --version` to check the version of WiX Toolset you have installed and replace `5.x.y` with the full version number without the Git revision.
### If you want to sign binaries with Authenticode

View file

@ -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 -arch x64 -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

View file

@ -4,114 +4,75 @@ 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_ == "INSTALLDIR":
id_prefix = "Folder"
if dir_id == "INSTALLFOLDER":
data["component_id"] = "ApplicationFiles"
else:
id_prefix = id_
data["component_id"] = "Component" + dir_id
data["component_guid"] = str(uuid.uuid4()).upper()
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:
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_ == "INSTALLDIR":
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
def build_dir_xml(root, data):
def build_directory_xml(root, data):
attrs = {}
if "id" in data:
attrs["Id"] = data["id"]
if "name" in data:
attrs["Name"] = data["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)
# 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="[INSTALLDIR]dangerzone.exe",
WorkingDirectory="INSTALLDIR",
)
ET.SubElement(
component_el,
"RegistryValue",
Root="HKCU",
Key="Software\Freedom of the Press Foundation\Dangerzone",
Name="installed",
Type="integer",
Value="1",
KeyPath="yes",
)
build_directory_xml(directory_el, subdata)
def build_components_xml(root, data):
component_ids = []
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"])
component_el = ET.SubElement(
dir_ref_el,
root,
"Component",
Id=subdata["component_id"],
Guid=subdata["component_guid"],
Id=data["component_id"],
Guid=data["component_guid"],
Directory=data["directory_id"],
)
for filename in subdata["files"]:
file_el = ET.SubElement(
component_el, "File", Source=filename, Id="file_" + uuid.uuid4().hex
)
component_ids += build_components_xml(root, subdata)
return component_ids
for filename in data["files"]:
ET.SubElement(component_el, "File", Source=filename)
for subdata in data["dirs"]:
build_components_xml(root, subdata)
def main():
@ -125,120 +86,193 @@ 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
data = {
"id": "TARGETDIR",
"name": "SourceDir",
"dirs": [
{
"id": "ProgramFilesFolder",
"dirs": [],
},
{
"id": "ProgramMenuFolder",
"dirs": [],
},
],
}
data["dirs"][0]["dirs"].append(
build_data(
# Prepare data for WiX file harvesting from the output of cx_Freeze
data = build_data(
dist_dir,
"exe.win-amd64-3.12",
"INSTALLDIR",
cx_freeze_dir,
"INSTALLFOLDER",
"Dangerzone",
)
# Add the Wix root element
wix_el = ET.Element(
"Wix",
{
"xmlns": "http://wixtoolset.org/schemas/v4/wxs",
"xmlns:ui": "http://wixtoolset.org/schemas/v4/wxs/ui",
},
)
root_el = ET.Element("Wix", xmlns="http://schemas.microsoft.com/wix/2006/wi")
product_el = ET.SubElement(
root_el,
"Product",
# Add the Package element
package_el = ET.SubElement(
wix_el,
"Package",
Name="Dangerzone",
Manufacturer="Freedom of the Press Foundation",
Id="*",
UpgradeCode="$(var.ProductUpgradeCode)",
UpgradeCode="12B9695C-965B-4BE0-BC33-21274E809576",
Language="1033",
Codepage="1252",
Version="$(var.ProductVersion)",
)
ET.SubElement(
product_el,
"Package",
Id="*",
Keywords="Installer",
Description="Dangerzone $(var.ProductVersion) Installer",
Manufacturer="Freedom of the Press Foundation",
InstallerVersion="100",
Languages="1033",
Compressed="yes",
SummaryCodepage="1252",
Codepage="1252",
Version=version,
)
ET.SubElement(product_el, "Media", Id="1", Cabinet="product.cab", EmbedCab="yes")
ET.SubElement(
product_el, "Icon", Id="ProductIcon", SourceFile="..\\share\\dangerzone.ico"
package_el,
"SummaryInformation",
Keywords="Installer",
Description="Dangerzone " + version + " Installer",
Codepage="1252",
)
ET.SubElement(product_el, "Property", Id="ARPPRODUCTICON", Value="ProductIcon")
ET.SubElement(package_el, "MediaTemplate", EmbedCab="yes")
ET.SubElement(
product_el,
package_el, "Icon", Id="ProductIcon", SourceFile="..\\share\\dangerzone.ico"
)
ET.SubElement(package_el, "Property", Id="ARPPRODUCTICON", Value="ProductIcon")
ET.SubElement(
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,
"Property",
Id="WIXUI_INSTALLDIR",
Value="INSTALLDIR",
)
ET.SubElement(product_el, "UIRef", Id="WixUI_InstallDir")
ET.SubElement(product_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)
# 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")
feature_el = ET.SubElement(product_el, "Feature", Id="DefaultFeature", Level="1")
for component_id in component_ids:
ET.SubElement(feature_el, "ComponentRef", Id=component_id)
# 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,
"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",
)
# Add the ProgramFilesFolder StandardDirectory
programfilesfolder_el = ET.SubElement(
package_el,
"StandardDirectory",
Id="ProgramFiles64Folder",
)
# Create the directory structure for the installed product
build_directory_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")
ET.SubElement(feature_el, "ComponentGroupRef", Id="ApplicationComponents")
ET.SubElement(feature_el, "ComponentRef", Id="ApplicationShortcuts")
print('<?xml version="1.0" encoding="windows-1252"?>')
print(f'<?define ProductVersion = "{version}"?>')
print('<?define ProductUpgradeCode = "12b9695c-965b-4be0-bc33-21274e809576"?>')
ET.indent(root_el)
print(ET.tostring(root_el).decode())
ET.indent(wix_el, space=" ")
with open(os.path.join(build_dir, "Dangerzone.wxs"), "w") as wxs_file:
wxs_file.write(ET.tostring(wix_el).decode())
if __name__ == "__main__":