Skip to content

feat: Python standalone images from python-build-standalone - #2154

Open
reneleonhardt wants to merge 9 commits into
GoogleContainerTools:mainfrom
reneleonhardt:feat/python-build-standalone
Open

feat: Python standalone images from python-build-standalone#2154
reneleonhardt wants to merge 9 commits into
GoogleContainerTools:mainfrom
reneleonhardt:feat/python-build-standalone

Conversation

@reneleonhardt

@reneleonhardt reneleonhardt commented Aug 10, 2026

Copy link
Copy Markdown

Summary

New python/ package (mirrors nodejs/) building distroless Python images from python-build-standalone (PBS) prebuilt tarballs instead of Debian-packaged python. The image base stays Debian (//cc provides glibc); only the python distribution method changes.

Matrix: 3.13.15 / 3.14.7 x debian13 x amd64 / arm64 / s390x / riscv64 x root/nonroot x normal/debug.

Tags: python3.13-debian13, python3.14-debian13 (+ per-arch and default-distro variants, nodejs publish pattern).

Why PBS

  • One tarball serves all archs; runtime deps are glibc only (full stdlib import walk passes with zero deb packages) — PBS statically links zlib/bz2/sqlite3/OpenSSL/ffi/lzma/ncurses into libpython, so there are no shared objects to ship and no PYTHON_PACKAGES list to maintain (only libc-bin for ldconfig).
  • Decouples Python versions from the Debian release (3.13 and 3.14 on debian13; 3.15 added by the updater when it reaches a stable release).
  • Pinned to PBS release 20260807; shas from the release SHA256SUMS, updated via knife update-python-archives.

Platform exclusions (verified, not oversights)

  • armv7: PBS publishes soft-float gnueabi builds (interpreter /lib/ld-linux.so.3); the distroless armhf base ships /usr/lib/ld-linux-armhf.so.3 - the binary cannot exec (confirmed in Docker). Removed.
  • ppc64le: not published by PBS.
  • s390x / riscv64 loaders verified against the cc bases (/usr/lib/ld64.so.1, /usr/lib/ld-linux-riscv64-lp64d.so.1).

Notes

  • debian12 intentionally absent: new debian12 builds are being deprecated upstream.
  • find_library() works for system libs (c/ssl/z verified): image ships /sbin/ldconfig (libc-bin) + a per-arch ld.so.cache (python3/ parity, maintainer request); regenerated via bazel run //python:update_ldconfig, checked by //python:check_ldconfig_tests (4/4) + a CST test. Statically-linked PBS libs have nothing to find; python -m venv works (ensurepip wheels bundled); crypt moot (removed in 3.13).
  • testdata schema 2.0.0 (nodejs parity); python3/ (deb) untouched.

Verification

  • bazelisk build //python/... green (debian13, 3.13+3.14, 4 archs).
  • 8/8 structure tests via colima docker (arm64, debian13, 3.13+3.14, root+nonroot).
  • amd64 image executed in Docker: 3.14.7 x86_64, ssl certs OK.
  • s390x/riscv64 built; OCI config (arch/os/annotations) verified; runtime in CI.
  • All required cc base permutations verified (arch configs correct).
  • armv7/ppc64le exclusions verified (loader/ABI evidence, see Platform exclusions).
  • find_library('c'/'ssl'/'z') resolves in the image (ldconfig + ld.so.cache); //python:check_ldconfig_tests 4/4 (amd64/arm64/s390x/riscv64); CST includes a find_library test; CDLL(soname) works (libc/libssl/libgcc/libpython).
  • Updater (knife update-python-archives): no-op idempotent; --verbose/--dry-run; fills new stable minors, drops EOL tail, sorts+dedupes the matrix; hermetic smoke test //python:update_python_archives_test; pre-flight validation (RED mutates nothing; recovery via git, lockfile reset documented).
  • buildifier check + lint clean.
  • CI wiring: images build via kind(oci_image, deps(:sign_and_push)); CST tagged manual+arch run via attr(tags, "amd64", ...) — same rails as python3/; check-ldconfig.yaml extended to cover //python:check_ldconfig_tests.

Checklist

  • Google CLA signed (per CONTRIBUTING.md)
  • Signed-off-by on all commits (repo convention)
  • CI green on fork: bazelisk build //python/..., structure tests

@google-cla

google-cla Bot commented Aug 10, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@loosebazooka

loosebazooka commented Aug 11, 2026

Copy link
Copy Markdown
Member

At a highlevel:

  1. don't add any new debian12 builds (deprecating those in a month)
  2. there's something in here I don't understand (but maybe it just means we don't need any new debs here)

    runtime deps are glibc only (full stdlib import walk passes with zero deb packages)

  3. images should just be 3.13 and 3.14, the code should be general purpose enough to add 3.15 when it is released
  4. an updater script should be included to find and update new releases of python

@reneleonhardt

Copy link
Copy Markdown
Author

don't add any new debian12 builds (deprecating those in a month)

Removed. python/ is debian13-only (config.bzl, README, publish map).

"runtime deps are glibc only (full stdlib import walk passes with zero deb packages)" — unclear

Explained above. PBS linux builds statically bundle zlib/bz2/sqlite3/OpenSSL/libffi/lzma/ncurses; the tarball binaries reference only glibc core libs, so the image adds zero python-specific deb packages. The 502-module stdlib import walk (ssl, sqlite3, zlib, ctypes, https) passes in-image with none; deliberate skips (GUI, _pyrepl._minimal_curses, dbm.gnu, windows-only, test) are documented so "zero debs" is exact, and any future missing lib fails the walk.

images should just be 3.13 and 3.14; code general enough to add 3.15 when it is released

Done. Matrix is 3.13 + 3.14; _bin_version + the matrix-driven config make 3.15 a one-entry addition when it reaches stable (no rc in the matrix anymore).

an updater script should be included to find and update new releases of python

Included: knife update-python-archives (knife.d/update_python_archives.sh). Reads the matrix from python/config.bzl, queries the latest PBS release, replaces patch versions + SHAs in place, fills new stable minors (3.15 once 3.15.0 ships), honors EOL tail drops, keeps testdata + MODULE.bazel use_repo in sync. --verbose/--dry-run flags, hermetic smoke test (//python:update_python_archives_test), pre-flight validation (a failed run mutates nothing).

@loosebazooka

Copy link
Copy Markdown
Member

I'm sorry I will get to this soon, lots of other things on my plate.

@reneleonhardt

Copy link
Copy Markdown
Author

No stress, my awesome contribution everyone's been waiting for isn't running away 😄

@loosebazooka

Copy link
Copy Markdown
Member

I'm no python expert, but the ldconfig seemed somewhat important to people. is CDLL sufficient in this case? are people not going to use "find_library()" ?

@loosebazooka

Copy link
Copy Markdown
Member

I had a teammate recommend we keep the ldconfig binary and continue to generate the ldconfig.so.cache to allow find_library to continue to work

@reneleonhardt
reneleonhardt force-pushed the feat/python-build-standalone branch from be7c93f to 3c51b57 Compare August 13, 2026 20:37
@reneleonhardt

Copy link
Copy Markdown
Author

Good catch! Now python3/ parity.

  • Image ships /sbin/ldconfig (libc-bin) + a per-arch ld.so.cache (amd64/arm64/s390x/riscv64), generated in-image via bazel run //python:update_ldconfig.
  • //python:check_ldconfig_tests 4/4 (also wired into check-ldconfig.yaml); CST find_library test added.

Verified in the image: find_library('c') -> libc.so.6, find_library('ssl') -> libssl.so.3, find_library('z') -> libz.so.1.

Scope note (deep-audited against the python3 image): PBS statically links zlib/bz2/sqlite3/OpenSSL/ffi/lzma/ncurses INTO libpython, so those modules work with no shared object to find - find_library() covers system libs (c/ssl/z), which is what the cache lists. python -m venv works (ensurepip wheels are bundled); crypt is moot (module removed in 3.13, PEP 594); dbm.gnu/tkinter are absent in both images (parity).

@loosebazooka loosebazooka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some minor concerns as to how scanners will pick up the PBS embedded native libraries. If one were to scan the python 3.14 image, what happens when the embedded zlib has a CVE? Is its version discoverable?

Comment thread python/README.md Outdated

## Provenance

Python comes from python-build-standalone release `20260807`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this part is a bit odd, and will have to be updated on every update. I think just linking to astral's pbs should be good. But not a specific version.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — Provenance links to astral's PBS, no version pin. Exact pin lives in //private/extensions:python.bzl, managed by the updater.

@@ -0,0 +1,335 @@
set -o pipefail -o errexit -o nounset

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gemini seems to think there's an issue with this script where a release tag change will go unnoticed 20260814 -> 20260815 which may be due to underlying library native library changes while the python version remains unchanged.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Was comparing patch-only; now compares the full patch+release pin, so a tag-only bump triggers an update. It was live: branch sat at 20260807 while 20260814 (expat 2.8.2 -> 2.8.3, same 3.13.15/3.14.7) was current — old logic said NO_CHANGE. Re-pinned to 20260814. Hermetic contract test covers tag-only bump, patch bump, new-minor fill, convergence.

@loosebazooka

loosebazooka commented Aug 18, 2026

Copy link
Copy Markdown
Member

I have some minor concerns as to how scanners will pick up the PBS embedded native libraries. If one were to scan the python 3.14 image, what happens when the embedded zlib has a CVE? Is its version discoverable?

I didn't mentioned this eariler, but these images would be considered new and will be categorized "experimental" for now. We don't have to solve the sbom/scanner problem now. I think we can try to get pbs themselves to produce and sbom and augment ours with theirs -- but none of this should block this PR or doing builds of these images.

@reneleonhardt

reneleonhardt commented Aug 18, 2026

Copy link
Copy Markdown
Author

I didn't mentioned this eariler, but these images would be considered new and will be categorized "experimental" for now. We don't have to solve the sbom/scanner problem now. I think we can try to get pbs themselves to produce and sbom and augment ours with theirs -- but none of this should block this PR or doing builds of these images.

Good... because trivy couldn't find a way to dissect pbs, embedded native libraries keep being embedded 😄
https://github.com/astral-sh/python-build-standalone/releases/tag/20260814
No SBOM in the release directly.

New python/ package: distroless Python images from python-build-standalone
(PBS) prebuilt tarballs instead of the Debian python packages used by python3/.

PBS statically links the native libraries (OpenSSL, zlib, sqlite3, libffi,
bz2, lzma, ncurses, expat) into the interpreter, so the image adds no python
runtime debs - only libc-bin, whose ldconfig plus a generated per-arch
ld.so.cache keeps ctypes.util.find_library() working (python3/ parity).

Matrix: 3.13.15 / 3.14.7 x debian13 x amd64/arm64/s390x/riscv64 x
root/nonroot x normal/debug - more Python versions than Debian ships on any
single release (trixie has 3.13 only) and an arch python3/ never had (s390x);
3.15 is added by the updater once PBS releases it stable. debian12 and
armv7/ppc64le are excluded (PBS publishes no usable builds). Pinned to PBS
release 20260814.
…chives)

knife update-python-archives keeps the PBS pins in
//private/extensions:python.bzl current: reads the matrix (minors + archs)
from python/config.bzl, queries the latest PBS release, rewrites every archive
(url/sha/version), and - when a new stable minor appears - extends the
config.bzl matrix, MODULE.bazel use_repo and the version testdata.

An update triggers on a patch move or on a release-tag move with unchanged
patches (PBS rebuilds its bundled native libraries, e.g. expat, under the
same CPython version). Idempotent; --verbose/--dry-run for inspection.

Tests: hermetic smoke test //python:update_python_archives_test (parser
consistency) + black-box contract test //python:update_python_archives_contract_test
running the updater offline against fixture releases - tag-only bump, patch
bump, new-minor fill, convergence.
The update now also regenerates an SPDX 2.3 SBOM from the release's component
manifest (pythonbuild/downloads.py): cpython versions, bundled pip/setuptools
and the statically embedded native libraries, each with version, license,
sha256 and download URL. Build-time-only tools are excluded. Covered by the
contract test (SBOM regenerated on tag/patch/new-minor updates, stable on
no-op runs).
The SBOM entries now carry purls: pkg:pypi for pip (scannable by trivy,
license MIT from the pypi fallback map) and pkg:generic for the native
libraries. Versions prefer the manifest's actual_version (sqlite shipped its
SQLITE_VERSION_NUMBER, 3530100, instead of 3.53.1.0), and setuptools is no
longer listed: install_only tarballs do not ship it, it is a PBS build-time
tool.
…RED)

A PBS release can bump its statically linked native libraries while the
CPython versions stay unchanged (maintainer-reported gap: python pin alone
would not notice). The updater now downloads the x86_64 install tarball
(sha-verified against SHA256SUMS), dissects libpython3*.so and compares the
embedded openssl/zlib/expat/ncurses/bzip2/sqlite/xz version markers against
the release manifest; a mismatch aborts the update. trivy cannot see these
libraries in any mode, so the dissection is the only drift check possible.

The contract test gained a hermetic tarball fixture (PBS_TARBALL_FILE) and a
drift phase: a tarball whose markers no longer match the manifest must fail
the updater without mutating the workspace. get_python_archs now returns an
empty result for a minor missing from config.bzl instead of aborting under
errexit+pipefail (the new-minor fallback relies on it).
pbs_cve_check.py queries the NVD API for the dissection-verified versions of
the seven embedded libraries (openssl/sqlite/zlib/expat/bzip2/ncurses/xz)
and aborts the update on HIGH/CRITICAL CVEs; dependency-reference false
positives (mutt/OpenLDAP-style CPE matches with vulnerable=false) are
filtered out. trivy has no advisory feed for pkg:generic, so this is the
only real CVE signal for those libraries. NVD_API_KEY removes the rate-limit
sleeps, PBS_SKIP_CVE_CHECK=1 bypasses.

Hermetic test with fixture NVD responses covers the clean path, the RED path
and the false-positive filter. Live result for the 20260814 pin: sqlite
3.53.1 carries CVE-2026-11822/CVE-2026-11824 (fixed in 3.53.2); the rest are
clean.
@reneleonhardt
reneleonhardt force-pushed the feat/python-build-standalone branch from 7b9584b to f205dd4 Compare August 19, 2026 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants