Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/1185.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Briefcase now uses WiX version 5.0.2. Any Windows apps created with previous versions of Briefcase will need to be re-generated by running ``briefcase create``.
2 changes: 1 addition & 1 deletion docs/reference/commands/upgrade.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Over time, it may be necessary to upgrade these tools. The ``upgrade`` command
provides a way to perform these upgrades.

If you are managing your own version of these tools (e.g., if you have
downloaded a version of WiX and have set the ``WIX_HOME`` environment variable),
downloaded the Android SDK and have set the ``ANDROID_HOME`` environment variable),
you must manage any upgrades on your own.

Usage
Expand Down
6 changes: 2 additions & 4 deletions docs/reference/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ or, for a platform-specific definition:

``[tool.briefcase.app.<app name>.<platform>.document_type.<document type id>]``

The ``document type id`` is an identifier, in alphanumeric format. It is appended to the app id of an application to identify documents of the same type.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed internal implementation detail which is meaningless to the Briefcase user.

The ``document type id`` is an identifier, in alphanumeric format.

The document type declaration requires the following settings:

Expand All @@ -620,9 +620,7 @@ A short, one-line description of the document format.

.. attribute:: extension

The :attr:`extension` is the file extension to register. For example, ``myapp``
could register as a handler for PNG image files by defining the configuration
section ``[tool.briefcase.app.myapp.document_type.png]``.
Comment on lines -623 to -625
Copy link
Member Author

@mhsmith mhsmith Jul 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implied that the section ID and the extension must be the same, or that the extension attribute is optional, neither of which are true.

The file extension to register, without a leading dot.

.. attribute:: icon

Expand Down
8 changes: 1 addition & 7 deletions docs/reference/platforms/windows/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,7 @@ Briefcase supports two packaging formats for a Windows app:
package windows -p zip``).

Briefcase uses the `WiX Toolset <https://www.firegiant.com/wixtoolset/>`__ to build an
MSI installer for a Windows App. WiX, in turn, requires that .NET Framework 3.5 is
enabled. To ensure .NET Framework 3.5 is enabled:

1. Open the Windows Control Panel
2. Traverse to Programs -> Programs and Features
3. Select "Turn Windows features On or Off"
4. Ensure that ".NET framework 3.5 (includes .NET 2.0 and 3.0)" is selected.
MSI installer for a Windows app.

Icon format
===========
Expand Down
3 changes: 3 additions & 0 deletions src/briefcase/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,9 @@ def update(self, data):
for key, configs in data.items():
setattr(self, key, configs)

def copy(self):
return type(self)(**self.__dict__)

def setdefault(self, field_name, default_value):
"""Return the field_name field or, if it does not exist, create it
to hold default_value.
Expand Down
155 changes: 71 additions & 84 deletions src/briefcase/integrations/wix.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

import os
import shutil
import re
from pathlib import Path
from subprocess import CalledProcessError

from packaging.version import Version

from briefcase.exceptions import BriefcaseCommandError, MissingToolError
from briefcase.integrations.base import ManagedTool, ToolCache
Expand All @@ -13,62 +15,51 @@ class WiX(ManagedTool):
full_name = "WiX"
supported_host_os = {"Windows"}

def __init__(
self,
tools: ToolCache,
wix_home: Path | None = None,
bin_install: bool = False,
):
# WARNING: version 6 and later have licensing issues: see
# https://github.com/beeware/briefcase/issues/1185.
version = Version("5.0.2")

def __init__(self, tools: ToolCache, wix_home: Path | None = None):
"""Create a wrapper around a WiX install.

:param tools: ToolCache of available tools.
:param wix_home: The path of the WiX installation.
:param bin_install: Is the install a binaries-only install? A full MSI install
of WiX has a `/bin` folder in the paths; a binaries-only install does not.
:returns: A valid WiX SDK wrapper. If WiX is not available, and was not
installed, raises MissingToolError.
"""
super().__init__(tools=tools)
if wix_home:
self.wix_home = wix_home
else:
self.wix_home = tools.base_path / "wix"
self.bin_install = bin_install

@property
def download_url(self) -> str:
return "https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip"

@property
def heat_exe(self) -> Path:
if self.bin_install:
return self.wix_home / "heat.exe"
else:
return self.wix_home / "bin/heat.exe"
return (
f"https://github.com/wixtoolset/wix/releases/download/v{self.version}/"
f"wix-cli-x64.msi"
)

@property
def light_exe(self) -> Path:
if self.bin_install:
return self.wix_home / "light.exe"
else:
return self.wix_home / "bin/light.exe"
def wix_exe(self) -> Path:
return (
self.wix_home
/ f"PFiles64/WiX Toolset v{self.version.major}.{self.version.minor}/bin"
/ "wix.exe"
)

@property
def candle_exe(self) -> Path:
if self.bin_install:
return self.wix_home / "candle.exe"
else:
return self.wix_home / "bin/candle.exe"
def ext_path(self, name: str) -> Path:
return (
self.wix_home
/ f"CFiles64/WixToolset/extensions/WixToolset.{name}.wixext/{self.version}"
/ f"wixext{self.version.major}/WixToolset.{name}.wixext.dll"
)

@classmethod
def verify_install(cls, tools: ToolCache, install: bool = True, **kwargs) -> WiX:
"""Verify that there is a WiX install available.

