Build Windows wheels#89
Merged
Merged
Conversation
Add Windows (x64/AMD64) support so wheels can be built and published for Windows. - build.py: force the zlib meson subproject fallback on Windows (no system zlib to link against) - setup.py: resolve static libs by globbing instead of hardcoding Unix '.a' names so MSVC '.lib' output is found; statically link zlib and use MSVC compile flags (/std:c++14, no -fPIC/-lz) on Windows - release.yml: add windows-latest to the build and publish matrices, set CIBW_ARCHS_WINDOWS, and set up the MSVC environment via msvc-dev-cmd - run_tests.yml: add a Windows build-and-test job - pyproject.toml / README.md: add Windows classifier, badge, and docs
The refactored _get_extra_objects() verifies the static libs exist on disk, but the Extension was built at module-load time -- so even the `setup.py download` step crashed before any libs were compiled, breaking both the Linux and Windows test jobs. Resolve extra_objects in a custom build_ext command instead, which runs after build.py has produced the static libs.
Two test bugs surfaced once the extension built and ran on Windows: - test_ots_fuzzing: EXPECT_FAIL uses '/'-separated paths but str(Path.relative_to()) yields '\' on Windows, so no expected-failure font matched and all 42 were flagged as unexpected. Compare via .as_posix() so the separator is normalized on every platform. - test_compare_*: the pyots wheel bundles a top-level 'ots' package, so `import ots` succeeds even without opentype-sanitizer installed, leaving have_ots=True and ots.sanitize missing. Guard on hasattr(ots, "sanitize") so the comparison tests skip unless the real package is present.
- Only run `publish_release` on tag pushes, so manual `workflow_dispatch` runs build and upload artifacts for verification without attempting to publish (which would fail on the setuptools_scm dev version). - Add `skip-existing` to the PyPI publish step so an already-uploaded filename is skipped instead of failing the release.
cibuildwheel v3 defaults to the 'python -m build' frontend, which -- run from the project root -- imports our top-level build.py instead of the pypa/build package and fails on build's --wheel/--outdir args. Set CIBW_BUILD_FRONTEND=pip to restore the frontend used by all prior releases (built under cibuildwheel v2).
distutils was removed from the stdlib in Python 3.12 (PEP 632), so `python setup.py sdist` under the runner's Python 3.14 failed with ModuleNotFoundError. Replace distutils.log with the stdlib logging module and distutils.errors.DistutilsSetupError with setuptools.errors.SetupError (setuptools>=61 is already required).
Python 3.12+ no longer bundles setuptools, so `python setup.py sdist` run against the bare runner Python failed with ModuleNotFoundError. Install setuptools and setuptools_scm before the sdist build.
The top-level build.py shadowed the pypa/build package: run from the
project root, `python -m build` imported our argparse script instead of
the real build backend. That forced two workarounds in release.yml:
- CIBW_BUILD_FRONTEND: pip, to avoid cibuildwheel v3's default
`python -m build` wheel frontend
- a manual `pip install setuptools setuptools_scm` before the legacy
`python setup.py sdist` (the fallback once `python -m build` was
unusable; setuptools is unbundled on Python 3.12+)
Renaming the script removes the name collision, so both workarounds are
dropped: wheels build through cibuildwheel's default frontend and the
sdist is built with an isolated `python -m build --sdist`.
ilammy/msvc-dev-cmd is unmaintained (last release Jan 2024) and still runs on Node.js 20, which GitHub Actions deprecates on June 16 2026. Its only job was to put cl.exe on PATH so the Windows build uses MSVC. That action is unnecessary: setuptools finds MSVC natively for the extension, and meson can do the same for the static libs via its built-in vswhere/vcvars detection. Pass meson --vsenv on Windows to force MSVC activation (otherwise meson would prefer the gcc/clang the runner ships on PATH, causing an ABI mismatch with the MSVC-built extension), and remove the msvc-dev-cmd step from both workflows.
--vsenv activates the MSVC environment inside meson's configure process,
but build_ots.py then ran bare ninja as a separate subprocess that didn't
inherit it, so cl.exe was missing at compile time ("Could not find
compiler cl in PATH"). Drive the Windows build through 'meson compile',
which re-activates the stored vsenv setting before invoking ninja. The
Unix path keeps calling ninja directly and is unchanged.
Now that the Windows build drives ninja through 'meson compile', use it everywhere instead of branching on platform. 'meson compile' autodetects the ninja backend and just runs ninja on Linux/macOS, so it's a drop-in for the previous bare-ninja call while keeping the MSVC env activation on Windows. Removes the per-platform condition in make().
josh-hadley
approved these changes
May 31, 2026
Contributor
josh-hadley
left a comment
There was a problem hiding this comment.
Yay! You finally got it working!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Build Windows wheels
Closes #46.
This PR makes
pyotsbuild and publish Windows (x64/AMD64) wheels alongside the existing macOS and Linux ones, for CPython 3.10–3.14.pyotscompiles a real C extension that statically links the OTS libraries, and the existing build was Unix-only. The bulk of the work was making that build toolchain-agnostic.Build system
setup.py: resolve the meson-built static libs by globbing for eitherlib<name>.a(GCC/Clang) or<name>.lib(MSVC) instead of hardcoding Unix names, and select MSVC vs. GCC/Clang compile flags per platform. Resolution ofextra_objectsis deferred tobuild_ext, since the libs don't exist on disk until the meson build has run.--vsenvand the build is driven throughmeson compile, so it locates and activates the Visual Studio environment itself.build.py→build_ots.pyso it no longer shadows the PyPAbuildpackage; this lets the wheel build use cibuildwheel's default frontend and the sdist use an isolatedpython -m build --sdist.CI
release.yml: build + publish Windows wheels; publishing is gated to tag pushes and usesskip-existing.run_tests.yml: added a Windows build-and-test job.otspackage).Validation
All three platforms build green in both workflows, and the full publish path was verified end-to-end against PyPI with throwaway pre-release tags (
9.2.0bN) so no.postNrelease version was consumed.Release
The next release will be tagged
9.2.0.post3. It will include Windows wheels. It will also include CPython 3.13 and 3.14 wheels for the first time. CPython 3.9 wheels will not be included.