Build Python Package #622
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Build Python Package | |
on: | |
pull_request: | |
paths: | |
- ".github/workflows/python-package.yml" | |
workflow_dispatch: | |
inputs: | |
incoming_ref: | |
description: > | |
The ref from Cantera/cantera to be built. Can be a tag, commit hash, | |
or branch name. | |
required: true | |
default: "main" | |
upload: | |
description: Attempt to upload to PyPI | |
required: true | |
default: "false" | |
permissions: {} | |
concurrency: | |
group: ${{ github.ref }}-${{ github.event.inputs.incoming_ref || 'main' }} | |
cancel-in-progress: true | |
env: | |
ACTION_URL: "https://github.com/Cantera/pypi-packages/actions/runs/${{ github.run_id }}" | |
jobs: | |
post-pending-status: | |
name: Post a pending workflow status to Cantera/cantera | |
runs-on: ubuntu-24.04 | |
env: | |
GITHUB_TOKEN: ${{ secrets.CANTERA_REPO_STATUS }} | |
outputs: | |
incoming-ref: ${{ steps.munge-incoming-ref.outputs.incoming-ref }} | |
incoming-sha: ${{ steps.munge-incoming-ref.outputs.incoming-sha }} | |
tag-ref: ${{ steps.munge-incoming-ref.outputs.tag-ref }} | |
steps: | |
- name: Munge the incoming ref | |
id: munge-incoming-ref | |
run: | | |
import os | |
import re | |
import subprocess | |
from pathlib import Path | |
INCOMING_REF = os.environ["INCOMING_REF"] | |
INCOMING_SHA = "" | |
if INCOMING_REF.startswith("refs/"): | |
INCOMING_REF = INCOMING_REF.replace("refs/", "") | |
elif re.match(r"^v\d\.\d\.\d.*$", INCOMING_REF) is not None: | |
INCOMING_REF = f"tags/{INCOMING_REF}" | |
elif re.match(r"^[a-f0-9]{6,40}", INCOMING_REF) is not None: | |
INCOMING_SHA = INCOMING_REF | |
else: | |
INCOMING_REF = f"heads/{INCOMING_REF}" | |
TAG_REF = "false" | |
if INCOMING_REF.startswith("tags"): | |
TAG_REF = "true" | |
if not INCOMING_SHA: | |
output = subprocess.run( | |
["gh", "api", f"repos/cantera/cantera/git/ref/{INCOMING_REF}", | |
"-H", "Accept: application/vnd.github.v3+json", "--jq", ".object.sha"], | |
capture_output=True, | |
text=True, | |
check=False, | |
) | |
if output.returncode == 0: | |
INCOMING_SHA = output.stdout.strip() | |
else: | |
print(f"gh api failed with error code {output.returncode}") | |
print(f"gh api output: {output.stderr}") | |
print(f"gh api stdout: {output.stdout}") | |
Path(os.environ["GITHUB_OUTPUT"]).write_text( | |
f"incoming-ref={INCOMING_REF}\n" | |
f"incoming-sha={INCOMING_SHA}\n" | |
f"tag-ref={TAG_REF}" | |
) | |
shell: python | |
env: | |
INCOMING_REF: "${{ inputs.incoming_ref || 'main' }}" | |
- name: Post the status to the upstream commit | |
id: set-the-status | |
if: steps.munge-incoming-ref.outputs.tag-ref == 'false' && github.event_name != 'pull_request' | |
run: | | |
gh api repos/cantera/cantera/statuses/${INCOMING_SHA} \ | |
-H "Accept: application/vnd.github.v3+json" \ | |
--field state='pending' \ | |
--field target_url=$ACTION_URL \ | |
--field context='PyPI Package Build' \ | |
--field description="Pending build" \ | |
--silent | |
env: | |
INCOMING_SHA: "${{ steps.munge-incoming-ref.outputs.incoming-sha }}" | |
sdist: | |
name: Build the sdist | |
runs-on: ubuntu-24.04 | |
needs: | |
- "post-pending-status" | |
outputs: | |
job-status: ${{ job.status }} | |
steps: | |
- uses: actions/checkout@v4 | |
name: Checkout the repository | |
with: | |
repository: "Cantera/cantera" | |
submodules: recursive | |
ref: ${{ inputs.incoming_ref || 'main'}} | |
persist-credentials: false | |
- name: Set Up Python 3.12 | |
uses: actions/setup-python@v5 | |
with: | |
python-version: "3.12" | |
cache: 'pip' | |
cache-dependency-path: interfaces/python_sdist/pyproject.toml.in | |
- name: Install dependencies | |
run: python3 -m pip install scons build | |
- name: Build the sdist | |
run: | | |
python3 `which scons` sdist | |
- name: Archive the built sdist | |
uses: actions/upload-artifact@v4 | |
with: | |
path: ./build/python_sdist/dist/*.tar.gz | |
name: cibw-sdist | |
if-no-files-found: error | |
# Copied from https://github.com/hynek/build-and-inspect-python-package/ | |
- name: Show SDist contents hierarchically, including metadata. | |
shell: bash | |
run: | | |
mkdir -p /tmp/out/sdist | |
cp build/python_sdist/dist/*.tar.gz /tmp/ | |
cd /tmp | |
tar xf *.tar.gz -C out/sdist | |
echo -e '\n<details><summary>SDist contents</summary>\n' >> $GITHUB_STEP_SUMMARY | |
(cd /tmp/out/sdist && tree -Da --timefmt="%Y-%m-%dT%H:%M:%SZ" * | sed 's/^/ /' | tee -a $GITHUB_STEP_SUMMARY) | |
echo -e '\n</details>\n' >> $GITHUB_STEP_SUMMARY | |
echo ----- Metadata Follows ----- | |
echo -e '\n<details><summary>Metadata</summary>\n' >> $GITHUB_STEP_SUMMARY | |
cat out/sdist/*/PKG-INFO | sed 's/^/ /' | tee -a $GITHUB_STEP_SUMMARY | |
echo -e '\n</details>\n' >> $GITHUB_STEP_SUMMARY | |
echo ----- End of Metadata ----- | |
build-wheels: | |
name: Build ${{ matrix.os }} ${{ matrix.arch }} | |
runs-on: ${{ matrix.os }} | |
needs: ["sdist", "post-pending-status"] | |
permissions: | |
contents: read | |
outputs: | |
job-status: ${{ job.status }} | |
strategy: | |
matrix: | |
include: | |
- os: ubuntu-24.04-arm | |
arch: aarch64 | |
- os: ubuntu-24.04 | |
arch: x86_64 | |
- os: windows-2022 | |
arch: AMD64 | |
boost-arch: x86 | |
boost-toolset: msvc | |
boost-platform-version: 2022 | |
boost-version: "1.86.0" | |
- os: macos-14 | |
arch: arm64 | |
boost-arch: aarch64 | |
boost-toolset: clang | |
# Since we only use the headers, we can use the platform version for this | |
# macos version | |
boost-platform-version: "14" | |
boost-version: "1.86.0" | |
- os: macos-13 | |
arch: x86_64 | |
boost-arch: x86 | |
boost-toolset: clang | |
# Since we only use the headers, we can use the platform version for this | |
# macos version | |
boost-platform-version: "13" | |
boost-version: "1.86.0" | |
fail-fast: false | |
steps: | |
- name: Checkout the repository | |
uses: actions/checkout@v4 | |
with: | |
persist-credentials: false | |
- name: Download pre-built sdist | |
uses: actions/download-artifact@v4 | |
with: | |
name: cibw-sdist | |
- name: Extract the sdist tarball | |
run: tar -xvf *.tar.gz --strip-components=1 | |
shell: bash | |
- name: Set test file download destination | |
id: download-test-files | |
run: | | |
mkdir -p "${RUNNER_TEMP}/ct-test-dir" | |
echo "test-root=${RUNNER_TEMP}/ct-test-dir" >> $GITHUB_OUTPUT | |
shell: bash | |
- name: Download and unpack the tarball | |
run: | | |
curl -fsSL "https://github.com/cantera/cantera/archive/${INCOMING_SHA}.tar.gz" -o cantera.tar.gz | |
tar -xzf cantera.tar.gz --strip-components=1 "cantera-${INCOMING_SHA}/test" | |
rm cantera.tar.gz | |
shell: bash | |
working-directory: "${{ steps.download-test-files.outputs.test-root }}" | |
env: | |
INCOMING_SHA: ${{ needs.post-pending-status.outputs.incoming-sha }} | |
# Non-Linux steps | |
- name: Install boost | |
# Our custom manylinux images already have Boost installed | |
if: runner.os != 'Linux' | |
uses: MarkusJx/install-boost@v2.5.0 | |
id: install-boost | |
with: | |
# REQUIRED: Specify the required boost version | |
# A list of supported versions can be found here: | |
# https://github.com/MarkusJx/prebuilt-boost/blob/main/versions-manifest.json | |
boost_version: ${{ matrix.boost-version }} | |
# OPTIONAL: Specify a custon install location | |
boost_install_dir: ${{ runner.temp }} | |
toolset: ${{ matrix.boost-toolset }} | |
platform_version: ${{ matrix.boost-platform-version }} | |
arch: ${{ matrix.boost-arch }} | |
- name: Restore the cached built libraries | |
id: restore-built-libraries | |
# Our custom manylinux images already have all our dependencies installed | |
if: runner.os != 'Linux' | |
uses: actions/cache/restore@v4 | |
with: | |
key: ${{ matrix.os }}-${{ matrix.arch }}-${{ hashFiles('dependencies.sh') }}-1 | |
path: ${{ github.workspace }}/cache | |
# Windows-only steps | |
- name: Build required libraries | |
run: bash ./cibw_before_all_windows.sh "${{ github.workspace }}" | |
if: runner.os == 'Windows' | |
- name: Set up CIBW environment | |
# On Windows, Boost_ROOT needs to have \ replaced by / because that's what | |
# cibuildwheel says. CANTERA_TEST_DIR doesn't need the replacement because | |
# it will be substituted in a cmd or pwsh session. | |
run: | | |
$BOOST_ROOT = "$Env:BOOST_ROOT" -replace "\\", "/" | |
echo "Boost_ROOT=$BOOST_ROOT" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append | |
shell: pwsh | |
if: runner.os == 'Windows' | |
env: | |
BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} | |
# macOS-only steps | |
- name: Build required libraries | |
run: bash ./cibw_before_all_macos.sh "${{ github.workspace }}" | |
if: runner.os == 'macOS' | |
- name: Set up CIBW environment | |
run: | | |
echo "Boost_ROOT=$BOOST_ROOT" >> $GITHUB_ENV | |
if: runner.os == 'macOS' | |
env: | |
BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} | |
- name: Save the cache | |
uses: actions/cache/save@v4 | |
if: always() && runner.os != 'Linux' && steps.restore-built-libraries.outputs.cache-hit != 'true' | |
with: | |
path: ${{ github.workspace }}/cache | |
key: ${{ steps.restore-built-libraries.outputs.cache-primary-key }} | |
- name: Build wheels | |
uses: pypa/cibuildwheel@v2.23.2 | |
env: | |
CANTERA_TEST_DIR: ${{ steps.download-test-files.outputs.test-root }} | |
CIBW_ENVIRONMENT_LINUX: CT_SKIP_SLOW=1 CANTERA_TEST_DIR=/host${{ steps.download-test-files.outputs.test-root }} | |
CIBW_ENVIRONMENT_WINDOWS: CT_SKIP_SLOW=1 CMAKE_BUILD_PARALLEL_LEVEL=4 | |
CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path %HDF5_LIB_DIR%;%SUNDIALS_LIB_DIR%;%YAML_CPP_LIB_DIR% -w {dest_dir} {wheel} | |
CIBW_ENVIRONMNET_MACOS: CT_SKIP_SLOW=1 | |
CIBW_BUILD: "cp*-*" | |
CIBW_ARCHS: ${{ matrix.arch }} | |
- name: Archive the built wheels | |
uses: actions/upload-artifact@v4 | |
with: | |
path: ./wheelhouse/*.whl | |
name: cibw-wheels-${{ runner.os }}-${{ strategy.job-index }} | |
publish-files-to-pypi: | |
name: Publish distribution files to PyPI | |
runs-on: ubuntu-24.04 | |
outputs: | |
job-status: ${{ job.status }} | |
permissions: | |
attestations: write | |
id-token: write | |
needs: | |
- "sdist" | |
- "build-wheels" | |
if: inputs.upload == 'true' | |
environment: pypi | |
steps: | |
- name: Download pre-built wheels | |
uses: actions/download-artifact@v4 | |
with: | |
path: dist | |
pattern: cibw-* | |
merge-multiple: true | |
- name: pypi-publish | |
uses: pypa/gh-action-pypi-publish@release/v1 | |
send_status_to_cantera: | |
name: Send jobs status to Cantera/cantera | |
runs-on: ubuntu-24.04 | |
needs: | |
- "post-pending-status" | |
- "sdist" | |
- "build-wheels" | |
- "publish-files-to-pypi" | |
if: always() && github.event_name != 'pull_request' | |
steps: | |
- name: Collect statuses | |
run: | # zizmor: ignore[template-injection] | |
import os | |
from collections import Counter | |
statuses = { | |
"sdist": "${{ needs.sdist.outputs.job-status }}", | |
"wheels": "${{ needs.build-wheels.outputs.job-status }}", | |
"publish": "${{ needs.publish-files-to-pypi.outputs.job-status }}", | |
} | |
# This is a deliberate comparison to the empty string. | |
if statuses["publish"] == "" and os.environ["UPLOAD"] == "false": | |
publish = statuses.pop("publish") | |
else: | |
publish = "" | |
if all(v == "success" for v in statuses.values()): | |
overall_status = "success" | |
elif any(v in ("cancelled", "") for v in statuses.values()): | |
overall_status = "error" | |
elif any(v == "failure" for v in statuses.values()): | |
overall_status = "failure" | |
status_counts = Counter(statuses.values()) | |
status_counts.update([publish]) | |
description = [] | |
if overall_status in ("error", "failure"): | |
if status_counts.get("success") is not None: | |
description.append(f"{status_counts['success']} succeeded") | |
if status_counts.get("cancelled") is not None: | |
description.append(f"{status_counts['cancelled']} cancelled") | |
if status_counts.get("failure") is not None: | |
description.append(f"{status_counts['failure']} failed") | |
if status_counts.get("") is not None: | |
description.append(f"{status_counts['']} skipped") | |
description = ", ".join(description) | |
else: | |
description = "Successfully built Python wheels!" | |
with open(os.environ["GITHUB_ENV"], "a") as gh_env: | |
gh_env.write(f"OVERALL_STATUS={overall_status}\nDESCRIPTION={description}") | |
shell: python | |
env: | |
UPLOAD: "${{ inputs.upload }}" | |
- name: Post the status to the upstream commit | |
if: needs.post-pending-status.outputs.tag-ref == 'false' | |
run: | | |
gh api repos/cantera/cantera/statuses/${INCOMING_SHA} \ | |
-H "Accept: application/vnd.github.v3+json" \ | |
--field state="${OVERALL_STATUS}" \ | |
--field target_url=$ACTION_URL \ | |
--field context='PyPI Package Build' \ | |
--field description="${DESCRIPTION}" \ | |
--silent | |
env: | |
GITHUB_TOKEN: ${{ secrets.CANTERA_REPO_STATUS }} | |
INCOMING_SHA: "${{ needs.post-pending-status.outputs.incoming-sha }}" |