From bac2c120814a8f69623d2a0f95cb61b8187b15e2 Mon Sep 17 00:00:00 2001 From: Greg Buzzard Date: Sun, 17 May 2026 13:50:17 -0400 Subject: [PATCH 01/14] Improve build and implement fixes to remove warnings. --- dev_scripts/clean_svmbir.sh | 2 +- dev_scripts/install_conda_environment.sh | 20 ++- dev_scripts/install_svmbir.sh | 11 +- docs/source/conf.py | 1 - pyproject.toml | 27 ++++ requirements.txt | 10 +- setup.py | 192 ++++++++++++++--------- svmbir/interface_cy_c.pyx | 2 +- 8 files changed, 175 insertions(+), 90 deletions(-) diff --git a/dev_scripts/clean_svmbir.sh b/dev_scripts/clean_svmbir.sh index 4de201b..5760f15 100644 --- a/dev_scripts/clean_svmbir.sh +++ b/dev_scripts/clean_svmbir.sh @@ -8,5 +8,5 @@ cd .. /bin/rm -r dist /bin/rm -r svmbir.egg-info -pip uninstall svmbir +pip uninstall -y svmbir cd dev_scripts diff --git a/dev_scripts/install_conda_environment.sh b/dev_scripts/install_conda_environment.sh index f5df8d9..c11c720 100644 --- a/dev_scripts/install_conda_environment.sh +++ b/dev_scripts/install_conda_environment.sh @@ -1,11 +1,27 @@ #!/bin/bash # This script destroys the conda environment named "svmbir" and reinstall it. +# On macOS, Xcode Command Line Tools are required for clang, git, and SDK headers. +if [[ "$(uname)" == "Darwin" ]]; then + if ! xcode-select -p &>/dev/null; then + echo "" + echo "ERROR: Xcode Command Line Tools are not installed." + echo "Run the following command and follow the prompts, then retry:" + echo "" + echo " xcode-select --install" + echo "" + return 1 + fi +fi + # Create and activate new conda environment cd .. conda deactivate -conda remove env --name svmbir --all -conda create --name svmbir python=3.10 +conda remove -y --name svmbir --all +conda create -y --name svmbir python=3.12 conda activate svmbir +if [[ "$(uname)" == "Darwin" ]]; then + conda install -n svmbir -y -c conda-forge llvm-openmp +fi cd dev_scripts diff --git a/dev_scripts/install_svmbir.sh b/dev_scripts/install_svmbir.sh index 58861f9..b07fa22 100644 --- a/dev_scripts/install_svmbir.sh +++ b/dev_scripts/install_svmbir.sh @@ -1,11 +1,8 @@ #!/bin/bash -# This script just installs svmbir along with requirements of svmbir, demos, and documation.. -# However, it does not remove the existing installation of svmbir. +# Install svmbir in editable mode together with all dev, docs, and demo dependencies. +# Does not remove an existing installation — run clean_svmbir.sh first if needed. cd .. -pip install -r requirements.txt -pip install . -pip install -r demo/requirements_demo.txt -pip install -r docs/requirements.txt +git submodule update --init --recursive +pip install -e ".[dev,docs,demo]" cd dev_scripts - diff --git a/docs/source/conf.py b/docs/source/conf.py index 83228ed..d9e831f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -102,7 +102,6 @@ # #html_theme = 'bizstyle' html_theme = 'sphinx_rtd_theme' -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_theme_options = { 'style_nav_header_background': '#4f8fb8ff', diff --git a/pyproject.toml b/pyproject.toml index 4385f60..841142c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,30 @@ [build-system] requires = ["setuptools", "wheel", "numpy>=1.26.0", "Cython"] build-backend = "setuptools.build_meta" + +[project] +# Keep version in sync with svmbir/__init__.py +name = "svmbir" +version = "0.4.0" +description = "Python code for fast parallel-beam MBIR (Model Based Iterative Reconstruction)" +readme = "README.md" +license = "BSD-3-Clause" +requires-python = ">=3.10" +maintainers = [{name = "Charles A. Bouman", email = "charles.bouman@gmail.edu"}] +dependencies = [ + "numpy>=1.26.0", + "psutil>=5.8", + "Pillow>=9.0", +] + +[project.optional-dependencies] +dev = ["pytest", "ruamel.yaml", "Cython"] +docs = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-bibtex", "ipython>=6.3.1"] +demo = ["matplotlib"] + +[project.urls] +Homepage = "https://github.com/cabouman/svmbir" + +[tool.setuptools.packages.find] +where = ["."] +include = ["svmbir*"] diff --git a/requirements.txt b/requirements.txt index b0a8a61..a223b64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,4 @@ -numpy>=1.26.0 -psutil>=5.8 -Pillow<=10.1.0 -Cython -ruamel.yaml -pytest +# Runtime and development dependencies are declared in pyproject.toml. +# To install svmbir with all development, docs, and demo dependencies run: +# +# pip install -e ".[dev,docs,demo]" diff --git a/setup.py b/setup.py index 0366fb7..0a9447c 100644 --- a/setup.py +++ b/setup.py @@ -1,107 +1,155 @@ import os import sys -from ast import parse import numpy as np from setuptools import setup, Extension from Cython.Distutils import build_ext -with open("README.md", "r") as fh: - long_description = fh.read() - -name = 'svmbir' -#version number set in svmbir/__init__.py -with open(os.path.join(name,"__init__.py")) as f: - version = parse(next(filter(lambda line: line.startswith("__version__"), f))).body[0].value.s -description="Python code for fast parallel-beam MBIR (Model Based Iterative Reconstruction) " -long_description_content_type="text/markdown" -url="https://github.com/cabouman/svmbir" -maintainer="Charles A. Bouman" -maintainer_email="charles.bouman@gmail.edu" -license="BSD-3-Clause" - -packages_dir = 'svmbir' -packages = [packages_dir] -src_dir = packages_dir + "/sv-mbirct/src/" - -# Dependencies for running svmbir. Dependencies for build/installation are in pyproject.toml -# 1/7/23: Pillow 10.2.0 (latest on this date) is missing a library libXau, that chokes on import PIL.Image -install_requires=['numpy>=1.26.0','psutil>=5.8','Pillow<=10.1.0'] - +src_dir = "svmbir/sv-mbirct/src/" # Set up install for Cython or Command line interface if os.environ.get('CLIB') != 'CMD_LINE': - # Check for compiler env variable. If not set, assume it's a gcc build (linux & macOS only). - if os.environ.get('CC') is None: - os.environ['CC']='gcc' - - if os.environ.get('CC') not in ['icc','clang','msvc']: - extra_compile_args=["-std=c11","-O3","-fopenmp","-Wno-unknown-pragmas"] - extra_link_args=["-lm","-fopenmp"] - - if os.environ.get('CC') =='icc': + # Fail fast: Xcode Command Line Tools provide clang, git, and SDK headers on macOS. + if sys.platform == 'darwin': + import subprocess as _sp + _xcode = _sp.run(['xcode-select', '-p'], capture_output=True) + if _xcode.returncode != 0: + sys.exit( + "\nERROR: Xcode Command Line Tools are not installed.\n" + "Run the following command and follow the prompts, then retry:\n\n" + " xcode-select --install\n" + ) + + # Fail fast: submodule must be initialized before compilation can proceed. + import glob as _glob + if not _glob.glob(src_dir + '*.c'): + sys.exit( + "\nERROR: C source files not found in " + src_dir + "\n" + "The git submodule has not been initialized. Run:\n\n" + " git submodule update --init --recursive\n" + ) + + # On macOS, force clang unless the user explicitly chose something other than gcc. + # Homebrew GCC at /usr/local is Intel-only and silently produces x86_64 binaries + # on arm64 machines. We override both the unset case and the generic 'gcc' default + # (which conda activation sometimes injects into the environment). + if sys.platform == 'darwin': + if os.environ.get('CC') in (None, 'gcc'): + os.environ['CC'] = 'clang' + elif os.environ.get('CC') is None: + os.environ['CC'] = 'gcc' + + # Fail fast: verify the selected compiler is actually on PATH. + import shutil as _shutil + _cc = os.environ['CC'] + if _cc != 'msvc' and _shutil.which(_cc) is None: + sys.exit( + f"\nERROR: Compiler '{_cc}' not found in PATH.\n" + "Set the CC environment variable to a compiler that is installed,\n" + "e.g.: CC=gcc pip install . or CC=clang pip install .\n" + ) + + if os.environ.get('CC') not in ['icc', 'clang', 'msvc']: + extra_compile_args = ["-std=c11", "-O3", "-fopenmp", "-Wno-unknown-pragmas"] + extra_link_args = ["-lm", "-fopenmp"] + + if os.environ.get('CC') == 'icc': if sys.platform == 'linux': os.environ['LDSHARED'] = 'icc -shared' - extra_compile_args=["-O3","-DICC","-qopenmp","-no-prec-div","-restrict","-inline-calloc","-qopt-calloc", - "-no-ansi-alias","-xCORE-AVX2"] - extra_link_args=["-lm","-qopenmp"] - - if os.environ.get('CC') =='clang': - extra_compile_args=["-O3","-Xclang", "-fopenmp","-Wno-unknown-pragmas"] - extra_link_args=["-lm","-lomp"] - - # build for Windows using MS Visual C++ - if os.environ.get('CC') =='msvc': - extra_compile_args=["/std:c11","/O2","/openmp","/DMSVC"] - extra_link_args=["-lm"] - - c_extension = Extension(packages_dir + ".interface_cy_c", + extra_compile_args = ["-O3", "-DICC", "-qopenmp", "-no-prec-div", "-restrict", + "-inline-calloc", "-qopt-calloc", "-no-ansi-alias", "-xCORE-AVX2"] + extra_link_args = ["-lm", "-qopenmp"] + + if os.environ.get('CC') == 'clang': + import subprocess + # Search for arm64 libomp in order of preference: + # 1. CONDA_PREFIX — the active conda env, inherited even inside pip's + # build-isolation subprocess, so this works for both direct builds + # and `pip install`. + # 2. sys.prefix — the Python env running this script (same as CONDA_PREFIX + # for direct invocations, different for pip build isolation). + # 3. brew --prefix libomp — last resort; only reliable on native arm64 + # Homebrew (/opt/homebrew). The Intel Homebrew at /usr/local ships an + # x86_64-only libomp that silently disables OpenMP at runtime on arm64. + libomp_prefix = None + for _candidate in filter(None, [ + os.environ.get('CONDA_PREFIX'), + sys.prefix, + ]): + if os.path.exists(os.path.join(_candidate, 'lib', 'libomp.dylib')): + libomp_prefix = _candidate + break + if libomp_prefix is None: + try: + _brew = subprocess.check_output(['brew', '--prefix', 'libomp'], + text=True).strip() + _dylib = os.path.join(_brew, 'lib', 'libomp.dylib') + if os.path.exists(_dylib): + # Verify the library matches this machine's architecture. + # Intel Homebrew at /usr/local ships x86_64-only libomp; linking + # it into an arm64 build silently disables OpenMP at runtime. + import platform as _platform + _machine = _platform.machine() # 'arm64' or 'x86_64' + _file_out = subprocess.run(['file', _dylib], + capture_output=True, text=True).stdout + if _machine in _file_out: + libomp_prefix = _brew + except Exception: + pass + # Fail fast: libomp is required on macOS; silent absence causes a dlopen crash. + if libomp_prefix is None: + sys.exit( + "\nERROR: libomp not found. OpenMP is required on macOS.\n" + "Install it into your active conda environment (recommended):\n\n" + " conda install -c conda-forge llvm-openmp\n\n" + "Or, if using native arm64 Homebrew (/opt/homebrew):\n\n" + " brew install libomp\n" + ) + extra_compile_args = ["-O3", "-Xclang", "-fopenmp", "-Wno-unknown-pragmas"] + extra_link_args = ["-lm", "-lomp"] + extra_compile_args += [f"-I{libomp_prefix}/include"] + extra_link_args += [f"-L{libomp_prefix}/lib", f"-Wl,-rpath,{libomp_prefix}/lib"] + + if os.environ.get('CC') == 'msvc': + extra_compile_args = ["/std:c11", "/O2", "/openmp", "/DMSVC"] + extra_link_args = ["-lm"] + + c_extension = Extension("svmbir.interface_cy_c", [src_dir + "A_comp.c", src_dir + "allocate.c", src_dir + "heap.c", src_dir + "icd3d.c", src_dir + "initialize.c", src_dir + "MBIRModularUtils.c", - src_dir + "recon3d.c", packages_dir + "/interface_cy_c.pyx"], + src_dir + "recon3d.c", "svmbir/interface_cy_c.pyx"], libraries=[], include_dirs=[np.get_include()], + define_macros=[('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION')], extra_compile_args=extra_compile_args, extra_link_args=extra_link_args) - package_data={} + package_data = {} cmdclass = {"build_ext": build_ext} ext_modules = [c_extension] - # set cython language level for all .pyx modules to Python 3 + # Set cython language level for all .pyx modules to Python 3. for e in ext_modules: e.cython_directives = {'language_level': "3"} else: - # Command-line interface install - - # Check for compiled executable + # Command-line interface install — requires a pre-compiled mbir_ct binary. if os.path.exists('svmbir/sv-mbirct/bin/mbir_ct'): exec_file = 'sv-mbirct/bin/mbir_ct' elif os.path.exists('svmbir/sv-mbirct/bin/mbir_ct.exe'): exec_file = 'sv-mbirct/bin/mbir_ct.exe' else: - exec_file = None - raise Exception("Compiled executable not present in 'svmbir/sv-mbirct/bin/' . Need to compile the binary executable first.") - - package_data={'svmbir': [exec_file]} - install_requires.append('ruamel.yaml') + raise Exception( + "Compiled executable not present in 'svmbir/sv-mbirct/bin/'. " + "Compile the binary first." + ) + package_data = {'svmbir': [exec_file]} cmdclass = {} ext_modules = None -setup(name=name, - version=version, - description=description, - long_description=long_description, - long_description_content_type=long_description_content_type, - url=url, - maintainer=maintainer, - maintainer_email=maintainer_email, - license=license, - packages=packages, - install_requires=install_requires, - package_data=package_data, - cmdclass=cmdclass, - ext_modules=ext_modules) - +setup( + package_data=package_data, + cmdclass=cmdclass, + ext_modules=ext_modules, +) diff --git a/svmbir/interface_cy_c.pyx b/svmbir/interface_cy_c.pyx index 509eedd..1293a0e 100644 --- a/svmbir/interface_cy_c.pyx +++ b/svmbir/interface_cy_c.pyx @@ -431,7 +431,7 @@ def multires_recon(sino, angles, weights, weight_type, init_image, prox_image, i #cdef cnp.ndarray[float, ndim=3, mode="c"] py_image cdef cnp.ndarray[char, ndim=1, mode="c"] Amatrix_fname - if 'py_image' not in locals(): + if not go_to_lower_resolution: if np.isscalar(init_image): py_image = np.zeros((num_slices, nrows, ncols), dtype=ctypes.c_float) + init_image else: From 5ca2a5856b3a5b1dccdb9a2fc91fe17352f95ffa Mon Sep 17 00:00:00 2001 From: Greg Buzzard Date: Sun, 17 May 2026 14:09:23 -0400 Subject: [PATCH 02/14] Replace travis with GitHub Workflow to verify correct installation. --- .github/workflows/ci.yml | 44 +++++++++++++++++++ .travis.yml | 93 ---------------------------------------- dev_scripts/README.md | 34 +++++++++++++++ 3 files changed, 78 insertions(+), 93 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml create mode 100644 dev_scripts/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..48f5c6b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build-and-test: + name: Python ${{ matrix.python-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-13, macos-14] + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up conda (Miniforge) + uses: conda-incubator/setup-miniconda@v3 + with: + python-version: ${{ matrix.python-version }} + miniforge-variant: Miniforge3 + use-mamba: true + auto-activate-base: false + activate-environment: test-env + + - name: Install llvm-openmp (macOS) + if: runner.os == 'macOS' + shell: bash -el {0} + run: conda install -y -c conda-forge llvm-openmp + + - name: Install svmbir with dev dependencies + shell: bash -el {0} + run: pip install -e ".[dev]" + + - name: Run tests + shell: bash -el {0} + run: pytest svmbir/tests/ -v diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7766dfa..0000000 --- a/.travis.yml +++ /dev/null @@ -1,93 +0,0 @@ -os: linux - -language: python - -python: - - "3.6" - - "3.7" - - "3.8" - -#matrix: -# include: -# - os: osx -# osx_image: xcode12.2 -# language: generic -# env: -# - MATRIX_EVAL="brew install gcc@10 &&CC=gcc-10 && CXX=g++-10" -# - PYTHON=3.8 -# - SO_COMPILER=gcc -# -# - os: osx -# osx_image: xcode12.2 -# language: generic -# env: -# - MATRIX_EVAL="curl https://mac.r-project.org/openmp/openmp-11.0.1-darwin20-Release.tar.gz | tar -xz&& sudo cp usr/local/lib/* /usr/local/lib/ && sudo cp usr/local/include/* /usr/local/include/ &&CC=clang " -# - PYTHON=3.8 -# - SO_COMPILER=clang -notifications: - email: false - -addons: - apt: - packages: - - libfftw3-dev - - libatlas-dev - - libatlas-base-dev - - liblapack-dev - - libblas-dev - - gfortran - -before_install: - - export URLROOT=https://repo.continuum.io/miniconda/; - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - brew update; - brew upgrade; - brew install fftw; - export MCINST=Miniconda3-latest-MacOSX-x86_64.sh; - else - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then - export MCINST=Miniconda2-latest-Linux-x86_64.sh; - else - export MCINST=Miniconda3-latest-Linux-x86_64.sh; - fi - fi; - export MCURL=$URLROOT$MCINST; - wget $MCURL -O miniconda.sh - - bash miniconda.sh -b -p $HOME/miniconda - - export PATH="$HOME/miniconda/bin:$PATH" - - hash -r - - conda config --set always_yes yes --set changeps1 no - - conda update -q conda - - conda info -a -# - eval "${MATRIX_EVAL}" - -install: - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - export PYTHON_VERSION=3.8; - else - export PYTHON_VERSION=$TRAVIS_PYTHON_VERSION; - fi; - conda create --yes -q -n test-env python=$PYTHON_VERSION - - source activate test-env - - pip install -r requirements.txt - - if [[ "$TRAVIS_OS_NAME" != "osx" && "$TRAVIS_PYTHON_VERSION" == "3.8" ]]; then - conda install -c conda-forge codecov; - fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - if [[ "$SO_COMPILER" == "gcc" ]]; then - CC=gcc-10 python setup.py build_ext --inplace; - else - CC=clang python setup.py build_ext --inplace; - fi; - else - python setup.py build_ext --inplace; - fi; - - -script: - - pytest - -after_success: - - if [[ "$TRAVIS_OS_NAME" != "osx" && "$TRAVIS_PYTHON_VERSION" == "3.8" ]]; then - codecov; - fi diff --git a/dev_scripts/README.md b/dev_scripts/README.md new file mode 100644 index 0000000..b7b1c98 --- /dev/null +++ b/dev_scripts/README.md @@ -0,0 +1,34 @@ +Considerations for package maintenance: +1. Package metadata (requires-python, dependencies in pyproject.toml) +This declares the minimum you need, not the maximum. The strong community consensus is: never put upper bounds on requires-python or on dependencies in published package metadata. Upper bounds are actively harmful — they prevent users from installing your package alongside other packages that have already moved forward. If numpy>=2.0 breaks something, the right fix is to fix the code, not to block numpy>=2.0. The only exception is when you have a known, tested incompatibility with a specific version. + +2. The CI test matrix +This is where you define what you actually support and verify. For 3.10–3.14, you'd have five jobs in GitHub Actions. When 3.15 comes out, you add it to the matrix and see if anything breaks. This is the right place to discover problems early, before users file bugs. + +3. The dev environment (install_conda_environment.sh) +This should pin to a specific version (currently 3.10) for a reproducible daily-driver environment. The version you pick here doesn't limit what users can run — it's just what developers work in. Updating this once a year when a new Python ships is reasonable. + +4. The CI workflow (.github/workflows/ci.yml) +This defines the test matrix — OS runners and Python versions — that GitHub Actions runs on every push and pull request. It must be kept in sync with `requires-python` in `pyproject.toml`. + +Updating Python versions (do this ~once a year): +- **Add a new version**: Each October, Python ships a new release. Add it to the `python-version` list in `ci.yml` and also update `requires-python` in `pyproject.toml` if you are dropping the oldest supported version. +- **Drop an EOL version**: Python versions reach end-of-life roughly 3 years after release (schedule at python.org/downloads). Remove the version from the `python-version` list and raise `requires-python` in `pyproject.toml` to match. + +Updating OS runners (do this when GitHub retires a runner): +- The current matrix is `[ubuntu-latest, macos-13, macos-14]`. `macos-13` is Intel (x86_64); `macos-14` is Apple Silicon (arm64). GitHub publishes deprecation notices before retiring runners — when `macos-13` is retired, remove it from the list. +- `ubuntu-latest` and `macos-latest` track GitHub's current default. Pinning to a numbered runner (e.g., `macos-14`) is more explicit and avoids surprise breakage when GitHub moves the `latest` pointer. + +Keeping current over time — the practical options: + +1. Dependabot (built into GitHub): opens automated PRs when dependencies release new versions. Very low friction — it just creates a PR, and your CI tells you if it breaks anything. This is the right tool for routine package bumps. + +2. Manual annual review: less automated but sufficient for a project that doesn't change often. When a new Python or NumPy ships, run the test suite against it and fix what breaks. + +3. pip-compile / lock files: great for applications that need exact reproducibility, but not the right tool for a library — it over-constrains what users can install alongside you. + +For this project specifically, Cython + C extensions have two real compatibility risks: + +1. New Python versions: Cython generates standard CPython API code, so new Pythons almost always Just Work once Cython itself is updated. The main friction is that Cython needs to release a version supporting the new Python's ABI before you can build wheels for it. + +2. NumPy major versions: NumPy 2.0 changed the C API in breaking ways. We already added NPY_NO_DEPRECATED_API which is the right first step, but a runtime test against NumPy 2.x is worth adding to the CI matrix. \ No newline at end of file From cc6ca8a6a1e50c706d31f3774b571b060e912cd8 Mon Sep 17 00:00:00 2001 From: Greg Buzzard Date: Sun, 17 May 2026 14:37:45 -0400 Subject: [PATCH 03/14] Update how to get version info and include workflow to build wheels. --- .github/workflows/release.yml | 68 +++++++++++++++++++++++++++++++++++ dev_scripts/README.md | 20 +++++++++++ docs/source/conf.py | 9 ++--- pyproject.toml | 10 ++++++ svmbir/__init__.py | 6 +++- 5 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b43185d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,68 @@ +name: Build and Release Wheels + +on: + push: + tags: + - "v*" + +jobs: + build-wheels: + name: Wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-13, macos-14] + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Build wheels + uses: pypa/cibuildwheel@v2.22.0 + + - uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.os }} + path: ./wheelhouse/*.whl + + build-sdist: + name: Source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Build sdist + run: pipx run build --sdist + + - uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + + create-release: + name: Create GitHub Release + needs: [build-wheels, build-sdist] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + merge-multiple: true + path: dist/ + + - uses: actions/download-artifact@v4 + with: + name: sdist + path: dist/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + generate_release_notes: true diff --git a/dev_scripts/README.md b/dev_scripts/README.md index b7b1c98..8afd589 100644 --- a/dev_scripts/README.md +++ b/dev_scripts/README.md @@ -19,6 +19,26 @@ Updating OS runners (do this when GitHub retires a runner): - The current matrix is `[ubuntu-latest, macos-13, macos-14]`. `macos-13` is Intel (x86_64); `macos-14` is Apple Silicon (arm64). GitHub publishes deprecation notices before retiring runners — when `macos-13` is retired, remove it from the list. - `ubuntu-latest` and `macos-latest` track GitHub's current default. Pinning to a numbered runner (e.g., `macos-14`) is more explicit and avoids surprise breakage when GitHub moves the `latest` pointer. +5. The release workflow (.github/workflows/release.yml) +This builds binary wheels for all supported platforms and Python versions, then attaches them as downloadable files to a GitHub Release. It is triggered automatically by pushing a version tag. + +How to cut a release: +1. Update the version in `pyproject.toml` (the single source of truth — `__init__.py` reads it at runtime via `importlib.metadata`). +2. Commit and push: `git commit -m "Release v0.X.Y" && git push` +3. Tag and push the tag: `git tag v0.X.Y && git push --tags` +4. GitHub Actions picks up the tag, builds wheels on Ubuntu x86_64, macOS Intel (macos-13), and macOS arm64 (macos-14) for Python 3.10–3.12, and creates a GitHub Release with all wheels and the source distribution attached. +5. Verify the release on the repo's Releases page. Users can install directly with `pip install` using the wheel URL, or download manually. + +Updating the release workflow over time: +- **Python versions**: keep the `build` setting in `[tool.cibuildwheel]` in `pyproject.toml` in sync with the CI matrix in `ci.yml` and `requires-python`. All three should agree. +- **OS runners**: the release workflow uses the same `[macos-13, macos-14]` matrix as `ci.yml`. Apply the same runner retirement process described above. +- **cibuildwheel version**: `pypa/cibuildwheel@v2.22.0` in `release.yml` is pinned for reproducibility. When a new Python version requires a newer cibuildwheel release, bump the pin. + +Adding PyPI distribution later: +When ready to publish to PyPI, add a final job to `release.yml` after `create-release`: +1. Set up PyPI Trusted Publishing in your PyPI project settings (links the GitHub repo without needing a stored API token). +2. Add a `publish-to-pypi` job that downloads the `dist/` artifacts and runs `pypa/gh-action-pypi-publish`. + Keeping current over time — the practical options: 1. Dependabot (built into GitHub): opens automated PRs when dependencies release new versions. Very low friction — it just creates a PR, and your CI tells you if it breaks anything. This is the right tool for routine package bumps. diff --git a/docs/source/conf.py b/docs/source/conf.py index d9e831f..da073d3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,7 +12,7 @@ # import os import sys -from ast import parse +from importlib.metadata import version as _version import sphinx_rtd_theme if os.environ.get('SVMBIR_BUILD_DOCS') =='true': @@ -25,12 +25,7 @@ copyright = '2020-2022, SVMBIR Development Team' author = 'SVMBIR Development Team' -# The full version, including alpha/beta/rc tags -#version = '0.2' -#release = '0.2' -# Retrieve the version number from svmbir/__init__.py -with open(os.path.join("../..",project,"__init__.py")) as f: - release = parse(next(filter(lambda line: line.startswith("__version__"), f))).body[0].value.s +release = _version("svmbir") # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 841142c..b19f568 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,3 +28,13 @@ Homepage = "https://github.com/cabouman/svmbir" [tool.setuptools.packages.find] where = ["."] include = ["svmbir*"] + +[tool.cibuildwheel] +# Build CPython 3.10–3.12 only; skip musl Linux (Alpine) and PyPy. +build = "cp310-* cp311-* cp312-*" +skip = "*-musllinux_*" + +[tool.cibuildwheel.macos] +# Install libomp before each build so setup.py's brew fallback finds it. +# delocate (run automatically by cibuildwheel) then bundles it into the wheel. +before-build = "brew install libomp" diff --git a/svmbir/__init__.py b/svmbir/__init__.py index b18d6e9..ef3f374 100644 --- a/svmbir/__init__.py +++ b/svmbir/__init__.py @@ -1,4 +1,8 @@ -__version__ = '0.4.0' +from importlib.metadata import version as _version, PackageNotFoundError as _PackageNotFoundError +try: + __version__ = _version("svmbir") +except _PackageNotFoundError: + __version__ = "unknown" from .svmbir import * from .phantom import * __all__ = ['recon','project','backproject','sino_sort','calc_weights','auto_sigma_x','auto_sigma_y','auto_sigma_p','_clear_cache','_svmbir_lib_path'] From d55374aba39726f3840eead78bcdc094ab976eaf Mon Sep 17 00:00:00 2001 From: Greg Buzzard Date: Sun, 17 May 2026 14:47:27 -0400 Subject: [PATCH 04/14] Update dev instructions. --- dev_scripts/README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/dev_scripts/README.md b/dev_scripts/README.md index 8afd589..73d7129 100644 --- a/dev_scripts/README.md +++ b/dev_scripts/README.md @@ -34,6 +34,35 @@ Updating the release workflow over time: - **OS runners**: the release workflow uses the same `[macos-13, macos-14]` matrix as `ci.yml`. Apply the same runner retirement process described above. - **cibuildwheel version**: `pypa/cibuildwheel@v2.22.0` in `release.yml` is pinned for reproducibility. When a new Python version requires a newer cibuildwheel release, bump the pin. +Testing the workflows before merging to master: +The workflow files only take effect once they exist in the repository — but you can test them from a feature branch before merging. Follow these steps in order. + +Step 1 — Test the CI workflow via a pull request: +GitHub Actions fires the `pull_request` trigger using the workflow file from the PR branch, not master. This is the normal way to validate CI changes before they land. + +1. Push your branch to GitHub if you haven't already: + `git push -u origin ` +2. Open a pull request from your branch to `master` on the GitHub website (or with `gh pr create --base master`). +3. GitHub automatically starts the CI workflow. Go to the PR page and click the "Checks" tab, or go to the repo's "Actions" tab, to watch the 9 jobs (3 Python versions × 3 OS runners) run. +4. If any job fails, click into it to read the log, fix the issue, push another commit to the branch, and the workflow re-runs automatically. + +Step 2 — Test the release workflow via a test tag: +The release workflow triggers on a version tag, which is repo-wide. GitHub uses the `release.yml` from the commit the tag points to — so tagging a commit on your branch exercises the release workflow as it exists there, before any merge. + +1. Make sure your branch is pushed and your working tree is clean (`git status`). +2. Push a test tag pointing to your current commit: + `git tag v0.4.0-test && git push --tags` +3. Go to the repo's "Actions" tab on GitHub and watch the release workflow run. It builds wheels on all three OS runners, then creates a GitHub Release. +4. Go to the repo's "Releases" page to confirm the release was created and the wheel files (`.whl`) and source distribution (`.tar.gz`) are attached. You can test-install a wheel directly: + `pip install ` +5. Clean up when done — delete the test tag and release: + - Delete the release: go to the Releases page, click the test release, click "Delete" (trash icon). + - Delete the remote tag: `git push --delete origin v0.4.0-test` + - Delete the local tag: `git tag -d v0.4.0-test` + +Step 3 — Merge the PR: +Once both workflows pass, merge the pull request into master. From this point on, every push to master runs CI automatically and every version tag triggers a real release build. + Adding PyPI distribution later: When ready to publish to PyPI, add a final job to `release.yml` after `create-release`: 1. Set up PyPI Trusted Publishing in your PyPI project settings (links the GitHub repo without needing a stored API token). From 4641b7d75e13ad1aa63eff7fa5a1f7fa56373977 Mon Sep 17 00:00:00 2001 From: Greg Buzzard Date: Sun, 17 May 2026 14:49:50 -0400 Subject: [PATCH 05/14] Update dev instructions. --- .github/workflows/ci.yml | 4 ++-- dev_scripts/README.md | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48f5c6b..e404168 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [master] + branches: [master, prerelease] pull_request: - branches: [master] + branches: [master, prerelease] jobs: build-and-test: diff --git a/dev_scripts/README.md b/dev_scripts/README.md index 73d7129..97ad985 100644 --- a/dev_scripts/README.md +++ b/dev_scripts/README.md @@ -34,20 +34,22 @@ Updating the release workflow over time: - **OS runners**: the release workflow uses the same `[macos-13, macos-14]` matrix as `ci.yml`. Apply the same runner retirement process described above. - **cibuildwheel version**: `pypa/cibuildwheel@v2.22.0` in `release.yml` is pinned for reproducibility. When a new Python version requires a newer cibuildwheel release, bump the pin. -Testing the workflows before merging to master: -The workflow files only take effect once they exist in the repository — but you can test them from a feature branch before merging. Follow these steps in order. +Testing the workflows — prerelease → master flow: +The standard workflow for this repo is: feature branch → PR to `prerelease` (for integration testing) → PR from `prerelease` to `master` (for release). The CI workflow is configured to fire on PRs targeting either `prerelease` or `master`, so it runs at both gates automatically. -Step 1 — Test the CI workflow via a pull request: -GitHub Actions fires the `pull_request` trigger using the workflow file from the PR branch, not master. This is the normal way to validate CI changes before they land. +Step 1 — Test the CI workflow via a PR to prerelease: +GitHub Actions fires the `pull_request` trigger using the workflow file from the PR's source branch, not the target branch. This means the workflow is tested as it exists in your branch, before anything is merged. 1. Push your branch to GitHub if you haven't already: `git push -u origin ` -2. Open a pull request from your branch to `master` on the GitHub website (or with `gh pr create --base master`). +2. Open a pull request from your branch to `prerelease` (not master) on the GitHub website, or with: + `gh pr create --base prerelease` 3. GitHub automatically starts the CI workflow. Go to the PR page and click the "Checks" tab, or go to the repo's "Actions" tab, to watch the 9 jobs (3 Python versions × 3 OS runners) run. 4. If any job fails, click into it to read the log, fix the issue, push another commit to the branch, and the workflow re-runs automatically. +5. Once CI passes, merge the PR into `prerelease`. Step 2 — Test the release workflow via a test tag: -The release workflow triggers on a version tag, which is repo-wide. GitHub uses the `release.yml` from the commit the tag points to — so tagging a commit on your branch exercises the release workflow as it exists there, before any merge. +The release workflow triggers on a version tag, which is repo-wide. GitHub uses the `release.yml` from the commit the tag points to — so tagging a commit on `prerelease` (or any branch) exercises the workflow as it exists there, before the final merge to `master`. 1. Make sure your branch is pushed and your working tree is clean (`git status`). 2. Push a test tag pointing to your current commit: @@ -60,8 +62,8 @@ The release workflow triggers on a version tag, which is repo-wide. GitHub uses - Delete the remote tag: `git push --delete origin v0.4.0-test` - Delete the local tag: `git tag -d v0.4.0-test` -Step 3 — Merge the PR: -Once both workflows pass, merge the pull request into master. From this point on, every push to master runs CI automatically and every version tag triggers a real release build. +Step 3 — PR from prerelease to master: +Once everything looks good on `prerelease`, open a PR from `prerelease` to `master`. CI runs again on this PR. When it passes, merge — from this point on, every push to `master` or `prerelease` runs CI automatically, and every version tag triggers a real release build. Adding PyPI distribution later: When ready to publish to PyPI, add a final job to `release.yml` after `create-release`: From 608f2d8e4a24dbb976c5c9a2ddecbfb9526eb750 Mon Sep 17 00:00:00 2001 From: Greg Buzzard Date: Mon, 18 May 2026 08:59:33 -0400 Subject: [PATCH 06/14] Update workflow to build wheels. --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 68 +++++++++------------------- .gitignore | 4 +- dev_scripts/README.md | 52 ++++++++++++---------- dev_scripts/build_mac_wheels.sh | 78 +++++++++++++++++++++++++++++++++ pyproject.toml | 2 + 6 files changed, 133 insertions(+), 73 deletions(-) create mode 100755 dev_scripts/build_mac_wheels.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e404168..3ca2734 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-13, macos-14] + os: [ubuntu-latest] python-version: ["3.10", "3.11", "3.12"] steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b43185d..065ddef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,63 +6,35 @@ on: - "v*" jobs: - build-wheels: - name: Wheels on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-13, macos-14] - + create-draft-release: + name: Create draft release + runs-on: ubuntu-latest + permissions: + contents: write steps: - - uses: actions/checkout@v4 + - uses: softprops/action-gh-release@v2 with: - submodules: recursive - - - name: Build wheels - uses: pypa/cibuildwheel@v2.22.0 - - - uses: actions/upload-artifact@v4 - with: - name: wheels-${{ matrix.os }} - path: ./wheelhouse/*.whl + draft: true + generate_release_notes: true - build-sdist: - name: Source distribution + build-linux: + name: Linux wheels and sdist runs-on: ubuntu-latest + needs: create-draft-release + permissions: + contents: write steps: - uses: actions/checkout@v4 with: submodules: recursive + - name: Build wheels + uses: pypa/cibuildwheel@v2.22.0 + - name: Build sdist run: pipx run build --sdist - - uses: actions/upload-artifact@v4 - with: - name: sdist - path: dist/*.tar.gz - - create-release: - name: Create GitHub Release - needs: [build-wheels, build-sdist] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/download-artifact@v4 - with: - pattern: wheels-* - merge-multiple: true - path: dist/ - - - uses: actions/download-artifact@v4 - with: - name: sdist - path: dist/ - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - files: dist/* - generate_release_notes: true + - name: Upload to draft release + run: gh release upload ${{ github.ref_name }} wheelhouse/*.whl dist/*.tar.gz --clobber + env: + GH_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore index 8d903b2..54bc4df 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,6 @@ sftp-config.json *.DS_Store # Pycharm idea folder -.idea/ \ No newline at end of file +.idea/ +# cibuildwheel output +wheelhouse/ diff --git a/dev_scripts/README.md b/dev_scripts/README.md index 97ad985..56f1a23 100644 --- a/dev_scripts/README.md +++ b/dev_scripts/README.md @@ -15,24 +15,26 @@ Updating Python versions (do this ~once a year): - **Add a new version**: Each October, Python ships a new release. Add it to the `python-version` list in `ci.yml` and also update `requires-python` in `pyproject.toml` if you are dropping the oldest supported version. - **Drop an EOL version**: Python versions reach end-of-life roughly 3 years after release (schedule at python.org/downloads). Remove the version from the `python-version` list and raise `requires-python` in `pyproject.toml` to match. -Updating OS runners (do this when GitHub retires a runner): -- The current matrix is `[ubuntu-latest, macos-13, macos-14]`. `macos-13` is Intel (x86_64); `macos-14` is Apple Silicon (arm64). GitHub publishes deprecation notices before retiring runners — when `macos-13` is retired, remove it from the list. -- `ubuntu-latest` and `macos-latest` track GitHub's current default. Pinning to a numbered runner (e.g., `macos-14`) is more explicit and avoids surprise breakage when GitHub moves the `latest` pointer. +Updating OS runners: +- CI runs Linux only (`ubuntu-latest`). macOS is no longer in the CI matrix — macOS compatibility is verified when building and test-installing the release wheels locally (see section 5 below). +- If a future Linux runner name change is needed, `ubuntu-latest` tracks GitHub's current default and rarely requires manual updates. -5. The release workflow (.github/workflows/release.yml) -This builds binary wheels for all supported platforms and Python versions, then attaches them as downloadable files to a GitHub Release. It is triggered automatically by pushing a version tag. +5. The release workflow (.github/workflows/release.yml) and local macOS build +Linux wheels and the source distribution are built automatically by GitHub Actions. macOS arm64 wheels are built locally using `dev_scripts/build_mac_wheels.sh` and uploaded to the same draft release. Intel Mac (x86_64) is no longer supported. How to cut a release: -1. Update the version in `pyproject.toml` (the single source of truth — `__init__.py` reads it at runtime via `importlib.metadata`). -2. Commit and push: `git commit -m "Release v0.X.Y" && git push` -3. Tag and push the tag: `git tag v0.X.Y && git push --tags` -4. GitHub Actions picks up the tag, builds wheels on Ubuntu x86_64, macOS Intel (macos-13), and macOS arm64 (macos-14) for Python 3.10–3.12, and creates a GitHub Release with all wheels and the source distribution attached. -5. Verify the release on the repo's Releases page. Users can install directly with `pip install` using the wheel URL, or download manually. +1. Update the version in `pyproject.toml` to the new version (e.g. `0.4.X`). This is the single source of truth — `__init__.py` reads it at runtime via `importlib.metadata`. +2. Commit and push the version bump: `git commit -m "Release v0.4.X" && git push` +3. Tag and push the tag: `git tag v0.4.X && git push --tags` +4. GitHub Actions immediately creates a draft release, then builds Linux wheels and the source distribution and uploads them to the draft. This takes a few minutes. +5. While that runs (or after), build and upload the macOS arm64 wheels from your Mac: + `cd dev_scripts && ./build_mac_wheels.sh v0.4.X` + Prerequisites (one-time setup): `pip install cibuildwheel` and `gh auth login`. +6. Go to the repo's Releases page on GitHub, confirm both Linux and macOS wheels are attached, then click "Publish release". Users can then `pip install svmbir` or download wheels directly. Updating the release workflow over time: - **Python versions**: keep the `build` setting in `[tool.cibuildwheel]` in `pyproject.toml` in sync with the CI matrix in `ci.yml` and `requires-python`. All three should agree. -- **OS runners**: the release workflow uses the same `[macos-13, macos-14]` matrix as `ci.yml`. Apply the same runner retirement process described above. -- **cibuildwheel version**: `pypa/cibuildwheel@v2.22.0` in `release.yml` is pinned for reproducibility. When a new Python version requires a newer cibuildwheel release, bump the pin. +- **cibuildwheel version**: `pypa/cibuildwheel@v2.22.0` in `release.yml` is pinned for reproducibility. When a new Python version requires a newer cibuildwheel release, bump the pin. The same version of cibuildwheel should be used locally — install it with `pip install cibuildwheel==2.22.0`. Testing the workflows — prerelease → master flow: The standard workflow for this repo is: feature branch → PR to `prerelease` (for integration testing) → PR from `prerelease` to `master` (for release). The CI workflow is configured to fire on PRs targeting either `prerelease` or `master`, so it runs at both gates automatically. @@ -48,19 +50,23 @@ GitHub Actions fires the `pull_request` trigger using the workflow file from the 4. If any job fails, click into it to read the log, fix the issue, push another commit to the branch, and the workflow re-runs automatically. 5. Once CI passes, merge the PR into `prerelease`. -Step 2 — Test the release workflow via a test tag: -The release workflow triggers on a version tag, which is repo-wide. GitHub uses the `release.yml` from the commit the tag points to — so tagging a commit on `prerelease` (or any branch) exercises the workflow as it exists there, before the final merge to `master`. - -1. Make sure your branch is pushed and your working tree is clean (`git status`). -2. Push a test tag pointing to your current commit: - `git tag v0.4.0-test && git push --tags` -3. Go to the repo's "Actions" tab on GitHub and watch the release workflow run. It builds wheels on all three OS runners, then creates a GitHub Release. -4. Go to the repo's "Releases" page to confirm the release was created and the wheel files (`.whl`) and source distribution (`.tar.gz`) are attached. You can test-install a wheel directly: +Step 2 — Bump the version, then test the release workflow via a test tag: +The release workflow triggers on a version tag, which is repo-wide. GitHub uses the `release.yml` from the commit the tag points to — so tagging a commit on `prerelease` exercises the workflow as it exists there, before the final merge to `master`. This is also the right point to update the version number, since the tag and the version in the package should always match. + +1. Update the version in `pyproject.toml` to the intended release version (e.g. `0.4.X`). This is the single source of truth — `__init__.py` reads it at runtime via `importlib.metadata`. +2. Commit and push the version bump: + `git commit -m "Release v0.4.X" && git push` +3. Push a test tag pointing to that commit: + `git tag v0.4.X-test && git push --tags` +4. Go to the repo's "Actions" tab on GitHub and watch the release workflow run. It creates a draft release, then builds Linux wheels and the source distribution and uploads them. +5. While that runs (or after), test the local macOS build: + `cd dev_scripts && ./build_mac_wheels.sh v0.4.X-test` +6. Go to the repo's "Releases" page to confirm the draft release was created and all wheel files (`.whl`) and the source distribution (`.tar.gz`) are attached. You can test-install a wheel directly: `pip install ` -5. Clean up when done — delete the test tag and release: +7. Clean up when done — delete the test tag and release (do NOT publish the draft): - Delete the release: go to the Releases page, click the test release, click "Delete" (trash icon). - - Delete the remote tag: `git push --delete origin v0.4.0-test` - - Delete the local tag: `git tag -d v0.4.0-test` + - Delete the remote tag: `git push --delete origin v0.4.X-test` + - Delete the local tag: `git tag -d v0.4.X-test` Step 3 — PR from prerelease to master: Once everything looks good on `prerelease`, open a PR from `prerelease` to `master`. CI runs again on this PR. When it passes, merge — from this point on, every push to `master` or `prerelease` runs CI automatically, and every version tag triggers a real release build. diff --git a/dev_scripts/build_mac_wheels.sh b/dev_scripts/build_mac_wheels.sh new file mode 100755 index 0000000..d91c0dd --- /dev/null +++ b/dev_scripts/build_mac_wheels.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Build macOS arm64 wheels locally and upload them to the draft GitHub Release +# for the given tag. +# +# Prerequisites: +# pip install cibuildwheel (once; any Python environment) +# gh auth login (once; authenticates the gh CLI) +# +# Usage (run from dev_scripts/): +# ./build_mac_wheels.sh v0.4.1 + +set -euo pipefail + +TAG=${1:?"Usage: build_mac_wheels.sh e.g. ./build_mac_wheels.sh v0.4.1"} + +# Always work from the repo root regardless of where the script is called from. +cd "$(dirname "$0")/.." + +# Fetch tags from remote so we can verify the tag exists. +echo "Fetching tags ..." +git fetch --tags --quiet + +# Verify the tag exists. +git rev-parse "$TAG" >/dev/null 2>&1 || { + echo "ERROR: Tag $TAG not found on remote. Push the tag first:" + echo " git tag $TAG && git push --tags" + exit 1 +} + +# Verify HEAD matches the tag so the wheel is built from the correct commit. +HEAD_COMMIT=$(git rev-parse HEAD) +TAG_COMMIT=$(git rev-parse "$TAG") +if [ "$HEAD_COMMIT" != "$TAG_COMMIT" ]; then + echo "ERROR: HEAD does not match tag $TAG." + echo "Check out the tagged commit first:" + echo " git checkout $TAG" + exit 1 +fi + +# Verify the working tree is clean. +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "ERROR: Working tree has uncommitted changes." + echo "Commit or stash all changes before building a release wheel." + exit 1 +fi + +# Verify the git submodule is initialized. +if ! ls svmbir/sv-mbirct/src/*.c >/dev/null 2>&1; then + echo "ERROR: C source files not found — git submodule not initialized." + echo "Run: git submodule update --init --recursive" + exit 1 +fi + +# Verify cibuildwheel is available. +if ! command -v cibuildwheel &>/dev/null; then + echo "ERROR: cibuildwheel not found." + echo "Install it with: pip install cibuildwheel" + exit 1 +fi + +# Verify the gh CLI is authenticated. +if ! gh auth status &>/dev/null; then + echo "ERROR: gh CLI is not authenticated." + echo "Run: gh auth login" + exit 1 +fi + +echo "Building macOS arm64 wheels for $TAG ..." +cibuildwheel --platform macos + +echo "Uploading wheels to draft release $TAG ..." +gh release upload "$TAG" wheelhouse/*.whl --clobber + +echo "" +echo "macOS wheels uploaded. Check that the Linux wheels are also attached, then" +echo "publish the release at:" +REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner) +echo " https://github.com/$REPO/releases/tag/$TAG" diff --git a/pyproject.toml b/pyproject.toml index b19f568..62ec283 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,8 @@ build = "cp310-* cp311-* cp312-*" skip = "*-musllinux_*" [tool.cibuildwheel.macos] +# arm64 only — Intel Mac support has been dropped. # Install libomp before each build so setup.py's brew fallback finds it. # delocate (run automatically by cibuildwheel) then bundles it into the wheel. +archs = ["arm64"] before-build = "brew install libomp" From 1264572a81a4eea05a7bb40746b620c399488182 Mon Sep 17 00:00:00 2001 From: Greg Buzzard Date: Mon, 18 May 2026 09:11:16 -0400 Subject: [PATCH 07/14] Update workflow to build wheels. --- dev_scripts/README.md | 24 ++-- dev_scripts/cut_release.sh | 234 +++++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+), 7 deletions(-) create mode 100755 dev_scripts/cut_release.sh diff --git a/dev_scripts/README.md b/dev_scripts/README.md index 56f1a23..b4a9092 100644 --- a/dev_scripts/README.md +++ b/dev_scripts/README.md @@ -23,14 +23,24 @@ Updating OS runners: Linux wheels and the source distribution are built automatically by GitHub Actions. macOS arm64 wheels are built locally using `dev_scripts/build_mac_wheels.sh` and uploaded to the same draft release. Intel Mac (x86_64) is no longer supported. How to cut a release: -1. Update the version in `pyproject.toml` to the new version (e.g. `0.4.X`). This is the single source of truth — `__init__.py` reads it at runtime via `importlib.metadata`. +All steps up to and including tagging are done on the `prerelease` branch. The release stays as a non-public draft until you explicitly publish it, so you can verify everything before it goes live. + +The normal path is a single command: + `cd dev_scripts && ./cut_release.sh` + +The script runs all preflight checks, prompts for the new version (showing the current pyproject.toml version and the latest published release for reference), asks for confirmation, then automates steps 1–5 below. If the script fails partway through, the manual steps below can be used to complete the release. + +Prerequisites (one-time setup): `pip install cibuildwheel` and `gh auth login`. + +Manual steps (for reference or recovery): +1. On the `prerelease` branch, update the version in `pyproject.toml` to the new version (e.g. `0.4.X`). This is the single source of truth — `__init__.py` reads it at runtime via `importlib.metadata`. 2. Commit and push the version bump: `git commit -m "Release v0.4.X" && git push` -3. Tag and push the tag: `git tag v0.4.X && git push --tags` -4. GitHub Actions immediately creates a draft release, then builds Linux wheels and the source distribution and uploads them to the draft. This takes a few minutes. -5. While that runs (or after), build and upload the macOS arm64 wheels from your Mac: - `cd dev_scripts && ./build_mac_wheels.sh v0.4.X` - Prerequisites (one-time setup): `pip install cibuildwheel` and `gh auth login`. -6. Go to the repo's Releases page on GitHub, confirm both Linux and macOS wheels are attached, then click "Publish release". Users can then `pip install svmbir` or download wheels directly. +3. Tag the commit and push the tag: `git tag v0.4.X && git push --tags` + GitHub Actions immediately creates a draft release, then builds Linux wheels and the source distribution and uploads them. +4. Build and upload the macOS arm64 wheels: `cd dev_scripts && ./build_mac_wheels.sh v0.4.X` +5. Go to the repo's Releases page on GitHub and confirm both Linux and macOS wheels are attached. +6. Merge `prerelease` → `master` via a pull request: `gh pr create --base master --title "Release v0.4.X"` +7. After the PR merges, publish the draft release on GitHub. Updating the release workflow over time: - **Python versions**: keep the `build` setting in `[tool.cibuildwheel]` in `pyproject.toml` in sync with the CI matrix in `ci.yml` and `requires-python`. All three should agree. diff --git a/dev_scripts/cut_release.sh b/dev_scripts/cut_release.sh new file mode 100755 index 0000000..fe0e4ca --- /dev/null +++ b/dev_scripts/cut_release.sh @@ -0,0 +1,234 @@ +#!/bin/bash +set -euo pipefail +# +# Interactive release script for svmbir. +# +# Verifies preconditions, bumps the version, commits, tags, triggers the +# GitHub Actions draft release, builds macOS arm64 wheels locally, and +# prints the remaining manual steps. +# +# Usage (run from dev_scripts/): +# ./cut_release.sh + +# Always work from the repo root regardless of where the script is called from. +cd "$(dirname "$0")/.." + +echo "=== svmbir release script ===" +echo "" + +# --------------------------------------------------------------------------- +# Preflight checks +# --------------------------------------------------------------------------- + +echo "Preflight checks:" + +# Branch must be prerelease. +BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [ "$BRANCH" != "prerelease" ]; then + echo " FAIL branch is '$BRANCH' (must be 'prerelease')" + echo " Switch with: git checkout prerelease" + exit 1 +fi +echo " OK branch is prerelease" + +# Fetch latest state from remote (also needed for tag checks later). +git fetch --tags --quiet +echo " OK fetched from remote" + +# Local branch must not be behind remote. +LOCAL=$(git rev-parse HEAD) +REMOTE=$(git rev-parse "@{u}" 2>/dev/null || true) +if [ -n "$REMOTE" ] && [ "$LOCAL" != "$REMOTE" ]; then + echo " FAIL local branch is behind remote" + echo " Run: git pull" + exit 1 +fi +echo " OK up to date with remote" + +# Working tree must be clean. +if ! git diff --quiet || ! git diff --cached --quiet; then + echo " FAIL working tree has uncommitted changes" + echo " Commit or stash all changes before cutting a release." + exit 1 +fi +echo " OK working tree is clean" + +# Git submodule must be initialized. +if ! ls svmbir/sv-mbirct/src/*.c >/dev/null 2>&1; then + echo " FAIL git submodule not initialized" + echo " Run: git submodule update --init --recursive" + exit 1 +fi +echo " OK git submodule initialized" + +# gh CLI must be authenticated. +if ! gh auth status &>/dev/null; then + echo " FAIL gh CLI not authenticated" + echo " Run: gh auth login" + exit 1 +fi +echo " OK gh CLI authenticated" + +# cibuildwheel must be installed. +if ! command -v cibuildwheel &>/dev/null; then + echo " FAIL cibuildwheel not found" + echo " Run: pip install cibuildwheel" + exit 1 +fi +echo " OK cibuildwheel available" + +echo "" + +# --------------------------------------------------------------------------- +# Version selection +# --------------------------------------------------------------------------- + +# Read current version from pyproject.toml. +CURRENT=$(python3 -c " +import re, sys +m = re.search(r'^version = \"(.+)\"', open('pyproject.toml').read(), re.MULTILINE) +sys.exit(1) if not m else print(m.group(1)) +") + +# Read latest published release (strips leading 'v'). +LATEST_TAG=$(gh release list --exclude-drafts --limit 1 --json tagName \ + -q '.[0].tagName' 2>/dev/null || true) +LATEST="${LATEST_TAG#v}" +[ -z "$LATEST" ] && LATEST_DISPLAY="(none)" || LATEST_DISPLAY="$LATEST" + +echo "Current version in pyproject.toml : $CURRENT" +echo "Latest published GitHub release : $LATEST_DISPLAY" +echo "" +read -rp "New version (X.Y.Z): " NEW_VERSION + +# Must be valid semver. +if ! echo "$NEW_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "ERROR: '$NEW_VERSION' is not valid semver (expected X.Y.Z)." + exit 1 +fi + +# Must differ from the current pyproject.toml version. +if [ "$NEW_VERSION" = "$CURRENT" ]; then + echo "ERROR: $NEW_VERSION is already the version in pyproject.toml." + exit 1 +fi + +# Must differ from the latest published release. +if [ -n "$LATEST" ] && [ "$NEW_VERSION" = "$LATEST" ]; then + echo "ERROR: $NEW_VERSION is already the latest published release." + exit 1 +fi + +# Must be strictly greater than the latest published release. +if [ -n "$LATEST" ]; then + IS_GREATER=$(python3 -c " +a = tuple(int(x) for x in '$NEW_VERSION'.split('.')) +b = tuple(int(x) for x in '$LATEST'.split('.')) +print('yes' if a > b else 'no') +") + if [ "$IS_GREATER" != "yes" ]; then + echo "ERROR: $NEW_VERSION is not greater than the latest release ($LATEST)." + exit 1 + fi +fi + +TAG="v$NEW_VERSION" + +# --------------------------------------------------------------------------- +# Confirmation +# --------------------------------------------------------------------------- + +echo "" +echo "About to:" +echo " 1. Set version to $NEW_VERSION in pyproject.toml" +echo " 2. Commit: \"Release $TAG\"" +echo " 3. Push commit to prerelease" +echo " 4. Create and push tag $TAG" +echo " (GitHub Actions will create a draft release and build Linux wheels)" +echo " 5. Build macOS arm64 wheels locally and upload to the draft release" +echo "" +read -rp "Proceed? [yes/N]: " CONFIRM +[ "$CONFIRM" = "yes" ] || { echo "Aborted."; exit 0; } +echo "" + +# Print a helpful message if anything fails from here on. +trap 'echo ""; echo "Script failed at the step above. The remaining steps can be run manually — see dev_scripts/README.md."' ERR + +# --------------------------------------------------------------------------- +# Step 1/5 — Update pyproject.toml +# --------------------------------------------------------------------------- + +echo "--- 1/5 Updating pyproject.toml ---" +python3 - < Date: Mon, 18 May 2026 22:00:43 -0400 Subject: [PATCH 08/14] Update workflow and docs to build wheels. --- dev_scripts/README.md | 20 ++++++++++++++------ dev_scripts/install_conda_environment.sh | 10 ++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/dev_scripts/README.md b/dev_scripts/README.md index b4a9092..5ab567d 100644 --- a/dev_scripts/README.md +++ b/dev_scripts/README.md @@ -1,4 +1,6 @@ Considerations for package maintenance: +--------------------------------------- + 1. Package metadata (requires-python, dependencies in pyproject.toml) This declares the minimum you need, not the maximum. The strong community consensus is: never put upper bounds on requires-python or on dependencies in published package metadata. Upper bounds are actively harmful — they prevent users from installing your package alongside other packages that have already moved forward. If numpy>=2.0 breaks something, the right fix is to fix the code, not to block numpy>=2.0. The only exception is when you have a known, tested incompatibility with a specific version. @@ -23,30 +25,35 @@ Updating OS runners: Linux wheels and the source distribution are built automatically by GitHub Actions. macOS arm64 wheels are built locally using `dev_scripts/build_mac_wheels.sh` and uploaded to the same draft release. Intel Mac (x86_64) is no longer supported. How to cut a release: +--------------------- + All steps up to and including tagging are done on the `prerelease` branch. The release stays as a non-public draft until you explicitly publish it, so you can verify everything before it goes live. -The normal path is a single command: - `cd dev_scripts && ./cut_release.sh` +**The normal release is done with a single command in dev_scripts/ (and using this form, *not* `source cut_release.sh`):** + + `./cut_release.sh` The script runs all preflight checks, prompts for the new version (showing the current pyproject.toml version and the latest published release for reference), asks for confirmation, then automates steps 1–5 below. If the script fails partway through, the manual steps below can be used to complete the release. -Prerequisites (one-time setup): `pip install cibuildwheel` and `gh auth login`. +Prerequisites: `cibuildwheel` and `gh` (the GitHub CLI) are both installed automatically by `install_conda_environment.sh`. `gh` is GitHub's official command-line tool — it talks to the GitHub API to create releases and upload wheel files on your behalf, separate from your normal `git` push access. One extra one-time step after running the environment script: `gh auth login` (opens a browser to authenticate with your GitHub account). Manual steps (for reference or recovery): 1. On the `prerelease` branch, update the version in `pyproject.toml` to the new version (e.g. `0.4.X`). This is the single source of truth — `__init__.py` reads it at runtime via `importlib.metadata`. 2. Commit and push the version bump: `git commit -m "Release v0.4.X" && git push` 3. Tag the commit and push the tag: `git tag v0.4.X && git push --tags` GitHub Actions immediately creates a draft release, then builds Linux wheels and the source distribution and uploads them. -4. Build and upload the macOS arm64 wheels: `cd dev_scripts && ./build_mac_wheels.sh v0.4.X` +4. Build and upload the macOS arm64 wheels (requires `gh auth login` done once): `cd dev_scripts && ./build_mac_wheels.sh v0.4.X` 5. Go to the repo's Releases page on GitHub and confirm both Linux and macOS wheels are attached. 6. Merge `prerelease` → `master` via a pull request: `gh pr create --base master --title "Release v0.4.X"` 7. After the PR merges, publish the draft release on GitHub. Updating the release workflow over time: - **Python versions**: keep the `build` setting in `[tool.cibuildwheel]` in `pyproject.toml` in sync with the CI matrix in `ci.yml` and `requires-python`. All three should agree. -- **cibuildwheel version**: `pypa/cibuildwheel@v2.22.0` in `release.yml` is pinned for reproducibility. When a new Python version requires a newer cibuildwheel release, bump the pin. The same version of cibuildwheel should be used locally — install it with `pip install cibuildwheel==2.22.0`. +- **cibuildwheel version**: `pypa/cibuildwheel@v2.22.0` in `release.yml` is pinned for reproducibility. When a new Python version requires a newer cibuildwheel release, bump the pin here and also update the version in `install_conda_environment.sh` so local builds stay in sync. Testing the workflows — prerelease → master flow: +------------------------------------------------- + The standard workflow for this repo is: feature branch → PR to `prerelease` (for integration testing) → PR from `prerelease` to `master` (for release). The CI workflow is configured to fire on PRs targeting either `prerelease` or `master`, so it runs at both gates automatically. Step 1 — Test the CI workflow via a PR to prerelease: @@ -56,7 +63,7 @@ GitHub Actions fires the `pull_request` trigger using the workflow file from the `git push -u origin ` 2. Open a pull request from your branch to `prerelease` (not master) on the GitHub website, or with: `gh pr create --base prerelease` -3. GitHub automatically starts the CI workflow. Go to the PR page and click the "Checks" tab, or go to the repo's "Actions" tab, to watch the 9 jobs (3 Python versions × 3 OS runners) run. +3. GitHub automatically starts the CI workflow. Go to the PR page and click the "Checks" tab, or go to the repo's "Actions" tab, to watch the 3 jobs (3 Python versions × 1 OS runner: Linux) run. 4. If any job fails, click into it to read the log, fix the issue, push another commit to the branch, and the workflow re-runs automatically. 5. Once CI passes, merge the PR into `prerelease`. @@ -87,6 +94,7 @@ When ready to publish to PyPI, add a final job to `release.yml` after `create-re 2. Add a `publish-to-pypi` job that downloads the `dist/` artifacts and runs `pypa/gh-action-pypi-publish`. Keeping current over time — the practical options: +-------------------------------------------------- 1. Dependabot (built into GitHub): opens automated PRs when dependencies release new versions. Very low friction — it just creates a PR, and your CI tells you if it breaks anything. This is the right tool for routine package bumps. diff --git a/dev_scripts/install_conda_environment.sh b/dev_scripts/install_conda_environment.sh index c11c720..d423741 100644 --- a/dev_scripts/install_conda_environment.sh +++ b/dev_scripts/install_conda_environment.sh @@ -20,8 +20,18 @@ conda deactivate conda remove -y --name svmbir --all conda create -y --name svmbir python=3.12 conda activate svmbir + +# macOS: llvm-openmp is required for OpenMP support with Apple Clang. if [[ "$(uname)" == "Darwin" ]]; then conda install -n svmbir -y -c conda-forge llvm-openmp fi + +# gh (GitHub CLI) — used by cut_release.sh and build_mac_wheels.sh to create +# and upload to GitHub Releases. 'gh auth login' must be run once after install. +conda install -n svmbir -y -c conda-forge gh + +# cibuildwheel — used by build_mac_wheels.sh to build macOS wheels locally. +pip install cibuildwheel==2.22.0 + cd dev_scripts From 9e21c8b2477214cc9f24a5946e27c89860341169 Mon Sep 17 00:00:00 2001 From: Greg Buzzard Date: Mon, 18 May 2026 22:24:12 -0400 Subject: [PATCH 09/14] Update workflow and docs to build wheels. --- .github/workflows/ci.yml | 2 +- dev_scripts/README.md | 12 +++++++----- pyproject.toml | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ca2734..11d4bf6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 diff --git a/dev_scripts/README.md b/dev_scripts/README.md index 5ab567d..3986e5a 100644 --- a/dev_scripts/README.md +++ b/dev_scripts/README.md @@ -5,17 +5,19 @@ Considerations for package maintenance: This declares the minimum you need, not the maximum. The strong community consensus is: never put upper bounds on requires-python or on dependencies in published package metadata. Upper bounds are actively harmful — they prevent users from installing your package alongside other packages that have already moved forward. If numpy>=2.0 breaks something, the right fix is to fix the code, not to block numpy>=2.0. The only exception is when you have a known, tested incompatibility with a specific version. 2. The CI test matrix -This is where you define what you actually support and verify. For 3.10–3.14, you'd have five jobs in GitHub Actions. When 3.15 comes out, you add it to the matrix and see if anything breaks. This is the right place to discover problems early, before users file bugs. +CI (Continuous Integration) is automated testing that runs in the cloud every time code is pushed to the repository or a pull request is opened. For svmbir, this means GitHub automatically installs the package and runs the test suite on Linux across all supported Python versions. The goal is to catch breakage early — before a bug reaches users — and to confirm that changes work correctly across the Python versions the package claims to support. + +The CI matrix is the list of Python versions CI tests against. The current matrix is under jobs->matrix->python-version in `ci.yml`. When a new python version comes out in October, add it to the list and see if anything breaks. This is the right place to discover problems early, before users file bugs. 3. The dev environment (install_conda_environment.sh) -This should pin to a specific version (currently 3.10) for a reproducible daily-driver environment. The version you pick here doesn't limit what users can run — it's just what developers work in. Updating this once a year when a new Python ships is reasonable. +This should pin to a specific recent version for a reproducible daily-driver environment. The version you pick here doesn't limit what users can run — it's just what developers work in. Updating this once a year when a new Python ships is reasonable. 4. The CI workflow (.github/workflows/ci.yml) -This defines the test matrix — OS runners and Python versions — that GitHub Actions runs on every push and pull request. It must be kept in sync with `requires-python` in `pyproject.toml`. +This is the configuration file that tells GitHub how to run CI. It specifies which Python versions to test, which operating system to use (Linux only — see section 5 for macOS), and what commands to run (install the package, run pytest). It fires automatically on every push to `master` or `prerelease`, and on every pull request targeting either branch — no manual action needed. Updating Python versions (do this ~once a year): -- **Add a new version**: Each October, Python ships a new release. Add it to the `python-version` list in `ci.yml` and also update `requires-python` in `pyproject.toml` if you are dropping the oldest supported version. -- **Drop an EOL version**: Python versions reach end-of-life roughly 3 years after release (schedule at python.org/downloads). Remove the version from the `python-version` list and raise `requires-python` in `pyproject.toml` to match. +- **Add a new version**: Each October, Python ships a new release. Add it to the `python-version` list in `ci.yml`, and add the matching `cp3XX-*` entry to the `build` setting in `[tool.cibuildwheel]` in `pyproject.toml` so release wheels are built for it too. +- **Drop an EOL version**: Python versions reach end-of-life roughly 3 years after release (schedule at python.org/downloads). Remove it from the `python-version` list in `ci.yml`, the `build` setting in `pyproject.toml`, and raise `requires-python` in `pyproject.toml` to match. Updating OS runners: - CI runs Linux only (`ubuntu-latest`). macOS is no longer in the CI matrix — macOS compatibility is verified when building and test-installing the release wheels locally (see section 5 below). diff --git a/pyproject.toml b/pyproject.toml index 62ec283..1c42a31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ include = ["svmbir*"] [tool.cibuildwheel] # Build CPython 3.10–3.12 only; skip musl Linux (Alpine) and PyPy. -build = "cp310-* cp311-* cp312-*" +build = "cp310-* cp311-* cp312-* cp313-* cp314-*" skip = "*-musllinux_*" [tool.cibuildwheel.macos] From 82ea3113ccf2b601da811b5a4e8b0fd4b537bfee Mon Sep 17 00:00:00 2001 From: Greg Buzzard Date: Mon, 18 May 2026 22:26:55 -0400 Subject: [PATCH 10/14] Update workflow and docs to build wheels. --- dev_scripts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev_scripts/README.md b/dev_scripts/README.md index 3986e5a..60ce6c3 100644 --- a/dev_scripts/README.md +++ b/dev_scripts/README.md @@ -5,7 +5,7 @@ Considerations for package maintenance: This declares the minimum you need, not the maximum. The strong community consensus is: never put upper bounds on requires-python or on dependencies in published package metadata. Upper bounds are actively harmful — they prevent users from installing your package alongside other packages that have already moved forward. If numpy>=2.0 breaks something, the right fix is to fix the code, not to block numpy>=2.0. The only exception is when you have a known, tested incompatibility with a specific version. 2. The CI test matrix -CI (Continuous Integration) is automated testing that runs in the cloud every time code is pushed to the repository or a pull request is opened. For svmbir, this means GitHub automatically installs the package and runs the test suite on Linux across all supported Python versions. The goal is to catch breakage early — before a bug reaches users — and to confirm that changes work correctly across the Python versions the package claims to support. +CI (Continuous Integration) is automated testing that runs in the cloud every time code is pushed to the repository or a pull request is opened. For svmbir, this means GitHub automatically installs the package and runs the test suite on Linux across all supported Python versions. The goal is to catch breakage early — before a bug reaches users — and to confirm that changes work correctly across the Python versions the package claims to support. Check https://github.com/cabouman/svmbir/actions for status. The CI matrix is the list of Python versions CI tests against. The current matrix is under jobs->matrix->python-version in `ci.yml`. When a new python version comes out in October, add it to the list and see if anything breaks. This is the right place to discover problems early, before users file bugs. From afde70dec22cf246f9b88a1458472f8527945229 Mon Sep 17 00:00:00 2001 From: Greg Buzzard Date: Mon, 18 May 2026 22:37:09 -0400 Subject: [PATCH 11/14] Update workflow and docs to build wheels. --- dev_scripts/README.md | 4 ++-- dev_scripts/build_mac_wheels.sh | 12 ++++++++---- dev_scripts/cut_release.sh | 10 +++++++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/dev_scripts/README.md b/dev_scripts/README.md index 60ce6c3..dca5c63 100644 --- a/dev_scripts/README.md +++ b/dev_scripts/README.md @@ -37,7 +37,7 @@ All steps up to and including tagging are done on the `prerelease` branch. The r The script runs all preflight checks, prompts for the new version (showing the current pyproject.toml version and the latest published release for reference), asks for confirmation, then automates steps 1–5 below. If the script fails partway through, the manual steps below can be used to complete the release. -Prerequisites: `cibuildwheel` and `gh` (the GitHub CLI) are both installed automatically by `install_conda_environment.sh`. `gh` is GitHub's official command-line tool — it talks to the GitHub API to create releases and upload wheel files on your behalf, separate from your normal `git` push access. One extra one-time step after running the environment script: `gh auth login` (opens a browser to authenticate with your GitHub account). +Prerequisites: `cibuildwheel` and `gh` (the GitHub CLI) are both installed automatically by `install_conda_environment.sh`. `gh` is GitHub's official command-line tool — it talks to the GitHub API to create releases and upload wheel files on your behalf, separate from your normal `git` push access. `gh` requires a one-time login to your GitHub account, but you do not need to do this manually first — `./cut_release.sh` detects whether you are logged in and runs `gh auth login` for you if needed (it opens a browser to complete authentication). Manual steps (for reference or recovery): 1. On the `prerelease` branch, update the version in `pyproject.toml` to the new version (e.g. `0.4.X`). This is the single source of truth — `__init__.py` reads it at runtime via `importlib.metadata`. @@ -50,7 +50,7 @@ Manual steps (for reference or recovery): 7. After the PR merges, publish the draft release on GitHub. Updating the release workflow over time: -- **Python versions**: keep the `build` setting in `[tool.cibuildwheel]` in `pyproject.toml` in sync with the CI matrix in `ci.yml` and `requires-python`. All three should agree. +- **Python versions**: keep the `build` setting in `[tool.cibuildwheel]` in `pyproject.toml` in sync with the CI matrix in `ci.yml` and `requires-python`. All three should agree. Note that cython may lag a bit behind python, so you may need to wait a few months after a python release for cython compatibility. - **cibuildwheel version**: `pypa/cibuildwheel@v2.22.0` in `release.yml` is pinned for reproducibility. When a new Python version requires a newer cibuildwheel release, bump the pin here and also update the version in `install_conda_environment.sh` so local builds stay in sync. Testing the workflows — prerelease → master flow: diff --git a/dev_scripts/build_mac_wheels.sh b/dev_scripts/build_mac_wheels.sh index d91c0dd..f7b4a5b 100755 --- a/dev_scripts/build_mac_wheels.sh +++ b/dev_scripts/build_mac_wheels.sh @@ -58,11 +58,15 @@ if ! command -v cibuildwheel &>/dev/null; then exit 1 fi -# Verify the gh CLI is authenticated. +# Verify the gh CLI is authenticated; offer to log in if not. if ! gh auth status &>/dev/null; then - echo "ERROR: gh CLI is not authenticated." - echo "Run: gh auth login" - exit 1 + echo "gh CLI not authenticated — launching 'gh auth login' now ..." + echo "(This is a one-time step. Follow the prompts to log in via browser.)" + gh auth login + if ! gh auth status &>/dev/null; then + echo "ERROR: gh auth login did not succeed. Re-run this script to try again." + exit 1 + fi fi echo "Building macOS arm64 wheels for $TAG ..." diff --git a/dev_scripts/cut_release.sh b/dev_scripts/cut_release.sh index fe0e4ca..9e74c0f 100755 --- a/dev_scripts/cut_release.sh +++ b/dev_scripts/cut_release.sh @@ -63,9 +63,13 @@ echo " OK git submodule initialized" # gh CLI must be authenticated. if ! gh auth status &>/dev/null; then - echo " FAIL gh CLI not authenticated" - echo " Run: gh auth login" - exit 1 + echo " gh CLI not authenticated — launching 'gh auth login' now ..." + echo " (This is a one-time step. Follow the prompts to log in via browser.)" + gh auth login + if ! gh auth status &>/dev/null; then + echo " FAIL gh auth login did not succeed. Re-run this script to try again." + exit 1 + fi fi echo " OK gh CLI authenticated" From 2a9a1285262dc08d95d58f9b62fb604582ad6311 Mon Sep 17 00:00:00 2001 From: Greg Buzzard Date: Mon, 18 May 2026 23:04:38 -0400 Subject: [PATCH 12/14] Update workflow and docs to build wheels. --- dev_scripts/README.md | 99 ++++++++++---------- dev_scripts/test_release.sh | 174 ++++++++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+), 49 deletions(-) create mode 100755 dev_scripts/test_release.sh diff --git a/dev_scripts/README.md b/dev_scripts/README.md index dca5c63..fee7687 100644 --- a/dev_scripts/README.md +++ b/dev_scripts/README.md @@ -5,9 +5,9 @@ Considerations for package maintenance: This declares the minimum you need, not the maximum. The strong community consensus is: never put upper bounds on requires-python or on dependencies in published package metadata. Upper bounds are actively harmful — they prevent users from installing your package alongside other packages that have already moved forward. If numpy>=2.0 breaks something, the right fix is to fix the code, not to block numpy>=2.0. The only exception is when you have a known, tested incompatibility with a specific version. 2. The CI test matrix -CI (Continuous Integration) is automated testing that runs in the cloud every time code is pushed to the repository or a pull request is opened. For svmbir, this means GitHub automatically installs the package and runs the test suite on Linux across all supported Python versions. The goal is to catch breakage early — before a bug reaches users — and to confirm that changes work correctly across the Python versions the package claims to support. Check https://github.com/cabouman/svmbir/actions for status. +CI (Continuous Integration) is automated testing that runs in the cloud every time code is pushed to the repository or a pull request is opened. For svmbir, this means GitHub automatically installs the package and runs the test suite on Linux across all supported Python versions. The goal is to catch breakage early — before a bug reaches users — and to confirm that changes work correctly across the Python versions the package claims to support. Check https://github.com/cabouman/svmbir/actions for status. -The CI matrix is the list of Python versions CI tests against. The current matrix is under jobs->matrix->python-version in `ci.yml`. When a new python version comes out in October, add it to the list and see if anything breaks. This is the right place to discover problems early, before users file bugs. +The CI matrix is the list of Python versions CI tests against. The current matrix is under jobs->matrix->python-version in `ci.yml`. When a new Python version comes out in October, add it to the list and see if anything breaks. This is the right place to discover problems early, before users file bugs. 3. The dev environment (install_conda_environment.sh) This should pin to a specific recent version for a reproducible daily-driver environment. The version you pick here doesn't limit what users can run — it's just what developers work in. Updating this once a year when a new Python ships is reasonable. @@ -16,7 +16,7 @@ This should pin to a specific recent version for a reproducible daily-driver env This is the configuration file that tells GitHub how to run CI. It specifies which Python versions to test, which operating system to use (Linux only — see section 5 for macOS), and what commands to run (install the package, run pytest). It fires automatically on every push to `master` or `prerelease`, and on every pull request targeting either branch — no manual action needed. Updating Python versions (do this ~once a year): -- **Add a new version**: Each October, Python ships a new release. Add it to the `python-version` list in `ci.yml`, and add the matching `cp3XX-*` entry to the `build` setting in `[tool.cibuildwheel]` in `pyproject.toml` so release wheels are built for it too. +- **Add a new version**: Each October, Python ships a new release. Add it to the `python-version` list in `ci.yml`, and add the matching `cp3XX-*` entry to the `build` setting in `[tool.cibuildwheel]` in `pyproject.toml` so release wheels are built for it too. Note that Cython may lag a few months behind a new Python release, so if CI fails on the new version due to a Cython build error, simply remove it from both lists and try again after the next Cython release. - **Drop an EOL version**: Python versions reach end-of-life roughly 3 years after release (schedule at python.org/downloads). Remove it from the `python-version` list in `ci.yml`, the `build` setting in `pyproject.toml`, and raise `requires-python` in `pyproject.toml` to match. Updating OS runners: @@ -26,74 +26,75 @@ Updating OS runners: 5. The release workflow (.github/workflows/release.yml) and local macOS build Linux wheels and the source distribution are built automatically by GitHub Actions. macOS arm64 wheels are built locally using `dev_scripts/build_mac_wheels.sh` and uploaded to the same draft release. Intel Mac (x86_64) is no longer supported. -How to cut a release: ---------------------- +New releases: +------------- -All steps up to and including tagging are done on the `prerelease` branch. The release stays as a non-public draft until you explicitly publish it, so you can verify everything before it goes live. +Each release is staged on the `prerelease` branch and stays as a non-public draft until you explicitly publish it, so you can verify everything before it goes live. The process has two steps: test first, then release. -**The normal release is done with a single command in dev_scripts/ (and using this form, *not* `source cut_release.sh`):** +Note that the scripts need to be run as `./