Releasing and the native wheels#
solprop is released as two distributions from one repository, at one version taken from
the v* git tag; no other tag affects it
(SEP 29):
solprop, pure Python: a source distribution and one universal wheel.solprop-native, the compiled backend built fromnative/: wheels only, one per supported platform. No source distribution is published, so that where no wheel matches,pip install solpropfails to resolve instead of starting a C++ build; source builds are made from the repository (Building the native backend).
Versions are 0.x for now: a minor release may break the API, the output schema or the resume key, and every break is listed under “Changed (breaking)” in the changelog (ADR-058 in decisions). The first release is 0.1.0.
The wheels are built, repaired, qualified and published by
.github/workflows/wheels.yml. cibuildwheel’s configuration is the
[tool.cibuildwheel] table of native/pyproject.toml; the libraries each wheel bundles
are built from pinned, checksummed sources by the scripts in native/ci/, each of which
also runs by hand.
The wheels#
Linux x86-64 |
Linux aarch64 |
macOS arm64 |
Windows x64 |
Windows Arm |
|
|---|---|---|---|---|---|
CI runner |
|
|
|
|
|
build environment |
|
|
Homebrew’s Clang, deployment target 14.0 (macOS Sonoma) |
Clang on the MSVC ABI (MSVC STL and CRT, |
the same |
CPU baseline |
x86-64-v3 and x86-64-v2 |
Neoverse N1 |
Apple M1 |
x86-64-v3 and x86-64-v2 |
Armv8-A, the compiler’s default |
host execution space |
Kokkos OpenMP |
Kokkos OpenMP |
Kokkos OpenMP |
Kokkos OpenMP |
Kokkos OpenMP |
OpenMP runtime |
GCC’s |
GCC’s |
LLVM |
LLVM |
the same |
BLAS |
OpenBLAS 0.3.34, OpenMP, |
the same |
Accelerate |
OpenBLAS 0.3.34, OpenMP, one fixed target per build (Haswell, Nehalem; no |
OpenBLAS 0.3.34, OpenMP, Neoverse N1 target, static |
FFTW 3.3.11 |
shared, SSE2 and AVX codelets |
shared, cycle counter, NEON codelets |
shared, cycle counter, NEON codelets |
static, SSE2 and AVX codelets |
static, cycle counter, NEON codelets |
vector maths (ADR-059) |
SLEEF 3.9.0, static, AVX2 in the v3 build and SSE2 in the v2 |
the system libm |
the system libm |
SLEEF 3.9.0, static, AVX2 in the v3 build and SSE2 in the v2 |
the system libm |
repair |
|
|
|
|
|
An x86-64 wheel holds two extensions, _core at x86-64-v3 and _core_v2 at x86-64-v2, and
the package loads the best one the processor runs (solprop_native/_cpu.py). The setting
SOLPROP_NATIVE_CPU_VARIANT=x86-64-v2 asks for the lower one. native/ci/build_variant.py
builds the second before the wheel and native/ci/merge_variant.py adds it before the
repair, which then treats both alike. On Linux both link one OpenBLAS.
Every bundled library runs on the same OpenMP runtime as Kokkos, so a process holds one
thread pool for the backend. The wheel carries the licence text of every library compiled
into it or copied in by the repair (LICENSE-*.txt beside the extension). One wheel, built
on CPython 3.14 against the stable ABI (cp314-abi3), serves 3.14 and later; abi3audit
and a load on 3.15 must pass before it is published. Free-threaded CPython is not a target.
The same workflow’s python-3-15-tests job runs the default lane on Linux on 3.15, with h5py
built from source. It’s a qualification lane and publishing doesn’t wait for it, because 3.15
is not yet supported (#319).
Building a wheel locally#
Every wheel is the distribution profile (SOLPROP_NATIVE_PROFILE=DISTRIBUTION), never
a build for the build machine’s own architecture.
macOS arm64. cibuildwheel needs python.org’s framework Python, which it installs only
on CI. native/ci/build-wheel-macos.sh runs the same steps with uv: it builds libomp
and FFTW for the deployment target (native/ci/build-deps.sh), builds the wheel and
repairs it with delocate.
brew install llvm cmake ninja
native/ci/build-wheel-macos.sh /tmp/wheels # the repaired wheel lands in /tmp/wheels
Keep the script and [tool.cibuildwheel.macos] in step: they set the same deployment
target, CMake arguments and delocate check.
Linux (x86-64 or aarch64, on a machine of that architecture with Docker):
uvx cibuildwheel --platform linux --output-dir wheelhouse native
Windows x64 or on Arm, from a Visual Studio developer prompt (that architecture’s MSVC tools and SDK in the environment), with LLVM, CMake and Ninja installed:
uvx cibuildwheel --platform windows --output-dir wheelhouse native
To build one platform’s dependencies by hand: native/ci/build-deps.sh PREFIX (Linux,
macOS) or native/ci/build-deps-windows.ps1 -Prefix PREFIX.
Qualifying a wheel#
The workflow’s test jobs install each repaired wheel beside solprop from the same checkout and run, in order:
python -I native/ci/smoke.py --version VERSION: the package imports and passescheck_build(), is the distribution build at the release’s version, loads the FFT, BLAS and OpenMP libraries from inside the wheel, and carries their licences; on Arm, both precisions of its FFTW carry the NEON codelets;python -I -m solprop_native: the build record;python -I native/ci/coexistence.py: NumPy and SciPy imported before and aftersolprop_native, each order in a fresh process, every package’s thread pool started, and a short propagation run on the native backend, onnumpy_refand on the native backend again in the same process;uv run --no-sync poe testwithSOLPROP_TEST_BACKEND=native: solprop’s default lane on the native backend. It runs a second time on Linux and on Windows x64 with the x86-64-v2 build forced.
On Linux x86-64, the cpu-floor job runs the wheel under Intel’s Software Development
Emulator: the smoke test on a Haswell, which loads the x86-64-v3 build; a Nehalem (no AVX)
and a Sandy Bridge (AVX without AVX2, FMA or BMI), which load the x86-64-v2 build, with
backend="auto" resolving to native and no warning; and a refusal before any native code
runs on a Penryn (no SSE4.2). It runs when the repository variables SDE_URL and
SDE_SHA256 name an SDE release, and publishing waits for it then, as it always waits for
minimum-systems. The other platforms’ checks (Windows through
IsProcessorFeaturePresent, Linux aarch64 through /proc/cpuinfo’s Features) are
unit-tested with stand-in processors in native/tests/test_cpu_baseline.py; no CI job
runs them on a processor below the baseline.
macOS and pyFFTW. solprop no longer uses pyFFTW, but a script or another package may.
pyFFTW’s macOS wheel carries its own libomp, which it initialises
when it is imported. LLVM’s OpenMP runtime refuses, or crashes on, a second initialised
copy, so the native package raises at import when pyFFTW is already loaded
(OpenMP runtimes), with a message
that says what to do. No CI job exercises that guard on a real pyFFTW any longer.
solprop’s distributions. The solprop-dist job builds solprop’s sdist and wheel and
installs each from the file in a fresh environment, outside the checkout, beside this run’s
Linux x86-64 wheel. In each it runs the check of the
installation page: solprop
imports at the distributions’ version, which on a release tag must be the tag’s, and
backend="auto" resolves to native.
solprop’s built metadata pins solprop-native at solprop’s own version (hatch_build.py), so
two distributions built at different versions fail here, before anything is published.
Making a release#
The rolling release PR#
Implementation PRs add independent Markdown fragments under changelog.d/, named
<short-slug>.<category>.md. A PR number can be the slug; no separate issue is needed.
The categories are added, changed, breaking, deprecated, removed, fixed and
security; breaking becomes Changed (breaking). Write one entry without a heading
or leading bullet, including migration instructions for a break. Markdown links are
relative to the repository-root changelog. Existing Unreleased entries remain in place;
released sections are copied unchanged.
After each push to main, .github/workflows/release-notes.yml creates or refreshes
one draft PR, Prepare the next release notes, on
chore/release-after-<last-version>. The latest released version comes from
CITATION.cff; the next version is chosen by the maintainer. The draft groups every
pending fragment into CHANGELOG.md and removes the collected fragments on its branch.
Only this PR normally changes the shared changelog. Edit pending fragments in ordinary
PRs, not the generated draft. A manual refresh is available with:
gh workflow run release-notes.yml --ref main
The updater verifies that its previous branch tree is exactly the generated result for
its recorded base before writing another commit. It uses the current main tree for
all other files and keeps the previous head as a parent, so updates are ordinary
fast-forward pushes. It never force-pushes, approves, merges, tags or publishes. Manual
branch edits stop the workflow without overwriting them. A closed PR or one marked
ready pauses updates. If a draft was closed by mistake, reopen it to resume.
One-time setup: under GitHub Settings → Actions → General → Workflow permissions,
enable Allow GitHub Actions to create and approve pull requests. Keep the default
token permissions read-only; only this job requests contents: write and
pull-requests: write. The workflow only runs from trusted main. No personal token
or additional dependency is needed. The required checks and branch rules stay in force.
GitHub may require a maintainer to approve CI runs created by GITHUB_TOKEN; use
Approve workflows to run on the PR before merging. See
GitHub’s token documentation.
When ready to release, mark the PR ready before editing it. This freezes automatic
refreshes. If more changes arrive, bring main into the release worktree and run
uv run poe changelog-collect before updating the release heading. Resolve any conflicts
as usual; the command appends only fragments still present in that worktree. Follow the
checklist below in this PR and wait for its required checks. Once the release commit
reaches main, the next pending fragment starts a new draft for the next cycle.
Pre-flight checklist#
A release tag builds and tests everything but publishes nothing until a maintainer with admin rights on the repository and an account on PyPI has set up publishing. Once, before the first tag:
On PyPI, under Your account → Publishing, add a pending trusted publisher for each project, both naming the owner
LupoLab, the repositorysolpropand the workflowwheels.yml:the project
solpropwith the environmentpypi;the project
solprop-nativewith the environmentpypi-native;the project
solprop-guiwith the environmentpypi-gui, once the front end is to be published (below).
PyPI ties a pending publisher to one project name per workflow and environment, hence the two environments. The first upload through a pending publisher creates its project.
On GitHub, under Settings → Environments, give both
pypiandpypi-nativea required reviewer, and restrict their deployments to Selected branches and tags with the single tag rulev*. Without the reviewer, anyone with write access could publish by pushing a tag.Under Settings → Secrets and variables → Actions, add the repository variable
PUBLISHwith the valuetrue. The publish jobs are skipped without it. The front end,solprop-gui, is published only whenPUBLISH_GUIistrueas well, after solprop itself, whose version it requires; until then each release builds it and checks an install ofsolprop[gui]from the files, and the extra resolves only in the repository’s workspace.On Zenodo, signed in with GitHub, switch on
LupoLab/solpropunder GitHub (the organisation must grant Zenodo’s application access). Zenodo archives each GitHub release, not a tag, with the metadata of.zenodo.json, and gives it a DOI, plus one concept DOI for every version.
Then, for every release:
In the rolling release PR, marked ready to freeze refreshes, check that every pending fragment has been collected and the changelog’s
Unreleasedsection describes the release. Move it under the new version,## [X.Y.Z] - YYYY-MM-DD, retaining an empty## Unreleasedsection above it, and giveCITATION.cffthe sameversionanddate-released(tests/test_release_metadata.pychecks that they agree). The commit that does so is the release commit; it reachesmainthrough this PR, after the required checks.Check that CI passed on the release commit. When it merged a pull request whose head has the same tree and passed CI, the push to
mainruns none of CI’s lanes (CI), and that pull request’s run is the release commit’s record.With the release commit at the head of
main, run the wheels workflow on it,gh workflow run wheels.yml --ref main, and wait for it to pass. It is the only workflow that builds and tests the Linux aarch64 wheel, and onmainit otherwise runs only weekly. On a branch the publish jobs are skipped, so this is a dry run of the release.Then tag that commit, as below.
Tagging and publishing#
Tag the release commit,
git tag -a vX.Y.Z COMMIT, and push that tag on its own,git push origin vX.Y.Z: GitHub starts no workflow for a push of more than three tags at once. The tag starts the wheels workflow again, on the tagged commit (CI does not run on tags): it builds and tests every wheel, and builds solprop’s distributions and checks an install of each. Both publish jobs wait for every wheel’stestjob and forsolprop-dist, so the run that publishes has itself tested the release commit on every wheel platform, whichever lanes CI skipped on the push tomain.Publishing, through PyPI’s trusted publishing, then waits for each environment’s reviewer.
publish-nativeuploads solprop-native’s wheels frompypi-nativefirst;publishuploads solprop’s sdist and wheel frompypionly once every native wheel is up, since solprop requires solprop-native at the same version. If an upload fails partway, re-run the failed jobs: they upload the same files, and PyPI accepts a file identical to one it holds. It refuses different files under a name it has seen, so a release that needs a change takes a new version.Wait for
published-install, which starts after both publish jobs succeed. On Linux x86-64, Linux aarch64, macOS arm64, Windows x64 and Windows on Arm it creates a fresh CPython 3.14 environment outside the checkout and uses pip to resolve and installsolprop==X.Y.Zfrom PyPI. It checks both distributions’ versions,pip checkand thatbackend="auto"resolves tonative. It uses no local distribution, dependency lock or cached environment. Its per-platform artifacts retain the install report, logs, resolved versions and platform information, including on failure; each job also writes a summary. Check these before publishing the GitHub release. A failed check does not undo the PyPI upload: record the failure in an issue and repair it in a new version if the published files need changing. If PyPI has not yet made the new files available, rerun the failed install jobs once they appear; the check does not retry a failed resolution or hide its first result.Publish a GitHub release from the tag, with the version’s changelog section as its notes:
gh release create vX.Y.Z --verify-tag --title "solprop X.Y.Z" --notes-file NOTES.md. Zenodo archives it and gives it a DOI; on the first release, add the concept DOI, which names every version, toCITATION.cffand the README.
Rechecking a published release#
The Published install workflow can also be dispatched by hand with a required
version, written as X.Y.Z or vX.Y.Z. It runs the same four-platform check without
building or publishing anything:
gh workflow run published-install.yml -f version=0.1.0
Its optional readme_example=true input additionally runs the first Python block of
that release’s tagged README as a script, using Matplotlib’s noninteractive Agg
backend. The source and output join the evidence artifact. This full-length example
is a manual release qualification, requested explicitly; the automatic post-publish
check only installs and checks the backend. It does not propagate a pulse. The manual
example is available to complete the first release’s outstanding platform checks
(issue 74).
gh workflow run published-install.yml -f version=0.1.0 -f readme_example=true
Before the new workflow is on main, GitHub cannot dispatch it by name. The existing
Wheels workflow also accepts published_version: with that input it skips the
wheel builds and their dependent checks and runs only the published-install check.
The publishing jobs remain dependent on the skipped builds, so this mode cannot
publish. It has a separate concurrency group and does not cancel a wheel build on the
same branch. Select the branch whose workflow is being qualified:
gh workflow run wheels.yml --ref fix/published-install-checks \
-f published_version=0.1.0 -f readme_example=true
Leaving published_version empty keeps the ordinary Wheels workflow unchanged.
The actions in the workflows are pinned by commit; when updating one, take the commit of its release tag and keep the version in the trailing comment.
The release recipe on this page was set up and qualified by SEP 29.