Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 35.2k
gh-122931 CI for Linux multiarch co-installability#152831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
6a3708315cd434db6e1a9216fae0f20a5b0f0594782f5cd99e6dd647b84e7193fce58066d9cf85443e16ace1e1ff12b47af17d76180629957ef137fd4c6100d390e7a5be0ae73eb814b356df50d758215058c925621a078e3ff3e5e0735a420b87b644531f9dec761521b64327d503e832f21fe46d5263e0567f9981cbdf02e180efb9ec690bce099abe3f10727b533273088b1968b919770ca4eba5adc09cdce65dc25dbc8fbe8f9af8639c1392c4065ab4cFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| name: Reusable Ubuntu co-install comparison | ||
| on: | ||
| workflow_call: {} | ||
| permissions: | ||
| contents: read | ||
| env: | ||
| FORCE_COLOR: 1 | ||
| jobs: | ||
| install-compare: | ||
| name: Ubuntu co-install comparison | ||
| runs-on: ubuntu-latest | ||
| 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 install hashes | ||
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | ||
| with: | ||
| path: install-hashes | ||
| pattern: install-hashes-* | ||
| merge-multiple: true | ||
| - name: Compare install hashes | ||
| run: python3 Tools/coinstall-check/compare.py install-hashes |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -22,6 +22,11 @@ on: | ||
| required: false | ||
| type: string | ||
| default: '' | ||
| upload-install-hashes: | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this input is always passed exactly opposite to ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They may currently be opposite, but I think they are orthogonal, I wouldn't want to compute one from the other.
I could imagine adding a check that aborts a build if | ||
| description: Install Python and upload the result artifact | ||
| required: false | ||
| type: boolean | ||
| default: false | ||
| permissions: | ||
| contents: read | ||
| @@ -35,6 +40,12 @@ jobs: | ||
| runs-on: ${{ inputs.os }} | ||
| timeout-minutes: 60 | ||
| env: | ||
stefanor marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| INSTALL_HASHES_FILE: >- | ||
| install-hashes-${{ | ||
| inputs.os | ||
| }}-${{ | ||
| case(inputs.free-threading, 't', '') | ||
| }}d.json.gz | ||
| OPENSSL_VER: 3.5.7 | ||
| PYTHONSTRICTEXTENSIONBUILD: 1 | ||
| TERM: linux | ||
| @@ -52,6 +63,8 @@ jobs: | ||
| # On ubuntu-26.04 image, LLVM is LLVM-21 by default | ||
| sudo apt-get install --no-install-recommends bolt-21 | ||
| echo PATH="$(llvm-config-21 --bindir):$PATH" >> $GITHUB_ENV | ||
| - name: Reproducible install timestamps | ||
| run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> $GITHUB_ENV | ||
| - name: Configure OpenSSL env vars | ||
| run: | | ||
| echo "MULTISSL_DIR=${GITHUB_WORKSPACE}/multissl" >> "$GITHUB_ENV" | ||
| @@ -90,6 +103,7 @@ jobs: | ||
| --with-openssl="$OPENSSL_DIR" | ||
| ${{ inputs.free-threading && '--disable-gil' || '' }} | ||
| ${{ inputs.bolt-optimizations && '--enable-bolt' || '' }} | ||
| ${{ 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 }} | ||
| @@ -119,3 +133,42 @@ jobs: | ||
| run: xvfb-run make ci EXTRATESTOPTS="${TEST_OPTS}" | ||
| env: | ||
| TEST_OPTS: ${{ inputs.test-opts }} | ||
| - name: Install Python | ||
| if: inputs.upload-install-hashes | ||
| working-directory: ${{ env.CPYTHON_BUILDDIR }} | ||
| run: make install DESTDIR=install | ||
| - name: Install test C extension | ||
| if: inputs.upload-install-hashes | ||
| working-directory: ${{ env.CPYTHON_BUILDDIR }} | ||
| 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 | ||
| working-directory: ${{ env.CPYTHON_BUILDDIR }} | ||
| run: >- | ||
| install/usr/bin/python3 | ||
| ../cpython-ro-srcdir/Tools/coinstall-check/hash-r.py | ||
| install -o "$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 }} | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this also expose a wildcard for the consumers to look up the artifacts? Though, this probably won't work with a matrix 🤔 | ||
| path: ${{ env.CPYTHON_BUILDDIR }}/${{ env.INSTALL_HASHES_FILE }} | ||
| archive: false | ||
| retention-days: 1 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -107,6 +107,8 @@ def main(): | ||
| if internal: | ||
| cflags.append('-DTEST_INTERNAL_C_API=1') | ||
| py_limited_api = limited or abi3t | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks unrelated to the purpose of this PR, but is a correctness fix, so 👍🏼 ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was necessary to tell setuptools to produce abi3/abi3t filenames. | ||
| # 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]) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| CI Tests to ensure Debian `multi-arch co-installability | ||
| <https://www.debian.org/doc/debian-policy/ch-controlfields.html#multi-arch>`__ | ||
| of Python. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| # 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 | ||
| from argparse import ArgumentParser | ||
| from pathlib import Path | ||
| from typing import Any | ||
| import _colorize | ||
| import gzip | ||
| import json | ||
| def compare_install_manifests(base: Path) -> bool: | ||
| """Compare all json manifests inside the directory at base.""" | ||
| hashes_seen: dict[str, tuple[str, str]] = {} | ||
| tags_seen_by_platform: dict[str, set[frozenset[str]]] = {} | ||
| colors = _colorize.get_colors() | ||
| success: bool = True | ||
| for tree in base.iterdir(): | ||
| if not tree.is_file(): | ||
| continue | ||
| print(f"Examining {tree}") | ||
| with gzip.open(tree) as f: | ||
| data = json.load(f) | ||
| build_details = data["build_details"] | ||
| hashes = data["hashes"] | ||
| tags_seen_by_platform.setdefault(build_details["platform"], set()).add( | ||
| frozenset(build_details["abi"]["flags"]) | ||
| ) | ||
| for path, digest in hashes.items(): | ||
| if is_ignored(path, build_details): | ||
| continue | ||
| if path not in hashes_seen: | ||
| hashes_seen[path] = (digest, tree.name) | ||
| continue | ||
| expected, source_name = hashes_seen[path] | ||
| if digest != expected: | ||
| print(f"{colors.RED}Mismatch found{colors.RESET}: {path}") | ||
| print(f"{digest} ({tree.name}) != {expected} ({source_name})") | ||
| success = False | ||
stefanor marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| # Did we see enough builds to make a useful comparison? | ||
| if len(tags_seen_by_platform) < 2: | ||
| print( | ||
| f"{colors.RED}ERROR{colors.RESET}: Insufficient platforms " | ||
| "(architectures) to compare. Expected >= 2." | ||
| ) | ||
| success = False | ||
| for tagsets in tags_seen_by_platform.values(): | ||
| if len(tagsets) >= 2: | ||
| break | ||
| else: | ||
| print( | ||
| f"{colors.RED}ERROR{colors.RESET}: Insufficient configuration " | ||
| f"variants tested. Expected >= 2." | ||
| ) | ||
| success = False | ||
| 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 | ||
| return False | ||
| def main() -> None: | ||
stefanor marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| p = ArgumentParser("Compare multiple hash-r files") | ||
| p.add_argument( | ||
| "base_directory", | ||
| type=Path, | ||
| help="Directory containing hashes of Python installs.", | ||
| ) | ||
| args = p.parse_args() | ||
| if not compare_install_manifests(args.base_directory): | ||
| raise SystemExit(1) | ||
stefanor marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| colors = _colorize.get_colors() | ||
| print(f"{args.base_directory} {colors.GREEN}OK{colors.RESET}") | ||
| if __name__ == "__main__": | ||
| main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # Export a SHA-512 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 | ||
| import gzip | ||
| import json | ||
| from argparse import ArgumentParser | ||
| from hashlib import file_digest | ||
| from pathlib import Path | ||
| from typing import Any, cast | ||
| def load_build_details(base: Path) -> dict[str, Any]: | ||
| for path in base.glob("usr/lib/python*/build-details*.json"): | ||
| details = json.loads(path.read_bytes()) | ||
| return cast(dict[str, Any], details) | ||
| raise AssertionError(f"build-details.json not found in {base}") | ||
| def hash_tree(base: Path, algorithm: str = "sha512") -> dict[str, str]: | ||
| hashes: dict[str, str] = {} | ||
| for dirpath, dirnames, filenames in base.walk(): | ||
| for file in filenames: | ||
| 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 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( | ||
| "destdir", | ||
| type=Path, | ||
| help="Directory below which Python is installed", | ||
| ) | ||
stefanor marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| p.add_argument( | ||
| "-o", | ||
| "--output", | ||
| type=Path, | ||
| help="Output file (gzipped)", | ||
| required=True, | ||
| ) | ||
| args = p.parse_args() | ||
| write_json(args.destdir, args.output) | ||
| if __name__ == "__main__": | ||
| main() | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.