-
-
Notifications
You must be signed in to change notification settings - Fork 34.8k
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?
Changes from all commits
6a37083
15cd434
db6e1a9
216fae0
f20a5b0
f059478
2f5cd99
e6dd647
b84e719
3fce580
66d9cf8
5443e16
ace1e1f
f12b47a
f17d761
8062995
7ef137f
d4c6100
d390e7a
5be0ae7
3eb814b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,20 +8,30 @@ 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 | ||||||||||||||||||||
| 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: '' | ||||||||||||||||||||
| upload-install-hashes: | ||||||||||||||||||||
| description: Install Python and upload the result artifact | ||||||||||||||||||||
| required: false | ||||||||||||||||||||
| type: boolean | ||||||||||||||||||||
| default: false | ||||||||||||||||||||
|
|
||||||||||||||||||||
| permissions: | ||||||||||||||||||||
| contents: read | ||||||||||||||||||||
|
|
@@ -35,6 +45,7 @@ jobs: | |||||||||||||||||||
| runs-on: ${{ inputs.os }} | ||||||||||||||||||||
| timeout-minutes: 60 | ||||||||||||||||||||
| env: | ||||||||||||||||||||
|
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. There's no actual problem right now, but setting |
||||||||||||||||||||
| INSTALL_HASHES_FILE: install-hashes-${{ inputs.os }}-${{ case(inputs.free-threading, 't', '')}}${{ case(inputs.debug, 'd', '') }}.json.gz | ||||||||||||||||||||
|
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. (readability)
Suggested change
|
||||||||||||||||||||
| OPENSSL_VER: 3.5.7 | ||||||||||||||||||||
| PYTHONSTRICTEXTENSIONBUILD: 1 | ||||||||||||||||||||
| TERM: linux | ||||||||||||||||||||
|
|
@@ -47,7 +58,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 | ||||||||||||||||||||
|
|
@@ -84,14 +95,15 @@ 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' || '' }} | ||||||||||||||||||||
| ${{ 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) | ||||||||||||||||||||
|
|
@@ -119,3 +131,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 }} | ||||||||||||||||||||
| 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 👍🏼 |
||
|
|
||
| # 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,104 @@ | ||
| #!/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 | ||
|
|
||
| from argparse import ArgumentParser | ||
| from typing import Any | ||
| from pathlib import Path | ||
| import gzip | ||
| import json | ||
|
|
||
|
|
||
| def compare_trees(base: Path) -> bool: | ||
| seen: dict[str, str] = {} | ||
| success: bool = True | ||
| for tree in base.iterdir(): | ||
| if not tree.is_file(): | ||
| continue | ||
|
|
||
| hashes: dict[str, str] = {} | ||
| print(f"Examining {tree}") | ||
| 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 | ||
| if digest != seen[path]: | ||
| print(f"Mismatch found in {tree}: {path}") | ||
| print(f"{digest} != {seen[path]}") | ||
| 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 | ||
|
|
||
| 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: | ||
|
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. It looks like either here, within |
||
| 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_trees(args.base_directory): | ||
| raise SystemExit(1) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| #!/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 | ||
| from typing import Any, cast | ||
| import gzip | ||
| import json | ||
|
|
||
|
|
||
| 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( | ||
| "-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() | ||
| write_json(args.destdir, args.output) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is unrelated but is probably fine provided it's backported into the older branches.