Skip to content

Commit 923f565

Browse files
kyleniemeyerclaude
andcommitted
Replace Travis and AppVeyor with GitHub Actions
Add a test workflow: 15 combinations across Python 3.10-3.14 on Linux, macOS and Windows, a full-suite job including the slow cache-optimizer tests, lint via pre-commit, a build job, and a CUDA compile check. Add a publish workflow using PyPI trusted publishing on release, so no API token is stored, guarded by a check that the git tag matches __version__. The build job verifies that a wheel rebuilt from the sdist matches one built from the repository. The wheel carries 21 non-Python files that the pywrap build reads at runtime, and no test would notice them going missing, since tests run from the source tree. The CUDA job installs nvcc from NVIDIA's apt repository, and builds through pyjac.libgen so the arch plumbing is exercised too. It runs against both CUDA 12.6 and 13.3. Fixes libgen.compiler calling sys.exit from inside a multiprocessing worker when the compiler is missing, which left the parent's pool.map waiting forever rather than reporting. Found by running the CUDA job locally without nvcc. Also fixes three test modules that asserted on sys.modules without importing what they checked, so they passed only when another module imported it first, and two Windows portability bugs: compiling to /dev/null, and globbing for a .so extension. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 833ca77 commit 923f565

12 files changed

Lines changed: 345 additions & 186 deletions

File tree

