Skip to content

Commit

Permalink
add a new build_utils.py script to simplify the yaml logic
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin-sift committed Dec 11, 2024
1 parent c6c4568 commit cdc447b
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 71 deletions.
104 changes: 58 additions & 46 deletions .github/workflows/python_build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,61 @@ jobs:
];
core.setOutput('matrix', JSON.stringify(matrix));
build_wheel:
name: Build sift-stack-py distributions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.8" # Use lowest supported version for maximum compatibility

- name: Install build tools
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build distributions
working-directory: python
run: |
python -m build
- name: Verify distributions
working-directory: python
shell: bash
run: |
# Check all distributions with twine
twine check dist/*
# Verify we have a universal wheel
# We want to ensure that the wheel is compatible with all Python versions
# and all architectures. If this fails, we will need to update our build strategy to
# build separate wheels for each Python version and architecture.
WHEEL_NAME=$(ls dist/sift_stack_py*.whl)
if [[ ! $WHEEL_NAME =~ "py3-none-any.whl" ]]; then
echo "Error: Expected a universal wheel (py3-none-any) but got: $WHEEL_NAME"
exit 1
fi
echo "Verified universal wheel: $WHEEL_NAME"
- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: sift-stack-py-dist
path: python/dist/*
retention-days: 14

build_and_verify:
name: Build and verify on ${{ matrix.platform.os }} (${{ matrix.platform.arch }}) with Python ${{ matrix.python-version }}
needs: get-matrix-config
name: Build offline archive for ${{ matrix.platform.os }} (${{ matrix.platform.arch }}) with Python ${{ matrix.python-version }}
needs: [get-matrix-config, build_wheel]
runs-on: ${{ matrix.platform.runner }}
strategy:
fail-fast: false
matrix:
platform: ${{ fromJson(needs.get-matrix-config.outputs.platforms) }}
python-version: ${{fromJson(needs.get-matrix-config.outputs.supported_python_versions)}}
outputs:
matrix-config: ${{ toJson(matrix) }}

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -92,53 +136,21 @@ jobs:
--prefer-binary \
--no-deps
- name: Build sift-py wheel
working-directory: python
shell: bash
run: |
python -m build --wheel
- name: Verify wheel
working-directory: python
shell: bash
run: |
twine check dist/sift_stack_py*.whl
- name: Download sift-stack-py wheel
uses: actions/download-artifact@v4
with:
name: sift-stack-py-wheel
path: python/dist/

- name: Test installations
working-directory: python
shell: bash
run: |
# Get the wheel file name
WHEEL_FILE=$(ls dist/sift_stack_py*.whl)
# Function to test installation with specific extras
test_install() {
local extras=$1
echo "Testing installation with extras: ${extras:-none}"
python -m venv "test_venv_${extras:-base}"
if [ "${{ matrix.platform.os }}" = "windows" ]; then
source "test_venv_${extras:-base}/Scripts/activate"
else
source "test_venv_${extras:-base}/bin/activate"
fi
if [ -z "$extras" ]; then
pip install --no-index --find-links=dist/ "$WHEEL_FILE"
else
pip install --no-index --find-links=dist/ "$WHEEL_FILE[$extras]"
fi
deactivate
rm -rf "test_venv_${extras:-base}"
}
# Test each combination
test_install "" # base installation
test_install "openssl"
test_install "build"
test_install "development"
test_install "openssl,build"
test_install "openssl,development"
test_install "build,development"
test_install "openssl,build,development"
pip install wheel
python scripts/build_utils.py \
--dist-dir dist \
--package-name sift-stack-py \
--is-windows ${{ matrix.platform.os == 'windows' }}
- name: Create distribution archive
working-directory: python
Expand Down
45 changes: 20 additions & 25 deletions .github/workflows/python_release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,22 @@ jobs:
name: Upload release to PyPI
needs: [python-ci, build-offline-archives]
runs-on: ubuntu-latest
defaults:
run:
working-directory: python
environment:
name: pypi
url: https://pypi.org/p/sift_py
permissions:
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
- name: Download distributions
uses: actions/download-artifact@v4
with:
python-version: "3.8"

- name: Pip install
run: |
python -m pip install --upgrade pip
pip install '.[build]'
pip install .
- name: Build distributions
working-directory: python
run: |
python -m build
name: sift-stack-py-dist
path: python/dist/

- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: ./python/dist/
packages-dir: python/dist/

create-github-release:
name: Create GitHub Release
Expand All @@ -60,6 +44,12 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Download distributions
uses: actions/download-artifact@v4
with:
name: sift-stack-py-dist
path: python/dist/

- name: Download all platform archives
uses: actions/download-artifact@v4
with:
Expand All @@ -86,10 +76,15 @@ jobs:
--title "sift-stack-py $TAG_NAME" \
--notes-file release_notes.md
# Upload each platform archive
# Upload Python package distributions
for dist in python/dist/*; do
echo "Uploading distribution: $dist"
gh release upload "$TAG_NAME" "$dist" --clobber
done
# Upload platform archives
for archive in platform_archives/*/sift-py-dist-*-py3-*.zip; do
echo "Uploading $archive to release $TAG_NAME"
gh release upload "$TAG_NAME" "$archive" \
--clobber
echo "Uploading archive: $archive"
gh release upload "$TAG_NAME" "$archive" --clobber
done
123 changes: 123 additions & 0 deletions python/scripts/build_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env python3

import argparse
import os
import subprocess
import venv
from itertools import combinations
from pathlib import Path
from typing import List, Optional
from wheel.pkginfo import read_pkg_info_bytes
from zipfile import ZipFile


def get_extras_from_wheel(wheel_path: str) -> List[str]:
"""Extract the list of extras from a wheel's metadata.
Args:
wheel_path: Path to the wheel file to inspect.
Returns:
List of extra names declared in the wheel metadata.
"""
with ZipFile(wheel_path) as wheel:
for name in wheel.namelist():
if name.endswith('.dist-info/METADATA'):
metadata = read_pkg_info_bytes(wheel.read(name))
return [
line.split(':')[1].strip()
for line in metadata.get_all('Provides-Extra') or []
]
return []


def get_extra_combinations(extras: List[str]) -> List[str]:
"""Generate all possible combinations of extras.
Args:
extras: List of extra names to generate combinations from.
Returns:
List of comma-separated strings representing each combination of extras.
"""
all_combinations = []
for r in range(len(extras) + 1):
all_combinations.extend(','.join(c) for c in combinations(extras, r))
return all_combinations


def test_install(
package_name: str,
extras: Optional[str],
dist_dir: str,
venv_dir: str,
is_windows: bool
) -> None:
"""Test package installation with given extras in a fresh venv.
Args:
package_name: Name of the package to install.
extras: Optional comma-separated string of extras to install.
dist_dir: Directory containing wheel and dependencies.
venv_dir: Directory to create virtual environment in.
is_windows: Whether running on Windows platform.
"""
print(f"Testing installation with extras: {extras or 'none'}")

# Create and activate venv
venv.create(venv_dir, with_pip=True)

# Build activation command based on OS
if is_windows:
activate_script = os.path.join(venv_dir, "Scripts", "activate")
else:
activate_script = os.path.join(venv_dir, "bin", "activate")

# Build installation command
if extras:
install_cmd = f'pip install --no-index --find-links="{dist_dir}" "{package_name}[{extras}]"'
else:
install_cmd = f'pip install --no-index --find-links="{dist_dir}" {package_name}'

# Run installation in the venv
full_cmd = f'source "{activate_script}" && {install_cmd} && deactivate'
subprocess.run(full_cmd, shell=True, check=True, executable='/bin/bash')


def main():
parser = argparse.ArgumentParser(description='Test package installation with all extra combinations')
parser.add_argument('--dist-dir', required=True, help='Directory containing wheel and dependencies')
parser.add_argument('--package-name', required=True, help='Name of the package to install')
parser.add_argument('--is-windows', action='store_true', help='Whether running on Windows')
args = parser.parse_args()

dist_dir = Path(args.dist_dir)
wheel_file = next(dist_dir.glob(f"{args.package_name.replace('-', '_')}*.whl"))

# Get all extras from the wheel
extras = get_extras_from_wheel(str(wheel_file))
combinations = get_extra_combinations(extras)

# Test base installation first
test_install(
package_name=args.package_name,
extras=None,
dist_dir=str(dist_dir),
venv_dir='test_venv_base',
is_windows=args.is_windows
)

# Test each combination of extras
for combo in combinations:
if combo: # Skip empty string from base combination
test_install(
package_name=args.package_name,
extras=combo,
dist_dir=str(dist_dir),
venv_dir=f'test_venv_{combo.replace(",", "_")}',
is_windows=args.is_windows
)


if __name__ == '__main__':
main()

0 comments on commit cdc447b

Please sign in to comment.