Skip to content

Build Python Package #622

Build Python Package

Build Python Package #622

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 }}"