Skip to content

Commit cc68036

Browse files
committed
build(python): publish manylinux_2_28 wheels for broader glibc compatibility
Build released Linux wheels inside a PyPA manylinux_2_28 container so the resulting binary runs on any Linux distribution with glibc >= 2.28 (RHEL 8, Debian 10+, Ubuntu 18.04+). The previous native build on the noble (glibc 2.39) CI image produced manylinux_2_39 wheels that uv refused to install on common LTS hosts such as Ubuntu 22.04. - Add deploy/docker/Dockerfile.python-wheels-linux based on quay.io/pypa/manylinux_2_28_{x86_64,aarch64}. - Add build:python:wheel:linux:docker mise task and per-arch aliases. Re-point python:build:linux:{amd64,arm64} (used by release workflows) to the Docker-based path. The legacy native build:python:wheel:linux task remains for fast local iteration. - Mount the host docker socket and set up buildx in the build-python-wheels-linux job in release-tag.yml and release-dev.yml, mirroring the existing macOS wheel job. - Update architecture/build-containers.md to describe the new flow. Signed-off-by: phamgialinhlx <phamgialinhlx2@gmail.com>
1 parent 78f0b6f commit cc68036

6 files changed

Lines changed: 212 additions & 11 deletions

File tree

.github/workflows/release-dev.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ jobs:
120120
username: ${{ github.actor }}
121121
password: ${{ secrets.GITHUB_TOKEN }}
122122
options: --privileged
123+
volumes:
124+
- /var/run/docker.sock:/var/run/docker.sock
123125
env:
124126
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
125127
SCCACHE_MEMCACHED_ENDPOINT: ${{ vars.SCCACHE_MEMCACHED_ENDPOINT }}
@@ -129,6 +131,9 @@ jobs:
129131
with:
130132
fetch-depth: 0
131133

134+
- name: Set up Docker Buildx
135+
uses: ./.github/actions/setup-buildx
136+
132137
- name: Mark workspace safe for git
133138
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
134139

.github/workflows/release-tag.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ jobs:
140140
username: ${{ github.actor }}
141141
password: ${{ secrets.GITHUB_TOKEN }}
142142
options: --privileged
143+
volumes:
144+
- /var/run/docker.sock:/var/run/docker.sock
143145
env:
144146
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
145147
SCCACHE_MEMCACHED_ENDPOINT: ${{ vars.SCCACHE_MEMCACHED_ENDPOINT }}
@@ -150,6 +152,9 @@ jobs:
150152
ref: ${{ inputs.tag || github.ref }}
151153
fetch-depth: 0
152154

155+
- name: Set up Docker Buildx
156+
uses: ./.github/actions/setup-buildx
157+
153158
- name: Mark workspace safe for git
154159
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
155160

