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
2 changes: 2 additions & 0 deletions CONSTRUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,7 @@ The output filename will be:
- `win`
- `win32`
- `win64`
- `win_arm64`
- `x86`
- `x86_64`

Expand All @@ -755,5 +756,6 @@ If provided, this argument must be formated as `<platform>-<architecture>`, e.g.
- `linux-ppc64le`
- `linux-s390x`
- `win-64`
- `win-arm64`
- `osx-64`
- `osx-arm64`
1 change: 1 addition & 0 deletions constructor/conda_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"linux-ppc64le",
"linux-s390x",
"win-64",
"win-arm64",
"osx-64",
"osx-arm64",
]
Expand Down
1 change: 1 addition & 0 deletions constructor/construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def ns_platform(platform):
win=p.startswith("win-"),
win32=bool(p == "win-32"),
win64=bool(p == "win-64"),
win_arm64=bool(p == "win-arm64"),
)


Expand Down
21 changes: 16 additions & 5 deletions constructor/nsis/main.nsi.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -658,12 +658,23 @@ Function .onInit
# on whether it's a 32-bit or 64-bit installer
SetRegView {{ BITS }}
{%- if win64 %}
# If we're a 64-bit installer, make sure it's 64-bit Windows
${IfNot} ${RunningX64}
# Make sure we're not on 32-bit or native ARM64 Windows (the latter would
Comment thread
lrandersson marked this conversation as resolved.
# otherwise silently run this x64 installer under x64 emulation).
${IfNot} ${IsNativeAMD64}
MessageBox MB_OK|MB_ICONEXCLAMATION \
"This installer is for a 64-bit version for ${NAME}$\n\
but your system is 32-bit. Please use the 32-bit Windows$\n\
${NAME} installer." \
"This installer is for the 64-bit (x86_64) version of ${NAME}$\n\
but your system is not x86_64. Please use the installer that$\n\
matches your system's architecture." \
/SD IDOK
Abort
${EndIf}
{%- elif win_arm64 %}
# Make sure we're actually on native ARM64 Windows.
${IfNot} ${IsNativeARM64}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we have a similar check for the MSI installers?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did some investigation and couldnt find anything that exists today unfortunately. @freakboy3742 do you know if there is anything to check if an .msi installer is running on the wrong architecture? If not, let me know I'm happy to create a ticket.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WiX (and therefore Briefcase-generated) installers do some CPU detection by default - if you try to run an ARM64 installer on x86-64, you get an error:

Screenshot 2026-08-17 at 6 32 43 am

The inverse isn't true, however - ARM64 machines can run x86-64 installers (using CPU emulation mode). It might be possible to prevent this case as well, but it's not currently prevented by default.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see, perhaps also due to emulation it currently makes sense not to prevent it by default but perhaps exposing an option in pyproject.toml to enable/disable it would do the job? If you think so, let me know and Im happy to open a new ticket.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Exposing it as an option seems plausible as an option; my main concern would be whether there's actually demand for it. From what I've seen, it won't be a trivial addition (as in - it's not a simple "if" statement or pre-existing MSI config option - we'd need to build it from low-level primitives). If this is a case of "theoretical" interest rather than a use case, then I'd rather avoid the complexity.

That said, I guess a ticket couldn't hurt from at least an information gathering capacity...

MessageBox MB_OK|MB_ICONEXCLAMATION \
"This installer is for the ARM64 version of ${NAME}$\n\
but your system is not ARM64. Please use the installer that$\n\
matches your system's architecture." \
/SD IDOK
Abort
${EndIf}
Expand Down
27 changes: 11 additions & 16 deletions constructor/winexe.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@
logger = logging.getLogger(__name__)


def parse_arch(platform: str) -> tuple[str, int]:
"""Return a tuple (display string, bit width) for a Windows platform string."""
arch = platform.split("-")[1]
if arch == "arm64":
return "ARM64", 64
bits = int(arch)
return "%d-bit" % bits, bits


