Build pre-populated Python distributions #907
Workflow file for this run
This file contains 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 hatch | |
on: | |
push: | |
tags: | |
- hatch-v* | |
branches: | |
- master | |
pull_request: | |
branches: | |
- master | |
concurrency: | |
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} | |
cancel-in-progress: true | |
defaults: | |
run: | |
shell: bash | |
env: | |
APP_NAME: hatch | |
PYTHON_VERSION: "3.11" | |
PYOXIDIZER_VERSION: "0.24.0" | |
jobs: | |
python-artifacts: | |
name: Build wheel and source distribution | |
runs-on: ubuntu-latest | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
- name: Install build frontend | |
run: python -m pip install --upgrade build | |
- name: Build | |
run: python -m build | |
- name: Upload artifacts | |
uses: actions/upload-artifact@v4 | |
with: | |
name: python-artifacts | |
path: dist/* | |
if-no-files-found: error | |
binaries: | |
name: ${{ matrix.job.target }} (${{ matrix.job.os }}) | |
needs: | |
- python-artifacts | |
runs-on: ${{ matrix.job.os }} | |
strategy: | |
fail-fast: false | |
matrix: | |
job: | |
# Linux | |
- target: aarch64-unknown-linux-gnu | |
os: ubuntu-22.04 | |
cross: true | |
- target: x86_64-unknown-linux-gnu | |
os: ubuntu-22.04 | |
cross: true | |
- target: x86_64-unknown-linux-musl | |
os: ubuntu-22.04 | |
cross: true | |
- target: powerpc64le-unknown-linux-gnu | |
os: ubuntu-22.04 | |
cross: true | |
# Windows | |
- target: x86_64-pc-windows-msvc | |
os: windows-2022 | |
- target: i686-pc-windows-msvc | |
os: windows-2022 | |
# macOS | |
- target: aarch64-apple-darwin | |
os: macos-12 | |
- target: x86_64-apple-darwin | |
os: macos-12 | |
outputs: | |
version: ${{ steps.version.outputs.version }} | |
env: | |
CARGO: cargo | |
CARGO_BUILD_TARGET: ${{ matrix.job.target }} | |
PYAPP_REPO: pyapp | |
PYAPP_VERSION: "0.19.0" | |
PYAPP_UV_ENABLED: "true" | |
PYAPP_PASS_LOCATION: "true" | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
- name: Fetch PyApp | |
run: >- | |
mkdir $PYAPP_REPO && curl -L | |
https://github.com/ofek/pyapp/releases/download/v$PYAPP_VERSION/source.tar.gz | |
| | |
tar --strip-components=1 -xzf - -C $PYAPP_REPO | |
- name: Set up Python ${{ env.PYTHON_VERSION }} | |
uses: actions/setup-python@v5 | |
with: | |
python-version: ${{ env.PYTHON_VERSION }} | |
- name: Install Hatch | |
run: |- | |
pip install -e . | |
pip install -e ./backend | |
- name: Install Rust toolchain | |
uses: dtolnay/rust-toolchain@stable | |
with: | |
targets: ${{ matrix.job.target }} | |
- name: Set up cross compiling | |
if: matrix.job.cross | |
uses: taiki-e/install-action@v2 | |
with: | |
tool: cross | |
- name: Configure cross compiling | |
if: matrix.job.cross | |
run: echo "CARGO=cross" >> $GITHUB_ENV | |
- name: Configure target | |
run: |- | |
config_file="$PYAPP_REPO/.cargo/config_${{ matrix.job.target }}.toml" | |
if [[ -f "$config_file" ]]; then | |
mv "$config_file" "$PYAPP_REPO/.cargo/config.toml" | |
fi | |
- name: Download Python artifacts | |
if: ${{ !startsWith(github.event.ref, 'refs/tags') }} | |
uses: actions/download-artifact@v4 | |
with: | |
name: python-artifacts | |
path: dist | |
- name: Configure embedded project | |
if: ${{ !startsWith(github.event.ref, 'refs/tags') }} | |
run: |- | |
cd dist | |
wheel="$(echo *.whl)" | |
mv "$wheel" "../$PYAPP_REPO" | |
echo "PYAPP_PROJECT_PATH=$wheel" >> $GITHUB_ENV | |
- name: Build binary | |
run: hatch build --target app | |
# Windows installers don't accept non-integer versions so we ubiquitously | |
# perform the following transformation: X.Y.Z.devN -> X.Y.Z.N | |
- name: Set project version | |
id: version | |
run: |- | |
old_version="$(hatch version)" | |
version="${old_version/dev/}" | |
if [[ "$version" != "$old_version" ]]; then | |
cd dist/app | |
old_binary="$(ls)" | |
binary="${old_binary/$old_version/$version}" | |
mv "$old_binary" "$binary" | |
fi | |
echo "version=$version" >> $GITHUB_OUTPUT | |
echo "$version" | |
- name: Archive binary | |
run: |- | |
mkdir packaging | |
cd dist/app | |
binary="$(ls)" | |
if [[ "$binary" =~ -pc-windows- ]]; then | |
7z a "../../packaging/${binary:0:-4}.zip" "$binary" | |
else | |
chmod +x "$binary" | |
tar -czf "../../packaging/$binary.tar.gz" "$binary" | |
fi | |
- name: Upload staged archive | |
if: runner.os != 'Linux' | |
uses: actions/upload-artifact@v4 | |
with: | |
name: staged-${{ runner.os }}-${{ matrix.job.target }} | |
path: packaging/* | |
if-no-files-found: error | |
- name: Upload archive | |
if: runner.os == 'Linux' | |
uses: actions/upload-artifact@v4 | |
with: | |
name: standalone-${{ matrix.job.target }} | |
path: packaging/* | |
if-no-files-found: error | |
windows-packaging: | |
name: Build Windows installers | |
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository | |
needs: binaries | |
runs-on: windows-2022 | |
env: | |
VERSION: ${{ needs.binaries.outputs.version }} | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
- name: Set up Python ${{ env.PYTHON_VERSION }} | |
uses: actions/setup-python@v5 | |
with: | |
python-version: ${{ env.PYTHON_VERSION }} | |
- name: Install PyOxidizer ${{ env.PYOXIDIZER_VERSION }} | |
run: pip install pyoxidizer==${{ env.PYOXIDIZER_VERSION }} | |
- name: Download staged binaries | |
uses: actions/download-artifact@v4 | |
with: | |
pattern: staged-${{ runner.os }}-* | |
path: archives | |
merge-multiple: true | |
- name: Extract staged binaries | |
run: |- | |
mkdir bin | |
for f in archives/*; do | |
7z e "$f" -obin | |
done | |
# bin/<APP_NAME>-<VERSION>-<TARGET>.exe -> targets/<TARGET>/<APP_NAME>.exe | |
- name: Prepare binaries | |
run: |- | |
mkdir targets | |
for f in bin/*; do | |
if [[ "$f" =~ ${{ env.VERSION }}-(.+).exe$ ]]; then | |
target="${BASH_REMATCH[1]}" | |
mkdir "targets/$target" | |
mv "$f" "targets/$target/${{ env.APP_NAME }}.exe" | |
fi | |
done | |
- name: Build installers | |
run: >- | |
pyoxidizer build windows_installers | |
--release | |
--var version ${{ env.VERSION }} | |
- name: Prepare installers | |
run: |- | |
mkdir installers | |
mv build/*/release/*/*.{exe,msi} installers | |
- name: Upload binaries | |
uses: actions/upload-artifact@v4 | |
with: | |
name: standalone-${{ runner.os }} | |
path: archives/* | |
if-no-files-found: error | |
- name: Upload installers | |
uses: actions/upload-artifact@v4 | |
with: | |
name: installers-${{ runner.os }} | |
path: installers/* | |
if-no-files-found: error | |
macos-packaging: | |
name: Build macOS installer and sign/notarize artifacts | |
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository | |
needs: binaries | |
runs-on: macos-12 | |
env: | |
VERSION: ${{ needs.binaries.outputs.version }} | |
NOTARY_WAIT_TIME: "3600" # 1 hour | |
steps: | |
- name: Checkout code | |
uses: actions/checkout@v4 | |
- name: Set up Python ${{ env.PYTHON_VERSION }} | |
uses: actions/setup-python@v5 | |
with: | |
python-version: ${{ env.PYTHON_VERSION }} | |
- name: Install PyOxidizer ${{ env.PYOXIDIZER_VERSION }} | |
run: pip install pyoxidizer==${{ env.PYOXIDIZER_VERSION }} | |
- name: Install rcodesign | |
env: | |
ARCHIVE_NAME: "apple-codesign-0.27.0-x86_64-apple-darwin" | |
run: >- | |
curl -L | |
"https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.27.0/$ARCHIVE_NAME.tar.gz" | |
| | |
tar --strip-components=1 -xzf - -C /usr/local/bin "$ARCHIVE_NAME/rcodesign" | |
- name: Download staged binaries | |
uses: actions/download-artifact@v4 | |
with: | |
pattern: staged-${{ runner.os }}-* | |
path: archives | |
merge-multiple: true | |
- name: Extract staged binaries | |
run: |- | |
mkdir bin | |
for f in archives/*; do | |
tar -xzf "$f" -C bin | |
done | |
- name: Write credentials | |
env: | |
APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE: "${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE }}" | |
APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY: "${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY }}" | |
APPLE_DEVELOPER_ID_INSTALLER_CERTIFICATE: "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERTIFICATE }}" | |
APPLE_DEVELOPER_ID_INSTALLER_PRIVATE_KEY: "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_PRIVATE_KEY }}" | |
APPLE_APP_STORE_CONNECT_API_DATA: "${{ secrets.APPLE_APP_STORE_CONNECT_API_DATA }}" | |
run: |- | |
echo "$APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE" > /tmp/certificate-application.pem | |
echo "$APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY" > /tmp/private-key-application.pem | |
echo "$APPLE_DEVELOPER_ID_INSTALLER_CERTIFICATE" > /tmp/certificate-installer.pem | |
echo "$APPLE_DEVELOPER_ID_INSTALLER_PRIVATE_KEY" > /tmp/private-key-installer.pem | |
echo "$APPLE_APP_STORE_CONNECT_API_DATA" > /tmp/app-store-connect.json | |
# https://developer.apple.com/documentation/security/hardened_runtime | |
- name: Sign binaries | |
run: |- | |
for f in bin/*; do | |
rcodesign sign -vv \ | |
--pem-source /tmp/certificate-application.pem \ | |
--pem-source /tmp/private-key-application.pem \ | |
--code-signature-flags runtime \ | |
"$f" | |
done | |
# https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution | |
- name: Notarize binaries | |
run: |- | |
mkdir notarize-bin | |
cd bin | |
for f in *; do | |
zip "../notarize-bin/$f.zip" "$f" | |
done | |
cd ../notarize-bin | |
for f in *; do | |
rcodesign notary-submit -vv \ | |
--max-wait-seconds ${{ env.NOTARY_WAIT_TIME }} \ | |
--api-key-path /tmp/app-store-connect.json \ | |
"$f" | |
done | |
- name: Archive binaries | |
run: |- | |
rm archives/* | |
cd bin | |
for f in *; do | |
tar -czf "../archives/$f.tar.gz" "$f" | |
done | |
# bin/<APP_NAME>-<VERSION>-<TARGET> -> targets/<TARGET>/<APP_NAME> | |
- name: Prepare binaries | |
run: |- | |
mkdir targets | |
for f in bin/*; do | |
if [[ "$f" =~ ${{ env.VERSION }}-(.+)$ ]]; then | |
target="${BASH_REMATCH[1]}" | |
mkdir "targets/$target" | |
mv "$f" "targets/$target/${{ env.APP_NAME }}" | |
fi | |
done | |
- name: Build universal binary | |
run: >- | |
pyoxidizer build macos_universal_binary | |
--release | |
--var version ${{ env.VERSION }} | |
- name: Prepare universal binary | |
id: binary | |
run: |- | |
binary=$(echo build/*/release/*/${{ env.APP_NAME }}) | |
chmod +x "$binary" | |
echo "path=$binary" >> "$GITHUB_OUTPUT" | |
- name: Build PKG | |
run: >- | |
python release/macos/build_pkg.py | |
--binary ${{ steps.binary.outputs.path }} | |
--version ${{ env.VERSION }} | |
staged | |
- name: Stage PKG | |
id: pkg | |
run: |- | |
mkdir signed | |
pkg_file="$(ls staged)" | |
echo "path=$pkg_file" >> "$GITHUB_OUTPUT" | |
- name: Sign PKG | |
run: >- | |
rcodesign sign -vv | |
--pem-source /tmp/certificate-installer.pem | |
--pem-source /tmp/private-key-installer.pem | |
"staged/${{ steps.pkg.outputs.path }}" | |
"signed/${{ steps.pkg.outputs.path }}" | |
- name: Notarize PKG | |
run: >- | |
rcodesign notary-submit -vv | |
--max-wait-seconds ${{ env.NOTARY_WAIT_TIME }} | |
--api-key-path /tmp/app-store-connect.json | |
--staple | |
"signed/${{ steps.pkg.outputs.path }}" | |
- name: Upload binaries | |
uses: actions/upload-artifact@v4 | |
with: | |
name: standalone-${{ runner.os }} | |
path: archives/* | |
if-no-files-found: error | |
- name: Upload installer | |
uses: actions/upload-artifact@v4 | |
with: | |
name: installers-${{ runner.os }} | |
path: signed/${{ steps.pkg.outputs.path }} | |
if-no-files-found: error | |
publish-project: | |
name: Publish release artifacts | |
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') | |
needs: | |
- python-artifacts | |
- binaries | |
- windows-packaging | |
- macos-packaging | |
runs-on: ubuntu-latest | |
permissions: | |
contents: write | |
id-token: write | |
steps: | |
- name: Download Python artifacts | |
uses: actions/download-artifact@v4 | |
with: | |
name: python-artifacts | |
path: dist | |
- name: Download binaries | |
uses: actions/download-artifact@v4 | |
with: | |
pattern: standalone-* | |
path: archives | |
merge-multiple: true | |
- name: Download installers | |
uses: actions/download-artifact@v4 | |
with: | |
pattern: installers-* | |
path: installers | |
merge-multiple: true | |
- name: Add assets to current release | |
uses: softprops/action-gh-release@v2 | |
with: | |
files: |- | |
archives/* | |
installers/* | |
- name: Push Python artifacts to PyPI | |
uses: pypa/[email protected] | |
distributions-release: | |
name: Build release distributions | |
needs: | |
- binaries | |
- publish-project | |
if: startsWith(github.event.ref, 'refs/tags') | |
uses: ./.github/workflows/build-distributions.yml | |
with: | |
version: ${{ needs.binaries.outputs.version }} | |
distributions-dev: | |
name: Build development distributions | |
if: ${{ !startsWith(github.event.ref, 'refs/tags') }} | |
uses: ./.github/workflows/build-distributions.yml | |
# This actually does not need the binary jobs but we want to prioritize | |
# resources for the test jobs therefore this forces these later on | |
needs: binaries | |
publish-distributions: | |
name: Publish distribution artifacts | |
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') | |
needs: | |
- distributions-release | |
runs-on: ubuntu-latest | |
permissions: | |
contents: write | |
id-token: write | |
steps: | |
- name: Download distributions | |
uses: actions/download-artifact@v4 | |
with: | |
name: distribution-* | |
path: distributions | |
merge-multiple: true | |
- name: Add assets to current release | |
uses: softprops/action-gh-release@v2 | |
with: | |
files: |- | |
distributions/* |