Skip to content

towards pipx release, 2 #9

towards pipx release, 2

towards pipx release, 2 #9

Workflow file for this run

name: Release
on:
push:
tags: ['v*']
branches: [release]
workflow_dispatch:
inputs:
tag:
description: ''
required: false
jobs:
build:
name: Build ${{ matrix.artifact }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# ubuntu-22.04 pins glibc 2.35 as the portability floor. Newer runners
# raise glibc and break older distros.
- os: ubuntu-22.04
artifact: linux-x86_64
# - os: macos-13
# artifact: macos-x86_64
- os: macos-14
artifact: macos-arm64
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Resolve version
id: version
shell: bash
run: |
tag="${{ github.event.inputs.tag }}"
[ -n "$tag" ] || tag="${GITHUB_REF_NAME}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update -q
sudo apt-get install -y \
cmake build-essential \
libeigen3-dev libboost-dev libboost-program-options-dev \
libgmp-dev libmpfr-dev \
autoconf automake libtool \
texlive-latex-base texlive-latex-recommended \
lp-solve liblpsolve55-dev \
patchelf \
pkg-config
- name: Install system dependencies (macOS)
if: runner.os == 'macOS'
run: |
brew install \
cmake \
eigen boost \
gmp mpfr \
autoconf automake libtool \
basictex \
pkg-config
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Compute dependency cache key
id: deps-cache-key
shell: bash
run: |
hash256() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$@"
else
shasum -a 256 "$@"
fi
}
submodule_hash=$(git submodule status --cached --recursive | hash256 | awk '{print $1}')
recipe_hash=$(hash256 configure.sh .gitmodules | hash256 | awk '{print $1}')
toolchain_hash=$(printf '%s\n%s\n%s\n' \
"$(uname -m)" \
"$(cmake --version | head -n1)" \
"$(${CC:-cc} --version | head -n1 2>/dev/null || true)" \
| hash256 | awk '{print $1}')
echo "key=${RUNNER_OS}-deps-v1-${submodule_hash}-${recipe_hash}-${toolchain_hash}" >> "$GITHUB_OUTPUT"
- name: Restore dependency cache
id: deps-cache
uses: actions/cache/restore@v4
with:
key: ${{ steps.deps-cache-key.outputs.key }}
path: |
bin/deps/cddlib/install
bin/deps/VolEsti/external/_deps
bin/deps/VolEsti/examples/volume/build
bin/deps/allsat-circuits/build
bin/deps/pact/build
bin/deps/upstream-lrslib
bin/cvc5
bin/hall_tool
bin/lrs
bin/sample
bin/volume
- name: Build native dependencies
run: bash configure.sh
- name: Save dependency cache
if: steps.deps-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
key: ${{ steps.deps-cache-key.outputs.key }}
path: |
bin/deps/cddlib/install
bin/deps/VolEsti/external/_deps
bin/deps/VolEsti/examples/volume/build
bin/deps/allsat-circuits/build
bin/deps/pact/build
bin/deps/upstream-lrslib
bin/cvc5
bin/hall_tool
bin/lrs
bin/sample
bin/volume
- name: Report native binary linkage
shell: bash
run: |
binaries=(
bin/cvc5
bin/hall_tool
bin/lrs
bin/sample
bin/volume
)
report_linux_linkage() {
local bin="$1"
local output
output=$(ldd "$bin" 2>&1 || true)
printf '%s\n' "$output"
case "$output" in
*"not a dynamic executable"*|*"statically linked"*)
echo "Summary: statically linked"
;;
*)
echo "Summary: dynamically linked or mixed"
;;
esac
}
report_macos_linkage() {
local bin="$1"
local output
output=$(otool -L "$bin" 2>&1 || true)
printf '%s\n' "$output"
if printf '%s\n' "$output" | tail -n +2 | grep -Eq '^[[:space:]]+(/|@)'; then
echo "Summary: dynamically linked"
else
echo "Summary: unable to confirm dynamic linkage"
fi
}
for bin in "${binaries[@]}"; do
echo "::group::${bin}"
if [ ! -e "$bin" ]; then
echo "Summary: binary not present"
echo "::endgroup::"
continue
fi
file_output=$(file "$bin" 2>&1 || true)
printf '%s\n' "$file_output"
case "$file_output" in
*"script"*|*"text executable"*)
echo "Summary: not a native binary (script)"
echo "::endgroup::"
continue
;;
esac
if command -v ldd >/dev/null 2>&1; then
report_linux_linkage "$bin"
elif command -v otool >/dev/null 2>&1; then
report_macos_linkage "$bin"
else
echo "Summary: no supported linkage inspection tool available"
fi
echo "::endgroup::"
done
- name: Install wheel build/repair tooling
shell: bash
run: |
python -m pip install --upgrade pip wheel
if [ "$(uname)" = "Darwin" ]; then
python -m pip install delocate
else
python -m pip install auditwheel
fi
- name: Build pycddlib wheel against bundled cddlib
shell: bash
run: |
CDDLIB_PREFIX="$PWD/bin/deps/cddlib/install"
GMP_PREFIX=$(brew --prefix gmp 2>/dev/null || echo "")
GMP_CFLAGS=${GMP_PREFIX:+-I$GMP_PREFIX/include}
GMP_LDFLAGS=${GMP_PREFIX:+-L$GMP_PREFIX/lib}
ARCHFLAGS=""
WHEEL_ENV=(
"CFLAGS=-I$CDDLIB_PREFIX/include -I$CDDLIB_PREFIX/include/cddlib $GMP_CFLAGS"
"LDFLAGS=-L$CDDLIB_PREFIX/lib $GMP_LDFLAGS"
"ARCHFLAGS=$ARCHFLAGS"
)
PYTHON_HOST_PLATFORM=""
MACOSX_DEPLOYMENT_TARGET=""
if [ "$(uname)" = "Darwin" ]; then
ARCHFLAGS="-arch $(uname -m)"
# GitHub's macOS Python 3.11 can default wheel tags to universal2
# even when we only build arm64 binaries. Force an arm64 platform
# tag so delocate does not require a missing x86_64 slice.
MACOSX_DEPLOYMENT_TARGET="11.0"
PYTHON_HOST_PLATFORM="macosx-${MACOSX_DEPLOYMENT_TARGET}-$(uname -m)"
WHEEL_ENV[2]="ARCHFLAGS=$ARCHFLAGS"
WHEEL_ENV+=(
"MACOSX_DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET"
"_PYTHON_HOST_PLATFORM=$PYTHON_HOST_PLATFORM"
)
fi
mkdir -p dist-raw
env "${WHEEL_ENV[@]}" \
pip wheel --no-binary pycddlib --no-deps -w dist-raw pycddlib
if [ "$(uname)" = "Darwin" ]; then
raw_wheel=$(echo dist-raw/pycddlib-*.whl)
case "$raw_wheel" in
*arm64.whl) ;;
*)
echo "ERROR: expected an arm64 pycddlib wheel, got: $raw_wheel"
exit 1
;;
esac
fi
- name: Repair pycddlib wheel (vendor libcddgmp + libgmp)
shell: bash
run: |
mkdir -p wheels
CDDLIB_PREFIX="$PWD/bin/deps/cddlib/install"
if [ "$(uname)" = "Darwin" ]; then
DYLD_LIBRARY_PATH="$CDDLIB_PREFIX/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" \
delocate-wheel -v -w wheels dist-raw/pycddlib-*.whl
else
# Target manylinux_2_35 to match the ubuntu-22.04 runner's glibc.
# Raise this if you bump the runner; lower it only if you build
# inside an older manylinux container.
LD_LIBRARY_PATH="$CDDLIB_PREFIX/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
auditwheel repair --plat manylinux_2_35_x86_64 -w wheels dist-raw/pycddlib-*.whl
fi
- name: Build polytope wheel from source
shell: bash
run: |
mkdir -p wheels
pip wheel --no-binary polytope --no-deps -w wheels polytope
if ! compgen -G 'wheels/polytope-*.whl' > /dev/null; then
echo "ERROR: expected a built polytope wheel"
exit 1
fi
- name: Collect other dependency wheels
shell: bash
run: |
# The archive requirements should still list every runtime package,
# including pycddlib and polytope, because those wheels are bundled
# locally and installed offline by install.sh.
cp requirements.txt requirements-release.txt
if [ "$(uname)" = "Darwin" ]; then
# z3-solver 4.16.0.0 only publishes macOS ARM64 wheels tagged for
# macOS 15.0+, which a macOS 14 builder will not vendor into an
# offline archive. Pin the macOS release artifact to the newest Z3
# wheel that still targets macOS 11.0+ ARM64.
awk '
/^z3-solver([[:space:]]|$)/ { print "z3-solver==4.13.1.0"; next }
{ print }
' requirements-release.txt > requirements-release.tmp
mv requirements-release.tmp requirements-release.txt
fi
# Derive the download list AFTER the pin so pip fetches the exact
# versions requirements.txt inside the archive will ask for.
grep -Ev '^(pycddlib|polytope)$' requirements-release.txt > requirements-download.txt
pip download --only-binary=:all: -d wheels -r requirements-download.txt
pip download --only-binary=:all: -d wheels scipy networkx
if [ "$(uname)" = "Darwin" ] && ! compgen -G 'wheels/z3_solver-*.whl' > /dev/null; then
echo "ERROR: expected a bundled z3-solver wheel for the macOS archive"
exit 1
fi
for pkg in pycddlib polytope; do
if ! grep -Fxq "$pkg" requirements-release.txt; then
echo "ERROR: release requirements are missing $pkg"
exit 1
fi
done
- name: Stage release payload
shell: bash
run: |
TAG="${{ steps.version.outputs.tag }}"
STAGE="ttc-${TAG#v}-${{ matrix.artifact }}"
mkdir -p "$STAGE/bin" "$STAGE/wheels"
install -m 755 ttc.py "$STAGE/ttc"
cp ttc.py _bootstrap.py README.md LICENSE "$STAGE/"
cp requirements-release.txt "$STAGE/requirements.txt"
cp -R src example "$STAGE/"
for b in cvc5 hall_tool lrs sample volume; do
cp "bin/$b" "$STAGE/bin/"
done
cp wheels/*.whl "$STAGE/wheels/"
install -m 755 utils/install.sh "$STAGE/install.sh"
tar czf "${STAGE}.tar.gz" "$STAGE"
echo "ARCHIVE=${STAGE}.tar.gz" >> "$GITHUB_ENV"
echo "STAGE=${STAGE}" >> "$GITHUB_ENV"
- name: Smoke-test the archive in a clean directory
shell: bash
run: |
WORK="$(mktemp -d)"
tar xzf "$ARCHIVE" -C "$WORK"
cd "$WORK/$STAGE"
./install.sh
set +e
output=$(python ttc.py example/box_or_lra.smt2 2>&1)
status=$?
set -e
printf '%s\n' "$output"
[ "$status" -eq 0 ] || { echo "ERROR: ttc exited $status"; exit "$status"; }
echo "$output" | grep -E '^s vol [0-9]+(\.[0-9]+)?' \
|| { echo "ERROR: expected 's vol <float>' line not found"; exit 1; }
- name: End-to-end pipx test against local archive
if: startsWith(github.ref, 'refs/tags/')
shell: bash
run: |
# Serve the just-built tarball so _bootstrap.py can fetch it locally,
# simulating the published-release download path without needing the
# tag to be published yet.
python -m http.server 8765 --directory "$PWD" >/tmp/http.log 2>&1 &
HTTP_PID=$!
trap "kill $HTTP_PID 2>/dev/null || true" EXIT
for _ in 1 2 3 4 5; do
if curl -fsS "http://localhost:8765/${ARCHIVE}" -o /dev/null; then
break
fi
sleep 1
done
python -m pip install --quiet --upgrade pipx
python -m pipx install --python "$(command -v python)" .
PIPX_BIN_DIR=$(python -m pipx environment --value PIPX_BIN_DIR)
export PATH="$PIPX_BIN_DIR:$PATH"
export TTC_ARCHIVE_URL="http://localhost:8765/${ARCHIVE}"
set +e
output=$(ttc example/box_or_lra.smt2 2>&1)
status=$?
set -e
printf '%s\n' "$output"
[ "$status" -eq 0 ] || { echo "ERROR: ttc exited $status"; exit "$status"; }
echo "$output" | grep -E '^s vol [0-9]+(\.[0-9]+)?' \
|| { echo "ERROR: expected 's vol <float>' line not found"; exit 1; }
# Second invocation should hit the cache — unset the URL to prove
# no network is touched on re-run.
unset TTC_ARCHIVE_URL
ttc example/box_or_lra.smt2 >/dev/null
python -m pipx uninstall ttc
- name: Upload archive as workflow artifact
uses: actions/upload-artifact@v4
with:
name: ttc-${{ matrix.artifact }}
path: ${{ env.ARCHIVE }}
retention-days: 14
release:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
files: artifacts/*.tar.gz
draft: false
generate_release_notes: true