Skip to content

Commit 6a435e4

Browse files
authored
fix: pass full manylinux range to pip so compiled wheels are selected [v0.5.2] (#3)
A single explicit --platform matches ~that exact tag and does not broaden across manylinux baselines. --platform manylinux_2_17 therefore dropped compiled wheels tagged only at a higher baseline: wrapt's cp313 wheel is tagged manylinux_2_28 (no 2_17), so pip fell back to py3-none-any, which broke aws-xray-sdk's runtime boto3 patching and 500'd a Lambda. The v0.4.1 lowering (2_28 -> 2_17 for pydantic-core's 2_17-only wheel) had created the opposite miss; a single floor cannot satisfy both. docker_runner.py now emits repeated --platform flags from manylinux_2_34 down to manylinux1 (x86_64) / manylinux2014 (aarch64), capped at the AL2023 runtime's glibc 2.34, word-split unquoted into the pip command. pip picks the most-specific compiled wheel each package offers, falling back to py3-none-any only when none exists. Verified end to end: a real verify_webhook build now ships wrapt/_wrappers.*.so again. Bump 0.5.1 -> 0.5.2 (re-keys all content hashes, as expected).
1 parent ae1d356 commit 6a435e4

6 files changed

Lines changed: 75 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## v0.5.2 - 2026-06-21
4+
5+
### Fixed
6+
- Pass the full compatible manylinux range to `pip install` instead of a single `--platform`, so compiled wheels are selected for every package. A single explicit `--platform` matches (close to) that exact tag - it does not broaden across baselines - so `manylinux_2_17` silently dropped compiled wheels tagged only at a higher baseline. Concretely, `wrapt`'s cp313 wheel is tagged `manylinux_2_28` (no `2_17`), so pip fell back to the pure-Python `py3-none-any` build; that broke `aws-xray-sdk`'s runtime boto3 patching and 500'd a Lambda whose package included it. The v0.4.1 change (2_28 -> 2_17, to catch `pydantic-core`'s 2_17-only wheel) had created the opposite failure - a single floor cannot satisfy both. Now `docker_runner.py` emits repeated `--platform` flags from `manylinux_2_34` down to `manylinux1` (x86_64) / `manylinux2014` (aarch64), capped at the AL2023 runtime's glibc 2.34, and pip picks the most-specific compiled wheel each package offers, only using `py3-none-any` when no compiled wheel exists. Verified end to end: a real build now ships `wrapt/_wrappers.*.so` again. The builder version bump re-keys all content hashes, as expected.
7+
38
## v0.5.1 - 2026-06-21
49

510
### Fixed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "repro-lambda"
3-
version = "0.5.1"
3+
version = "0.5.2"
44
description = "Build reproducible AWS Lambda packages outside Terraform, optimized for terraform-aws-lambda by serverless.tf."
55
readme = "README.md"
66
requires-python = ">=3.11"

src/repro_lambda/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""repro-lambda — reproducible AWS Lambda packaging outside Terraform."""
22

3-
__version__ = "0.5.1"
3+
__version__ = "0.5.2"

src/repro_lambda/docker_runner.py

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,50 @@
1212
"x86_64": "linux/amd64",
1313
}
1414

15-
# manylinux_2_17 (== manylinux2014) is the broadest baseline the AWS Lambda base
16-
# images (Amazon Linux 2023, glibc 2.34) still run, and pip's explicit --platform does
17-
# NOT expand a higher tag (e.g. 2_28) down to lower-baseline wheels. Many compiled
18-
# wheels (e.g. pydantic-core) ship only manylinux_2_17 for a given Python/arch, so a
19-
# 2_28 floor misses them with --only-binary=:all:. 2_17 matches 2_17 wheels and, via
20-
# pip's tag expansion, any lower baseline too.
21-
ARCH_TO_PIP_PLATFORM: dict[str, str] = {
22-
"arm64": "manylinux_2_17_aarch64",
23-
"x86_64": "manylinux_2_17_x86_64",
15+
# The AWS Lambda base images (Amazon Linux 2023) ship glibc 2.34, so the runtime can load
16+
# any manylinux wheel up to manylinux_2_34. pip's explicit --platform matches (close to)
17+
# that exact tag, so a SINGLE platform misses wheels tagged with other baselines in both
18+
# directions: a 2_28 floor misses 2_17-only wheels (e.g. pydantic-core), and a 2_17 floor
19+
# misses higher-baseline compiled wheels (e.g. wrapt ships cp313 only as manylinux_2_28 ->
20+
# pip silently falls back to py3-none-any, which broke aws-xray-sdk's runtime boto3 patching
21+
# and 500'd a Lambda). A single floor cannot satisfy both. Pass the FULL compatible range as
22+
# repeated --platform flags, newest -> oldest, capped at 2_34 (the runtime's glibc): pip then
23+
# selects the most-specific COMPILED wheel each package offers and only falls back to
24+
# py3-none-any when no compiled wheel exists. Never list a baseline ABOVE 2_34 - that would
25+
# let pip pick a wheel the runtime cannot load. (arm64 manylinux starts at 2014/2_17; the
26+
# manylinux1/2010/2_5 legacy aliases are x86_64-only.)
27+
ARCH_TO_PIP_PLATFORMS: dict[str, list[str]] = {
28+
"x86_64": [
29+
"manylinux_2_34_x86_64",
30+
"manylinux_2_28_x86_64",
31+
"manylinux_2_24_x86_64",
32+
"manylinux2014_x86_64",
33+
"manylinux_2_17_x86_64",
34+
"manylinux_2_12_x86_64",
35+
"manylinux2010_x86_64",
36+
"manylinux_2_5_x86_64",
37+
"manylinux1_x86_64",
38+
],
39+
"arm64": [
40+
"manylinux_2_34_aarch64",
41+
"manylinux_2_28_aarch64",
42+
"manylinux_2_24_aarch64",
43+
"manylinux2014_aarch64",
44+
"manylinux_2_17_aarch64",
45+
],
2446
}
2547

48+
49+
def pip_platform_flags(arch: str) -> str:
50+
"""Repeated ``--platform <tag>`` flags (space-joined) for an arch's manylinux range.
51+
52+
Word-split UNQUOTED into the pip command in the container so each tag becomes its own
53+
flag. pip treats a wheel as compatible with ANY listed platform and prefers the
54+
earliest (newest baseline) match, so compiled wheels win over the py3-none-any fallback.
55+
"""
56+
return " ".join(f"--platform {tag}" for tag in ARCH_TO_PIP_PLATFORMS[arch])
57+
58+
2659
ARCH_TO_NPM_CPU: dict[str, str] = {
2760
"arm64": "arm64",
2861
"x86_64": "x64",
@@ -31,9 +64,9 @@
3164
# Invariance: keys must match across all arch lookup tables. Adding a new arch
3265
# to one without the other would cause install_nodejs_dependencies to raise
3366
# KeyError instead of DockerRunError. Caught at import time.
34-
assert set(ARCH_TO_DOCKER_PLATFORM) == set(ARCH_TO_PIP_PLATFORM) == set(ARCH_TO_NPM_CPU), (
67+
assert set(ARCH_TO_DOCKER_PLATFORM) == set(ARCH_TO_PIP_PLATFORMS) == set(ARCH_TO_NPM_CPU), (
3568
"arch lookup tables must share the same key set; "
36-
f"DOCKER={set(ARCH_TO_DOCKER_PLATFORM)} PIP={set(ARCH_TO_PIP_PLATFORM)} NPM={set(ARCH_TO_NPM_CPU)}"
69+
f"DOCKER={set(ARCH_TO_DOCKER_PLATFORM)} PIP={set(ARCH_TO_PIP_PLATFORMS)} NPM={set(ARCH_TO_NPM_CPU)}"
3770
)
3871

3972

@@ -49,7 +82,7 @@ class DockerRunError(RuntimeError):
4982
5083
pip install \
5184
--no-cache-dir --no-compile --require-hashes --only-binary=:all: \
52-
--platform "$PIP_PLATFORM" \
85+
$PIP_PLATFORM_FLAGS \
5386
--abi "$PIP_ABI" \
5487
--python-version "$PIP_PYVER" \
5588
--implementation cp \
@@ -130,7 +163,7 @@ def build_python_lambda(
130163
python_version: str,
131164
) -> None:
132165
"""v0.1-compatible: install + pack inside the Python container."""
133-
if arch not in ARCH_TO_PIP_PLATFORM:
166+
if arch not in ARCH_TO_PIP_PLATFORMS:
134167
raise DockerRunError(f"unsupported arch {arch!r}")
135168
if shutil.which("docker") is None:
136169
raise DockerRunError("docker CLI not found on PATH")
@@ -158,7 +191,7 @@ def build_python_lambda(
158191
"-e",
159192
"PYTHONPATH=/builder",
160193
"-e",
161-
f"PIP_PLATFORM={ARCH_TO_PIP_PLATFORM[arch]}",
194+
f"PIP_PLATFORM_FLAGS={pip_platform_flags(arch)}",
162195
"-e",
163196
f"PIP_ABI=cp{pyver_compact}",
164197
"-e",

tests/test_docker_runner.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,39 @@
55

66
from repro_lambda.docker_runner import (
77
ARCH_TO_DOCKER_PLATFORM,
8-
ARCH_TO_PIP_PLATFORM,
8+
ARCH_TO_PIP_PLATFORMS,
99
DockerRunError,
1010
build_python_lambda,
11+
pip_platform_flags,
1112
)
1213

1314

1415
def test_arch_mapping_tables_are_complete():
1516
for arch in ("arm64", "x86_64"):
1617
assert arch in ARCH_TO_DOCKER_PLATFORM
17-
assert arch in ARCH_TO_PIP_PLATFORM
18+
assert arch in ARCH_TO_PIP_PLATFORMS
1819

1920

2021
def test_arch_mapping_values():
2122
assert ARCH_TO_DOCKER_PLATFORM["arm64"] == "linux/arm64"
2223
assert ARCH_TO_DOCKER_PLATFORM["x86_64"] == "linux/amd64"
23-
assert ARCH_TO_PIP_PLATFORM["arm64"] == "manylinux_2_17_aarch64"
24-
assert ARCH_TO_PIP_PLATFORM["x86_64"] == "manylinux_2_17_x86_64"
24+
25+
26+
def test_pip_platform_flags_span_the_range_and_cap_at_2_34():
27+
"""Multiple --platform flags so pip picks the best COMPILED wheel per package; a single
28+
tag silently dropped compiled wheels (e.g. wrapt -> py3-none-any). Cap at glibc 2.34."""
29+
for arch, tag_arch in (("x86_64", "x86_64"), ("arm64", "aarch64")):
30+
flags = pip_platform_flags(arch)
31+
tags = ARCH_TO_PIP_PLATFORMS[arch]
32+
# Repeated --platform, one per tag, in declared (newest-first) order.
33+
assert flags == " ".join(f"--platform {t}" for t in tags)
34+
assert flags.count("--platform ") == len(tags)
35+
# Must include both the historical floors that each broke one direction.
36+
assert f"manylinux_2_17_{tag_arch}" in flags # pydantic-core (2_17-only) still resolves
37+
assert f"manylinux_2_28_{tag_arch}" in flags # wrapt's compiled cp313 wheel resolves
38+
# Never a baseline above the runtime's glibc 2.34 (would be unloadable).
39+
for n in (35, 36, 38, 40):
40+
assert f"manylinux_2_{n}_" not in flags
2541

2642

2743
def test_build_python_lambda_raises_on_unknown_arch(tmp_path: Path):

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)