From 6a3708306a74d73c9017b818d779520ea21722fe Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Wed, 1 Jul 2026 16:58:24 -0400 Subject: [PATCH 01/21] In test_cext: Tell setuptools when we are generating a limited_api module --- Lib/test/test_cext/setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_cext/setup.py b/Lib/test/test_cext/setup.py index 1eca44bdf823dc2..9a5f0a3f57815ea 100644 --- a/Lib/test/test_cext/setup.py +++ b/Lib/test/test_cext/setup.py @@ -107,6 +107,8 @@ def main(): if internal: cflags.append('-DTEST_INTERNAL_C_API=1') + py_limited_api = limited or abi3t + # Add additional include and library directories, typically for in-tree # testing where not all directories are inferred include_dirs = [] @@ -131,7 +133,9 @@ def main(): sources=sources, extra_compile_args=cflags, include_dirs=include_dirs, - library_dirs=library_dirs) + library_dirs=library_dirs, + py_limited_api=py_limited_api, + ) setup(name=f'internal_{module_name}', version='0.0', ext_modules=[ext]) From 15cd43488a987a7ffab29fcf923dea6b142d0634 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Wed, 1 Jul 2026 10:39:12 -0400 Subject: [PATCH 02/21] Make debug builds configurable in reusable-ubuntu --- .github/workflows/reusable-ubuntu.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index b8f264ff13b2cef..20afc2a213d0121 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -8,6 +8,11 @@ on: required: false type: boolean default: false + debug: + description: Whether to create a Debug Build + required: false + type: boolean + default: true free-threading: description: Whether to use free-threaded mode required: false @@ -84,10 +89,10 @@ jobs: PROFILE_TASK='-m test --pgo --ignore test_unpickle_module_race' ../cpython-ro-srcdir/configure --config-cache - --with-pydebug --enable-slower-safety --enable-safety --with-openssl="$OPENSSL_DIR" + ${{ fromJSON(inputs.debug) && '--with-pydebug' || '' }} ${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }} ${{ fromJSON(inputs.bolt-optimizations) && '--enable-bolt' || '' }} - name: Build CPython out-of-tree From db6e1a92e05ebc07e7632e20645738e458c93318 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Wed, 1 Jul 2026 10:42:06 -0400 Subject: [PATCH 03/21] Tidy up YAML --- .github/workflows/reusable-ubuntu.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index 20afc2a213d0121..f72d1917d269587 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -19,14 +19,14 @@ on: type: boolean default: false os: - description: OS to run the job - required: true - type: string + description: OS to run the job + required: true + type: string test-opts: - description: Extra options to pass to the test runner via TESTOPTS - required: false - type: string - default: '' + description: Extra options to pass to the test runner via TESTOPTS + required: false + type: string + default: '' permissions: contents: read From 216fae0f792c5e330f4563771afbba6c3f1cd7a0 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Wed, 1 Jul 2026 10:49:24 -0400 Subject: [PATCH 04/21] Make tests optional in reusable-ubuntu --- .github/workflows/reusable-ubuntu.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index f72d1917d269587..9a05a2e153f1d25 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -22,6 +22,11 @@ on: description: OS to run the job required: true type: string + test: + description: Whether to run the Python test suite + required: false + type: boolean + default: true test-opts: description: Extra options to pass to the test runner via TESTOPTS required: false @@ -36,7 +41,7 @@ env: jobs: build-ubuntu-reusable: - name: build and test (${{ inputs.os }}) + name: build ${{ fromJSON(inputs.test) && 'and test ' || '' }}(${{ inputs.os }}) runs-on: ${{ inputs.os }} timeout-minutes: 60 env: @@ -120,6 +125,7 @@ jobs: # some tests write to srcdir, lack of pyc files slows down testing run: sudo mount "$CPYTHON_RO_SRCDIR" -oremount,rw - name: Tests + if: ${{ inputs.test }} working-directory: ${{ env.CPYTHON_BUILDDIR }} run: xvfb-run make ci EXTRATESTOPTS="${TEST_OPTS}" env: From f20a5b0ad405ec1b6ae1eb2b1b5a9d55481b1778 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Wed, 1 Jul 2026 09:04:09 -0400 Subject: [PATCH 05/21] Allow reusable-ubuntu to install and upload results --- .github/workflows/reusable-ubuntu.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index 9a05a2e153f1d25..caa34e92bb19665 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -32,6 +32,11 @@ on: required: false type: string default: '' + upload-install: + description: Install Python and upload the result artifact + required: false + type: boolean + default: false permissions: contents: read @@ -130,3 +135,13 @@ jobs: run: xvfb-run make ci EXTRATESTOPTS="${TEST_OPTS}" env: TEST_OPTS: ${{ inputs.test-opts }} + - name: Install Python + if: ${{ inputs.upload-install }} + working-directory: ${{ env.CPYTHON_BUILDDIR }} + run: make install DESTDIR=build/install + - name: Upload Installed Python + if: ${{ inputs.upload-install }} + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: install-tree-${{ runner.arch }}-${{ case(inputs.free-threading, 't', '')}}${{ case(inputs.debug, 'd', '') }} + path: ${{ env.CPYTHON_BUILDDIR }}/build/install From f05947831b80bcaf9ff42dc74e617562400811fb Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Wed, 1 Jul 2026 10:56:12 -0400 Subject: [PATCH 06/21] Matrix to run installs on Ubuntu --- .github/workflows/build.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0edf4602bfaf97a..c1c31fca64b895f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -608,6 +608,36 @@ jobs: run: | "$BUILD_DIR/cross-python/bin/python3" -m test test_sysconfig test_site test_embed + linux-install-build: + name: >- + Ubuntu Install + ${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }} + ${{ fromJSON(matrix.debug) && '(debug)' || '' }} + needs: build-context + if: needs.build-context.outputs.run-ubuntu == 'true' + strategy: + fail-fast: false + matrix: + free-threading: + - false + - true + debug: + - false + - true + os: + - ubuntu-24.04 + include: + - free-threading: false + debug: false + os: ubuntu-24.04-arm + uses: ./.github/workflows/reusable-ubuntu.yml + with: + debug: ${{ matrix.debug }} + free-threading: ${{ matrix.free-threading }} + os: ${{ matrix.os }} + test: false + upload-install: true + cifuzz: # ${{ '' } is a hack to nest jobs under the same sidebar category. name: CIFuzz${{ '' }} # zizmor: ignore[obfuscation] From 2f5cd99a1bd6a36a859b872feb3be83aa0fead1d Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Wed, 1 Jul 2026 11:42:27 -0400 Subject: [PATCH 07/21] Compare multiple install results --- .github/CODEOWNERS | 2 + .github/workflows/build.yml | 32 ++++++++++++ .github/workflows/reusable-ubuntu.yml | 16 ++++++ Tools/README | 3 ++ Tools/coinstall-check/compare.py | 71 +++++++++++++++++++++++++++ 5 files changed, 124 insertions(+) create mode 100755 Tools/coinstall-check/compare.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0cdf17838b5798f..2893ccd1085581e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -150,6 +150,8 @@ Tools/build/generate_sbom.py @sethmlarson # ABI check Misc/libabigail.abignore @encukou +# Multiarch +Tools/coinstall-check/ @stefanor # ---------------------------------------------------------------------------- # Platform Support diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1c31fca64b895f..5e88f58a55df674 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -638,6 +638,37 @@ jobs: test: false upload-install: true + linux-install-compare: + name: Ubuntu Co-install comparison + runs-on: ubuntu-latest + needs: linux-install-build + steps: + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.x' + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Download installs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: installs + pattern: install-tree-* + - name: Remove files we don't expect to match + # .pyc files include timestamps + # /usr/bin/ is not expected to be co-installable + # The generic python3.pc and python3.X.pc files are not expected to be co-installable + # pyconfig.h is not expected to be co-installable + # dist-info RECORD and WHEEL are not co-installable + run: | + find installs -type d -name __pycache__ | xargs rm -r + rm -r installs/*/usr/bin/ + rm installs/install-tree-*-*[td]/usr/lib/*/pkgconfig/python*[0-9]{,-embed}.pc + rm installs/*/usr/include/python*/pyconfig.h + rm installs/*/usr/lib/python*/site-packages/*.dist-info/{RECORD,WHEEL} + - name: Compare installs + run: python3 Tools/coinstall-check/compare.py installs/ + cifuzz: # ${{ '' } is a hack to nest jobs under the same sidebar category. name: CIFuzz${{ '' }} # zizmor: ignore[obfuscation] @@ -705,6 +736,7 @@ jobs: - build-san - cross-build-linux - cifuzz + - linux-install-compare if: always() steps: diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index caa34e92bb19665..3fa347f24c4ea91 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -105,6 +105,7 @@ jobs: ${{ fromJSON(inputs.debug) && '--with-pydebug' || '' }} ${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }} ${{ fromJSON(inputs.bolt-optimizations) && '--enable-bolt' || '' }} + ${{ fromJSON(inputs.upload-install) && '--prefix=/usr --libdir=/usr/lib/$(gcc --print-multiarch) --with-build-details-suffix' || '' }} - name: Build CPython out-of-tree if: ${{ inputs.free-threading }} working-directory: ${{ env.CPYTHON_BUILDDIR }} @@ -139,6 +140,21 @@ jobs: if: ${{ inputs.upload-install }} working-directory: ${{ env.CPYTHON_BUILDDIR }} run: make install DESTDIR=build/install + - name: Install some test packages in the Python + if: ${{ inputs.upload-install }} + working-directory: ${{ env.CPYTHON_BUILDDIR }} + run: | + CPYTHON_TEST_EXT_NAME=c_mod \ + build/install/usr/bin/python3 -m pip install ../cpython-ro-srcdir/Lib/test/test_cext + CPYTHON_TEST_LIMITED=1 CPYTHON_TEST_EXT_NAME=abi3_mod \ + build/install/usr/bin/python3 -m pip install ../cpython-ro-srcdir/Lib/test/test_cext + CPYTHON_TEST_ABI3T=1 CPYTHON_TEST_EXT_NAME=abi3t_mod \ + build/install/usr/bin/python3 -m pip install ../cpython-ro-srcdir/Lib/test/test_cext + # Until https://github.com/pypa/setuptools/pull/5193 is released + if [ ${{ fromJSON(inputs.free-threading) && '1' || '0' }} = '0' ]; then \ + (cd build/install/usr/lib/python*/site-packages && \ + mv abi3t_mod.abi3.so abi3t_mod.abi3t.so); \ + fi - name: Upload Installed Python if: ${{ inputs.upload-install }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 diff --git a/Tools/README b/Tools/README index 90acb2614820ee4..60416d3b6d60f71 100644 --- a/Tools/README +++ b/Tools/README @@ -14,6 +14,9 @@ clinic A preprocessor for CPython C files in order to automate the boilerplate involved with writing argument parsing code for "builtins". +coinstall-check A tool to ensure that multiple CPython builds can be + co-installed on Linux. + freeze Create a stand-alone executable from a Python program. ftscalingbench Benchmarks for free-threading and finding bottlenecks. diff --git a/Tools/coinstall-check/compare.py b/Tools/coinstall-check/compare.py new file mode 100755 index 000000000000000..d41274d2d3fd2ae --- /dev/null +++ b/Tools/coinstall-check/compare.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# Compare that multiple installs of Python don't have conflicting files +# +# This is a requirement for Debian's Multi-Arch installs of Python +# https://www.debian.org/doc/debian-policy/ch-controlfields.html#multi-arch +# +# Excluded from this should be: +# * /usr/bin/*: only libraries are multi-arch co-installed, one arch's binaries +# are installed at a time +# * pyconfig.h: Varies according to config, installed into a tag-specific +# directory. +# * Non-tag suffixed .pc files: Only the suffixed versions are co-installable +# * .dist-info/RECORD: Contains hashes, not co-installable. +# * .dist-info/WHEEL: Contains arch and version tags. Can be merged in some +# cases, but not typically co-installable. + +from argparse import ArgumentParser +from hashlib import file_digest +from pathlib import Path + + +def hash_tree(base: Path, algorithm: str = "sha512") -> dict[str, str]: + print(f"Hashing {base}") + seen: dict[str, str] = {} + for dirpath, dirnames, filenames in base.walk(): + if dirpath.name == "__pycache__": + # Includes a timestamp, we expect a mismatch + continue + for file in filenames: + filepath = dirpath / file + with filepath.open("rb") as f: + digest = file_digest(f, algorithm) + seen[str(filepath.relative_to(base))] = digest.hexdigest() + return seen + + +def compare_trees(base: Path) -> bool: + seen: dict[str, str] = {} + success: bool = True + for tree in base.iterdir(): + if not tree.is_dir(): + continue + hashes = hash_tree(tree) + for path, digest in hashes.items(): + if path not in seen: + seen[path] = digest + continue + if digest != seen[path]: + print(f"Mismatch found in {tree}: {path}") + print(f"{digest} != {seen[path]}") + success = False + return success + + +def main() -> None: + p = ArgumentParser("Compare multiple installs of Python") + p.add_argument( + "base_directory", + type=Path, + help=( + "Directory below which multiple Pythons are installed, " + "each inside their own directory." + ), + ) + args = p.parse_args() + if not compare_trees(args.base_directory): + raise SystemExit(1) + + +if __name__ == "__main__": + main() From e6dd647bd46400b35df57ec54741be4b18854177 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Wed, 1 Jul 2026 18:10:49 -0400 Subject: [PATCH 08/21] Skip comparing abi3 and abi3t for now --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e88f58a55df674..c18ac609afceade 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -660,12 +660,14 @@ jobs: # The generic python3.pc and python3.X.pc files are not expected to be co-installable # pyconfig.h is not expected to be co-installable # dist-info RECORD and WHEEL are not co-installable + # abi3 and abi3t are not currently co-installable (#122931) run: | find installs -type d -name __pycache__ | xargs rm -r rm -r installs/*/usr/bin/ rm installs/install-tree-*-*[td]/usr/lib/*/pkgconfig/python*[0-9]{,-embed}.pc rm installs/*/usr/include/python*/pyconfig.h rm installs/*/usr/lib/python*/site-packages/*.dist-info/{RECORD,WHEEL} + rm installs/*/usr/lib/python*/site-packages/*.abi3{,t}.so - name: Compare installs run: python3 Tools/coinstall-check/compare.py installs/ From b84e7199cfc5c0450264cb7a18aa05046ba50eb8 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Wed, 1 Jul 2026 18:14:32 -0400 Subject: [PATCH 09/21] NEWS entry --- .../next/Tests/2026-07-01-18-14-25.gh-issue-122931.OeRDeo.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Tests/2026-07-01-18-14-25.gh-issue-122931.OeRDeo.rst diff --git a/Misc/NEWS.d/next/Tests/2026-07-01-18-14-25.gh-issue-122931.OeRDeo.rst b/Misc/NEWS.d/next/Tests/2026-07-01-18-14-25.gh-issue-122931.OeRDeo.rst new file mode 100644 index 000000000000000..bcfdaadace7cb35 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-07-01-18-14-25.gh-issue-122931.OeRDeo.rst @@ -0,0 +1,3 @@ +CI Tests to ensure Debian `multi-arch co-installability +`_ +of Python. From 3fce58075ff900bbf9e2390305c87f1cd86c1869 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Wed, 1 Jul 2026 20:01:18 -0400 Subject: [PATCH 10/21] Upload hashes rather than installs --- .github/workflows/build.yml | 27 ++------ .github/workflows/reusable-ubuntu.yml | 29 ++++++--- Tools/coinstall-check/compare.py | 44 ++++--------- Tools/coinstall-check/hash-r.py | 94 +++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 61 deletions(-) create mode 100755 Tools/coinstall-check/hash-r.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c18ac609afceade..03ac59d789af1d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -636,7 +636,7 @@ jobs: free-threading: ${{ matrix.free-threading }} os: ${{ matrix.os }} test: false - upload-install: true + upload-install-hashes: true linux-install-compare: name: Ubuntu Co-install comparison @@ -649,27 +649,14 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - name: Download installs + - name: Download install hashes uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - path: installs - pattern: install-tree-* - - name: Remove files we don't expect to match - # .pyc files include timestamps - # /usr/bin/ is not expected to be co-installable - # The generic python3.pc and python3.X.pc files are not expected to be co-installable - # pyconfig.h is not expected to be co-installable - # dist-info RECORD and WHEEL are not co-installable - # abi3 and abi3t are not currently co-installable (#122931) - run: | - find installs -type d -name __pycache__ | xargs rm -r - rm -r installs/*/usr/bin/ - rm installs/install-tree-*-*[td]/usr/lib/*/pkgconfig/python*[0-9]{,-embed}.pc - rm installs/*/usr/include/python*/pyconfig.h - rm installs/*/usr/lib/python*/site-packages/*.dist-info/{RECORD,WHEEL} - rm installs/*/usr/lib/python*/site-packages/*.abi3{,t}.so - - name: Compare installs - run: python3 Tools/coinstall-check/compare.py installs/ + path: install-hashes + pattern: install-hashes-* + merge-multiple: true + - name: Compare install hashes + run: python3 Tools/coinstall-check/compare.py install-hashes cifuzz: # ${{ '' } is a hack to nest jobs under the same sidebar category. diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index 3fa347f24c4ea91..3124afe430bc4dc 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -32,7 +32,7 @@ on: required: false type: string default: '' - upload-install: + upload-install-hashes: description: Install Python and upload the result artifact required: false type: boolean @@ -105,7 +105,7 @@ jobs: ${{ fromJSON(inputs.debug) && '--with-pydebug' || '' }} ${{ fromJSON(inputs.free-threading) && '--disable-gil' || '' }} ${{ fromJSON(inputs.bolt-optimizations) && '--enable-bolt' || '' }} - ${{ fromJSON(inputs.upload-install) && '--prefix=/usr --libdir=/usr/lib/$(gcc --print-multiarch) --with-build-details-suffix' || '' }} + ${{ fromJSON(inputs.upload-install-hashes) && '--prefix=/usr --libdir=/usr/lib/$(gcc --print-multiarch) --with-build-details-suffix' || '' }} - name: Build CPython out-of-tree if: ${{ inputs.free-threading }} working-directory: ${{ env.CPYTHON_BUILDDIR }} @@ -137,11 +137,11 @@ jobs: env: TEST_OPTS: ${{ inputs.test-opts }} - name: Install Python - if: ${{ inputs.upload-install }} + if: ${{ inputs.upload-install-hashes }} working-directory: ${{ env.CPYTHON_BUILDDIR }} run: make install DESTDIR=build/install - name: Install some test packages in the Python - if: ${{ inputs.upload-install }} + if: ${{ inputs.upload-install-hashes }} working-directory: ${{ env.CPYTHON_BUILDDIR }} run: | CPYTHON_TEST_EXT_NAME=c_mod \ @@ -155,9 +155,22 @@ jobs: (cd build/install/usr/lib/python*/site-packages && \ mv abi3t_mod.abi3.so abi3t_mod.abi3t.so); \ fi - - name: Upload Installed Python - if: ${{ inputs.upload-install }} + - name: Generate the install hashes filename + if: ${{ inputs.upload-install-hashes }} + run: | + echo "INSTALL_HASHES_FILE=install-hashes-${{ runner.arch }}-${{ case(inputs.free-threading, 't', '')}}${{ case(inputs.debug, 'd', '') }}" >> "$GITHUB_ENV" + - name: Hash the installed Python + if: ${{ inputs.upload-install-hashes }} + working-directory: ${{ env.CPYTHON_BUILDDIR }} + run: >- + build/install/usr/bin/python3 + ../cpython-ro-srcdir/Tools/coinstall-check/hash-r.py + build/install > "$INSTALL_HASHES_FILE" + - name: Upload the hashes + if: ${{ inputs.upload-install-hashes }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - name: install-tree-${{ runner.arch }}-${{ case(inputs.free-threading, 't', '')}}${{ case(inputs.debug, 'd', '') }} - path: ${{ env.CPYTHON_BUILDDIR }}/build/install + name: ${{ env.INSTALL_HASHES_FILE }} + path: ${{ env.CPYTHON_BUILDDIR }}/${{ env.INSTALL_HASHES_FILE }} + archive: false + retention-days: 1 diff --git a/Tools/coinstall-check/compare.py b/Tools/coinstall-check/compare.py index d41274d2d3fd2ae..85667a6687b0d3c 100755 --- a/Tools/coinstall-check/compare.py +++ b/Tools/coinstall-check/compare.py @@ -3,44 +3,25 @@ # # This is a requirement for Debian's Multi-Arch installs of Python # https://www.debian.org/doc/debian-policy/ch-controlfields.html#multi-arch -# -# Excluded from this should be: -# * /usr/bin/*: only libraries are multi-arch co-installed, one arch's binaries -# are installed at a time -# * pyconfig.h: Varies according to config, installed into a tag-specific -# directory. -# * Non-tag suffixed .pc files: Only the suffixed versions are co-installable -# * .dist-info/RECORD: Contains hashes, not co-installable. -# * .dist-info/WHEEL: Contains arch and version tags. Can be merged in some -# cases, but not typically co-installable. from argparse import ArgumentParser -from hashlib import file_digest from pathlib import Path -def hash_tree(base: Path, algorithm: str = "sha512") -> dict[str, str]: - print(f"Hashing {base}") - seen: dict[str, str] = {} - for dirpath, dirnames, filenames in base.walk(): - if dirpath.name == "__pycache__": - # Includes a timestamp, we expect a mismatch - continue - for file in filenames: - filepath = dirpath / file - with filepath.open("rb") as f: - digest = file_digest(f, algorithm) - seen[str(filepath.relative_to(base))] = digest.hexdigest() - return seen - - def compare_trees(base: Path) -> bool: seen: dict[str, str] = {} success: bool = True for tree in base.iterdir(): - if not tree.is_dir(): + if not tree.is_file(): continue - hashes = hash_tree(tree) + + hashes: dict[str, str] = {} + print(f"Examining {tree}") + with tree.open("r") as f: + for line in f: + digest, path = line.strip().split("\t") + hashes[path] = digest + for path, digest in hashes.items(): if path not in seen: seen[path] = digest @@ -53,14 +34,11 @@ def compare_trees(base: Path) -> bool: def main() -> None: - p = ArgumentParser("Compare multiple installs of Python") + p = ArgumentParser("Compare multiple hash-r files") p.add_argument( "base_directory", type=Path, - help=( - "Directory below which multiple Pythons are installed, " - "each inside their own directory." - ), + help="Directory containing hashes of Python installs.", ) args = p.parse_args() if not compare_trees(args.base_directory): diff --git a/Tools/coinstall-check/hash-r.py b/Tools/coinstall-check/hash-r.py new file mode 100755 index 000000000000000..49b1ad20644bc0d --- /dev/null +++ b/Tools/coinstall-check/hash-r.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# Export a SHA512 manifest of installed files, so that we can ensure that +# multiple installs of Python don't have conflicting files +# +# This is a requirement for Debian's Multi-Arch installs of Python +# https://www.debian.org/doc/debian-policy/ch-controlfields.html#multi-arch + +from argparse import ArgumentParser +from hashlib import file_digest +from pathlib import Path +import json + + +def load_build_details(base: Path): + for path in base.glob("usr/lib/python*/build-details*.json"): + with path.open("rb") as f: + return json.load(f) + + +def hash_tree(base: Path, algorithm: str = "sha512") -> dict[str, str]: + hashes: dict[str, str] = {} + build_details = load_build_details(base) + flags = build_details["abi"]["flags"] + version = build_details["language"]["version"] + for dirpath, dirnames, filenames in base.walk(): + if dirpath.name == "__pycache__": + # Includes a timestamp, we expect a mismatch + continue + relative_dirpath = dirpath.relative_to(base) + if relative_dirpath.is_relative_to("usr/bin"): + # Only libraries are multi-arch co-installed, only one arch can + # have binaries in /usr/bin at a time. + continue + + for file in filenames: + if relative_dirpath.is_relative_to("usr/include") and file == "pyconfig.h": + # Varies according to config, installed into a tag-specific + # include directory + continue + + if ( + relative_dirpath.is_relative_to("usr/lib") + and relative_dirpath.name == "pkgconfig" + and file in ("python3.pc", "python3-embed.pc") + ): + # Only the tag-suffixed .pc files are co-installable + continue + + if ( + relative_dirpath.is_relative_to("usr/lib") + and relative_dirpath.name == "pkgconfig" + and flags # non-default install + and file in (f"python-{version}.pc", f"python-{version}-embed.pc") + ): + # Only the tag-suffixed .pc files are co-installable + continue + + if relative_dirpath.name.endswith(".dist-info") and file in ( + "RECORD", + "WHEEL", + ): + # RECORD: Contains hashes, not co-installable. + # WHEEL: Contains arch and version tags. Tags can be merged but + # not architectures. + continue + + if relative_dirpath.name == "site-packages" and file.endswith( + (".abi3.so", ".abi3t.so") + ): + # abi3 and abi3t are not current co-installable (#122931) + continue + + filepath = dirpath / file + with filepath.open("rb") as f: + digest = file_digest(f, algorithm) + hashes[str(filepath.relative_to(base))] = digest.hexdigest() + return hashes + + +def main() -> None: + p = ArgumentParser("Hash a python install for comparison later") + p.add_argument( + "destdir", + type=Path, + help="Directory below which Python is installed", + ) + args = p.parse_args() + hashes = hash_tree(args.destdir) + for path, digest in sorted(hashes.items()): + print(f"{digest}\t{path}") # compatible with sha512sum + + +if __name__ == "__main__": + main() From 66d9cf88c7150c049d0898ef62d716239ffab1e4 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Fri, 3 Jul 2026 09:49:16 -0400 Subject: [PATCH 11/21] strict type Tools/coinstall-check/ --- Tools/coinstall-check/hash-r.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tools/coinstall-check/hash-r.py b/Tools/coinstall-check/hash-r.py index 49b1ad20644bc0d..bfcbadc8a36d797 100755 --- a/Tools/coinstall-check/hash-r.py +++ b/Tools/coinstall-check/hash-r.py @@ -8,13 +8,15 @@ from argparse import ArgumentParser from hashlib import file_digest from pathlib import Path +from typing import Any, cast import json -def load_build_details(base: Path): +def load_build_details(base: Path) -> dict[str, Any]: for path in base.glob("usr/lib/python*/build-details*.json"): with path.open("rb") as f: - return json.load(f) + return cast(dict[str, Any], json.load(f)) + raise AssertionError(f"build-details.json not found in {base}") def hash_tree(base: Path, algorithm: str = "sha512") -> dict[str, str]: From 5443e16248b1aadc663e2f33510f9ce07da80199 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Fri, 3 Jul 2026 09:56:51 -0400 Subject: [PATCH 12/21] Use Path.read_bytes() --- Tools/coinstall-check/hash-r.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tools/coinstall-check/hash-r.py b/Tools/coinstall-check/hash-r.py index bfcbadc8a36d797..5093130050fba0f 100755 --- a/Tools/coinstall-check/hash-r.py +++ b/Tools/coinstall-check/hash-r.py @@ -14,8 +14,8 @@ def load_build_details(base: Path) -> dict[str, Any]: for path in base.glob("usr/lib/python*/build-details*.json"): - with path.open("rb") as f: - return cast(dict[str, Any], json.load(f)) + details = json.loads(path.read_bytes()) + return cast(dict[str, Any], details) raise AssertionError(f"build-details.json not found in {base}") From ace1e1f7b47c3af1ca24b08e98339720dc01d930 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Fri, 3 Jul 2026 09:57:03 -0400 Subject: [PATCH 13/21] Factor out dirname patch checks --- Tools/coinstall-check/hash-r.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/Tools/coinstall-check/hash-r.py b/Tools/coinstall-check/hash-r.py index 5093130050fba0f..bca0820f35cb750 100755 --- a/Tools/coinstall-check/hash-r.py +++ b/Tools/coinstall-check/hash-r.py @@ -33,42 +33,37 @@ def hash_tree(base: Path, algorithm: str = "sha512") -> dict[str, str]: # Only libraries are multi-arch co-installed, only one arch can # have binaries in /usr/bin at a time. continue + in_usr_include = relative_dirpath.is_relative_to("usr/include") + in_usr_lib = relative_dirpath.is_relative_to("usr/lib") + in_pkgconfig = in_usr_lib and relative_dirpath.name == "pkgconfig" + in_dist_info = relative_dirpath.name.endswith(".dist-info") + in_site_packages = relative_dirpath.name == "site-packages" for file in filenames: - if relative_dirpath.is_relative_to("usr/include") and file == "pyconfig.h": + if in_usr_include and file == "pyconfig.h": # Varies according to config, installed into a tag-specific # include directory continue - if ( - relative_dirpath.is_relative_to("usr/lib") - and relative_dirpath.name == "pkgconfig" - and file in ("python3.pc", "python3-embed.pc") - ): + if in_pkgconfig and file in ("python3.pc", "python3-embed.pc"): # Only the tag-suffixed .pc files are co-installable continue if ( - relative_dirpath.is_relative_to("usr/lib") - and relative_dirpath.name == "pkgconfig" + in_pkgconfig and flags # non-default install and file in (f"python-{version}.pc", f"python-{version}-embed.pc") ): # Only the tag-suffixed .pc files are co-installable continue - if relative_dirpath.name.endswith(".dist-info") and file in ( - "RECORD", - "WHEEL", - ): + if in_dist_info and file in ("RECORD", "WHEEL"): # RECORD: Contains hashes, not co-installable. # WHEEL: Contains arch and version tags. Tags can be merged but # not architectures. continue - if relative_dirpath.name == "site-packages" and file.endswith( - (".abi3.so", ".abi3t.so") - ): + if in_site_packages and file.endswith((".abi3.so", ".abi3t.so")): # abi3 and abi3t are not current co-installable (#122931) continue From f12b47acdd2e64c94c90280229229225ad2f25fd Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Fri, 3 Jul 2026 10:39:39 -0400 Subject: [PATCH 14/21] Restructure GitHub CI workflow, from review --- .github/workflows/reusable-ubuntu.yml | 54 ++++++++++++++------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index 3124afe430bc4dc..8055c7d5f911f57 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -50,6 +50,7 @@ jobs: runs-on: ${{ inputs.os }} timeout-minutes: 60 env: + INSTALL_HASHES_FILE: install-hashes-${{ inputs.os }}-${{ case(inputs.free-threading, 't', '')}}${{ case(inputs.debug, 'd', '') }} OPENSSL_VER: 3.5.7 PYTHONSTRICTEXTENSIONBUILD: 1 TERM: linux @@ -131,43 +132,44 @@ jobs: # some tests write to srcdir, lack of pyc files slows down testing run: sudo mount "$CPYTHON_RO_SRCDIR" -oremount,rw - name: Tests - if: ${{ inputs.test }} + if: inputs.test working-directory: ${{ env.CPYTHON_BUILDDIR }} run: xvfb-run make ci EXTRATESTOPTS="${TEST_OPTS}" env: TEST_OPTS: ${{ inputs.test-opts }} - name: Install Python - if: ${{ inputs.upload-install-hashes }} + if: inputs.upload-install-hashes working-directory: ${{ env.CPYTHON_BUILDDIR }} - run: make install DESTDIR=build/install - - name: Install some test packages in the Python - if: ${{ inputs.upload-install-hashes }} + run: make install DESTDIR=install + - name: Install test C extension + if: inputs.upload-install-hashes working-directory: ${{ env.CPYTHON_BUILDDIR }} - run: | - CPYTHON_TEST_EXT_NAME=c_mod \ - build/install/usr/bin/python3 -m pip install ../cpython-ro-srcdir/Lib/test/test_cext - CPYTHON_TEST_LIMITED=1 CPYTHON_TEST_EXT_NAME=abi3_mod \ - build/install/usr/bin/python3 -m pip install ../cpython-ro-srcdir/Lib/test/test_cext - CPYTHON_TEST_ABI3T=1 CPYTHON_TEST_EXT_NAME=abi3t_mod \ - build/install/usr/bin/python3 -m pip install ../cpython-ro-srcdir/Lib/test/test_cext - # Until https://github.com/pypa/setuptools/pull/5193 is released - if [ ${{ fromJSON(inputs.free-threading) && '1' || '0' }} = '0' ]; then \ - (cd build/install/usr/lib/python*/site-packages && \ - mv abi3t_mod.abi3.so abi3t_mod.abi3t.so); \ - fi - - name: Generate the install hashes filename - if: ${{ inputs.upload-install-hashes }} - run: | - echo "INSTALL_HASHES_FILE=install-hashes-${{ runner.arch }}-${{ case(inputs.free-threading, 't', '')}}${{ case(inputs.debug, 'd', '') }}" >> "$GITHUB_ENV" + env: + CPYTHON_TEST_EXT_NAME: c_mod + run: install/usr/bin/python3 -m pip install ../cpython-ro-srcdir/Lib/test/test_cext + - name: Install test stable ABI extension + if: inputs.upload-install-hashes && !inputs.free-threading + working-directory: ${{ env.CPYTHON_BUILDDIR }} + env: + CPYTHON_TEST_EXT_NAME: abi3_mod + CPYTHON_TEST_LIMITED: 1 + run: install/usr/bin/python3 -m pip install ../cpython-ro-srcdir/Lib/test/test_cext + - name: Install test free-threaded stable ABI extension + if: inputs.upload-install-hashes + working-directory: ${{ env.CPYTHON_BUILDDIR }} + env: + CPYTHON_TEST_EXT_NAME: abi3t_mod + CPYTHON_TEST_ABI3T: 1 + run: install/usr/bin/python3 -m pip install ../cpython-ro-srcdir/Lib/test/test_cext - name: Hash the installed Python - if: ${{ inputs.upload-install-hashes }} + if: inputs.upload-install-hashes working-directory: ${{ env.CPYTHON_BUILDDIR }} run: >- - build/install/usr/bin/python3 + install/usr/bin/python3 ../cpython-ro-srcdir/Tools/coinstall-check/hash-r.py - build/install > "$INSTALL_HASHES_FILE" - - name: Upload the hashes - if: ${{ inputs.upload-install-hashes }} + install > "$INSTALL_HASHES_FILE" + - name: Upload the installed Python hashes + if: inputs.upload-install-hashes uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ env.INSTALL_HASHES_FILE }} From f17d76172e8ed20a904bd459d8e23b3b7c2490dd Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Fri, 3 Jul 2026 10:40:35 -0400 Subject: [PATCH 15/21] Remove some other unnecessary if: ${{}} wrapping --- .github/workflows/reusable-ubuntu.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index 8055c7d5f911f57..5e3971776f4fe33 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -63,7 +63,7 @@ jobs: - name: Install dependencies run: sudo ./.github/workflows/posix-deps-apt.sh - name: Install Clang and BOLT - if: ${{ fromJSON(inputs.bolt-optimizations) }} + if: fromJSON(inputs.bolt-optimizations) run: | sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" ./llvm.sh 19 sudo apt-get install --no-install-recommends bolt-19 @@ -108,7 +108,7 @@ jobs: ${{ fromJSON(inputs.bolt-optimizations) && '--enable-bolt' || '' }} ${{ fromJSON(inputs.upload-install-hashes) && '--prefix=/usr --libdir=/usr/lib/$(gcc --print-multiarch) --with-build-details-suffix' || '' }} - name: Build CPython out-of-tree - if: ${{ inputs.free-threading }} + if: inputs.free-threading working-directory: ${{ env.CPYTHON_BUILDDIR }} run: make -j - name: Build CPython out-of-tree (for compiler warning check) From 8062995a75c727c5b9193419384399ffeaaf53be Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Fri, 3 Jul 2026 15:43:51 -0400 Subject: [PATCH 16/21] Upload a JSON manifest so that we can do ignores during comparison --- .github/workflows/reusable-ubuntu.yml | 4 +- Tools/coinstall-check/compare.py | 63 ++++++++++++++++++++++++-- Tools/coinstall-check/hash-r.py | 65 ++++++++------------------- 3 files changed, 79 insertions(+), 53 deletions(-) diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index 5e3971776f4fe33..592fd13c1378926 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -50,7 +50,7 @@ jobs: runs-on: ${{ inputs.os }} timeout-minutes: 60 env: - INSTALL_HASHES_FILE: install-hashes-${{ inputs.os }}-${{ case(inputs.free-threading, 't', '')}}${{ case(inputs.debug, 'd', '') }} + INSTALL_HASHES_FILE: install-hashes-${{ inputs.os }}-${{ case(inputs.free-threading, 't', '')}}${{ case(inputs.debug, 'd', '') }}.json.gz OPENSSL_VER: 3.5.7 PYTHONSTRICTEXTENSIONBUILD: 1 TERM: linux @@ -167,7 +167,7 @@ jobs: run: >- install/usr/bin/python3 ../cpython-ro-srcdir/Tools/coinstall-check/hash-r.py - install > "$INSTALL_HASHES_FILE" + install -o "$INSTALL_HASHES_FILE" - name: Upload the installed Python hashes if: inputs.upload-install-hashes uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 diff --git a/Tools/coinstall-check/compare.py b/Tools/coinstall-check/compare.py index 85667a6687b0d3c..af124c18b0cce7d 100755 --- a/Tools/coinstall-check/compare.py +++ b/Tools/coinstall-check/compare.py @@ -5,7 +5,10 @@ # https://www.debian.org/doc/debian-policy/ch-controlfields.html#multi-arch from argparse import ArgumentParser +from typing import Any from pathlib import Path +import gzip +import json def compare_trees(base: Path) -> bool: @@ -17,12 +20,14 @@ def compare_trees(base: Path) -> bool: hashes: dict[str, str] = {} print(f"Examining {tree}") - with tree.open("r") as f: - for line in f: - digest, path = line.strip().split("\t") - hashes[path] = digest + with gzip.open(tree) as f: + data = json.load(f) + build_details = data["build_details"] + hashes = data["hashes"] for path, digest in hashes.items(): + if is_ignored(path, build_details): + continue if path not in seen: seen[path] = digest continue @@ -33,6 +38,56 @@ def compare_trees(base: Path) -> bool: return success +def is_ignored(pathname: str, build_details: dict[str, Any]) -> bool: + """Is this a path that we should ignore?""" + + path = Path(pathname) + + if path.parent.name == "__pycache__": + # Includes a timestamp, we expect a mismatch + return True + + if path.is_relative_to("usr/bin"): + # Only libraries are multi-arch co-installed, only one arch can + # have binaries in /usr/bin at a time. + return True + + in_usr_include = path.is_relative_to("usr/include") + if in_usr_include and path.name == "pyconfig.h": + # Varies according to config, installed into a tag-specific + # include directory + return True + + in_usr_lib = path.is_relative_to("usr/lib") + in_pkgconfig = in_usr_lib and path.parent.name == "pkgconfig" + if in_pkgconfig and path.name in ("python3.pc", "python3-embed.pc"): + # Only the tag-suffixed .pc files are co-installable + return True + + version = build_details["language"]["version"] + if ( + in_pkgconfig + and build_details["abi"]["flags"] # non-default install + and path.name in (f"python-{version}.pc", f"python-{version}-embed.pc") + ): + # Only the tag-suffixed .pc files are co-installable + return True + + in_dist_info = path.parent.name.endswith(".dist-info") + if in_dist_info and path.name in ("RECORD", "WHEEL"): + # RECORD: Contains hashes, not co-installable. + # WHEEL: Contains arch and version tags. Tags can be merged but + # not architectures. + return True + + in_site_packages = path.parent.name == "site-packages" + if in_site_packages and path.name.endswith((".abi3.so", ".abi3t.so")): + # abi3 and abi3t are not current co-installable (#122931) + return True + + return False + + def main() -> None: p = ArgumentParser("Compare multiple hash-r files") p.add_argument( diff --git a/Tools/coinstall-check/hash-r.py b/Tools/coinstall-check/hash-r.py index bca0820f35cb750..a9ad77694f4d842 100755 --- a/Tools/coinstall-check/hash-r.py +++ b/Tools/coinstall-check/hash-r.py @@ -9,6 +9,7 @@ from hashlib import file_digest from pathlib import Path from typing import Any, cast +import gzip import json @@ -21,52 +22,8 @@ def load_build_details(base: Path) -> dict[str, Any]: def hash_tree(base: Path, algorithm: str = "sha512") -> dict[str, str]: hashes: dict[str, str] = {} - build_details = load_build_details(base) - flags = build_details["abi"]["flags"] - version = build_details["language"]["version"] for dirpath, dirnames, filenames in base.walk(): - if dirpath.name == "__pycache__": - # Includes a timestamp, we expect a mismatch - continue - relative_dirpath = dirpath.relative_to(base) - if relative_dirpath.is_relative_to("usr/bin"): - # Only libraries are multi-arch co-installed, only one arch can - # have binaries in /usr/bin at a time. - continue - in_usr_include = relative_dirpath.is_relative_to("usr/include") - in_usr_lib = relative_dirpath.is_relative_to("usr/lib") - in_pkgconfig = in_usr_lib and relative_dirpath.name == "pkgconfig" - in_dist_info = relative_dirpath.name.endswith(".dist-info") - in_site_packages = relative_dirpath.name == "site-packages" - for file in filenames: - if in_usr_include and file == "pyconfig.h": - # Varies according to config, installed into a tag-specific - # include directory - continue - - if in_pkgconfig and file in ("python3.pc", "python3-embed.pc"): - # Only the tag-suffixed .pc files are co-installable - continue - - if ( - in_pkgconfig - and flags # non-default install - and file in (f"python-{version}.pc", f"python-{version}-embed.pc") - ): - # Only the tag-suffixed .pc files are co-installable - continue - - if in_dist_info and file in ("RECORD", "WHEEL"): - # RECORD: Contains hashes, not co-installable. - # WHEEL: Contains arch and version tags. Tags can be merged but - # not architectures. - continue - - if in_site_packages and file.endswith((".abi3.so", ".abi3t.so")): - # abi3 and abi3t are not current co-installable (#122931) - continue - filepath = dirpath / file with filepath.open("rb") as f: digest = file_digest(f, algorithm) @@ -74,17 +31,31 @@ def hash_tree(base: Path, algorithm: str = "sha512") -> dict[str, str]: return hashes +def write_json(destdir: Path, output: Path) -> None: + """Hash the Python install at destdir, write gzipped JSON to output.""" + data = { + "build_details": load_build_details(destdir), + "hashes": hash_tree(destdir), + } + with gzip.open(output, "wt") as f: + f.write(json.dumps(data)) + + def main() -> None: p = ArgumentParser("Hash a python install for comparison later") + p.add_argument( + "-o", + "--output", + type=Path, + help="Output file (gzipped)", + ) p.add_argument( "destdir", type=Path, help="Directory below which Python is installed", ) args = p.parse_args() - hashes = hash_tree(args.destdir) - for path, digest in sorted(hashes.items()): - print(f"{digest}\t{path}") # compatible with sha512sum + write_json(args.destdir, args.output) if __name__ == "__main__": From 7ef137f171990288615909dde929079cb0e39f5b Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Fri, 3 Jul 2026 15:53:42 -0400 Subject: [PATCH 17/21] Build the co-install manifests in the normal test runs --- .github/workflows/build.yml | 33 ++------------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03ac59d789af1d8..a36a377044476a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -260,6 +260,7 @@ jobs: free-threading: ${{ matrix.free-threading }} os: ${{ matrix.os }} test-opts: ${{ matrix.test-opts || '' }} + upload-install-hashes: ${{ !matrix.bolt }} build-ubuntu-ssltests: name: 'Ubuntu SSL tests' @@ -608,40 +609,10 @@ jobs: run: | "$BUILD_DIR/cross-python/bin/python3" -m test test_sysconfig test_site test_embed - linux-install-build: - name: >- - Ubuntu Install - ${{ fromJSON(matrix.free-threading) && '(free-threading)' || '' }} - ${{ fromJSON(matrix.debug) && '(debug)' || '' }} - needs: build-context - if: needs.build-context.outputs.run-ubuntu == 'true' - strategy: - fail-fast: false - matrix: - free-threading: - - false - - true - debug: - - false - - true - os: - - ubuntu-24.04 - include: - - free-threading: false - debug: false - os: ubuntu-24.04-arm - uses: ./.github/workflows/reusable-ubuntu.yml - with: - debug: ${{ matrix.debug }} - free-threading: ${{ matrix.free-threading }} - os: ${{ matrix.os }} - test: false - upload-install-hashes: true - linux-install-compare: name: Ubuntu Co-install comparison runs-on: ubuntu-latest - needs: linux-install-build + needs: build-ubuntu steps: - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: From d4c6100c87a9707100440ac0dd69fdc4bb0f9e0d Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Fri, 3 Jul 2026 16:21:35 -0400 Subject: [PATCH 18/21] Update test_build_details to work with --with-build-details-suffix --- Lib/test/test_build_details.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_build_details.py b/Lib/test/test_build_details.py index 30d9c213077ab75..8d836be1e1ad645 100644 --- a/Lib/test/test_build_details.py +++ b/Lib/test/test_build_details.py @@ -130,7 +130,8 @@ def location(self): dirname = os.path.join(projectdir, f.read()) else: dirname = sysconfig.get_path('stdlib') - return os.path.join(dirname, 'build-details.json') + filename = sysconfig.get_config_var('BUILD_DETAILS') + return os.path.join(dirname, filename) @property def contents(self): From d390e7a22c450a29a7e74a9aa6fce4873360bff0 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Wed, 2 Oct 2024 18:24:57 +0200 Subject: [PATCH 19/21] Strip absolute --libdir paths from configure args in test_freeze We are trying to install into a prefix, any absolute path would not necessarily be writeable. e.g. if Python is configured with --libdir=/usr/lib/$(MULTIARCH)/ during a Debian build. --- Tools/freeze/test/freeze.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tools/freeze/test/freeze.py b/Tools/freeze/test/freeze.py index 1e3687dbb807a69..e8d90796ea38ce6 100644 --- a/Tools/freeze/test/freeze.py +++ b/Tools/freeze/test/freeze.py @@ -128,6 +128,7 @@ def prepare(script=None, outdir=None): # Run configure. print(f'configuring python in {builddir}...') config_args = shlex.split(sysconfig.get_config_var('CONFIG_ARGS') or '') + config_args = [arg for arg in config_args if not arg.startswith("--libdir=/")] cmd = [os.path.join(srcdir, 'configure'), *config_args] ensure_opt(cmd, 'cache-file', os.path.join(outdir, 'python-config.cache')) prefix = os.path.join(outdir, 'python-installation') From 5be0ae75a9704ff5f68a136081eb26023bc5e420 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Fri, 3 Jul 2026 17:07:10 -0400 Subject: [PATCH 20/21] Properly check in all-required-green --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a36a377044476a8..3b4e315f9389c08 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -743,6 +743,7 @@ jobs: build-asan, build-san, cross-build-linux, + linux-install-compare, ' || '' }} From 3eb814b580d228cd0c167a1acc514a6e8bcee536 Mon Sep 17 00:00:00 2001 From: Stefano Rivera Date: Fri, 3 Jul 2026 17:14:14 -0400 Subject: [PATCH 21/21] Drop reusable-ubuntu:inputs.test, no longer needed --- .github/workflows/reusable-ubuntu.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index 592fd13c1378926..a0e47500574e622 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -22,11 +22,6 @@ on: description: OS to run the job required: true type: string - test: - description: Whether to run the Python test suite - required: false - type: boolean - default: true test-opts: description: Extra options to pass to the test runner via TESTOPTS required: false @@ -46,7 +41,7 @@ env: jobs: build-ubuntu-reusable: - name: build ${{ fromJSON(inputs.test) && 'and test ' || '' }}(${{ inputs.os }}) + name: build and test (${{ inputs.os }}) runs-on: ${{ inputs.os }} timeout-minutes: 60 env: @@ -132,7 +127,6 @@ jobs: # some tests write to srcdir, lack of pyc files slows down testing run: sudo mount "$CPYTHON_RO_SRCDIR" -oremount,rw - name: Tests - if: inputs.test working-directory: ${{ env.CPYTHON_BUILDDIR }} run: xvfb-run make ci EXTRATESTOPTS="${TEST_OPTS}" env: