diff --git a/.github/workflows/install-vs-components.py b/.github/workflows/install-vs-components.py index ed2deb62f..2c75a5eb1 100644 --- a/.github/workflows/install-vs-components.py +++ b/.github/workflows/install-vs-components.py @@ -21,11 +21,11 @@ text=True, shell=True, ).strip() -components_to_add = ( - ["Microsoft.VisualStudio.Component.VC.14.29.16.11.ATL.ARM64"] +components_to_add = [ + "Microsoft.VisualStudio.Component.VC.14.29.16.11.ATL.ARM64" if platform.machine() == "ARM64" - else ["Microsoft.VisualStudio.Component.VC.14.29.16.11.ATL"] -) + else "Microsoft.VisualStudio.Component.VC.14.29.16.11.ATL" +] args = ( "vs_installer.exe", "modify", diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a3a4a9ab9..a8866a7e0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,13 +15,25 @@ concurrency: jobs: test: name: Build and test - runs-on: windows-2022 + runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] - architecture: [x64, x86] + python-architecture: [x64, x86, arm64] + include: + - os: windows-2022 + - python-architecture: arm64 + os: windows-11-arm + exclude: + # actions/setup-python does not provide prebuilt arm64 Python before 3.11 + - python-architecture: arm64 + python-version: "3.8" + - python-architecture: arm64 + python-version: "3.9" + - python-architecture: arm64 + python-version: "3.10" env: # TODO: We can't yet run tests with PYTHONDEVMODE=1, let's emulated it as much as we can # https://docs.python.org/3/library/devmode.html#effects-of-the-python-development-mode @@ -37,7 +49,7 @@ jobs: uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - architecture: ${{ matrix.architecture }} + architecture: ${{ matrix.python-architecture }} cache: pip cache-dependency-path: .github/workflows/main.yml check-latest: true @@ -83,21 +95,20 @@ jobs: # Upload artifacts even if tests fail if: ${{ always() }} with: - name: artifacts-${{ matrix.python-version }}-${{ matrix.architecture }} + name: artifacts-${{ matrix.python-version }}-${{ matrix.python-architecture }} path: dist/*.whl if-no-files-found: error - # We cannot build and test on ARM64, so we cross-compile. - # Later, when available, we can add tests using this wheel on ARM64 VMs - build_arm64: - name: Cross-compile ARM + # actions/setup-python does not provide prebuilt arm64 Python before 3.11, so we cross-compile. + cross_compile_arm64: + name: Cross-compile ARM64 runs-on: windows-2022 timeout-minutes: 30 strategy: fail-fast: false matrix: - # pythonarm64 NuGet's has no download for Python ~=3.9.11 - python-version: ["3.9.10", "3.10", "3.11", "3.12", "3.13", "3.14"] + # pythonarm64 NuGet has no download for Python 3.8 and Python ~=3.9.11 + python-version: ["3.9.10", "3.10"] steps: - uses: actions/checkout@v4 @@ -115,10 +126,10 @@ jobs: run: pip install --upgrade build - name: Obtain ARM64 library files - run: python .github\workflows\download-arm64-libs.py .\arm64libs + run: python .github\workflows\download-arm64-libs.py ./arm64libs - name: Build wheels - run: python -m build --wheel --config-setting=--build-option=build_ext --config-setting=--build-option=-L.\arm64libs --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=build --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=bdist_wheel --config-setting=--build-option=--plat-name=win-arm64 + run: python -m build --wheel --config-setting=--build-option="build_ext -L./arm64libs --plat-name=win-arm64 build --plat-name=win-arm64 bdist_wheel --plat-name=win-arm64" - uses: actions/upload-artifact@v4 with: @@ -128,7 +139,7 @@ jobs: merge: runs-on: windows-latest - needs: [test, build_arm64] + needs: [test, cross_compile_arm64] steps: - name: Merge Artifacts uses: actions/upload-artifact/merge@v4 diff --git a/build_env.md b/build_env.md index 8000f04d9..742d972b3 100644 --- a/build_env.md +++ b/build_env.md @@ -137,12 +137,12 @@ configuration, please [open an issue](https://github.com/mhammond/pywin32/issues - Follow the `For Visual Studio XXXX` instructions above and pick the optional ARM64 build tools - Download prebuilt Python ARM64 binaries to a temporary location on your machine. You will need this location in a later step. + - This script downloads a Python ARM64 build [from NuGet](https://www.nuget.org/packages/pythonarm64/#versions-tab) that matches the version you used to run it. ```shell - python .github\workflows\download-arm64-libraries.py "" + python .github\workflows\download-arm64-libs.py ./arm64libs ``` - - This script downloads a Python ARM64 build [from NuGet](https://www.nuget.org/packages/pythonarm64/#versions-tab) that matches the version you used to run it. - Setup the cross-compilation environment: ```shell @@ -156,13 +156,12 @@ configuration, please [open an issue](https://github.com/mhammond/pywin32/issues ``` - Build the extensions, passing the directory from earlier. You may optionally add the `bdist_wheel` command to generate a wheel. + - If you are not using an initialized build environment, you will need to specify the `build_ext`, `build` and `bdist_wheel` commands and pass `--plat-name win-arm64` to *each* of them separately. Otherwise you may get a mixed platform build and/or linker errors. ```shell - python -m build --wheel --config-setting=--build-option=build_ext --config-setting=--build-option=-L.\arm64libs --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=bdist_wheel --config-setting=--build-option=--plat-name=win-arm64 + python -m build --wheel --config-setting=--build-option="build_ext -L./arm64libs --plat-name=win-arm64 bdist_wheel --plat-name=win-arm64" ``` - - If you are not using an initialized build environment, you will need to specify the `build_ext`, `build` and `bdist_wheel` commands and pass `--plat-name win-arm64` to *each* of them separately. Otherwise you may get a mixed platform build and/or linker errors. - - Copy the built wheel to the target machine and install directly: ```shell diff --git a/com/win32com/test/testArrays.py b/com/win32com/test/testArrays.py index 0576efe67..d3014e1b9 100644 --- a/com/win32com/test/testArrays.py +++ b/com/win32com/test/testArrays.py @@ -1,11 +1,15 @@ # Originally contributed by Stefan Schukat as part of this arbitrary-sized # arrays patch. +from __future__ import annotations + +import platform +import unittest from win32com.client import gencache from win32com.test import util ZeroD = 0 -OneDEmpty = [] +OneDEmpty: list[int] = [] OneD = [1, 2, 3] TwoD = [[1, 2, 3], [1, 2, 3], [1, 2, 3]] @@ -49,6 +53,12 @@ def _normalize_array(a): return ret +@unittest.skipIf( + platform.machine() == "ARM64", + "PyCOMTest.ArrayTest cannot currently be run on ARM64 " + + "due to lacking win32com.universal implementation " + + "in com/win32com/src/univgw.cpp", +) class ArrayTest(util.TestCase): def setUp(self): self.arr = gencache.EnsureDispatch("PyCOMTest.ArrayTest", bForDemand=False) diff --git a/com/win32com/test/testPyComTest.py b/com/win32com/test/testPyComTest.py index 921a45630..f1426296b 100644 --- a/com/win32com/test/testPyComTest.py +++ b/com/win32com/test/testPyComTest.py @@ -15,7 +15,6 @@ import win32timezone import winerror from win32api import CloseHandle, GetCurrentProcessId, OpenProcess -from win32com import universal from win32com.client import ( VARIANT, CastTo, @@ -25,6 +24,7 @@ gencache, register_record_class, ) +from win32com.universal import RegisterInterfaces from win32process import GetProcessMemoryInfo # This test uses a Python implemented COM server - ensure correctly registered. @@ -46,10 +46,6 @@ print(f"The PyCOMTest module can not be located or generated.\n{importMsg}\n") raise RuntimeError(importMsg) from error -# We had a bg where RegisterInterfaces would fail if gencache had -# already been run - exercise that here -universal.RegisterInterfaces("{6BCDCB60-5605-11D0-AE5F-CADD4C000000}", 0, 1, 1) - verbose = 0 @@ -175,7 +171,7 @@ def _DumpFireds(self): if not self.fireds: print("ERROR: Nothing was received!") for firedId, no in self.fireds.items(): - progress("ID %d fired %d times" % (firedId, no)) + progress(f"ID {firedId} fired {no} times") # Test everything which can be tested using both the "dynamic" and "generated" @@ -891,34 +887,39 @@ def TestVTableMI(): pass -def TestQueryInterface(long_lived_server=0, iterations=5): +def TestQueryInterface(long_lived_server=False, iterations=5): tester = win32com.client.Dispatch("PyCOMTest.PyCOMTest") if long_lived_server: # Create a local server t0 = win32com.client.Dispatch( "Python.Test.PyCOMTest", clsctx=pythoncom.CLSCTX_LOCAL_SERVER ) - # Request custom interfaces a number of times - prompt = [ - "Testing QueryInterface without long-lived local-server #%d of %d...", - "Testing QueryInterface with long-lived local-server #%d of %d...", - ] + # Request custom interfaces a number of time for i in range(iterations): - progress(prompt[long_lived_server != 0] % (i + 1, iterations)) + progress( + f"Testing QueryInterface " + + ("with" if long_lived_server else "without") + + f" long-lived local-server #{i + 1} of {iterations}..." + ) tester.TestQueryInterface() class Tester(win32com.test.util.TestCase): - def testVTableInProc(self): + def testRegisterInterfacesAfterGencache(self) -> None: + # We had a bug where RegisterInterfaces would fail if gencache had + # already been run - exercise that here + RegisterInterfaces("{6BCDCB60-5605-11D0-AE5F-CADD4C000000}", 0, 1, 1) + + def testVTableInProc(self) -> None: # We used to crash running this the second time - do it a few times for i in range(3): - progress("Testing VTables in-process #%d..." % (i + 1)) + progress(f"Testing VTables in-process #{(i + 1)}...") TestVTable(pythoncom.CLSCTX_INPROC_SERVER) - def testVTableLocalServer(self): + def testVTableLocalServer(self) -> None: for i in range(3): - progress("Testing VTables out-of-process #%d..." % (i + 1)) + progress(f"Testing VTables out-of-process #{(i + 1)}...") TestVTable(pythoncom.CLSCTX_LOCAL_SERVER) def testVTable2(self): @@ -930,15 +931,15 @@ def testVTableMI(self): TestVTableMI() def testMultiQueryInterface(self): - TestQueryInterface(0, 6) + TestQueryInterface(False, 6) # When we use the custom interface in the presence of a long-lived # local server, i.e. a local server that is already running when # we request an instance of our COM object, and remains afterwards, # then after repeated requests to create an instance of our object # the custom interface disappears -- i.e. QueryInterface fails with # E_NOINTERFACE. Set the upper range of the following test to 2 to - # pass this test, i.e. TestQueryInterface(1,2) - TestQueryInterface(1, 6) + # pass this test, i.e. TestQueryInterface(True, 2) + TestQueryInterface(True, 6) def testDynamic(self): TestDynamic() diff --git a/com/win32com/test/testall.py b/com/win32com/test/testall.py index 5e6fbe41a..cf9ded20d 100644 --- a/com/win32com/test/testall.py +++ b/com/win32com/test/testall.py @@ -1,5 +1,6 @@ import getopt import os +import platform import re import sys import traceback @@ -22,7 +23,6 @@ win32com.__path__[0] = win32com_src_dir import pythoncom -import win32com.client from pywin32_testutil import TestLoader, TestRunner from win32com.test.util import ( CapturingFunctionTestCase, @@ -52,7 +52,7 @@ def CleanGenerated(): if os.path.isdir(win32com.__gen_path__): if verbosity > 1: - print("Deleting files from %s" % (win32com.__gen_path__)) + print(f"Deleting files from", win32com.__gen_path__) shutil.rmtree(win32com.__gen_path__) import win32com.client.gencache @@ -78,11 +78,17 @@ def ExecuteSilentlyIfOK(cmd, testcase): rc = f.close() if rc: print(data) - testcase.fail("Executing '%s' failed (%d)" % (cmd, rc)) + testcase.fail(f"Executing '{cmd}' failed ({rc})") # for "_d" builds, strip the '[xxx refs]' line return RemoveRefCountOutput(data) +@unittest.skipIf( + platform.machine() == "ARM64", + "PyCOMTest cannot currently be run on ARM64 " + + "due to lacking win32com.universal implementation " + + "in com/win32com/src/univgw.cpp", +) class PyCOMTest(TestCase): no_leak_tests = True # done by the test itself @@ -291,8 +297,7 @@ def usage(why): print("These tests may take *many* minutes to run - be patient!") print("(running from python.exe will avoid these leak tests)") print( - "Executing level %d tests - %d test cases will be run" - % (testLevel, suite.countTestCases()) + f"Executing level {testLevel} tests - {suite.countTestCases()} test cases will be run" ) if verbosity == 1 and suite.countTestCases() < 70: # A little row of markers so the dots show how close to finished @@ -307,7 +312,7 @@ def usage(why): desc = "\n".join(traceback.format_exception_only(exc_type, exc_val)) testResult.stream.write(f"{mod_name}: {desc}") testResult.stream.writeln( - "*** %d test(s) could not be run ***" % len(import_failures) + f"*** {len(import_failures)} test(s) could not be run ***" ) # re-print unit-test error here so it is noticed diff --git a/make_all.bat b/make_all.bat index 7c35ab225..f19792693 100644 --- a/make_all.bat +++ b/make_all.bat @@ -28,12 +28,12 @@ py -3.13 -m build --wheel py -3.14-32 -m build --wheel py -3.14 -m build --wheel -rem Check /build_env.md#build-environment to make sure you have all the required ARM64 components installed -py -3.10 -m build --wheel --config-setting=--build-option=build_ext --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=build --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=bdist_wheel --config-setting=--build-option=--plat-name=win-arm64 -py -3.11 -m build --wheel --config-setting=--build-option=build_ext --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=build --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=bdist_wheel --config-setting=--build-option=--plat-name=win-arm64 -py -3.12 -m build --wheel --config-setting=--build-option=build_ext --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=build --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=bdist_wheel --config-setting=--build-option=--plat-name=win-arm64 -py -3.13 -m build --wheel --config-setting=--build-option=build_ext --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=build --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=bdist_wheel --config-setting=--build-option=--plat-name=win-arm64 -py -3.14 -m build --wheel --config-setting=--build-option=build_ext --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=build --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=bdist_wheel --config-setting=--build-option=--plat-name=win-arm64 +rem Check /build_env.md#cross-compiling-for-arm64-microsoft-visual-c-141-and-up to make sure you have all the required ARM64 components installed +py -3.10 -m build --wheel --config-setting=--build-option="build_ext --plat-name=win-arm64 build --plat-name=win-arm64 bdist_wheel --plat-name=win-arm64" +py -3.11 -m build --wheel --config-setting=--build-option="build_ext --plat-name=win-arm64 build --plat-name=win-arm64 bdist_wheel --plat-name=win-arm64" +py -3.12 -m build --wheel --config-setting=--build-option="build_ext --plat-name=win-arm64 build --plat-name=win-arm64 bdist_wheel --plat-name=win-arm64" +py -3.13 -m build --wheel --config-setting=--build-option="build_ext --plat-name=win-arm64 build --plat-name=win-arm64 bdist_wheel --plat-name=win-arm64" +py -3.14 -m build --wheel --config-setting=--build-option="build_ext --plat-name=win-arm64 build --plat-name=win-arm64 bdist_wheel --plat-name=win-arm64" @goto xit :couldnt_rm diff --git a/setup.py b/setup.py index a45dbc7e5..803fcf7f2 100644 --- a/setup.py +++ b/setup.py @@ -13,10 +13,10 @@ For a debug (_d) version, you need a local debug build of Python, but must use the release version executable for the build. eg: - pip install . -v --config-setting=--build-option=build --config-setting=--build-option=--debug + pip install . -v --config-setting=--build-option="build --debug" Cross-compilation from x86 to ARM is well supported (assuming installed vs tools etc) - eg: - python -m build --wheel --config-setting=--build-option=build_ext --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=build --config-setting=--build-option=--plat-name=win-arm64 --config-setting=--build-option=bdist_wheel --config-setting=--build-option=--plat-name=win-arm64 + python -m build --wheel --config-setting=--build-option="build_ext --plat-name=win-arm64 build --plat-name=win-arm64 bdist_wheel --plat-name=win-arm64" Some modules require special SDKs or toolkits to build (eg, mapi/exchange), which often aren't available in CI. The build process treats them as optional -