.github/workflows/publish.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Publish
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
8+
jobs:
9+
build:
10+
name: Build distributions
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-python@v5
15+
with:
16+
python-version: '3.14'
17+
18+
- name: Build
19+
run: python -m pip install build && python -m build
20+
21+
- name: Check metadata
22+
run: python -m pip install twine && twine check dist/*
23+
24+
# The tag and the package version must agree, or the release points at
25+
# something other than what it claims to.
26+
- name: Check the tag matches the package version
27+
if: github.event_name == 'release'
28+
run: |
29+
python - <<'EOF'
30+
import os, pathlib, re, sys
31+
source = pathlib.Path('src/pyjac/_version.py').read_text()
32+
version = re.search(r"__version__ = '([^']+)'", source).group(1)
33+
tag = os.environ['GITHUB_REF_NAME'].lstrip('v')
34+
if tag != version:
35+
sys.exit(f'tag {tag!r} does not match __version__ {version!r}')
36+
print(f'tag and version agree: {version}')
37+
EOF
38+
39+
- uses: actions/upload-artifact@v4
40+
with:
41+
name: distributions
42+
path: dist/
43+
44+
publish:
45+
name: Publish to PyPI
46+
needs: build
47+
runs-on: ubuntu-latest
48+
environment:
49+
name: pypi
50+
url: https://pypi.org/p/pyjac
51+
# Trusted publishing: PyPI verifies this workflow's identity, so no API
52+
# token is stored in the repository.
53+
permissions:
54+
id-token: write
55+
steps:
56+
- uses: actions/download-artifact@v4
57+
with:
58+
name: distributions
59+
path: dist/
60+
- uses: pypa/gh-action-pypi-publish@release/v1

.github/workflows/test.yml

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main, modernize]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
test:
15+
name: ${{ matrix.os }} / Python ${{ matrix.python-version }}
16+
runs-on: ${{ matrix.os }}
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
os: [ubuntu-latest, macos-latest, windows-latest]
21+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
22+
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- uses: actions/setup-python@v5
27+
with:
28+
python-version: ${{ matrix.python-version }}
29+
cache: pip
30+
31+
- name: Install
32+
run: python -m pip install --upgrade pip && pip install -e '.[test,pywrap]'
33+
34+
# The cache-optimizer tests run a randomized search and dominate the
35+
# runtime; they are covered by the full-suite job below.
36+
- name: Test
37+
run: pytest -m "not slow" --cov=pyjac --cov-report=xml
38+
39+
- name: Upload coverage
40+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'
41+
uses: actions/upload-artifact@v4
42+
with:
43+
name: coverage
44+
path: coverage.xml
45+
46+
full-suite:
47+
name: Full suite including slow tests
48+
runs-on: ubuntu-latest
49+
steps:
50+
- uses: actions/checkout@v4
51+
- uses: actions/setup-python@v5
52+
with:
53+
python-version: '3.14'
54+
cache: pip
55+
- name: Install
56+
run: python -m pip install --upgrade pip && pip install -e '.[test,pywrap,cache-opt]'
57+
- name: Test
58+
run: pytest
59+
60+
lint:
61+
name: Lint and format
62+
runs-on: ubuntu-latest
63+
steps:
64+
- uses: actions/checkout@v4
65+
- uses: actions/setup-python@v5
66+
with:
67+
python-version: '3.14'
68+
cache: pip
69+
- uses: pre-commit/action@v3.0.1
70+
71+
build:
72+
name: Build distributions
73+
runs-on: ubuntu-latest
74+
steps:
75+
- uses: actions/checkout@v4
76+
- uses: actions/setup-python@v5
77+
with:
78+
python-version: '3.14'
79+
- name: Build
80+
run: python -m pip install build && python -m build
81+
82+
# A wheel built from the sdist must match one built from the repository,
83+
# otherwise the sdist is missing something the build needs.
84+
- name: Check the sdist can rebuild the wheel
85+
run: |
86+
python -m pip install twine
87+
twine check dist/*
88+
mkdir -p from-sdist && tar xzf dist/*.tar.gz -C from-sdist --strip-components=1
89+
(cd from-sdist && python -m build --wheel --outdir ../dist-from-sdist)
90+
python - <<'EOF'
91+
import pathlib, zipfile
92+
def names(pattern):
93+
path = next(pathlib.Path(pattern[0]).glob(pattern[1]))
94+
with zipfile.ZipFile(path) as z:
95+
return {n for n in z.namelist() if '.dist-info' not in n}
96+
direct = names(('dist', '*.whl'))
97+
via_sdist = names(('dist-from-sdist', '*.whl'))
98+
missing = direct - via_sdist
99+
assert not missing, f'sdist cannot reproduce the wheel; missing {sorted(missing)}'
100+
print(f'sdist reproduces all {len(direct)} wheel entries')
101+
EOF
102+
103+
- uses: actions/upload-artifact@v4
104+
with:
105+
name: distributions
106+
path: dist/
107+
108+
cuda:
109+
name: CUDA ${{ matrix.cuda }} / ${{ matrix.arch }}
110+
# Pinned rather than ubuntu-latest: NVIDIA's apt repository path is
111+
# distro-version specific.
112+
runs-on: ubuntu-24.04
113+
strategy:
114+
fail-fast: false
115+
matrix:
116+
include:
117+
# CUDA 12 still supports Volta, which is pyJac's default target.
118+
- cuda: '12-6'
119+
cuda_path: '12.6'
120+
arch: sm_70
121+
# CUDA 13 dropped Maxwell, Pascal and Volta, so the oldest it accepts
122+
# is Turing. This entry is what tells us whether the default needs to
123+
# move.
124+
- cuda: '13-3'
125+
cuda_path: '13.3'
126+
arch: sm_75
127+
128+
steps:
129+
- uses: actions/checkout@v4
130+
- uses: actions/setup-python@v5
131+
with:
132+
python-version: '3.14'
133+
cache: pip
134+
135+
# Installed from NVIDIA's own apt repository rather than a third-party
136+
# action, and only the compiler: the full toolkit is several GB.
137+
- name: Install nvcc ${{ matrix.cuda }}
138+
run: |
139+
wget -q https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb
140+
sudo dpkg -i cuda-keyring_1.1-1_all.deb
141+
sudo apt-get update
142+
sudo apt-get install -y --no-install-recommends cuda-nvcc-${{ matrix.cuda }}
143+
echo "/usr/local/cuda-${{ matrix.cuda_path }}/bin" >> "$GITHUB_PATH"
144+
145+
- name: Install
146+
run: python -m pip install --upgrade pip && pip install -e '.[test]'
147+
148+
# There is no GPU on the runner, so the generated CUDA is compiled but
149+
# never executed. This catches syntax and API errors, not wrong numbers.
150+
# It goes through pyjac.libgen rather than a hand-rolled nvcc loop, so
151+
# the build machinery and the --cuda-arch plumbing are exercised too.
152+
- name: Generate CUDA sources
153+
run: python -m pyjac --lang cuda --input data/h2o2.inp --build_path out-cuda
154+
155+
- name: Build the CUDA library
156+
run: |
157+
nvcc --version
158+
python -m pyjac.libgen --lang cuda --source_dir out-cuda \
159+
--out_dir lib-cuda --obj_dir obj-cuda \
160+
--static --cuda-arch ${{ matrix.arch }}
161+
162+
- name: Confirm the library was produced
163+
run: test -f lib-cuda/libcu_pyjac.a && ls -l lib-cuda/

.travis.yml

Lines changed: 0 additions & 83 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ regenerating with the old table and diffing.
3131
`np.random`, so successive runs legitimately differ.
3232
- Regression tests for Chebyshev parsing and generation, the CLI, the
3333
unsupported-language guard, and `option_cases`
34+
- GitHub Actions workflows: a test matrix over Python 3.10-3.14 on Linux,
35+
macOS and Windows; a full-suite job including the slow cache-optimizer
36+
tests; lint via pre-commit; a build job that checks the sdist can rebuild
37+
the wheel; a CUDA job that compiles generated sources through
38+
`pyjac.libgen` against both CUDA 12 and 13; and PyPI publishing on release
39+
via trusted publishing.
3440
- `pyproject.toml` with PEP 621 metadata, optional-dependency extras
3541
(`pywrap`, `cache-opt`, `test`, `docs`), and ruff/pytest/coverage config,
3642
built with hatchling
@@ -108,6 +114,12 @@ regenerating with the old table and diffing.
108114
- Path parsing now uses `pathlib` rather than `os.path`
109115

110116
### Fixed
117+
- `libgen.compiler` called `sys.exit` from inside a `multiprocessing.Pool`
118+
worker when the compiler was missing, so `generate_library` hung waiting on
119+
a result that never arrived rather than reporting the missing compiler.
120+
- Three test modules asserted on `sys.modules` without importing what they
121+
checked, so they passed only when another module had imported it first and
122+
failed when run alone.
111123
- `pywrap.generate_wrapper` built the wrapper with a reconstructed `pythonX.Y`
112124
name resolved against `PATH`, escaping the active environment and its
113125
Cython, NumPy and setuptools. It now uses `sys.executable`.
@@ -146,6 +158,7 @@ regenerating with the old table and diffing.
146158
an exit status.
147159

148160
### Removed
161+
- `.travis.yml` and `appveyor.yml`, replaced by GitHub Actions
149162
- `data/h2o2.cti` and `data/h2o2_performance/h2o2.cti`, replaced by YAML
150163
equivalents; Cantera 3.x cannot read the CTI format
151164
- `setup.py`, `setup.cfg`, `MANIFEST.in` (superseded by `pyproject.toml`)

appveyor.yml

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/pyjac/libgen/libgen.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,10 @@ def compiler(fstruct):
110110
print(' '.join(args))
111111
subprocess.check_call(args)
112112
except OSError:
113-
print(
114-
f'Error: Compiler {args[0]} not found, generation of pyjac library failed.'
115-
)
116-
sys.exit(-1)
113+
# this runs in a multiprocessing worker, where sys.exit leaves the
114+
# parent's pool.map waiting for a result that never arrives
115+
print(f'Error: compiler {args[0]} not found.')
116+
return -1
117117
except subprocess.CalledProcessError:
118118
print(
119119
'Error: compilation failed for '

0 commit comments

Comments
 (0)