Skip to content

build: migrate to scikit-build-core + uv, add manylinux wheels, re-arm PyPI release - #1117

Merged
dolittle007 merged 5 commits into
mainfrom
build/scikit-build-core-uv
Aug 13, 2026
Merged

build: migrate to scikit-build-core + uv, add manylinux wheels, re-arm PyPI release#1117
dolittle007 merged 5 commits into
mainfrom
build/scikit-build-core-uv

Conversation

@dolittle007

Copy link
Copy Markdown
Collaborator

Summary

Four-step PyPI + bioconda publishing migration. Steps 1-3 are in this PR; bioconda (Step 4) is a follow-up.

  • Step 1 (9930985) — replace poetry-core + build.py with scikit-build-core + CMake; move dev workflow to uv. Fixes packaging bugs found along the way: undeclared pyyaml/requests runtime deps, pybind11 misclassified as a runtime dep, oversized LICENSE embedding, stale commitizen tag format.
  • Step 2 (9d5c0d3) — cibuildwheel manylinux wheels with htslib 1.23.1 bundled via auditwheel. scripts/check_wheel.py gates the two failure modes that produce a wheel which installs cleanly and breaks at import (extension in the wrong place, htslib not bundled). Verified by a real green CI run with artifacts downloaded and re-checked locally (see CLAUDE.md for the run link and details).
  • Step 3 (efce9c8) — re-arm release.yml on v* tags with PyPI Trusted Publishing (OIDC, uv publish, no stored token). wheels.yml is now a reusable workflow (workflow_call) that release.yml invokes, avoiding a duplicate cibuildwheel build on tag push. A check-version job blocks the whole run if the tag doesn't match pyproject.toml's version, since PyPI uploads are permanent.

Full context and load-bearing constraints are documented in CLAUDE.md at the repo root.

Test plan

  • CI run 31572447407wheels.yml build, both jobs green; wheel artifacts downloaded and manually verified (.so placement, RPATH, clean-venv smoke test with LD_LIBRARY_PATH unset)
  • tests.yml passes on this branch (htslib via apt, HTSLIB_ROOT=/usr)
  • After merge: dispatch release.yml against the v0.1.9 tag ref (the tag predates this branch and can't be re-pushed to fire the tag trigger) to perform the actual first publish to PyPI, now that the pending Trusted Publisher is configured (ylab-hi/ScanMST, workflow release.yml, environment pypi)

Replace poetry-core + build.py with scikit-build-core + CMake, and move the
dev workflow to uv. Prerequisite for publishing to PyPI and bioconda: the
previous backend could not build in CI at all, since build.py hard-failed
without CONDA_PREFIX and no workflow installed conda or htslib.

Build system:
- Add CMakeLists.txt building scanmst._cppext with exact source parity to
  build.py (binding.cpp stays uncompiled -- it declares a second
  PYBIND11_MODULE and would give a duplicate PyInit__cppext symbol).
- Resolve htslib via HTSLIB_ROOT then CONDA_PREFIX, treating empty strings as
  unset. HTSLIB_ROOT is the non-conda entry point for CI, Docker, cibuildwheel
  and bioconda.
- Set INSTALL_RPATH behind SCANMST_EMBED_RPATH. BUILD_RPATH alone does not
  survive cmake --install, and the previous build only had an RPATH by
  accident, via conda's LDFLAGS carrying -Wl,-rpath for the base env.
- Install the extension to scanmst/, not scanmst/cppext/: cppext/__init__.py
  does `from scanmst._cppext.cppext import *`.

Packaging:
- Convert [tool.poetry] to PEP 621. requires-python is >=3.9,<3.11, capped by
  pysam==0.19.0, which ships no wheels above cp310.
- Declare pyyaml and requests, previously imported by scanmst.base.helper and
  scanmst.blat but only ever resolved transitively.
- Move pybind11 from a runtime to a build dependency.
- Use a PEP 639 license expression; the file form embedded 675 lines of GPL
  text into METADATA (52KB -> 11KB).
- Exclude the runtime-downloaded UCSC BLAT binaries from wheels, which the
  poetry build shipped whenever they were present on disk.
- Read __version__ from package metadata; pyproject said 0.1.9 while
  __init__.py said 0.1.8.

Dev workflow:
- Drop nox-poetry; replace export_requirements() with `uv export`. Commit
  uv.lock and remove the *.lock catch-all that would have swallowed it.
- Add PEP 735 dependency groups.
- Fix mypy being handed a nonexistent docs/conf.py, and refurb passing its
  args to install() instead of run() so it never ran.

CI:
- tests.yml installs htslib and builds via uv; matrix drops 3.8, which was
  already below the declared floor.
- release.yml is disarmed to workflow_dispatch until Trusted Publishing is
  configured; it built with poetry and would otherwise fire on push to main.
Step 2 of the PyPI/bioconda migration. The wheel Step 1 produces is tagged
linux_x86_64 and links the developer's conda libhts, so PyPI rejects it and it
is not portable. Build proper manylinux wheels with htslib bundled instead.

- scripts/build-htslib.sh compiles htslib 1.23.1 inside the manylinux
  container, pinned to the version the verified local build linked against.
  libcurl/gcs/s3/libdeflate are disabled: the extension calls only core
  BAM/SAM APIs, and enabling them would drag libcurl and openssl into the
  bundled wheel.
- [tool.cibuildwheel] targets cp39/cp310 manylinux_2_28 x86_64 only. macOS is
  excluded because pysam==0.19.0 ships no arm64 wheel, so Apple Silicon can
  never resolve the dependency chain; tests.yml drops its macos-latest entry
  for the same reason, as those runners are arm64.
- SCANMST_EMBED_RPATH=OFF hands RPATH ownership to auditwheel. An RPATH
  pointing at the build container's /usr/local/lib is meaningless on a user's
  machine.
- scripts/check_wheel.py gates on the two failure modes that yield a wheel
  which installs cleanly and then breaks at import: the extension landing
  somewhere other than scanmst/_cppext*.so, and libhts not being bundled. It
  also rejects a non-manylinux tag.
- wheels.yml runs on workflow_dispatch and v* tags only, uploading artifacts.
  Nothing is published; that is Step 3. cibuildwheel rebuilds htslib per
  interpreter, which is too slow for per-commit CI.
workflow_dispatch requires the workflow file to be on the default branch, so
this is the only way to prove the manylinux build before it lands. The
branches: block is removed before merge.
The manylinux build is proven (run 31572447407): auditwheel bundled
libhts-ead13fb8.so.1.23.1 and the wheels install and run on a non-conda
Python. wheels.yml is back to workflow_dispatch + v* tags only.
release.yml now builds via wheels.yml (converted to a reusable workflow
to avoid double-building on tag push) and publishes with `uv publish`
under OIDC trusted publishing -- no stored token. A check-version job
gates the whole run on the tag matching pyproject.toml before anything
builds or publishes, since PyPI uploads are permanent.
@dolittle007
dolittle007 merged commit c94ac9c into main Aug 13, 2026
1 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant