Skip to content

Make Orchestrator::evaluate take `const Expression * instead of Exp… #255

Make Orchestrator::evaluate take `const Expression * instead of Exp…

Make Orchestrator::evaluate take `const Expression * instead of Exp… #255

Workflow file for this run

# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: wheels
on:
push:
branches: [main]
tags: ['v*']
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'pyproject.toml'
- 'src/rebalancer/**'
- 'CMakeLists.txt'
- 'version.txt'
- '.github/workflows/wheels.yml'
- 'tools/wheels/**'
- 'build/fbcode_builder/**'
workflow_dispatch:
permissions:
contents: read
actions: read
concurrency:
group: wheels-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
# Fan out across (os, python ABI) so each cell is its own GHA job and
# the matrix runs in parallel. cibuildwheel inside each cell builds
# exactly one ABI -- without this split, cibuildwheel iterates the
# ABIs sequentially in a single job and the wall-clock cost is N x
# one-wheel-build.
name: wheels-${{ matrix.os }}-${{ matrix.py }}
runs-on: ${{ matrix.runs_on }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false
matrix:
os: [linux, macos]
py: [cp312, cp313, cp314]
include:
- os: linux
runs_on: 16-core-ubuntu
cibw_archs: x86_64
cibw_image: quay.io/pypa/manylinux_2_28_x86_64
timeout_minutes: 60
- os: macos
runs_on: macos-14-xlarge
cibw_archs: arm64
timeout_minutes: 60
steps:
- uses: actions/checkout@v6
with:
# cibuildwheel reads version.txt indirectly via scikit-build-core; no
# tags are needed at wheel-build time.
fetch-depth: 1
- uses: pypa/cibuildwheel@v3.1
env:
# One Python ABI per matrix cell.
CIBW_BUILD: "${{ matrix.py }}-*"
CIBW_SKIP: "*-musllinux* *_i686 pp*"
CIBW_ARCHS: ${{ matrix.cibw_archs }}
# Linux: build inside the same manylinux container the main C++ build
# uses, so the wheel-bundled .so is binary-identical to the .deb/.rpm
# version.
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.cibw_image }}
CIBW_CONTAINER_ENGINE: docker
# CLANG19_PREINSTALLED tells the clang19 getdeps manifest a compatible
# clang is already on PATH (before_all_linux.sh dnf-installs clang 20,
# which is forward-compatible with the project's "clang19+" need).
# CC/CXX point scikit-build-core's wheel-build cmake at clang
# (cibuildwheel default is the gcc-toolset gcc).
CIBW_ENVIRONMENT_LINUX: >-
CLANG19_PREINSTALLED=1
CC=clang
CXX=clang++
# Repair: bundle getdeps-installed shared libs (libgflags.so, etc.)
# into the wheel via auditwheel.
CIBW_REPAIR_WHEEL_COMMAND_LINUX: bash tools/wheels/repair_linux.sh {dest_dir} {wheel}
# Run getdeps to produce the dep tree, then write .cmake_prefix_path
# for the wheel-build cmake invocation to read via CMakeLists.txt.
CIBW_BEFORE_ALL_LINUX: bash tools/wheels/before_all_linux.sh
# macOS environment:
# * MACOSX_DEPLOYMENT_TARGET=14.0 -- Homebrew on macos-14-xlarge
# ships bottles compiled with target 14.0 (Sonoma). delocate
# refuses to bundle those into a wheel announcing an older target,
# so we pin to the highest target any bundled dylib advertises.
# Decision (2026-05-08, autonomous):
# Rejected -- bumping target down to 11.0 by source-building
# every dep via getdeps. Would require pulling boost/openssl/
# icu/etc. into the getdeps tree instead of relying on Homebrew
# bottles. Substantial extra compile time and a different
# fragility surface. Not worth it pre-1.0.
# Rejected -- shipping wheels per OS minor (11/12/13/14). No
# other Python project does this for marginal coverage gains.
# Cost: wheel only installs on macOS 14+ (Sonoma, Sept 2023+).
# Older users can build from sdist or wait until source-built
# deps land.
# * CC/CXX -> Homebrew LLVM clang for C++23 features (static_assert
# in templates, lambda capture of structured bindings, ranges::join)
# that AppleClang 15 lacks.
# * LIBRARY_PATH adds the keg-only icu4c@78 lib dir to the linker
# search path so bare -licudata / -licuuc / -licui18n flags pulled
# in transitively by Boost can be resolved.
CIBW_ENVIRONMENT_MACOS: >-
MACOSX_DEPLOYMENT_TARGET=14.0
CC=/opt/homebrew/opt/llvm/bin/clang
CXX=/opt/homebrew/opt/llvm/bin/clang++
LIBRARY_PATH=/opt/homebrew/opt/icu4c@78/lib
CIBW_BEFORE_ALL_MACOS: bash tools/wheels/before_all_macos.sh
# Repair: cmake fails to embed LC_RPATH in MODULE/SHARED targets for
# this cmake/scikit-build-core version on macOS arm64. repair_macos.sh
# patches the missing rpaths before handing off to delocate-wheel.
CIBW_REPAIR_WHEEL_COMMAND_MACOS: bash tools/wheels/repair_macos.sh {dest_dir} {wheel} {delocate_archs}
# Smoke-test the installed wheel. The wrapper script runs the
# one-liner test first; on failure it dumps wheel layout, ldd
# output, LD_DEBUG=files, and a gdb backtrace so we can root-
# cause crashes (linux wheels have been segfaulting at import
# with no diagnostic output).
CIBW_TEST_COMMAND: bash {project}/tools/wheels/test_wheel.sh
# Linux wheels are built with cmake.build-type = "RelWithDebInfo" (see
# pyproject.toml), which retains full DWARF debug sections in every
# bundled .so. That produces ~133 MB wheels, exceeding PyPI's 100 MB
# per-file limit. We strip debug symbols here rather than switching to a
# Release build so that the smoke tests above (CIBW_TEST_COMMAND) run
# against the full-symbol binary — preserving crash diagnostic quality
# during CI — while the artifact we upload and eventually publish to PyPI
# is lean (~13 MB). strip --strip-unneeded removes only debug sections;
# it leaves the export symbol table and all executable code untouched,
# which is safe for dynamically-linked .so files. macOS wheels are ~25 MB
# unstripped and do not need this treatment.
- if: matrix.os == 'linux'
name: Strip debug symbols from Linux wheels
run: |
python3 - <<'PYEOF'
import hashlib, os, subprocess, zipfile, tempfile, glob
for whl_path in glob.glob('wheelhouse/*.whl'):
before = os.path.getsize(whl_path)
with tempfile.TemporaryDirectory() as tmpdir:
with zipfile.ZipFile(whl_path) as zf:
zf.extractall(tmpdir)
for root, _, files in os.walk(tmpdir):
for f in files:
if '.so' not in f:
continue
fp = os.path.join(root, f)
rel = os.path.relpath(fp, tmpdir)
# Skip bundled deps (rebalancer.libs/) and the
# internal shared library (_lib/librebalancer.so).
# auditwheel's patchelf extends .dynstr in these files
# by adding a new PT_LOAD segment; strip --strip-unneeded
# then fails to re-place .dynstr in that segment and
# leaves it unmapped, causing glibc 2.39 to find only
# empty strings at DT_STRTAB. These files are already
# pre-stripped in repair_linux.sh before auditwheel runs
# (strip --strip-unneeded on the getdeps sources), so
# stripping them again here is a no-op on content but
# dangerous on ELF structure.
if 'rebalancer.libs' in rel or rel.startswith('rebalancer/_lib/'):
continue
subprocess.run(
['strip', '--strip-unneeded', fp],
check=True)
# Regenerate RECORD so pip's integrity check passes after we
# modified the .so files.
record_path = next(
os.path.join(r, f)
for r, _, fs in os.walk(tmpdir)
for f in fs if f == 'RECORD')
entries = []
for root, _, files in os.walk(tmpdir):
for f in files:
fp = os.path.join(root, f)
rel = os.path.relpath(fp, tmpdir)
if fp == record_path:
continue
digest = hashlib.sha256(open(fp, 'rb').read()).hexdigest()
entries.append(f'{rel},sha256={digest},{os.path.getsize(fp)}')
entries.append(f'{os.path.relpath(record_path, tmpdir)},,')
open(record_path, 'w').write('\n'.join(entries) + '\n')
os.remove(whl_path)
with zipfile.ZipFile(whl_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for root, _, files in os.walk(tmpdir):
for f in files:
fp = os.path.join(root, f)
zf.write(fp, os.path.relpath(fp, tmpdir))
after = os.path.getsize(whl_path)
print(f'{os.path.basename(whl_path)}: {before/1e6:.1f} MB -> {after/1e6:.1f} MB')
PYEOF
- uses: actions/upload-artifact@v6
with:
name: wheels-${{ matrix.os }}-${{ matrix.py }}
path: wheelhouse/*.whl
if-no-files-found: error
# Cross-glibc smoke test: run the Linux wheel on ubuntu-latest (glibc 2.39)
# to catch patchelf string-table corruption that only manifests on newer
# glibc. The cibuildwheel test (CIBW_TEST_COMMAND) runs inside the
# manylinux_2_28 build container (AlmaLinux 8, glibc 2.28) where the
# corruption silently passes; this job runs on a fresh Ubuntu 24.04 host
# where the dynamic linker reads DT_STRTAB strictly and fails on corrupted
# NEEDED entries such as "ev" or "3.1.5".
test-linux-glibc:
name: test-linux-glibc-${{ matrix.py }}
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
py: [cp312, cp313, cp314]
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: actions/download-artifact@v4
with:
name: wheels-linux-${{ matrix.py }}
path: wheelhouse
- name: Set up Python
uses: actions/setup-python@v5
with:
# Map cibuildwheel ABI tags to setup-python version strings.
python-version: |
${{ matrix.py == 'cp312' && '3.12' ||
matrix.py == 'cp313' && '3.13' ||
'3.14' }}
allow-prereleases: true
- name: Install wheel and run smoke test
run: |
pip install wheelhouse/rebalancer-*-manylinux*.whl
# Minimal import check first so we can distinguish import-time crashes
# (patchelf .init_array corruption) from test-execution crashes.
# On failure, capture a gdb C-level backtrace to identify the exact
# constructor function that segfaults.
if python -X faulthandler -c "
from rebalancer import ProblemSolver
ps = ProblemSolver(service_name='rebalancer', service_scope='glibc-test')
assert ps.ping() == 4, f'ping returned {ps.ping()}'
print('import + ping ok')
"; then
python -X faulthandler tools/wheels/test_bindings.py -v
else
echo "--- import failed; running gdb backtrace ---"
sudo apt-get install -y gdb 2>/dev/null || true
gdb -batch \
-ex 'set confirm off' \
-ex 'set pagination off' \
-ex 'handle SIGSEGV stop print' \
-ex 'run' \
-ex 'bt full' \
-ex 'info sharedlibrary' \
--args python3 -X faulthandler -c \
"from rebalancer import ProblemSolver; print('ok')" 2>&1 || true
exit 1
fi