architecture/build-containers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ Both the standalone artifact and the deployed container image use the `openshell
4343

4444
OpenShell also publishes Python wheels for `linux/amd64`, `linux/arm64`, and macOS ARM64.
4545

46-
- Linux wheels are built natively on matching Linux runners via `build:python:wheel:linux:amd64` and `build:python:wheel:linux:arm64` in `tasks/python.toml`.
47-
- There is no local Linux multiarch wheel build task. Release workflows own the per-arch Linux wheel production.
46+
- Released Linux wheels are built per-arch inside `deploy/docker/Dockerfile.python-wheels-linux`, which uses the PyPA `manylinux_2_28_{x86_64,aarch64}` images as a base. The resulting wheels are tagged `manylinux_2_28` and install on any Linux distribution shipping glibc >= 2.28 (RHEL 8, Debian 10+, Ubuntu 18.04+). The matrix job invokes `mise run python:build:linux:amd64` / `python:build:linux:arm64`, which fan out to `build:python:wheel:linux:{amd64,arm64}:docker`.
47+
- For fast local iteration, `build:python:wheel:linux:amd64` / `build:python:wheel:linux:arm64` still produce a non-portable wheel tagged for the host's glibc. These tasks are no longer used by release workflows.
4848
- The macOS ARM64 wheel is cross-compiled with `deploy/docker/Dockerfile.python-wheels-macos` via `build:python:wheel:macos`.
4949
- Release workflows mirror the CLI layout: a Linux matrix job for amd64/arm64, a separate macOS job, and release jobs that download the per-platform wheel artifacts directly before publishing.
5050

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# syntax=docker/dockerfile:1.6
2+
3+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
# Build the Linux Python wheel inside a PyPA manylinux_2_28 container so the
7+
# resulting binary is compatible with any Linux distribution shipping glibc
8+
# >= 2.28 (RHEL 8, Ubuntu 18.04+, Debian 10+). The host CI runner is on noble
9+
# (glibc 2.39), which previously produced manylinux_2_39 wheels that uv refuses
10+
# to install on Ubuntu 22.04 / Debian 11 (glibc 2.31 / 2.35).
11+
12+
ARG TARGETARCH
13+
ARG MANYLINUX_AMD64_IMAGE=quay.io/pypa/manylinux_2_28_x86_64:latest
14+
ARG MANYLINUX_ARM64_IMAGE=quay.io/pypa/manylinux_2_28_aarch64:latest
15+
ARG PYTHON_VERSION=cp312-cp312
16+
ARG RUST_VERSION=1.95.0
17+
18+
# Selector stages — Docker resolves only the matching one based on TARGETARCH.
19+
FROM ${MANYLINUX_AMD64_IMAGE} AS base-amd64
20+
FROM ${MANYLINUX_ARM64_IMAGE} AS base-arm64
21+
FROM base-${TARGETARCH} AS builder
22+
23+
ARG TARGETARCH
24+
ARG PYTHON_VERSION
25+
ARG RUST_VERSION
26+
ARG CARGO_TARGET_CACHE_SCOPE=default
27+
28+
ENV PATH="/opt/python/${PYTHON_VERSION}/bin:/root/.cargo/bin:${PATH}"
29+
30+
# manylinux_2_28 ships gcc-toolset-14, cmake, and patchelf. We add clang for
31+
# bindgen-driven crates (libclang-dev equivalent) and openssl-devel for any
32+
# Rust crates that link against the system libssl during dependency resolution.
33+
RUN dnf install -y --setopt=install_weak_deps=False \
34+
clang \
35+
llvm-devel \
36+
openssl-devel \
37+
perl-core \
38+
perl-IPC-Cmd \
39+
&& dnf clean all
40+
41+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
42+
| sh -s -- -y --default-toolchain ${RUST_VERSION} --profile minimal
43+
RUN python -m pip install --no-cache-dir maturin
44+
45+
WORKDIR /build
46+
47+
# Copy dependency manifests first for better layer caching.
48+
COPY Cargo.toml Cargo.lock ./
49+
COPY crates/openshell-cli/Cargo.toml crates/openshell-cli/Cargo.toml
50+
COPY crates/openshell-core/Cargo.toml crates/openshell-core/Cargo.toml
51+
COPY crates/openshell-ocsf/Cargo.toml crates/openshell-ocsf/Cargo.toml
52+
COPY crates/openshell-providers/Cargo.toml crates/openshell-providers/Cargo.toml
53+
COPY crates/openshell-router/Cargo.toml crates/openshell-router/Cargo.toml
54+
COPY crates/openshell-sandbox/Cargo.toml crates/openshell-sandbox/Cargo.toml
55+
COPY crates/openshell-server/Cargo.toml crates/openshell-server/Cargo.toml
56+
COPY crates/openshell-bootstrap/Cargo.toml crates/openshell-bootstrap/Cargo.toml
57+
COPY crates/openshell-policy/Cargo.toml crates/openshell-policy/Cargo.toml
58+
COPY crates/openshell-prover/Cargo.toml crates/openshell-prover/Cargo.toml
59+
COPY crates/openshell-tui/Cargo.toml crates/openshell-tui/Cargo.toml
60+
COPY crates/openshell-core/build.rs crates/openshell-core/build.rs
61+
COPY proto/ proto/
62+
63+
# Create dummy source files to build dependencies.
64+
RUN mkdir -p crates/openshell-cli/src crates/openshell-core/src crates/openshell-ocsf/src \
65+
crates/openshell-policy/src crates/openshell-providers/src crates/openshell-prover/src \
66+
crates/openshell-router/src crates/openshell-sandbox/src crates/openshell-server/src \
67+
crates/openshell-bootstrap/src crates/openshell-tui/src && \
68+
echo "fn main() {}" > crates/openshell-cli/src/main.rs && \
69+
echo "fn main() {}" > crates/openshell-sandbox/src/main.rs && \
70+
echo "fn main() {}" > crates/openshell-server/src/main.rs && \
71+
touch crates/openshell-core/src/lib.rs && \
72+
touch crates/openshell-ocsf/src/lib.rs && \
73+
touch crates/openshell-providers/src/lib.rs && \
74+
touch crates/openshell-router/src/lib.rs && \
75+
touch crates/openshell-bootstrap/src/lib.rs && \
76+
touch crates/openshell-policy/src/lib.rs && \
77+
touch crates/openshell-prover/src/lib.rs && \
78+
touch crates/openshell-tui/src/lib.rs
79+
80+
# Warm the dependency build (cached unless Cargo.toml/lock changes).
81+
RUN --mount=type=cache,id=cargo-registry-python-wheels-linux-${TARGETARCH},sharing=locked,target=/root/.cargo/registry \
82+
--mount=type=cache,id=cargo-git-python-wheels-linux-${TARGETARCH},sharing=locked,target=/root/.cargo/git \
83+
--mount=type=cache,id=cargo-target-python-wheels-linux-${TARGETARCH}-${CARGO_TARGET_CACHE_SCOPE},sharing=locked,target=/build/target \
84+
cargo build --release -p openshell-cli --features bundled-z3 2>/dev/null || true
85+
86+
# Copy actual source code and Python packaging files.
87+
COPY crates/ crates/
88+
COPY pyproject.toml README.md ./
89+
COPY python/ python/
90+
91+
# Touch source files so cargo rebuilds them (not the cached dummy).
92+
RUN touch crates/openshell-cli/src/main.rs \
93+
crates/openshell-cli/src/lib.rs \
94+
crates/openshell-bootstrap/src/lib.rs \
95+
crates/openshell-core/src/lib.rs \
96+
crates/openshell-providers/src/lib.rs \
97+
crates/openshell-router/src/lib.rs \
98+
crates/openshell-sandbox/src/main.rs \
99+
crates/openshell-server/src/main.rs \
100+
crates/openshell-core/build.rs \
101+
proto/*.proto
102+
103+
# Declare version ARGs here (not earlier) so the git-hash-bearing values do not
104+
# invalidate the expensive dependency-build layers above on every commit.
105+
ARG OPENSHELL_CARGO_VERSION
106+
ARG OPENSHELL_IMAGE_TAG
107+
RUN --mount=type=cache,id=cargo-registry-python-wheels-linux-${TARGETARCH},sharing=locked,target=/root/.cargo/registry \
108+
--mount=type=cache,id=cargo-git-python-wheels-linux-${TARGETARCH},sharing=locked,target=/root/.cargo/git \
109+
--mount=type=cache,id=cargo-target-python-wheels-linux-${TARGETARCH}-${CARGO_TARGET_CACHE_SCOPE},sharing=locked,target=/build/target \
110+
if [ -n "${OPENSHELL_CARGO_VERSION:-}" ]; then \
111+
sed -i -E '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*".*"/version = "'"${OPENSHELL_CARGO_VERSION}"'"/}' Cargo.toml; \
112+
fi && \
113+
maturin build --release --features bundled-z3 \
114+
--compatibility manylinux_2_28 \
115+
--out /wheels && \
116+
ls -la /wheels/*.whl
117+
118+
FROM scratch AS wheels
119+
COPY --from=builder /wheels/*.whl /

mise.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tasks/python.toml

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,23 +83,93 @@ ls -la "$WHEEL_OUTPUT_DIR"/*.whl
8383
hide = true
8484

8585
["build:python:wheel:linux:amd64"]
86-
description = "Build Python wheel for Linux amd64 natively"
86+
description = "Build Python wheel for Linux amd64 natively (host glibc; non-portable)"
8787
depends = ["EXPECTED_HOST_ARCH=amd64 WHEEL_OUTPUT_DIR=target/wheels/linux-amd64 build:python:wheel:linux"]
8888
hide = true
8989

90-
["python:build:linux:amd64"]
91-
description = "Alias for build:python:wheel:linux:amd64"
92-
depends = ["build:python:wheel:linux:amd64"]
93-
hide = true
94-
9590
["build:python:wheel:linux:arm64"]
96-
description = "Build Python wheel for Linux arm64 natively"
91+
description = "Build Python wheel for Linux arm64 natively (host glibc; non-portable)"
9792
depends = ["EXPECTED_HOST_ARCH=arm64 WHEEL_OUTPUT_DIR=target/wheels/linux-arm64 build:python:wheel:linux"]
9893
hide = true
9994

95+
["build:python:wheel:linux:docker"]
96+
description = "Build a portable manylinux_2_28 Python wheel via Docker (glibc >= 2.28)"
97+
depends = ["python:proto"]
98+
run = """
99+
#!/usr/bin/env bash
100+
set -euo pipefail
101+
102+
source tasks/scripts/container-engine.sh
103+
104+
WHEEL_OUTPUT_DIR=${WHEEL_OUTPUT_DIR:?Set WHEEL_OUTPUT_DIR to a per-platform wheel output directory}
105+
TARGETARCH=${TARGETARCH:?Set TARGETARCH to amd64 or arm64}
106+
107+
sha256_16() {
108+
if command -v sha256sum >/dev/null 2>&1; then
109+
sha256sum "$1" | awk '{print substr($1, 1, 16)}'
110+
else
111+
shasum -a 256 "$1" | awk '{print substr($1, 1, 16)}'
112+
fi
113+
}
114+
115+
sha256_16_stdin() {
116+
if command -v sha256sum >/dev/null 2>&1; then
117+
sha256sum | awk '{print substr($1, 1, 16)}'
118+
else
119+
shasum -a 256 | awk '{print substr($1, 1, 16)}'
120+
fi
121+
}
122+
123+
CARGO_VERSION=${OPENSHELL_CARGO_VERSION:-}
124+
if [ -z "$CARGO_VERSION" ] && [ -n "${CI:-}" ]; then
125+
CARGO_VERSION=$(uv run python tasks/scripts/release.py get-version --cargo)
126+
fi
127+
128+
LOCK_HASH=$(sha256_16 Cargo.lock)
129+
RUST_SCOPE=${RUST_TOOLCHAIN_SCOPE:-rustup-1.95.0}
130+
CACHE_SCOPE_INPUT="v1|python-wheels-linux-${TARGETARCH}|manylinux_2_28|${LOCK_HASH}|${RUST_SCOPE}"
131+
CARGO_TARGET_CACHE_SCOPE=$(printf '%s' "$CACHE_SCOPE_INPUT" | sha256_16_stdin)
132+
133+
rm -rf "$WHEEL_OUTPUT_DIR"
134+
mkdir -p "$WHEEL_OUTPUT_DIR"
135+
136+
ce build \
137+
-f deploy/docker/Dockerfile.python-wheels-linux \
138+
--target wheels \
139+
--build-arg "TARGETARCH=${TARGETARCH}" \
140+
--build-arg "CARGO_TARGET_CACHE_SCOPE=${CARGO_TARGET_CACHE_SCOPE}" \
141+
${CARGO_VERSION:+--build-arg "OPENSHELL_CARGO_VERSION=${CARGO_VERSION}"} \
142+
${OPENSHELL_IMAGE_TAG:+--build-arg "OPENSHELL_IMAGE_TAG=${OPENSHELL_IMAGE_TAG}"} \
143+
--output "type=local,dest=${WHEEL_OUTPUT_DIR}" \
144+
.
145+
146+
ls -la "$WHEEL_OUTPUT_DIR"/*.whl
147+
"""
148+
hide = true
149+
150+
["build:python:wheel:linux:amd64:docker"]
151+
description = "Build portable manylinux_2_28 wheel for Linux amd64"
152+
depends = ["TARGETARCH=amd64 WHEEL_OUTPUT_DIR=target/wheels/linux-amd64 build:python:wheel:linux:docker"]
153+
hide = true
154+
155+
["build:python:wheel:linux:arm64:docker"]
156+
description = "Build portable manylinux_2_28 wheel for Linux arm64"
157+
depends = ["TARGETARCH=arm64 WHEEL_OUTPUT_DIR=target/wheels/linux-arm64 build:python:wheel:linux:docker"]
158+
hide = true
159+
160+
# Release-pipeline aliases. These produce manylinux_2_28-tagged wheels via
161+
# Docker so the published artifacts install on any glibc >= 2.28 host (RHEL 8,
162+
# Ubuntu 18.04+, Debian 10+). The native `build:python:wheel:linux:amd64` /
163+
# `build:python:wheel:linux:arm64` tasks remain available for fast local
164+
# iteration and produce wheels tagged for the host glibc only.
165+
["python:build:linux:amd64"]
166+
description = "Build portable manylinux_2_28 wheel for Linux amd64 (release path)"
167+
depends = ["build:python:wheel:linux:amd64:docker"]
168+
hide = true
169+
100170
["python:build:linux:arm64"]
101-
description = "Alias for build:python:wheel:linux:arm64"
102-
depends = ["build:python:wheel:linux:arm64"]
171+
description = "Build portable manylinux_2_28 wheel for Linux arm64 (release path)"
172+
depends = ["build:python:wheel:linux:arm64:docker"]
103173
hide = true
104174

105175
["build:python:wheel:macos"]

0 commit comments

Comments
 (0)