If the WIX environment variable is set, that location will be checked for a
valid WiX installation.

If the location provided doesn't contain an SDK, or no location is provided, an
SDK is downloaded.
WiX is a small tool, and there's a close relationship between the WiX version
and the template syntax, so we always use a Briefcase-managed copy, and upgrade
it automatically.

:param tools: ToolCache of available tools
:param install: Should WiX be installed if it is not found?
Expand All @@ -77,36 +68,38 @@ def verify_install(cls, tools: ToolCache, install: bool = True, **kwargs) -> WiX
if hasattr(tools, "wix"):
return tools.wix

# Look for the WIX environment variable
if wix_env := tools.os.environ.get("WIX"):
tools.console.debug("Evaluating WIX...", prefix=cls.full_name)
tools.console.debug(f"WIX={wix_env}")
wix_home = Path(wix_env)

# Set up the paths for the WiX executables we will use.
wix = WiX(tools=tools, wix_home=wix_home)

if not wix.exists():
raise BriefcaseCommandError(
f"""\
The WIX environment variable:

{wix_home}

does not point to an install of the WiX Toolset.
"""
wix = WiX(tools)
if not wix.exists():
if install:
tools.console.info(
"The WiX toolset was not found; downloading and installing...",
prefix=cls.name,
)

wix.install()
else:
raise MissingToolError("WiX")
else:
wix = WiX(tools=tools, bin_install=True)
try:
# The string returned by --version may include "+" followed by a
# commit ID; ignore this.
installed_version = re.sub(
r"\+.*",
"",
tools.subprocess.check_output([wix.wix_exe, "--version"]),
).strip()
except (OSError, CalledProcessError) as e:
installed_version = None
tools.console.error(
f"The WiX toolset is unusable ({type(e).__name__}: {e})"
)

if not wix.exists():
if install:
if installed_version != str(wix.version):
if installed_version is not None:
tools.console.info(
"The WiX toolset was not found; downloading and installing...",
prefix=cls.name,
f"The WiX toolset is an unsupported version ({installed_version})"
)
wix.install()
if install:
wix.upgrade()
else:
raise MissingToolError("WiX")

Expand All @@ -115,49 +108,43 @@ def verify_install(cls, tools: ToolCache, install: bool = True, **kwargs) -> WiX
return wix

def exists(self) -> bool:
return (
self.heat_exe.is_file()
and self.light_exe.is_file()
and self.candle_exe.is_file()
)
return self.wix_exe.is_file()

@property
def managed_install(self) -> bool:
try:
# Determine if wix_home is relative to the briefcase data directory.
# If wix_home isn't inside this directory, this will raise a ValueError,
# indicating it is a non-managed install.
self.wix_home.relative_to(self.tools.base_path)
return True
except ValueError:
return False
return True

def install(self):
"""Download and install WiX."""
wix_zip_path = self.tools.file.download(
wix_msi_path = self.tools.file.download(
url=self.download_url,
download_path=self.tools.base_path,
role="WiX",
)

try:
with self.tools.console.wait_bar("Installing WiX..."):
self.tools.file.unpack_archive(
os.fsdecode(wix_zip_path),
extract_dir=os.fsdecode(self.wix_home),
self.tools.subprocess.run(
[
"msiexec",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we certain we can rely on msiexec existing? And that it can be executed as a non-privileged user?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed on my daughter's school-provided (and thus locked-down) windows device - msiexec is available, and can be executed by non-admin users.

"/a", # Unpack the MSI into individual files
wix_msi_path,
"/qn", # Disable GUI interaction
f"TARGETDIR={self.wix_home}",
],
check=True,
)
except (shutil.ReadError, EOFError) as e:
except CalledProcessError as e:
raise BriefcaseCommandError(
f"""\
Unable to unpack WiX ZIP file. The download may have been
Unable to unpack WiX MSI file. The download may have been
interrupted or corrupted.

Delete {wix_zip_path} and run briefcase again.
Delete {wix_msi_path} and run briefcase again.
"""
) from e

# Zip file no longer needed once unpacked.
wix_zip_path.unlink()
wix_msi_path.unlink()

def uninstall(self):
"""Uninstall WiX."""
Expand Down
Loading
Loading