diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8dbab1f5..0bd8ef3e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -128,8 +128,12 @@ jobs: - ubuntu-latest - macOS-latest - windows-latest + # Since the oldest Python available on windows-11-arm on GitHub + # is 3.11.0 (see https://raw.githubusercontent.com/actions/\ + # python-versions/main/versions-manifest.json), older wheels + # cannot be tested... so just skip pre-building them cibw_skip: - - '*-win32 *-win32 cp313-musllinux_i686' + - '*-win32 cp3{9,10}-win_arm64 cp313-musllinux_i686' arch: - auto steps: @@ -137,7 +141,7 @@ jobs: uses: actions/checkout@v4.2.2 - name: Enable MSVC 64bit uses: ilammy/msvc-dev-cmd@v1 - if: matrix.os == 'windows-latest' && ${{ contains(matrix.cibw_skip, '*-win32') }} + if: ${{ startsWith(matrix.os, 'windows-') }} && ${{ contains(matrix.cibw_skip, '*-win32') }} - name: Set up QEMU uses: docker/setup-qemu-action@v3.0.0 if: runner.os == 'Linux' && matrix.arch != 'auto' @@ -150,9 +154,17 @@ jobs: config-file: pyproject.toml env: CIBW_SKIP: ${{ matrix.cibw_skip }} + # We're building on Windows-x64, so ARM64 wheels can't be tested + # locally by `cibuildwheel` (don't worry, we're testing them + # later though in `test_binpy_wheels`) + CIBW_TEST_SKIP: '*-win_arm64' CIBW_ARCHS_LINUX: ${{ matrix.arch }} CIBW_ENVIRONMENT: PYTHONUTF8=1 PYTHONUTF8: '1' + # `msvc-dev-cmd` sets this envvar, which interferes with + # cross-architecture building... + # just let `cibuildwheel` handle that + VSCMD_ARG_TGT_ARCH: '' - name: Show built files shell: bash run: ls -la wheelhouse @@ -224,6 +236,7 @@ jobs: install-extras: tests-strict,runtime-strict os: windows-latest arch: auto + # Note: cibuildwheel can't target 3.8 on Window ARM64 - python-version: '3.13' install-extras: tests-strict,runtime-strict,optional-strict os: ubuntu-latest @@ -236,6 +249,10 @@ jobs: install-extras: tests-strict,runtime-strict,optional-strict os: windows-latest arch: auto + - python-version: '3.13' + install-extras: tests-strict,runtime-strict,optional-strict + os: windows-11-arm + arch: auto - python-version: '3.13' install-extras: tests os: macOS-latest @@ -244,6 +261,10 @@ jobs: install-extras: tests os: windows-latest arch: auto + - python-version: '3.13' + install-extras: tests + os: windows-11-arm + arch: auto - python-version: '3.8' install-extras: tests,optional os: ubuntu-latest @@ -328,12 +349,35 @@ jobs: install-extras: tests,optional os: windows-latest arch: auto + # Again, cibuildwheel can't target 3.8 on Window ARM64, and + # GitHub doesn't have anything below Python 3.11 on their ARM64 + # machines, so just test the built wheels from 3.11+ + - python-version: '3.11' + install-extras: tests,optional + os: windows-11-arm + arch: auto + - python-version: '3.12' + install-extras: tests,optional + os: windows-11-arm + arch: auto + - python-version: '3.13' + install-extras: tests,optional + os: windows-11-arm + arch: auto + - python-version: 3.14.0-rc.1 + install-extras: tests,optional + os: windows-11-arm + arch: auto steps: - name: Checkout source uses: actions/checkout@v4.2.2 - name: Enable MSVC 64bit uses: ilammy/msvc-dev-cmd@v1 - if: matrix.os == 'windows-latest' + if: ${{ startsWith(matrix.os, 'windows-') }} + # As noted in msvc-dev-cmd #90 (and the Action docs), it currently + # # assumes `arch=x64`, so we have to manually set it here... + with: + arch: ${{ contains(matrix.os, 'arm') && 'arm64' || 'x64' }} - name: Set up QEMU uses: docker/setup-qemu-action@v3.0.0 if: runner.os == 'Linux' && matrix.arch != 'auto' diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e41e5c6b..f8be9b28 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,7 @@ Changes * FIX: ref-count leaks #372 * FIX: mitigate speed regressions introduced in 5.0.0 #376 * FIX: Use import system to locate module file run by ``kernprof -m`` #389 +* FIX: Fixed build on Windows-ARM64 and now building wheels therefor in CI #391 5.0.0 ~~~~~ diff --git a/line_profiler/c_trace_callbacks.h b/line_profiler/c_trace_callbacks.h index e4238f1b..ce221020 100644 --- a/line_profiler/c_trace_callbacks.h +++ b/line_profiler/c_trace_callbacks.h @@ -14,6 +14,12 @@ * and causes problems in 3.12 (see CPython #105268, #105350, #107348) * - Undefine the `HAVE_STD_ATOMIC` macro, which causes problems on * Linux in 3.12 (see CPython #108216) + * - Set `Py_ATOMIC_H` to true to circumvent the #include of + * `include/pycore_atomic.h` (in `include/pycore_interp.h`, so that + * problematic function definitions therein are replaced with dummy + * ones (see #390); note that we still need to vendor in parts + * therefrom which are used by `pycore_interp.h`, and its dependencies + * `pycore_ceval_state.h` and `pycore_gil.h` (or at least mock them) * Note in any case that we don't actually use `PyInterpreterState` * directly -- we just need its memory layout so that we can refer to * its `.last_restart_version` member @@ -24,17 +30,45 @@ # ifndef Py_BUILD_CORE # define Py_BUILD_CORE 1 # endif -# ifdef _PyGC_FINALIZED +# if PY_VERSION_HEX < 0x030d0000 // 3.13 # undef _PyGC_FINALIZED +# ifdef __linux__ +# undef HAVE_STD_ATOMIC +# endif +# if (defined(_M_ARM) || defined(_M_ARM64)) && (! defined(Py_ATOMIC_H)) +# define Py_ATOMIC_H + // Used in `pycore_interp.h` + typedef struct _Py_atomic_address { + volatile uintptr_t _value; + } _Py_atomic_address; + // Used in `pycore_gil.h` and `pycore_ceval_state.h` + typedef struct _Py_atomic_int { + volatile int _value; + } _Py_atomic_int; + /* Stub out macros in `pycore_atomic.h` used in macros in + * `pycore_interp.h` (which aren't related to the + * `struct _is` we need). + * If any stub is referenced, fail the build with an + * unresolved external. + * This ensures we never ship wheels that "use" these + * placeholders. */ +# ifdef _MSC_VER + __declspec(dllimport) void lp_link_error__stubbed_cpython_atomic_LOAD_relaxed_was_used_this_is_a_bug(void); + __declspec(dllimport) void lp_link_error__stubbed_cpython_atomic_STORE_relaxed_was_used_this_is_a_bug(void); +# else + extern void lp_link_error__stubbed_cpython_atomic_LOAD_relaxed_was_used_this_is_a_bug(void); + extern void lp_link_error__stubbed_cpython_atomic_STORE_relaxed_was_used_this_is_a_bug(void); +# endif +# define _LP_ATOMIC_PANIC_LOAD_EXPR() (lp_link_error__stubbed_cpython_atomic_LOAD_relaxed_was_used_this_is_a_bug(), 0) +# define _LP_ATOMIC_PANIC_STORE_STMT() do { lp_link_error__stubbed_cpython_atomic_STORE_relaxed_was_used_this_is_a_bug(); } while (0) + // Panic-on-use shims (expression/statement forms) +# undef _Py_atomic_load_relaxed +# undef _Py_atomic_store_relaxed +# define _Py_atomic_load_relaxed(obj) ((void)(obj), _LP_ATOMIC_PANIC_LOAD_EXPR()) +# define _Py_atomic_store_relaxed(obj, val) do { (void)(obj); (void)(val); _LP_ATOMIC_PANIC_STORE_STMT(); } while (0) +# endif # endif -# ifdef HAVE_STD_ATOMIC -# undef HAVE_STD_ATOMIC -# endif -# if PY_VERSION_HEX >= 0x030900a6 // 3.9.0a6 -# include "internal/pycore_interp.h" -# else -# include "internal/pycore_pystate.h" -# endif +# include "internal/pycore_interp.h" #endif typedef struct TraceCallback diff --git a/pyproject.toml b/pyproject.toml index bc62f15a..52511799 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,8 @@ test-extras = ["tests-strict", "runtime-strict"] # https://cibuildwheel.readthedocs.io/en/stable/options/#archs [tool.cibuildwheel.macos] archs = ["x86_64", "universal2", "arm64"] +[tool.cibuildwheel.windows] +archs = ['AMD64', 'ARM64'] [tool.mypy] diff --git a/tests/test_cython.py b/tests/test_cython.py index fdbf7124..d54e997d 100644 --- a/tests/test_cython.py +++ b/tests/test_cython.py @@ -18,7 +18,8 @@ from line_profiler._line_profiler import ( CANNOT_LINE_TRACE_CYTHON, find_cython_source_file) -from line_profiler.line_profiler import get_code_block, LineProfiler +from line_profiler.line_profiler import ( # type:ignore[attr-defined] + get_code_block, LineProfiler) def propose_name(prefix: str) -> Generator[str, None, None]: @@ -71,7 +72,8 @@ def _install_cython_example( @pytest.fixture(scope='module') def cython_example( - tmp_path_factory: pytest.TempPathFactory) -> Tuple[Path, ModuleType]: + tmp_path_factory: pytest.TempPathFactory, +) -> Generator[Tuple[Path, ModuleType], None, None]: """ Install the example Cython module, yield the path to the Cython source file and the corresponding module, uninstall it at teardown. @@ -83,7 +85,7 @@ def cython_example( yield (path, import_module(mod_name)) -def test_recover_cython_source(cython_example: Tuple[Path, str]) -> None: +def test_recover_cython_source(cython_example: Tuple[Path, ModuleType]) -> None: """ Check that Cython sources are correctly located by `line_profiler._line_profiler.find_cython_source_file()` and @@ -104,7 +106,7 @@ def test_recover_cython_source(cython_example: Tuple[Path, str]) -> None: CANNOT_LINE_TRACE_CYTHON, reason='Cannot line-trace Cython code in version ' + '.'.join(str(v) for v in sys.version_info[:3])) -def test_profile_cython_source(cython_example: Tuple[Path, str]) -> None: +def test_profile_cython_source(cython_example: Tuple[Path, ModuleType]) -> None: """ Check that calls to Cython functions (built with the appropriate compile-time options) can be profiled. @@ -112,8 +114,9 @@ def test_profile_cython_source(cython_example: Tuple[Path, str]) -> None: prof_cos = LineProfiler() prof_sin = LineProfiler() - cos = prof_cos(cython_example[1].cos) - sin = prof_sin(cython_example[1].sin) + _, module = cython_example + cos = prof_cos(module.cos) + sin = prof_sin(module.sin) assert pytest.approx(cos(.125, 10)) == math.cos(.125) assert pytest.approx(sin(2.5, 3)) == 2.5 - 2.5 ** 3 / 6 + 2.5 ** 5 / 120