diff --git a/CONSTRUCT.md b/CONSTRUCT.md index 935061b8c..2397f181e 100644 --- a/CONSTRUCT.md +++ b/CONSTRUCT.md @@ -744,6 +744,7 @@ The output filename will be: - `win` - `win32` - `win64` +- `win_arm64` - `x86` - `x86_64` @@ -755,5 +756,6 @@ If provided, this argument must be formated as `-`, e.g. - `linux-ppc64le` - `linux-s390x` - `win-64` +- `win-arm64` - `osx-64` - `osx-arm64` \ No newline at end of file diff --git a/constructor/conda_interface.py b/constructor/conda_interface.py index d40813165..ab1c46bf7 100644 --- a/constructor/conda_interface.py +++ b/constructor/conda_interface.py @@ -32,6 +32,7 @@ "linux-ppc64le", "linux-s390x", "win-64", + "win-arm64", "osx-64", "osx-arm64", ] diff --git a/constructor/construct.py b/constructor/construct.py index bca76e357..bfd3fd8e6 100644 --- a/constructor/construct.py +++ b/constructor/construct.py @@ -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"), ) diff --git a/constructor/nsis/main.nsi.tmpl b/constructor/nsis/main.nsi.tmpl index c9fa01517..e214fbbd6 100644 --- a/constructor/nsis/main.nsi.tmpl +++ b/constructor/nsis/main.nsi.tmpl @@ -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 + # 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} + 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} diff --git a/constructor/winexe.py b/constructor/winexe.py index ce6074971..cff693e72 100644 --- a/constructor/winexe.py +++ b/constructor/winexe.py @@ -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) @@ -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", "") @@ -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()) @@ -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"], - }, - ".", - ) diff --git a/docs/source/construct-yaml.md b/docs/source/construct-yaml.md index 935061b8c..2397f181e 100644 --- a/docs/source/construct-yaml.md +++ b/docs/source/construct-yaml.md @@ -744,6 +744,7 @@ The output filename will be: - `win` - `win32` - `win64` +- `win_arm64` - `x86` - `x86_64` @@ -755,5 +756,6 @@ If provided, this argument must be formated as `-`, e.g. - `linux-ppc64le` - `linux-s390x` - `win-64` +- `win-arm64` - `osx-64` - `osx-arm64` \ No newline at end of file diff --git a/news/1323-extend-windows-support b/news/1323-extend-windows-support new file mode 100644 index 000000000..1cacd6e8f --- /dev/null +++ b/news/1323-extend-windows-support @@ -0,0 +1,19 @@ +### Enhancements + +* Add support for Windows ARM64. (#1323) + +### Bug fixes + +* + +### Deprecations + +* + +### Docs + +* + +### Other + +* diff --git a/tests/test_construct.py b/tests/test_construct.py index 6d3274c16..af4032c7a 100644 --- a/tests/test_construct.py +++ b/tests/test_construct.py @@ -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 @@ -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}" diff --git a/tests/test_winexe.py b/tests/test_winexe.py new file mode 100644 index 000000000..7982d5618 --- /dev/null +++ b/tests/test_winexe.py @@ -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