def read_nsi_tmpl(info) -> str:
path = abspath(info.get("nsis_template", join(NSIS_DIR, "main.nsi.tmpl")))
logger.info("Reading: %s", path)
Expand Down Expand Up @@ -155,7 +164,7 @@ def make_nsi(
dists += env_info["_dists"]
dists = list({dist: None for dist in dists}) # de-duplicate

arch = int(info["_platform"].split("-")[1])
display_arch, arch = parse_arch(info["_platform"])
info["pre_install_desc"] = info.get("pre_install_desc", "")
info["post_install_desc"] = info.get("post_install_desc", "")

Expand All @@ -164,7 +173,7 @@ def make_nsi(
"installer_version": info["version"],
"company": info.get("company", "Unknown, Inc."),
"installer_platform": info["_platform"],
"arch": "%d-bit" % arch,
"arch": display_arch,
"default_prefix": info.get("default_prefix", join("%USERPROFILE%", name.lower())),
"default_prefix_domain_user": info.get(
"default_prefix_domain_user", join("%LOCALAPPDATA%", name.lower())
Expand Down Expand Up @@ -413,17 +422,3 @@ def create(info, verbose=False):

if not info.get("_debug"):
shutil.rmtree(tmp_dir)


if __name__ == "__main__":
make_nsi(
{
"name": "Maxi",
"version": "1.2",
"_platform": "win-64",
"_outpath": "dummy.exe",
"_download_dir": "dummy",
"_dists": ["python-2.7.9-0.tar.bz2", "vs2008_runtime-1.0-1.tar.bz2"],
},
".",
)
2 changes: 2 additions & 0 deletions docs/source/construct-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,7 @@ The output filename will be:
- `win`
- `win32`
- `win64`
- `win_arm64`
- `x86`
- `x86_64`

Expand All @@ -755,5 +756,6 @@ If provided, this argument must be formated as `<platform>-<architecture>`, e.g.
- `linux-ppc64le`
- `linux-s390x`
- `win-64`
- `win-arm64`
- `osx-64`
- `osx-arm64`
19 changes: 19 additions & 0 deletions news/1323-extend-windows-support
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### Enhancements

* Add support for Windows ARM64. (#1323)

### Bug fixes

* <news item>

### Deprecations

* <news item>

### Docs

* <news item>

### Other

* <news item>
24 changes: 23 additions & 1 deletion tests/test_construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import pytest

from constructor.conda_interface import cc_platform
from constructor.conda_interface import SUPPORTED_PLATFORMS, cc_platform
from constructor.construct import ns_platform
from constructor.construct import parse as construct_parse
from constructor.construct import render as construct_render

Expand Down Expand Up @@ -110,3 +111,24 @@ def test_parse_error(tmp_path):
construct_parse(construct_yaml_file, cc_platform)
assert exc.value.code != 0
assert "Unable to parse" in str(exc.getrepr())


NS_PLATFORM_TRUE_FLAGS = {
"linux-64": {"linux", "linux64", "x86", "x86_64", "unix"},
"linux-aarch64": {"linux", "aarch64", "unix"},
"linux-ppc64le": {"linux", "ppc64le", "unix"},
"linux-s390x": {"linux", "s390x", "unix"},
"win-64": {"x86", "x86_64", "win", "win64"},
"win-arm64": {"win", "win_arm64"},
"osx-64": {"x86", "x86_64", "osx", "unix"},
"osx-arm64": {"arm64", "osx", "unix"},
}


@pytest.mark.parametrize("platform", SUPPORTED_PLATFORMS)
def test_ns_platform(platform):
result = ns_platform(platform)
assert result
true_flags = NS_PLATFORM_TRUE_FLAGS[platform]
for flag, value in result.items():
assert value is (flag in true_flags), f"{platform}: expected {flag}={flag in true_flags}"
21 changes: 21 additions & 0 deletions tests/test_winexe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import sys

import pytest

# winexe.py needs Pillow, which isn't installed on Linux. Skip this file there
# and only import winexe inside each test, so collection doesn't fail.
pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="winexe is Windows-only")


@pytest.mark.parametrize(
"platform,expected",
[
("win-32", ("32-bit", 32)),
("win-64", ("64-bit", 64)),
("win-arm64", ("ARM64", 64)),
],
)
def test_parse_arch(platform, expected):
from constructor.winexe import parse_arch

assert parse_arch(platform) == expected
Loading