From 0c7d08f60b05a134da16ca4d6a8b551c8ad0e441 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Thu, 7 Aug 2025 11:23:05 -0500 Subject: [PATCH 01/26] nfpm.native_libs.scripts: add aiohttp requirement This commit was rebased on main. Before rebase, the lockfile was regenerated. After rebase, the lockfile regeneration is batched into a single follow-up commit. --- 3rdparty/python/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/3rdparty/python/requirements.txt b/3rdparty/python/requirements.txt index 6dcc90a3555..38518284aae 100644 --- a/3rdparty/python/requirements.txt +++ b/3rdparty/python/requirements.txt @@ -33,6 +33,7 @@ node-semver==0.9.0 # These dependencies are for scripts that rules run in an external process (and for script tests). +aiohttp==3.12.15 # see: pants.backends.nfpm.native_libs.scripts elfdeps==0.2.0 # see: pants.backends.nfpm.native_libs.elfdeps # These dependencies are only for debugging Pants itself (in VSCode/PyCharm respectively), # and should never be imported. From 950b563ef9b758ae7d8b42842292bbd18aad5820 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Fri, 8 Aug 2025 16:19:37 -0500 Subject: [PATCH 02/26] nfpm.native_libs.scripts: Add deb_search_for_sonames with tests includes: - fix flake8 C413 Unnecessary list call around sorted( - add type annotation for mypy --- .../backend/nfpm/native_libs/scripts/BUILD | 8 + .../nfpm/native_libs/scripts/__init__.py | 0 .../scripts/deb_search_for_sonames.py | 177 ++++++++++++++++++ ...deb_search_for_sonames_integration_test.py | 54 ++++++ .../scripts/deb_search_for_sonames_test.py | 38 ++++ 5 files changed, 277 insertions(+) create mode 100644 src/python/pants/backend/nfpm/native_libs/scripts/BUILD create mode 100644 src/python/pants/backend/nfpm/native_libs/scripts/__init__.py create mode 100644 src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py create mode 100644 src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py create mode 100644 src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_test.py diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/BUILD b/src/python/pants/backend/nfpm/native_libs/scripts/BUILD new file mode 100644 index 00000000000..2e7c6d32018 --- /dev/null +++ b/src/python/pants/backend/nfpm/native_libs/scripts/BUILD @@ -0,0 +1,8 @@ +# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +python_sources() + +python_tests( + name="tests", +) diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/__init__.py b/src/python/pants/backend/nfpm/native_libs/scripts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py new file mode 100644 index 00000000000..f834d7f3948 --- /dev/null +++ b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py @@ -0,0 +1,177 @@ +# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +import argparse +import asyncio +import json +import sys +from collections import defaultdict +from collections.abc import Generator, Iterable + +import aiohttp +from bs4 import BeautifulSoup + +DISTRO_PACKAGE_SEARCH_URL = { + "debian": "https://packages.debian.org/search", + "ubuntu": "https://packages.ubuntu.com/search", +} + + +async def deb_search_for_sonames( + distro: str, + distro_codename: str, + debian_arch: str, + sonames: Iterable[str], +) -> set[str]: + """Given a soname, lookup the deb package that provides it. + + Tools like 'apt-get -S' and 'apt-file' only work for the host's active distro and distro + version. This code, however, should be able to run on any host even non-debian and non-ubuntu + hosts. So, it uses an API call instead of local tooling. + """ + search_url = DISTRO_PACKAGE_SEARCH_URL[distro] + + # tasks are IO bound + async with aiohttp.ClientSession() as client, asyncio.TaskGroup() as tg: + tasks = [ + tg.create_task( + deb_search_for_soname(client, search_url, distro_codename, debian_arch, soname) + ) + for soname in sonames + ] + + # result parsing is CPU bound + packages: set[str] = set() + for task in tasks: + html_doc = task.result() + packages.update(package for package, _ in deb_packages_from_html_response(html_doc)) + + return packages + + +async def deb_search_for_soname( + http: aiohttp.ClientSession, + search_url: str, + distro_codename: str, + debian_arch: str, + soname: str, +) -> str: + """Use API to search for deb packages that contain soname. + + This HTTP+HTML package search API, sadly, does not support any format other than HTML (not JSON, + YAML, etc). + """ + # https://salsa.debian.org/webmaster-team/packages/-/blob/master/SEARCHES?ref_type=heads#L110-136 + query_params = { + "format": "html", # sadly, this API only supports format=html. + "searchon": "contents", + "mode": "exactfilename", # soname should be exact filename. + # mode=="" means find files where `filepath.endswith(keyword)` + # mode=="filename" means find files where `keyword in filename` + # mode=="exactfilename" means find files where `filename==keyword` + "arch": debian_arch, + "suite": distro_codename, + "keywords": soname, + } + + async with http.get(search_url, params=query_params) as response: + # response.status is 200 even if there was an error (like bad distro_codename), + # unless the service is unavailable which happens somewhat frequently. + response.raise_for_status() # TODO: retry this flaky API a few times instead of raising + + # sadly the "API" returns html and does not support other formats. + html_doc = await response.text() + + return html_doc + + +def deb_packages_from_html_response( + html_doc: str, +) -> Generator[tuple[str, tuple[str, ...]]]: + """Extract deb packages from an HTML search response. + + This uses beautifulsoup to parse the search API's HTML responses with logic that is very similar + to the MIT licensed apt-search CLI tool. This does not use apt-search directly because it is not + meant to be a library, and it hardcodes the ubuntu package search URL. https://github.com/david- + haerer/apt-search + """ + + # inspiration from (MIT licensed): + # https://github.com/david-haerer/apt-search/blob/main/apt_search/main.py + # (this script handles more API edge cases than apt-search and creates structured data) + + soup = BeautifulSoup(html_doc, "html.parser") + + # .table means 'search for a tag'. The response should only have one. + # In xmlpath, descending would look like one of these: + # /html/body/div[1]/div[3]/div[2]/table + # /html/body/div[@id="wrapper"]/div[@id="content"]/div[@id="pcontentsres"]/table + results_table = soup.table + + if results_table is None: + # No package(s) found + return + + # results_table is basically (nb: " [amd64] " is only present for arch=any and packages can be a list): + #
+ # + # + # + # + # + # + # + # + # + #
FilePackages
/usr/lib/x86_64-linux-gnu/libldap-2.5.so.0libldap-2.5.0 [amd64]
/usr/sbin/dnsmasqdnsmasq-base, dnsmasq-base-lua
+ # But, html is semi-structured, so assume that it can be in a broken state. + + packages2files: dict[str, list[str]] = defaultdict(list) + for row in results_table.find_all("tr"): + cells = tuple(row.find_all("td")) + if len(cells) < 2: + # ignore malformed rows with missing cell(s). + continue + file_cell, pkgs_cell = cells[:2] + file_text = file_cell.get_text(strip=True) + packages = [pkg_a.get_text(strip=True) for pkg_a in pkgs_cell.find_all("a")] + for package in packages: + packages2files[package].append(file_text) + + for package in sorted(packages2files): + yield package, tuple(packages2files[package]) + + return + + +def main() -> int: + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument( + "--distro", default="ubuntu", choices=tuple(DISTRO_PACKAGE_SEARCH_URL.keys()) + ) + arg_parser.add_argument("--distro-codename", required=True) + arg_parser.add_argument("--arch", default="amd64") + arg_parser.add_argument("sonames", nargs="+") + options = arg_parser.parse_args() + + packages = asyncio.get_event_loop().run_until_complete( + deb_search_for_sonames( + distro=options.distro, + distro_codename=options.distro_codename, + debian_arch=options.arch, + sonames=tuple(options.sonames), + ) + ) + + if not packages: + return 1 + + print(json.dumps(sorted(packages), indent=None, separators=(",", ":"))) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py new file mode 100644 index 00000000000..ce79f3d0a84 --- /dev/null +++ b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py @@ -0,0 +1,54 @@ +# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +import pytest + +from .deb_search_for_sonames import deb_search_for_sonames + + +@pytest.mark.parametrize( + "distro,distro_codename,debian_arch,sonames,expected", + ( + pytest.param("debian", "bookworm", "amd64", ("libldap-2.5.so.0",), {"libldap-2.5-0"}), + pytest.param("debian", "bookworm", "arm64", ("libldap-2.5.so.0",), {"libldap-2.5-0"}), + pytest.param("ubuntu", "jammy", "amd64", ("libldap-2.5.so.0",), {"libldap-2.5-0"}), + pytest.param("ubuntu", "jammy", "arm64", ("libldap-2.5.so.0",), {"libldap-2.5-0"}), + pytest.param( + "ubuntu", "foobar", "amd64", ("libldap-2.5.so.0",), set(), id="bad distro_codename" + ), + pytest.param( + "ubuntu", "jammy", "foobar", ("libldap-2.5.so.0",), set(), id="bad debian_arch" + ), + pytest.param("ubuntu", "jammy", "amd64", ("foobarbaz-9.9.so.9",), set(), id="bad soname"), + pytest.param( + "ubuntu", + "jammy", + "amd64", + ("libcurl.so",), # the search api returns a table like this: + # ------------------------------------------- | ----------------------------------------------------------- | + # File | Packages | + # ------------------------------------------- | ----------------------------------------------------------- | + # /usr/lib/cupt4-2/downloadmethods/libcurl.so | libcupt4-2-downloadmethod-curl | + # /usr/lib/x86_64-linux-gnu/libcurl.so | libcurl4-gnutls-dev, libcurl4-nss-dev, libcurl4-openssl-dev | + # ------------------------------------------- | ----------------------------------------------------------- | + { + "libcupt4-2-downloadmethod-curl", + "libcurl4-gnutls-dev", + "libcurl4-nss-dev", + "libcurl4-openssl-dev", + }, + id="same file in multiple packages", + ), + ), +) +async def test_deb_search_for_sonames( + distro: str, + distro_codename: str, + debian_arch: str, + sonames: tuple[str, ...], + expected: set[str], +): + result = await deb_search_for_sonames(distro, distro_codename, debian_arch, sonames) + assert result == expected diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_test.py b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_test.py new file mode 100644 index 00000000000..92af91d949e --- /dev/null +++ b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_test.py @@ -0,0 +1,38 @@ +# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +from .deb_search_for_sonames import deb_packages_from_html_response + +# simplified for readability and to keep it focused +SAMPLE_HTML_RESPONSE = """ + + + + +
...
+ + + + + + + + + + +
FilePackages
/usr/lib/x86_64-linux-gnu/libldap-2.5.so.0libldap-2.5.0 [amd64]
/usr/sbin/dnsmasqdnsmasq-base, dnsmasq-base-lua
+
...
+ + +""" + + +def test_deb_packages_from_html_response(): + results = list(deb_packages_from_html_response(SAMPLE_HTML_RESPONSE)) + assert results == [ + ("dnsmasq-base", ("/usr/sbin/dnsmasq",)), + ("dnsmasq-base-lua", ("/usr/sbin/dnsmasq",)), + ("libldap-2.5.0", ("/usr/lib/x86_64-linux-gnu/libldap-2.5.so.0",)), + ] From 06433076468a4eb27a297e89ba2da30e6379e3b0 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Wed, 8 Oct 2025 20:42:43 -0500 Subject: [PATCH 03/26] nfpm.native_libs: add deb_search_for_sonames rule The rule runs the deb_search_for_sonames.py script. It pulls the pex requirements from the pants venv so that the script runs with the same version of python and a subset of the dists used to run pants itself. This way, the deps are only defined once. --- .../pants/backend/nfpm/native_libs/rules.py | 106 +++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/src/python/pants/backend/nfpm/native_libs/rules.py b/src/python/pants/backend/nfpm/native_libs/rules.py index cfc7a8a83f1..2128e74b93a 100644 --- a/src/python/pants/backend/nfpm/native_libs/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/rules.py @@ -3,6 +3,9 @@ from __future__ import annotations +import importlib.metadata +import json +import sys from collections.abc import Iterable from dataclasses import dataclass @@ -19,13 +22,114 @@ InjectNfpmPackageFieldsRequest, ) from pants.backend.python.goals.package_pex_binary import PexBinaryFieldSet, package_pex_binary -from pants.backend.python.util_rules.pex import Pex, create_pex +from pants.backend.python.util_rules.pex import Pex, PexRequest, VenvPexProcess, create_pex, create_venv_pex +from pants.backend.python.util_rules.pex_environment import PythonExecutable from pants.backend.python.util_rules.pex_from_targets import PexFromTargetsRequest +from pants.backend.python.util_rules.pex_requirements import PexRequirements from pants.engine.addresses import Address +from pants.engine.fs import CreateDigest, FileContent from pants.engine.internals.selectors import concurrently +from pants.engine.intrinsics import create_digest +from pants.engine.process import ProcessResult, execute_process_or_raise from pants.engine.rules import Rule, collect_rules, implicitly, rule from pants.engine.target import Field, Target from pants.engine.unions import UnionMembership, UnionRule +from pants.init.import_util import find_matching_distributions +from pants.util.logging import LogLevel +from pants.util.resources import read_resource + +_SCRIPTS_PACKAGE = "pants.backend.nfpm.native_libs.scripts" +_DEB_SEARCH_FOR_SONAMES_SCRIPT = "deb_search_for_sonames.py" +_PEX_NAME = "native_libs_scripts.pex" + + +@dataclass(frozen=True) +class DebSearchForSonamesRequest: + distro: str + distro_codename: str + debian_arch: str + sonames: tuple[str, ...] + + def __init__(self, distro: str, distro_codename: str, debian_arch: str, sonames: Iterable[str]): + object.__setattr__(self, "distro", distro) + object.__setattr__(self, "distro_codename", distro_codename) + object.__setattr__(self, "debian_arch", debian_arch) + object.__setattr__(self, "sonames", tuple(sorted(sonames))) + + +@dataclass(frozen=True) +class DebPackagesForSonames: + packages: tuple[str, ...] + + def __init__(self, packages: Iterable[str]): + object.__setattr__(self, "packages", tuple(sorted(packages))) + + +@rule +async def deb_search_for_sonames( + request: DebSearchForSonamesRequest, +) -> DebPackagesForSonames: + script = read_resource(_SCRIPTS_PACKAGE, _DEB_SEARCH_FOR_SONAMES_SCRIPT) + if not script: + raise ValueError( + f"Unable to find source of {_DEB_SEARCH_FOR_SONAMES_SCRIPT!r} in {_SCRIPTS_PACKAGE}" + ) + + script_content = FileContent( + path=_DEB_SEARCH_FOR_SONAMES_SCRIPT, content=script, is_executable=True + ) + + # Pull python and requirements versions from the pants venv since that is what the script is tested with. + pants_python = PythonExecutable.fingerprinted( + sys.executable, ".".join(map(str, sys.version_info[:3])).encode("utf8") + ) + distributions_in_pants_venv: list[importlib.metadata.Distribution] = list( + find_matching_distributions(None) + ) + constraints = tuple(f"{dist.name}=={dist.version}" for dist in distributions_in_pants_venv) + requirements = { # requirements (and transitive deps) are constrained to the versions in the pants venv + "aiohttp", + "beautifulsoup4", + } + + script_digest, venv_pex = await concurrently( + create_digest(CreateDigest([script_content])), + create_venv_pex( + **implicitly( + PexRequest( + output_filename=_PEX_NAME, + internal_only=True, + python=pants_python, + requirements=PexRequirements( + requirements, + constraints_strings=constraints, + description_of_origin=f"Requirements for {_PEX_NAME}:{_DEB_SEARCH_FOR_SONAMES_SCRIPT}", + ), + ) + ) + ), + ) + + result: ProcessResult = await execute_process_or_raise( + **implicitly( + VenvPexProcess( + venv_pex, + argv=( + script_content.path, + f"--distro={request.distro}", + f"--distro-codename={request.distro_codename}", + f"--arch={request.debian_arch}", + *request.sonames, + ), + input_digest=script_digest, + description=f"Search deb packages for sonames: {request.sonames}", + level=LogLevel.DEBUG, + ) + ) + ) + + packages = json.loads(result.stdout) + return DebPackagesForSonames(packages) @dataclass(frozen=True) From 5908911cce7b49c25e726103331f84b6fff98e02 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Wed, 8 Oct 2025 22:38:26 -0500 Subject: [PATCH 04/26] nfpm.native_libs: add integration test for scripts.deb_search_for_sonames rule This is in the scripts integration test file instead of the one for rules to facilitate sharing TEST_CASES for both tests. --- .../pants/backend/nfpm/native_libs/BUILD | 4 +- .../pants/backend/nfpm/native_libs/rules.py | 16 ++- .../scripts/deb_search_for_sonames.py | 6 + ...deb_search_for_sonames_integration_test.py | 111 ++++++++++++------ 4 files changed, 97 insertions(+), 40 deletions(-) diff --git a/src/python/pants/backend/nfpm/native_libs/BUILD b/src/python/pants/backend/nfpm/native_libs/BUILD index 2e7c6d32018..13c13da8692 100644 --- a/src/python/pants/backend/nfpm/native_libs/BUILD +++ b/src/python/pants/backend/nfpm/native_libs/BUILD @@ -1,7 +1,9 @@ # Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -python_sources() +python_sources( + overrides={"rules.py": dict(dependencies=["./scripts"])}, +) python_tests( name="tests", diff --git a/src/python/pants/backend/nfpm/native_libs/rules.py b/src/python/pants/backend/nfpm/native_libs/rules.py index 2128e74b93a..7a31a5ba23a 100644 --- a/src/python/pants/backend/nfpm/native_libs/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/rules.py @@ -5,6 +5,7 @@ import importlib.metadata import json +import logging import sys from collections.abc import Iterable from dataclasses import dataclass @@ -29,8 +30,8 @@ from pants.engine.addresses import Address from pants.engine.fs import CreateDigest, FileContent from pants.engine.internals.selectors import concurrently -from pants.engine.intrinsics import create_digest -from pants.engine.process import ProcessResult, execute_process_or_raise +from pants.engine.intrinsics import create_digest, execute_process +from pants.engine.process import FallibleProcessResult from pants.engine.rules import Rule, collect_rules, implicitly, rule from pants.engine.target import Field, Target from pants.engine.unions import UnionMembership, UnionRule @@ -38,6 +39,8 @@ from pants.util.logging import LogLevel from pants.util.resources import read_resource +logger = logging.getLogger(__name__) + _SCRIPTS_PACKAGE = "pants.backend.nfpm.native_libs.scripts" _DEB_SEARCH_FOR_SONAMES_SCRIPT = "deb_search_for_sonames.py" _PEX_NAME = "native_libs_scripts.pex" @@ -110,7 +113,7 @@ async def deb_search_for_sonames( ), ) - result: ProcessResult = await execute_process_or_raise( + result: FallibleProcessResult = await execute_process( **implicitly( VenvPexProcess( venv_pex, @@ -128,7 +131,12 @@ async def deb_search_for_sonames( ) ) - packages = json.loads(result.stdout) + if result.exit_code == 0: + packages = json.loads(result.stdout) + else: + logger.warning(result.stderr) + packages = () + return DebPackagesForSonames(packages) diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py index f834d7f3948..ed055836d80 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py +++ b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py @@ -166,6 +166,12 @@ def main() -> int: ) if not packages: + print("[]") + print( + f"No {options.distro} {options.distro_codename} ({options.arch}) packages" + f" found for sonames: {options.sonames}", + file=sys.stderr, + ) return 1 print(json.dumps(sorted(packages), indent=None, separators=(",", ":"))) diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py index ce79f3d0a84..bbeaa5a816d 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py +++ b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py @@ -3,52 +3,93 @@ from __future__ import annotations +import sys + import pytest -from .deb_search_for_sonames import deb_search_for_sonames +from pants.backend.python.util_rules import pex_from_targets +from pants.engine.rules import QueryRule +from pants.testutil.rule_runner import RuleRunner +from ..rules import DebPackagesForSonames, DebSearchForSonamesRequest +from ..rules import rules as native_libs_rules +from .deb_search_for_sonames import deb_search_for_sonames -@pytest.mark.parametrize( - "distro,distro_codename,debian_arch,sonames,expected", - ( - pytest.param("debian", "bookworm", "amd64", ("libldap-2.5.so.0",), {"libldap-2.5-0"}), - pytest.param("debian", "bookworm", "arm64", ("libldap-2.5.so.0",), {"libldap-2.5-0"}), - pytest.param("ubuntu", "jammy", "amd64", ("libldap-2.5.so.0",), {"libldap-2.5-0"}), - pytest.param("ubuntu", "jammy", "arm64", ("libldap-2.5.so.0",), {"libldap-2.5-0"}), - pytest.param( - "ubuntu", "foobar", "amd64", ("libldap-2.5.so.0",), set(), id="bad distro_codename" - ), - pytest.param( - "ubuntu", "jammy", "foobar", ("libldap-2.5.so.0",), set(), id="bad debian_arch" - ), - pytest.param("ubuntu", "jammy", "amd64", ("foobarbaz-9.9.so.9",), set(), id="bad soname"), - pytest.param( - "ubuntu", - "jammy", - "amd64", - ("libcurl.so",), # the search api returns a table like this: - # ------------------------------------------- | ----------------------------------------------------------- | - # File | Packages | - # ------------------------------------------- | ----------------------------------------------------------- | - # /usr/lib/cupt4-2/downloadmethods/libcurl.so | libcupt4-2-downloadmethod-curl | - # /usr/lib/x86_64-linux-gnu/libcurl.so | libcurl4-gnutls-dev, libcurl4-nss-dev, libcurl4-openssl-dev | - # ------------------------------------------- | ----------------------------------------------------------- | - { - "libcupt4-2-downloadmethod-curl", - "libcurl4-gnutls-dev", - "libcurl4-nss-dev", - "libcurl4-openssl-dev", - }, - id="same file in multiple packages", +TEST_CASES = ( + pytest.param("debian", "bookworm", "amd64", ("libldap-2.5.so.0",), ("libldap-2.5-0",)), + pytest.param("debian", "bookworm", "arm64", ("libldap-2.5.so.0",), ("libldap-2.5-0",)), + pytest.param("ubuntu", "jammy", "amd64", ("libldap-2.5.so.0",), ("libldap-2.5-0",)), + pytest.param("ubuntu", "jammy", "arm64", ("libldap-2.5.so.0",), ("libldap-2.5-0",)), + pytest.param("ubuntu", "foobar", "amd64", ("libldap-2.5.so.0",), (), id="bad distro_codename"), + pytest.param("ubuntu", "jammy", "foobar", ("libldap-2.5.so.0",), (), id="bad debian_arch"), + pytest.param("ubuntu", "jammy", "amd64", ("foobarbaz-9.9.so.9",), (), id="bad soname"), + pytest.param( + "ubuntu", + "jammy", + "amd64", + ("libcurl.so",), # the search api returns a table like this: + # ------------------------------------------- | ----------------------------------------------------------- | + # File | Packages | + # ------------------------------------------- | ----------------------------------------------------------- | + # /usr/lib/cupt4-2/downloadmethods/libcurl.so | libcupt4-2-downloadmethod-curl | + # /usr/lib/x86_64-linux-gnu/libcurl.so | libcurl4-gnutls-dev, libcurl4-nss-dev, libcurl4-openssl-dev | + # ------------------------------------------- | ----------------------------------------------------------- | + ( + "libcupt4-2-downloadmethod-curl", + "libcurl4-gnutls-dev", + "libcurl4-nss-dev", + "libcurl4-openssl-dev", ), + id="same file in multiple packages", ), ) + + +@pytest.mark.parametrize("distro,distro_codename,debian_arch,sonames,expected", TEST_CASES) async def test_deb_search_for_sonames( distro: str, distro_codename: str, debian_arch: str, sonames: tuple[str, ...], - expected: set[str], + expected: tuple[str, ...], ): result = await deb_search_for_sonames(distro, distro_codename, debian_arch, sonames) - assert result == expected + assert result == set(expected) + + +@pytest.fixture +def rule_runner() -> RuleRunner: + rule_runner = RuleRunner( + rules=[ + *pex_from_targets.rules(), + *native_libs_rules(), + QueryRule(DebPackagesForSonames, (DebSearchForSonamesRequest,)), + ], + ) + + # The rule builds a pex with wheels for the pants venv. + _py_version = ".".join(map(str, sys.version_info[:3])) + + rule_runner.set_options( + [ + f"--python-interpreter-constraints=['CPython=={_py_version}']", + ], + env_inherit={"PATH", "PYENV_ROOT", "HOME"}, + ) + return rule_runner + + +@pytest.mark.parametrize("distro,distro_codename,debian_arch,sonames,expected", TEST_CASES) +def test_deb_search_for_sonames_rule( + distro: str, + distro_codename: str, + debian_arch: str, + sonames: tuple[str, ...], + expected: tuple[str, ...], + rule_runner: RuleRunner, +) -> None: + result = rule_runner.request( + DebPackagesForSonames, + [DebSearchForSonamesRequest(distro, distro_codename, debian_arch, sonames)], + ) + assert result.packages == expected From 7ae7baa5900c533a0c055381d70464125d1bb3db Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Fri, 10 Oct 2025 20:39:21 -0500 Subject: [PATCH 05/26] nfpm.native_libs.scripts: Refactor to preserve API response data We need to select the relevant package(s) based on the .so file path, following (a simplified set of) standard LD lookup rules. This commit updates the deb_search_for_sonames script and rule so that .so file names are available for that selection process. --- .../pants/backend/nfpm/native_libs/rules.py | 44 ++++- .../backend/nfpm/native_libs/scripts/BUILD | 1 + .../scripts/deb_search_for_sonames.py | 30 ++- ...deb_search_for_sonames_integration_test.py | 178 ++++++++++++++++-- .../scripts/deb_search_for_sonames_test.py | 5 +- 5 files changed, 215 insertions(+), 43 deletions(-) diff --git a/src/python/pants/backend/nfpm/native_libs/rules.py b/src/python/pants/backend/nfpm/native_libs/rules.py index 7a31a5ba23a..420cdb582ce 100644 --- a/src/python/pants/backend/nfpm/native_libs/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/rules.py @@ -7,7 +7,7 @@ import json import logging import sys -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from dataclasses import dataclass from pants.backend.nfpm.field_sets import NfpmRpmPackageFieldSet @@ -61,13 +61,47 @@ def __init__(self, distro: str, distro_codename: str, debian_arch: str, sonames: @dataclass(frozen=True) -class DebPackagesForSonames: +class DebPackagesForSoFile: + so_file: str packages: tuple[str, ...] - def __init__(self, packages: Iterable[str]): + def __init__(self, so_file: str, packages: Iterable[str]): + object.__setattr__(self, "so_file", so_file) object.__setattr__(self, "packages", tuple(sorted(packages))) +@dataclass(frozen=True) +class DebPackagesForSoname: + soname: str + packages_for_so_files: tuple[DebPackagesForSoFile, ...] + + def __init__(self, soname: str, packages_for_so_files: Iterable[DebPackagesForSoFile]): + object.__setattr__(self, "soname", soname) + object.__setattr__(self, "packages_for_so_files", tuple(packages_for_so_files)) + + # TODO: method to select best so_file for a soname + + +@dataclass(frozen=True) +class DebPackagesForSonames: + packages: tuple[DebPackagesForSoname, ...] + + @classmethod + def from_dict(cls, raw: Mapping[str, Mapping[str, Iterable[str]]]) -> DebPackagesForSonames: + return cls( + tuple( + DebPackagesForSoname( + soname, + ( + DebPackagesForSoFile(so_file, packages) + for so_file, packages in files_to_packages.items() + ), + ) + for soname, files_to_packages in raw.items() + ) + ) + + @rule async def deb_search_for_sonames( request: DebSearchForSonamesRequest, @@ -135,9 +169,9 @@ async def deb_search_for_sonames( packages = json.loads(result.stdout) else: logger.warning(result.stderr) - packages = () + packages = {} - return DebPackagesForSonames(packages) + return DebPackagesForSonames.from_dict(packages) @dataclass(frozen=True) diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/BUILD b/src/python/pants/backend/nfpm/native_libs/scripts/BUILD index 2e7c6d32018..481ee281882 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/BUILD +++ b/src/python/pants/backend/nfpm/native_libs/scripts/BUILD @@ -5,4 +5,5 @@ python_sources() python_tests( name="tests", + overrides={"deb_search_for_sonames_integration_test.py": dict(timeout=150)}, ) diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py index ed055836d80..f025c4ad5c3 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py +++ b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py @@ -24,7 +24,7 @@ async def deb_search_for_sonames( distro_codename: str, debian_arch: str, sonames: Iterable[str], -) -> set[str]: +) -> dict[str, dict[str, list[str]]]: """Given a soname, lookup the deb package that provides it. Tools like 'apt-get -S' and 'apt-file' only work for the host's active distro and distro @@ -35,20 +35,23 @@ async def deb_search_for_sonames( # tasks are IO bound async with aiohttp.ClientSession() as client, asyncio.TaskGroup() as tg: - tasks = [ - tg.create_task( + tasks = { + soname: tg.create_task( deb_search_for_soname(client, search_url, distro_codename, debian_arch, soname) ) for soname in sonames - ] + } # result parsing is CPU bound - packages: set[str] = set() - for task in tasks: + packages: defaultdict[str, dict[str, list[str]]] = defaultdict(dict) + for soname, task in tasks.items(): html_doc = task.result() - packages.update(package for package, _ in deb_packages_from_html_response(html_doc)) + for so_file, so_packages in deb_packages_from_html_response(html_doc): + packages[soname][so_file] = list( + so_packages + ) # list makes json serialization more predicatable - return packages + return dict(packages) async def deb_search_for_soname( @@ -128,7 +131,6 @@ def deb_packages_from_html_response( # # But, html is semi-structured, so assume that it can be in a broken state. - packages2files: dict[str, list[str]] = defaultdict(list) for row in results_table.find_all("tr"): cells = tuple(row.find_all("td")) if len(cells) < 2: @@ -137,11 +139,7 @@ def deb_packages_from_html_response( file_cell, pkgs_cell = cells[:2] file_text = file_cell.get_text(strip=True) packages = [pkg_a.get_text(strip=True) for pkg_a in pkgs_cell.find_all("a")] - for package in packages: - packages2files[package].append(file_text) - - for package in sorted(packages2files): - yield package, tuple(packages2files[package]) + yield file_text, tuple(packages) return @@ -166,7 +164,7 @@ def main() -> int: ) if not packages: - print("[]") + print("{}") print( f"No {options.distro} {options.distro_codename} ({options.arch}) packages" f" found for sonames: {options.sonames}", @@ -174,7 +172,7 @@ def main() -> int: ) return 1 - print(json.dumps(sorted(packages), indent=None, separators=(",", ":"))) + print(json.dumps(packages, indent=None, separators=(",", ":"))) return 0 diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py index bbeaa5a816d..c6711cfc78c 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py +++ b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py @@ -15,14 +15,133 @@ from ..rules import rules as native_libs_rules from .deb_search_for_sonames import deb_search_for_sonames +_libldap_soname = "libldap-2.5.so.0" +_libldap_so_file = "/usr/lib/{}-linux-gnu/" + _libldap_soname +_libldap_pkg = "libldap-2.5-0" + +_libc6_soname = "libc.so.6" + + +def _libc6_pkgs(_arch) -> dict[str, list[str]]: + return dict( + sorted( + { + f"/lib/libc6-prof/{_arch}-linux-gnu/{_libc6_soname}": ["libc6-prof"], + f"/lib/{_arch}-linux-gnu/{_libc6_soname}": ["libc6"], + }.items() + ) + ) + + +_libc6_pkgs_amd64 = { # only x86_64 not aarch64 + "/lib32/" + _libc6_soname: ["libc6-i386"], + "/libx32/" + _libc6_soname: ["libc6-x32"], +} +_libc6_cross_pkgs = { + f"/usr/{cross_machine}-linux-{cross_os_lib}/lib{cross_bits}/{_libc6_soname}": [ + f"libc6-{cross_arch}-cross" + ] + for cross_machine, cross_os_lib, cross_bits, cross_arch in [ + ("aarch64", "gnu", "", "arm64"), + ("arm", "gnueabi", "", "armel"), + ("arm", "gnueabihf", "", "armhf"), + ("hppa", "gnu", "", "hppa"), + ("i686", "gnu", "", "i386"), + ("i686", "gnu", "64", "amd64-i386"), + ("i686", "gnu", "x32", "x32-i386"), + ("m68k", "gnu", "", "m68k"), + ("mips", "gnu", "", "mips"), + ("mips", "gnu", "32", "mipsn32-mips"), + ("mips", "gnu", "64", "mips64-mips"), + ("mips64", "gnuabi64", "", "mips64"), + ("mips64", "gnuabi64", "32", "mipsn32-mips64"), + ("mips64", "gnuabi64", "o32", "mips32-mips64"), + ("mips64", "gnuabin32", "", "mipsn32"), + ("mips64", "gnuabin32", "64", "mips64-mipsn32"), + ("mips64", "gnuabin32", "o32", "mips32-mipsn32"), + ("mips64el", "gnuabi64", "", "mips64el"), + ("mips64el", "gnuabi64", "32", "mipsn32-mips64el"), + ("mips64el", "gnuabi64", "o32", "mips32-mips64el"), + ("mips64el", "gnuabin32", "", "mipsn32el"), + ("mips64el", "gnuabin32", "64", "mips64-mipsn32el"), + ("mips64el", "gnuabin32", "o32", "mips32-mipsn32el"), + ("mipsel", "gnu", "", "mipsel"), + ("mipsel", "gnu", "32", "mipsn32-mipsel"), + ("mipsel", "gnu", "64", "mips64-mipsel"), + ("mipsisa32r6", "gnu", "", "mipsr6"), + ("mipsisa32r6", "gnu", "32", "mipsn32-mipsr6"), + ("mipsisa32r6", "gnu", "64", "mips64-mipsr6"), + ("mipsisa32r6el", "gnu", "", "mipsr6el"), + ("mipsisa32r6el", "gnu", "32", "mipsn32-mipsr6el"), + ("mipsisa32r6el", "gnu", "64", "mips64-mipsr6el"), + ("mipsisa64r6", "gnuabi64", "", "mips64r6"), + ("mipsisa64r6", "gnuabi64", "32", "mipsn32-mips64r6"), + ("mipsisa64r6", "gnuabi64", "o32", "mips32-mips64r6"), + ("mipsisa64r6", "gnuabin32", "", "mipsn32r6"), + ("mipsisa64r6", "gnuabin32", "64", "mips64-mipsn32r6"), + ("mipsisa64r6", "gnuabin32", "o32", "mips32-mipsn32r6"), + ("mipsisa64r6el", "gnuabi64", "", "mips64r6el"), + ("mipsisa64r6el", "gnuabi64", "32", "mipsn32-mips64r6el"), + ("mipsisa64r6el", "gnuabi64", "o32", "mips32-mips64r6el"), + ("mipsisa64r6el", "gnuabin32", "", "mipsn32r6el"), + ("mipsisa64r6el", "gnuabin32", "64", "mips64-mipsn32r6el"), + ("mipsisa64r6el", "gnuabin32", "o32", "mips32-mipsn32r6el"), + ("powerpc", "gnu", "", "powerpc"), + ("powerpc", "gnu", "64", "ppc64-powerpc"), + ("powerpc64", "gnu", "", "ppc64"), + ("powerpc64", "gnu", "32", "powerpc-ppc64"), + ("powerpc64le", "gnu", "", "ppc64el"), + ("riscv64", "gnu", "", "riscv64"), + ("s390x", "gnu", "", "s390x"), + ("s390x", "gnu", "32", "s390-s390x"), + ("sh4", "gnu", "", "sh4"), + ("sparc64", "gnu", "", "sparc64"), + ("sparc64", "gnu", "32", "sparc-sparc64"), + ("x86_64", "gnu", "", "amd64"), + ("x86_64", "gnu", "32", "i386-amd64"), + ("x86_64", "gnu", "x32", "x32-amd64"), + ("x86_64", "gnux32", "", "x32"), + ("x86_64", "gnux32", "32", "i386-x32"), + ("x86_64", "gnux32", "64", "amd64-x32"), + ] +} + TEST_CASES = ( - pytest.param("debian", "bookworm", "amd64", ("libldap-2.5.so.0",), ("libldap-2.5-0",)), - pytest.param("debian", "bookworm", "arm64", ("libldap-2.5.so.0",), ("libldap-2.5-0",)), - pytest.param("ubuntu", "jammy", "amd64", ("libldap-2.5.so.0",), ("libldap-2.5-0",)), - pytest.param("ubuntu", "jammy", "arm64", ("libldap-2.5.so.0",), ("libldap-2.5-0",)), - pytest.param("ubuntu", "foobar", "amd64", ("libldap-2.5.so.0",), (), id="bad distro_codename"), - pytest.param("ubuntu", "jammy", "foobar", ("libldap-2.5.so.0",), (), id="bad debian_arch"), - pytest.param("ubuntu", "jammy", "amd64", ("foobarbaz-9.9.so.9",), (), id="bad soname"), + pytest.param( + "debian", + "bookworm", + "amd64", + (_libldap_soname,), + {_libldap_soname: {_libldap_so_file.format("x86_64"): [_libldap_pkg]}}, + id="debian-amd64-libldap", + ), + pytest.param( + "debian", + "bookworm", + "arm64", + (_libldap_soname,), + {_libldap_soname: {_libldap_so_file.format("aarch64"): [_libldap_pkg]}}, + id="debian-arm64-libldap", + ), + pytest.param( + "ubuntu", + "jammy", + "amd64", + (_libldap_soname,), + {_libldap_soname: {_libldap_so_file.format("x86_64"): [_libldap_pkg]}}, + id="ubuntu-amd64-libldap", + ), + pytest.param( + "ubuntu", + "jammy", + "arm64", + (_libldap_soname,), + {_libldap_soname: {_libldap_so_file.format("aarch64"): [_libldap_pkg]}}, + id="ubuntu-arm64-libldap", + ), + pytest.param("ubuntu", "foobar", "amd64", (_libldap_soname,), {}, id="bad-distro_codename"), + pytest.param("ubuntu", "jammy", "foobar", (_libldap_soname,), {}, id="bad-debian_arch"), + pytest.param("ubuntu", "jammy", "amd64", ("foobarbaz-9.9.so.9",), {}, id="bad-soname"), pytest.param( "ubuntu", "jammy", @@ -34,13 +153,33 @@ # /usr/lib/cupt4-2/downloadmethods/libcurl.so | libcupt4-2-downloadmethod-curl | # /usr/lib/x86_64-linux-gnu/libcurl.so | libcurl4-gnutls-dev, libcurl4-nss-dev, libcurl4-openssl-dev | # ------------------------------------------- | ----------------------------------------------------------- | - ( - "libcupt4-2-downloadmethod-curl", - "libcurl4-gnutls-dev", - "libcurl4-nss-dev", - "libcurl4-openssl-dev", - ), - id="same file in multiple packages", + { + "libcurl.so": { + "/usr/lib/cupt4-2/downloadmethods/libcurl.so": ["libcupt4-2-downloadmethod-curl"], + "/usr/lib/x86_64-linux-gnu/libcurl.so": [ + "libcurl4-gnutls-dev", + "libcurl4-nss-dev", + "libcurl4-openssl-dev", + ], + } + }, + id="same-file-in-multiple-packages", + ), + pytest.param( + "ubuntu", + "jammy", + "amd64", + (_libc6_soname,), + {_libc6_soname: _libc6_pkgs("x86_64") | _libc6_pkgs_amd64 | _libc6_cross_pkgs}, + id="ubuntu-amd64-libc6", + ), + pytest.param( + "ubuntu", + "jammy", + "arm64", + (_libc6_soname,), + {_libc6_soname: _libc6_pkgs("aarch64") | _libc6_cross_pkgs}, + id="ubuntu-arm64-libc6", ), ) @@ -51,10 +190,10 @@ async def test_deb_search_for_sonames( distro_codename: str, debian_arch: str, sonames: tuple[str, ...], - expected: tuple[str, ...], + expected: dict[str, dict[str, list[str]]], ): result = await deb_search_for_sonames(distro, distro_codename, debian_arch, sonames) - assert result == set(expected) + assert result == expected @pytest.fixture @@ -79,17 +218,18 @@ def rule_runner() -> RuleRunner: return rule_runner -@pytest.mark.parametrize("distro,distro_codename,debian_arch,sonames,expected", TEST_CASES) +@pytest.mark.parametrize("distro,distro_codename,debian_arch,sonames,expected_raw", TEST_CASES) def test_deb_search_for_sonames_rule( distro: str, distro_codename: str, debian_arch: str, sonames: tuple[str, ...], - expected: tuple[str, ...], + expected_raw: dict[str, dict[str, list[str]]], rule_runner: RuleRunner, ) -> None: + expected = DebPackagesForSonames.from_dict(expected_raw) result = rule_runner.request( DebPackagesForSonames, [DebSearchForSonamesRequest(distro, distro_codename, debian_arch, sonames)], ) - assert result.packages == expected + assert result == expected diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_test.py b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_test.py index 92af91d949e..3acaf82cc8f 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_test.py +++ b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_test.py @@ -32,7 +32,6 @@ def test_deb_packages_from_html_response(): results = list(deb_packages_from_html_response(SAMPLE_HTML_RESPONSE)) assert results == [ - ("dnsmasq-base", ("/usr/sbin/dnsmasq",)), - ("dnsmasq-base-lua", ("/usr/sbin/dnsmasq",)), - ("libldap-2.5.0", ("/usr/lib/x86_64-linux-gnu/libldap-2.5.so.0",)), + ("/usr/lib/x86_64-linux-gnu/libldap-2.5.so.0", ("libldap-2.5.0",)), + ("/usr/sbin/dnsmasq", ("dnsmasq-base", "dnsmasq-base-lua")), ] From 92df36e7f15932b4d2e54c0693548c6c6ebc8906 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Fri, 10 Oct 2025 20:55:25 -0500 Subject: [PATCH 06/26] nfpm.native_libs.scripts: Add aiohttp-retry requirement Also registers script dep on aiohttp-retry This commit was rebased. Lockfile was regenerated before rebase, but is batched up with other regenerations after the rebase. --- 3rdparty/python/requirements.txt | 1 + src/python/pants/backend/nfpm/native_libs/rules.py | 1 + 2 files changed, 2 insertions(+) diff --git a/3rdparty/python/requirements.txt b/3rdparty/python/requirements.txt index 38518284aae..35985c11739 100644 --- a/3rdparty/python/requirements.txt +++ b/3rdparty/python/requirements.txt @@ -34,6 +34,7 @@ node-semver==0.9.0 # These dependencies are for scripts that rules run in an external process (and for script tests). aiohttp==3.12.15 # see: pants.backends.nfpm.native_libs.scripts +aiohttp-retry==2.9.1 # see: pants.backends.nfpm.native_libs.scripts elfdeps==0.2.0 # see: pants.backends.nfpm.native_libs.elfdeps # These dependencies are only for debugging Pants itself (in VSCode/PyCharm respectively), # and should never be imported. diff --git a/src/python/pants/backend/nfpm/native_libs/rules.py b/src/python/pants/backend/nfpm/native_libs/rules.py index 420cdb582ce..1adc12667d4 100644 --- a/src/python/pants/backend/nfpm/native_libs/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/rules.py @@ -126,6 +126,7 @@ async def deb_search_for_sonames( constraints = tuple(f"{dist.name}=={dist.version}" for dist in distributions_in_pants_venv) requirements = { # requirements (and transitive deps) are constrained to the versions in the pants venv "aiohttp", + "aiohttp-retry", "beautifulsoup4", } From 3c8d6cd1105186b711a6eb8fbe74a18b12119def Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Fri, 10 Oct 2025 22:13:39 -0500 Subject: [PATCH 07/26] nfpm.native_libs.scripts: Use aiohttp-retry on flaky API NOTE: order of client and task group in async with block is very important. The task group needs to be the innermost block so that it can appropriately wait for the task group to finish. Putting the client as the innermost block closes the client before any of the tasks can finish leading to a bunch of errors returned by the task group. --- src/python/pants/backend/nfpm/native_libs/rules.py | 2 +- .../native_libs/scripts/deb_search_for_sonames.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/python/pants/backend/nfpm/native_libs/rules.py b/src/python/pants/backend/nfpm/native_libs/rules.py index 1adc12667d4..f29760bd0e0 100644 --- a/src/python/pants/backend/nfpm/native_libs/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/rules.py @@ -169,7 +169,7 @@ async def deb_search_for_sonames( if result.exit_code == 0: packages = json.loads(result.stdout) else: - logger.warning(result.stderr) + logger.warning(result.stderr.decode("utf-8")) packages = {} return DebPackagesForSonames.from_dict(packages) diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py index f025c4ad5c3..dab48efc74e 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py +++ b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py @@ -11,6 +11,8 @@ from collections.abc import Generator, Iterable import aiohttp +import aiohttp_retry +from aiohttp_retry.types import ClientType from bs4 import BeautifulSoup DISTRO_PACKAGE_SEARCH_URL = { @@ -34,7 +36,14 @@ async def deb_search_for_sonames( search_url = DISTRO_PACKAGE_SEARCH_URL[distro] # tasks are IO bound - async with aiohttp.ClientSession() as client, asyncio.TaskGroup() as tg: + async with ( + aiohttp_retry.RetryClient( + retry_options=aiohttp_retry.JitterRetry(attempts=5), + # version=aiohttp.HttpVersion11, # aiohttp does not support HTTP/2 (waiting for contribution) + # timeout=aiohttp.ClientTimeout(total=5 * 60, sock_connect=30), + ) as client, + asyncio.TaskGroup() as tg, # client must be before tg in this async with block + ): tasks = { soname: tg.create_task( deb_search_for_soname(client, search_url, distro_codename, debian_arch, soname) @@ -55,7 +64,7 @@ async def deb_search_for_sonames( async def deb_search_for_soname( - http: aiohttp.ClientSession, + http: ClientType, search_url: str, distro_codename: str, debian_arch: str, From fb242607b73408a088eb4766dcdaa21119b9139d Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Fri, 10 Oct 2025 22:15:58 -0500 Subject: [PATCH 08/26] nfpm.native_libs.scripts: Customize User-Agent header --- src/python/pants/backend/nfpm/native_libs/rules.py | 2 ++ .../nfpm/native_libs/scripts/deb_search_for_sonames.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/python/pants/backend/nfpm/native_libs/rules.py b/src/python/pants/backend/nfpm/native_libs/rules.py index f29760bd0e0..d4b9bb5f20b 100644 --- a/src/python/pants/backend/nfpm/native_libs/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/rules.py @@ -38,6 +38,7 @@ from pants.init.import_util import find_matching_distributions from pants.util.logging import LogLevel from pants.util.resources import read_resource +from pants.version import VERSION logger = logging.getLogger(__name__) @@ -154,6 +155,7 @@ async def deb_search_for_sonames( venv_pex, argv=( script_content.path, + f"--user-agent-suffix=pants/{VERSION}", f"--distro={request.distro}", f"--distro-codename={request.distro_codename}", f"--arch={request.debian_arch}", diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py index dab48efc74e..0d4ab8bcc54 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py +++ b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py @@ -12,6 +12,7 @@ import aiohttp import aiohttp_retry +from aiohttp.http import SERVER_SOFTWARE as DEFAULT_USER_AGENT from aiohttp_retry.types import ClientType from bs4 import BeautifulSoup @@ -26,6 +27,7 @@ async def deb_search_for_sonames( distro_codename: str, debian_arch: str, sonames: Iterable[str], + user_agent: str = DEFAULT_USER_AGENT, ) -> dict[str, dict[str, list[str]]]: """Given a soname, lookup the deb package that provides it. @@ -39,6 +41,7 @@ async def deb_search_for_sonames( async with ( aiohttp_retry.RetryClient( retry_options=aiohttp_retry.JitterRetry(attempts=5), + headers={aiohttp.hdrs.USER_AGENT: user_agent}, # version=aiohttp.HttpVersion11, # aiohttp does not support HTTP/2 (waiting for contribution) # timeout=aiohttp.ClientTimeout(total=5 * 60, sock_connect=30), ) as client, @@ -155,6 +158,7 @@ def deb_packages_from_html_response( def main() -> int: arg_parser = argparse.ArgumentParser() + arg_parser.add_argument("--user-agent-suffix") arg_parser.add_argument( "--distro", default="ubuntu", choices=tuple(DISTRO_PACKAGE_SEARCH_URL.keys()) ) @@ -163,12 +167,18 @@ def main() -> int: arg_parser.add_argument("sonames", nargs="+") options = arg_parser.parse_args() + user_agent_suffix = options.user_agent_suffix + user_agent = ( + DEFAULT_USER_AGENT if not user_agent_suffix else f"{DEFAULT_USER_AGENT} {user_agent_suffix}" + ) + packages = asyncio.get_event_loop().run_until_complete( deb_search_for_sonames( distro=options.distro, distro_codename=options.distro_codename, debian_arch=options.arch, sonames=tuple(options.sonames), + user_agent=user_agent, ) ) From ff024ec2677c09cb91add53d76d057fccce370d3 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Fri, 10 Oct 2025 22:44:57 -0500 Subject: [PATCH 09/26] nfpm.native_libs.scripts: Move TODO about search API errors --- src/python/pants/backend/nfpm/native_libs/rules.py | 3 +++ .../backend/nfpm/native_libs/scripts/deb_search_for_sonames.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/python/pants/backend/nfpm/native_libs/rules.py b/src/python/pants/backend/nfpm/native_libs/rules.py index d4b9bb5f20b..dd6a378e523 100644 --- a/src/python/pants/backend/nfpm/native_libs/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/rules.py @@ -171,6 +171,9 @@ async def deb_search_for_sonames( if result.exit_code == 0: packages = json.loads(result.stdout) else: + # The search API returns 200 even if no results were found. + # A 4xx or 5xx error means we gave up retrying because the server is unavailable. + # TODO: Should this raise an error instead of just a warning? logger.warning(result.stderr.decode("utf-8")) packages = {} diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py index 0d4ab8bcc54..acd75bb0b27 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py +++ b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py @@ -94,7 +94,7 @@ async def deb_search_for_soname( async with http.get(search_url, params=query_params) as response: # response.status is 200 even if there was an error (like bad distro_codename), # unless the service is unavailable which happens somewhat frequently. - response.raise_for_status() # TODO: retry this flaky API a few times instead of raising + response.raise_for_status() # That was the last retry. Give up and alert the user. # sadly the "API" returns html and does not support other formats. html_doc = await response.text() From fd8f2d92f7b1d9f853539475bdac03a74f6748c2 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Sat, 11 Oct 2025 17:02:17 -0500 Subject: [PATCH 10/26] nfpm.native_libs.scripts: Add from_best_so_files ld.so-like filtering This only adds package dependencies without any version constraints. It might be possible to add a version constraint based on the so_version. But, this is good enough for now. --- .../pants/backend/nfpm/native_libs/rules.py | 79 ++++++++++++++++--- ...deb_search_for_sonames_integration_test.py | 72 ++++++++++++----- 2 files changed, 120 insertions(+), 31 deletions(-) diff --git a/src/python/pants/backend/nfpm/native_libs/rules.py b/src/python/pants/backend/nfpm/native_libs/rules.py index dd6a378e523..b2c131a916e 100644 --- a/src/python/pants/backend/nfpm/native_libs/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/rules.py @@ -8,7 +8,8 @@ import logging import sys from collections.abc import Iterable, Mapping -from dataclasses import dataclass +from dataclasses import dataclass, replace +from pathlib import PurePath from pants.backend.nfpm.field_sets import NfpmRpmPackageFieldSet from pants.backend.nfpm.fields.rpm import NfpmRpmDependsField, NfpmRpmProvidesField @@ -53,16 +54,26 @@ class DebSearchForSonamesRequest: distro_codename: str debian_arch: str sonames: tuple[str, ...] - - def __init__(self, distro: str, distro_codename: str, debian_arch: str, sonames: Iterable[str]): + from_best_so_files: bool + + def __init__( + self, + distro: str, + distro_codename: str, + debian_arch: str, + sonames: Iterable[str], + *, + from_best_so_files: bool = False, + ): object.__setattr__(self, "distro", distro) object.__setattr__(self, "distro_codename", distro_codename) object.__setattr__(self, "debian_arch", debian_arch) object.__setattr__(self, "sonames", tuple(sorted(sonames))) + object.__setattr__(self, "from_best_so_files", from_best_so_files) @dataclass(frozen=True) -class DebPackagesForSoFile: +class DebPackagesPerSoFile: so_file: str packages: tuple[str, ...] @@ -71,21 +82,57 @@ def __init__(self, so_file: str, packages: Iterable[str]): object.__setattr__(self, "packages", tuple(sorted(packages))) +_TYPICAL_LD_PATH_PATTERNS = ( + # platform specific system libs (like libc) get selected first + # "/usr/local/lib/*-linux-*/", + "/lib/*-linux-*/", + "/usr/lib/*-linux-*/", + # Then look for a generic system libs + # "/usr/local/lib/", + "/lib/", + "/usr/lib/", + # Anything else has to be added manually to dependencies. + # These rules cannot use symbols or shlibs metadata to inform package selection. +) + + @dataclass(frozen=True) class DebPackagesForSoname: soname: str - packages_for_so_files: tuple[DebPackagesForSoFile, ...] + packages_per_so_files: tuple[DebPackagesPerSoFile, ...] - def __init__(self, soname: str, packages_for_so_files: Iterable[DebPackagesForSoFile]): + def __init__(self, soname: str, packages_per_so_files: Iterable[DebPackagesPerSoFile]): object.__setattr__(self, "soname", soname) - object.__setattr__(self, "packages_for_so_files", tuple(packages_for_so_files)) + object.__setattr__(self, "packages_per_so_files", tuple(packages_per_so_files)) + + @property + def from_best_so_files(self) -> DebPackagesForSoname: + """Pick best so_files from packages_for_so_files using a simplified ld.so-like algorithm. - # TODO: method to select best so_file for a soname + The most preferred is first. This is NOT a recursive match; Only match if direct child of + ld_path_patt dir. Anything that uses a subdir like /usr/lib//lib*.so.* uses rpath to + prefer the app's libs over system libs. If this vastly simplified form of ld.so-style + matching does not select the correct libs, then the package(s) that provide the shared lib + should be added manually to the nfpm requires field. + """ + if len(self.packages_per_so_files) <= 1: # shortcut; no filtering required for 0-1 results. + return self + + remaining = list(self.packages_per_so_files) + + packages_per_so_files = [] + for ld_path_patt in _TYPICAL_LD_PATH_PATTERNS: + for packages_per_so_file in remaining[:]: + if PurePath(packages_per_so_file.so_file).parent.match(ld_path_patt): + packages_per_so_files.append(packages_per_so_file) + remaining.remove(packages_per_so_file) + + return replace(self, packages_per_so_files=tuple(packages_per_so_files)) @dataclass(frozen=True) class DebPackagesForSonames: - packages: tuple[DebPackagesForSoname, ...] + packages_for_sonames: tuple[DebPackagesForSoname, ...] @classmethod def from_dict(cls, raw: Mapping[str, Mapping[str, Iterable[str]]]) -> DebPackagesForSonames: @@ -94,7 +141,7 @@ def from_dict(cls, raw: Mapping[str, Mapping[str, Iterable[str]]]) -> DebPackage DebPackagesForSoname( soname, ( - DebPackagesForSoFile(so_file, packages) + DebPackagesPerSoFile(so_file, packages) for so_file, packages in files_to_packages.items() ), ) @@ -102,6 +149,13 @@ def from_dict(cls, raw: Mapping[str, Mapping[str, Iterable[str]]]) -> DebPackage ) ) + @property + def from_best_so_files(self) -> DebPackagesForSonames: + packages = [] + for packages_for_soname in self.packages_for_sonames: + packages.append(packages_for_soname.from_best_so_files) + return DebPackagesForSonames(tuple(packages)) + @rule async def deb_search_for_sonames( @@ -177,7 +231,10 @@ async def deb_search_for_sonames( logger.warning(result.stderr.decode("utf-8")) packages = {} - return DebPackagesForSonames.from_dict(packages) + deb_packages_for_sonames = DebPackagesForSonames.from_dict(packages) + if request.from_best_so_files: + return deb_packages_for_sonames.from_best_so_files + return deb_packages_for_sonames @dataclass(frozen=True) diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py index c6711cfc78c..cfb467cf151 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py +++ b/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py @@ -4,6 +4,7 @@ from __future__ import annotations import sys +from typing import Any import pytest @@ -22,15 +23,12 @@ _libc6_soname = "libc.so.6" -def _libc6_pkgs(_arch) -> dict[str, list[str]]: - return dict( - sorted( - { - f"/lib/libc6-prof/{_arch}-linux-gnu/{_libc6_soname}": ["libc6-prof"], - f"/lib/{_arch}-linux-gnu/{_libc6_soname}": ["libc6"], - }.items() - ) - ) +def _libc6_pkgs(_arch: str, prof: bool = True) -> dict[str, list[str]]: + pkgs = {} + if prof: + pkgs[f"/lib/libc6-prof/{_arch}-linux-gnu/{_libc6_soname}"] = ["libc6-prof"] + pkgs[f"/lib/{_arch}-linux-gnu/{_libc6_soname}"] = ["libc6"] + return dict(sorted(pkgs.items())) _libc6_pkgs_amd64 = { # only x86_64 not aarch64 @@ -113,6 +111,7 @@ def _libc6_pkgs(_arch) -> dict[str, list[str]]: "amd64", (_libldap_soname,), {_libldap_soname: {_libldap_so_file.format("x86_64"): [_libldap_pkg]}}, + None, # from_best_so_files is the same result id="debian-amd64-libldap", ), pytest.param( @@ -121,6 +120,7 @@ def _libc6_pkgs(_arch) -> dict[str, list[str]]: "arm64", (_libldap_soname,), {_libldap_soname: {_libldap_so_file.format("aarch64"): [_libldap_pkg]}}, + None, # from_best_so_files is the same result id="debian-arm64-libldap", ), pytest.param( @@ -129,6 +129,7 @@ def _libc6_pkgs(_arch) -> dict[str, list[str]]: "amd64", (_libldap_soname,), {_libldap_soname: {_libldap_so_file.format("x86_64"): [_libldap_pkg]}}, + None, # from_best_so_files is the same result id="ubuntu-amd64-libldap", ), pytest.param( @@ -137,11 +138,14 @@ def _libc6_pkgs(_arch) -> dict[str, list[str]]: "arm64", (_libldap_soname,), {_libldap_soname: {_libldap_so_file.format("aarch64"): [_libldap_pkg]}}, + None, # from_best_so_files is the same result id="ubuntu-arm64-libldap", ), - pytest.param("ubuntu", "foobar", "amd64", (_libldap_soname,), {}, id="bad-distro_codename"), - pytest.param("ubuntu", "jammy", "foobar", (_libldap_soname,), {}, id="bad-debian_arch"), - pytest.param("ubuntu", "jammy", "amd64", ("foobarbaz-9.9.so.9",), {}, id="bad-soname"), + pytest.param( + "ubuntu", "foobar", "amd64", (_libldap_soname,), {}, None, id="bad-distro_codename" + ), + pytest.param("ubuntu", "jammy", "foobar", (_libldap_soname,), {}, None, id="bad-debian_arch"), + pytest.param("ubuntu", "jammy", "amd64", ("foobarbaz-9.9.so.9",), {}, None, id="bad-soname"), pytest.param( "ubuntu", "jammy", @@ -163,6 +167,15 @@ def _libc6_pkgs(_arch) -> dict[str, list[str]]: ], } }, + { # from_best_so_files is NOT the same result + "libcurl.so": { + "/usr/lib/x86_64-linux-gnu/libcurl.so": [ + "libcurl4-gnutls-dev", + "libcurl4-nss-dev", + "libcurl4-openssl-dev", + ], + } + }, id="same-file-in-multiple-packages", ), pytest.param( @@ -171,6 +184,7 @@ def _libc6_pkgs(_arch) -> dict[str, list[str]]: "amd64", (_libc6_soname,), {_libc6_soname: _libc6_pkgs("x86_64") | _libc6_pkgs_amd64 | _libc6_cross_pkgs}, + {_libc6_soname: _libc6_pkgs("x86_64", prof=False)}, id="ubuntu-amd64-libc6", ), pytest.param( @@ -179,18 +193,20 @@ def _libc6_pkgs(_arch) -> dict[str, list[str]]: "arm64", (_libc6_soname,), {_libc6_soname: _libc6_pkgs("aarch64") | _libc6_cross_pkgs}, + {_libc6_soname: _libc6_pkgs("aarch64", prof=False)}, id="ubuntu-arm64-libc6", ), ) -@pytest.mark.parametrize("distro,distro_codename,debian_arch,sonames,expected", TEST_CASES) +@pytest.mark.parametrize("distro,distro_codename,debian_arch,sonames,expected,_", TEST_CASES) async def test_deb_search_for_sonames( distro: str, distro_codename: str, debian_arch: str, sonames: tuple[str, ...], expected: dict[str, dict[str, list[str]]], + _: Any, # unused. This is for the next test. ): result = await deb_search_for_sonames(distro, distro_codename, debian_arch, sonames) assert result == expected @@ -218,18 +234,34 @@ def rule_runner() -> RuleRunner: return rule_runner -@pytest.mark.parametrize("distro,distro_codename,debian_arch,sonames,expected_raw", TEST_CASES) +@pytest.mark.parametrize( + "distro,distro_codename,debian_arch,sonames,expected_raw,expected_raw_from_best_so_files", + TEST_CASES, +) def test_deb_search_for_sonames_rule( distro: str, distro_codename: str, debian_arch: str, sonames: tuple[str, ...], expected_raw: dict[str, dict[str, list[str]]], + expected_raw_from_best_so_files: None | dict[str, dict[str, list[str]]], rule_runner: RuleRunner, ) -> None: - expected = DebPackagesForSonames.from_dict(expected_raw) - result = rule_runner.request( - DebPackagesForSonames, - [DebSearchForSonamesRequest(distro, distro_codename, debian_arch, sonames)], - ) - assert result == expected + for from_best_so_files, _expected_raw in ( + (False, expected_raw), + (True, expected_raw_from_best_so_files or expected_raw), + ): + expected = DebPackagesForSonames.from_dict(_expected_raw) + result = rule_runner.request( + DebPackagesForSonames, + [ + DebSearchForSonamesRequest( + distro, + distro_codename, + debian_arch, + sonames, + from_best_so_files=from_best_so_files, + ) + ], + ) + assert result == expected From 945664bfedef4bd70be2b8e11acff8840b52fe82 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Sat, 11 Oct 2025 17:13:21 -0500 Subject: [PATCH 11/26] nfpm.native_libs: rename package scripts->deb Next commit will move rules into that package --- 3rdparty/python/requirements.txt | 4 ++-- src/python/pants/backend/nfpm/native_libs/BUILD | 2 +- .../pants/backend/nfpm/native_libs/{scripts => deb}/BUILD | 2 +- .../backend/nfpm/native_libs/{scripts => deb}/__init__.py | 0 .../deb_search_for_sonames.py => deb/search_for_sonames.py} | 0 .../search_for_sonames_integration_test.py} | 2 +- .../search_for_sonames_test.py} | 2 +- src/python/pants/backend/nfpm/native_libs/rules.py | 4 ++-- 8 files changed, 8 insertions(+), 8 deletions(-) rename src/python/pants/backend/nfpm/native_libs/{scripts => deb}/BUILD (69%) rename src/python/pants/backend/nfpm/native_libs/{scripts => deb}/__init__.py (100%) rename src/python/pants/backend/nfpm/native_libs/{scripts/deb_search_for_sonames.py => deb/search_for_sonames.py} (100%) rename src/python/pants/backend/nfpm/native_libs/{scripts/deb_search_for_sonames_integration_test.py => deb/search_for_sonames_integration_test.py} (99%) rename src/python/pants/backend/nfpm/native_libs/{scripts/deb_search_for_sonames_test.py => deb/search_for_sonames_test.py} (94%) diff --git a/3rdparty/python/requirements.txt b/3rdparty/python/requirements.txt index 35985c11739..b4ff7e29077 100644 --- a/3rdparty/python/requirements.txt +++ b/3rdparty/python/requirements.txt @@ -33,8 +33,8 @@ node-semver==0.9.0 # These dependencies are for scripts that rules run in an external process (and for script tests). -aiohttp==3.12.15 # see: pants.backends.nfpm.native_libs.scripts -aiohttp-retry==2.9.1 # see: pants.backends.nfpm.native_libs.scripts +aiohttp==3.12.15 # see: pants.backends.nfpm.native_libs.deb +aiohttp-retry==2.9.1 # see: pants.backends.nfpm.native_libs.deb elfdeps==0.2.0 # see: pants.backends.nfpm.native_libs.elfdeps # These dependencies are only for debugging Pants itself (in VSCode/PyCharm respectively), # and should never be imported. diff --git a/src/python/pants/backend/nfpm/native_libs/BUILD b/src/python/pants/backend/nfpm/native_libs/BUILD index 13c13da8692..4b842489dc1 100644 --- a/src/python/pants/backend/nfpm/native_libs/BUILD +++ b/src/python/pants/backend/nfpm/native_libs/BUILD @@ -2,7 +2,7 @@ # Licensed under the Apache License, Version 2.0 (see LICENSE). python_sources( - overrides={"rules.py": dict(dependencies=["./scripts"])}, + overrides={"rules.py": dict(dependencies=["./deb"])}, ) python_tests( diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/BUILD b/src/python/pants/backend/nfpm/native_libs/deb/BUILD similarity index 69% rename from src/python/pants/backend/nfpm/native_libs/scripts/BUILD rename to src/python/pants/backend/nfpm/native_libs/deb/BUILD index 481ee281882..24052107d84 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/BUILD +++ b/src/python/pants/backend/nfpm/native_libs/deb/BUILD @@ -5,5 +5,5 @@ python_sources() python_tests( name="tests", - overrides={"deb_search_for_sonames_integration_test.py": dict(timeout=150)}, + overrides={"search_for_sonames_integration_test.py": dict(timeout=150)}, ) diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/__init__.py b/src/python/pants/backend/nfpm/native_libs/deb/__init__.py similarity index 100% rename from src/python/pants/backend/nfpm/native_libs/scripts/__init__.py rename to src/python/pants/backend/nfpm/native_libs/deb/__init__.py diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py similarity index 100% rename from src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames.py rename to src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py similarity index 99% rename from src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py rename to src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py index cfb467cf151..7c86f4fe48b 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_integration_test.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py @@ -14,7 +14,7 @@ from ..rules import DebPackagesForSonames, DebSearchForSonamesRequest from ..rules import rules as native_libs_rules -from .deb_search_for_sonames import deb_search_for_sonames +from .search_for_sonames import deb_search_for_sonames _libldap_soname = "libldap-2.5.so.0" _libldap_so_file = "/usr/lib/{}-linux-gnu/" + _libldap_soname diff --git a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_test.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_test.py similarity index 94% rename from src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_test.py rename to src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_test.py index 3acaf82cc8f..4634317c387 100644 --- a/src/python/pants/backend/nfpm/native_libs/scripts/deb_search_for_sonames_test.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_test.py @@ -3,7 +3,7 @@ from __future__ import annotations -from .deb_search_for_sonames import deb_packages_from_html_response +from .search_for_sonames import deb_packages_from_html_response # simplified for readability and to keep it focused SAMPLE_HTML_RESPONSE = """ diff --git a/src/python/pants/backend/nfpm/native_libs/rules.py b/src/python/pants/backend/nfpm/native_libs/rules.py index b2c131a916e..ea91cde86ed 100644 --- a/src/python/pants/backend/nfpm/native_libs/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/rules.py @@ -43,8 +43,8 @@ logger = logging.getLogger(__name__) -_SCRIPTS_PACKAGE = "pants.backend.nfpm.native_libs.scripts" -_DEB_SEARCH_FOR_SONAMES_SCRIPT = "deb_search_for_sonames.py" +_SCRIPTS_PACKAGE = "pants.backend.nfpm.native_libs.deb" +_DEB_SEARCH_FOR_SONAMES_SCRIPT = "search_for_sonames.py" _PEX_NAME = "native_libs_scripts.pex" From 2e012bb9d6012b8e1c9ef16f72a3792d5d68b369 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Sat, 11 Oct 2025 17:35:26 -0500 Subject: [PATCH 12/26] nfpm.native_libs: move search_for_sonames rule into deb package --- .../pants/backend/nfpm/native_libs/BUILD | 4 +- .../pants/backend/nfpm/native_libs/deb/BUILD | 4 +- .../backend/nfpm/native_libs/deb/rules.py | 225 ++++++++++++++++++ .../search_for_sonames_integration_test.py | 6 +- .../pants/backend/nfpm/native_libs/rules.py | 215 +---------------- 5 files changed, 235 insertions(+), 219 deletions(-) create mode 100644 src/python/pants/backend/nfpm/native_libs/deb/rules.py diff --git a/src/python/pants/backend/nfpm/native_libs/BUILD b/src/python/pants/backend/nfpm/native_libs/BUILD index 4b842489dc1..2e7c6d32018 100644 --- a/src/python/pants/backend/nfpm/native_libs/BUILD +++ b/src/python/pants/backend/nfpm/native_libs/BUILD @@ -1,9 +1,7 @@ # Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -python_sources( - overrides={"rules.py": dict(dependencies=["./deb"])}, -) +python_sources() python_tests( name="tests", diff --git a/src/python/pants/backend/nfpm/native_libs/deb/BUILD b/src/python/pants/backend/nfpm/native_libs/deb/BUILD index 24052107d84..bc8f9855ce8 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/BUILD +++ b/src/python/pants/backend/nfpm/native_libs/deb/BUILD @@ -1,7 +1,9 @@ # Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -python_sources() +python_sources( + overrides={"rules.py": dict(dependencies=["./search_for_sonames.py"])}, +) python_tests( name="tests", diff --git a/src/python/pants/backend/nfpm/native_libs/deb/rules.py b/src/python/pants/backend/nfpm/native_libs/deb/rules.py new file mode 100644 index 00000000000..4e7d88c0cf6 --- /dev/null +++ b/src/python/pants/backend/nfpm/native_libs/deb/rules.py @@ -0,0 +1,225 @@ +# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +import importlib.metadata +import json +import logging +import sys +from collections.abc import Iterable, Mapping +from dataclasses import dataclass, replace +from pathlib import PurePath + +from pants.backend.python.util_rules.pex import PexRequest, VenvPexProcess, create_venv_pex +from pants.backend.python.util_rules.pex_environment import PythonExecutable +from pants.backend.python.util_rules.pex_requirements import PexRequirements +from pants.engine.fs import CreateDigest, FileContent +from pants.engine.internals.native_engine import UnionRule +from pants.engine.internals.selectors import concurrently +from pants.engine.intrinsics import create_digest, execute_process +from pants.engine.process import FallibleProcessResult +from pants.engine.rules import Rule, collect_rules, implicitly, rule +from pants.init.import_util import find_matching_distributions +from pants.util.logging import LogLevel +from pants.util.resources import read_resource +from pants.version import VERSION + +logger = logging.getLogger(__name__) + +_NATIVE_LIBS_DEB_PACKAGE = "pants.backend.nfpm.native_libs.deb" +_SEARCH_FOR_SONAMES_SCRIPT = "search_for_sonames.py" +_PEX_NAME = "native_libs_deb.pex" + + +@dataclass(frozen=True) +class DebSearchForSonamesRequest: + distro: str + distro_codename: str + debian_arch: str + sonames: tuple[str, ...] + from_best_so_files: bool + + def __init__( + self, + distro: str, + distro_codename: str, + debian_arch: str, + sonames: Iterable[str], + *, + from_best_so_files: bool = False, + ): + object.__setattr__(self, "distro", distro) + object.__setattr__(self, "distro_codename", distro_codename) + object.__setattr__(self, "debian_arch", debian_arch) + object.__setattr__(self, "sonames", tuple(sorted(sonames))) + object.__setattr__(self, "from_best_so_files", from_best_so_files) + + +@dataclass(frozen=True) +class DebPackagesPerSoFile: + so_file: str + packages: tuple[str, ...] + + def __init__(self, so_file: str, packages: Iterable[str]): + object.__setattr__(self, "so_file", so_file) + object.__setattr__(self, "packages", tuple(sorted(packages))) + + +_TYPICAL_LD_PATH_PATTERNS = ( + # platform specific system libs (like libc) get selected first + # "/usr/local/lib/*-linux-*/", + "/lib/*-linux-*/", + "/usr/lib/*-linux-*/", + # Then look for a generic system libs + # "/usr/local/lib/", + "/lib/", + "/usr/lib/", + # Anything else has to be added manually to dependencies. + # These rules cannot use symbols or shlibs metadata to inform package selection. +) + + +@dataclass(frozen=True) +class DebPackagesForSoname: + soname: str + packages_per_so_files: tuple[DebPackagesPerSoFile, ...] + + def __init__(self, soname: str, packages_per_so_files: Iterable[DebPackagesPerSoFile]): + object.__setattr__(self, "soname", soname) + object.__setattr__(self, "packages_per_so_files", tuple(packages_per_so_files)) + + @property + def from_best_so_files(self) -> DebPackagesForSoname: + """Pick best so_files from packages_for_so_files using a simplified ld.so-like algorithm. + + The most preferred is first. This is NOT a recursive match; Only match if direct child of + ld_path_patt dir. Anything that uses a subdir like /usr/lib//lib*.so.* uses rpath to + prefer the app's libs over system libs. If this vastly simplified form of ld.so-style + matching does not select the correct libs, then the package(s) that provide the shared lib + should be added manually to the nfpm requires field. + """ + if len(self.packages_per_so_files) <= 1: # shortcut; no filtering required for 0-1 results. + return self + + remaining = list(self.packages_per_so_files) + + packages_per_so_files = [] + for ld_path_patt in _TYPICAL_LD_PATH_PATTERNS: + for packages_per_so_file in remaining[:]: + if PurePath(packages_per_so_file.so_file).parent.match(ld_path_patt): + packages_per_so_files.append(packages_per_so_file) + remaining.remove(packages_per_so_file) + + return replace(self, packages_per_so_files=tuple(packages_per_so_files)) + + +@dataclass(frozen=True) +class DebPackagesForSonames: + packages_for_sonames: tuple[DebPackagesForSoname, ...] + + @classmethod + def from_dict(cls, raw: Mapping[str, Mapping[str, Iterable[str]]]) -> DebPackagesForSonames: + return cls( + tuple( + DebPackagesForSoname( + soname, + ( + DebPackagesPerSoFile(so_file, packages) + for so_file, packages in files_to_packages.items() + ), + ) + for soname, files_to_packages in raw.items() + ) + ) + + @property + def from_best_so_files(self) -> DebPackagesForSonames: + packages = [] + for packages_for_soname in self.packages_for_sonames: + packages.append(packages_for_soname.from_best_so_files) + return DebPackagesForSonames(tuple(packages)) + + +@rule +async def deb_search_for_sonames( + request: DebSearchForSonamesRequest, +) -> DebPackagesForSonames: + script = read_resource(_NATIVE_LIBS_DEB_PACKAGE, _SEARCH_FOR_SONAMES_SCRIPT) + if not script: + raise ValueError( + f"Unable to find source of {_SEARCH_FOR_SONAMES_SCRIPT!r} in {_NATIVE_LIBS_DEB_PACKAGE}" + ) + + script_content = FileContent( + path=_SEARCH_FOR_SONAMES_SCRIPT, content=script, is_executable=True + ) + + # Pull python and requirements versions from the pants venv since that is what the script is tested with. + pants_python = PythonExecutable.fingerprinted( + sys.executable, ".".join(map(str, sys.version_info[:3])).encode("utf8") + ) + distributions_in_pants_venv: list[importlib.metadata.Distribution] = list( + find_matching_distributions(None) + ) + constraints = tuple(f"{dist.name}=={dist.version}" for dist in distributions_in_pants_venv) + requirements = { # requirements (and transitive deps) are constrained to the versions in the pants venv + "aiohttp", + "aiohttp-retry", + "beautifulsoup4", + } + + script_digest, venv_pex = await concurrently( + create_digest(CreateDigest([script_content])), + create_venv_pex( + **implicitly( + PexRequest( + output_filename=_PEX_NAME, + internal_only=True, + python=pants_python, + requirements=PexRequirements( + requirements, + constraints_strings=constraints, + description_of_origin=f"Requirements for {_PEX_NAME}:{_SEARCH_FOR_SONAMES_SCRIPT}", + ), + ) + ) + ), + ) + + result: FallibleProcessResult = await execute_process( + **implicitly( + VenvPexProcess( + venv_pex, + argv=( + script_content.path, + f"--user-agent-suffix=pants/{VERSION}", + f"--distro={request.distro}", + f"--distro-codename={request.distro_codename}", + f"--arch={request.debian_arch}", + *request.sonames, + ), + input_digest=script_digest, + description=f"Search deb packages for sonames: {request.sonames}", + level=LogLevel.DEBUG, + ) + ) + ) + + if result.exit_code == 0: + packages = json.loads(result.stdout) + else: + # The search API returns 200 even if no results were found. + # A 4xx or 5xx error means we gave up retrying because the server is unavailable. + # TODO: Should this raise an error instead of just a warning? + logger.warning(result.stderr.decode("utf-8")) + packages = {} + + deb_packages_for_sonames = DebPackagesForSonames.from_dict(packages) + if request.from_best_so_files: + return deb_packages_for_sonames.from_best_so_files + return deb_packages_for_sonames + + +def rules() -> Iterable[Rule | UnionRule]: + return collect_rules() diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py index 7c86f4fe48b..42e0cc480cb 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py @@ -12,8 +12,8 @@ from pants.engine.rules import QueryRule from pants.testutil.rule_runner import RuleRunner -from ..rules import DebPackagesForSonames, DebSearchForSonamesRequest -from ..rules import rules as native_libs_rules +from .rules import DebPackagesForSonames, DebSearchForSonamesRequest +from .rules import rules as native_libs_deb_rules from .search_for_sonames import deb_search_for_sonames _libldap_soname = "libldap-2.5.so.0" @@ -217,7 +217,7 @@ def rule_runner() -> RuleRunner: rule_runner = RuleRunner( rules=[ *pex_from_targets.rules(), - *native_libs_rules(), + *native_libs_deb_rules(), QueryRule(DebPackagesForSonames, (DebSearchForSonamesRequest,)), ], ) diff --git a/src/python/pants/backend/nfpm/native_libs/rules.py b/src/python/pants/backend/nfpm/native_libs/rules.py index ea91cde86ed..cfc7a8a83f1 100644 --- a/src/python/pants/backend/nfpm/native_libs/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/rules.py @@ -3,13 +3,8 @@ from __future__ import annotations -import importlib.metadata -import json -import logging -import sys -from collections.abc import Iterable, Mapping -from dataclasses import dataclass, replace -from pathlib import PurePath +from collections.abc import Iterable +from dataclasses import dataclass from pants.backend.nfpm.field_sets import NfpmRpmPackageFieldSet from pants.backend.nfpm.fields.rpm import NfpmRpmDependsField, NfpmRpmProvidesField @@ -24,217 +19,13 @@ InjectNfpmPackageFieldsRequest, ) from pants.backend.python.goals.package_pex_binary import PexBinaryFieldSet, package_pex_binary -from pants.backend.python.util_rules.pex import Pex, PexRequest, VenvPexProcess, create_pex, create_venv_pex -from pants.backend.python.util_rules.pex_environment import PythonExecutable +from pants.backend.python.util_rules.pex import Pex, create_pex from pants.backend.python.util_rules.pex_from_targets import PexFromTargetsRequest -from pants.backend.python.util_rules.pex_requirements import PexRequirements from pants.engine.addresses import Address -from pants.engine.fs import CreateDigest, FileContent from pants.engine.internals.selectors import concurrently -from pants.engine.intrinsics import create_digest, execute_process -from pants.engine.process import FallibleProcessResult from pants.engine.rules import Rule, collect_rules, implicitly, rule from pants.engine.target import Field, Target from pants.engine.unions import UnionMembership, UnionRule -from pants.init.import_util import find_matching_distributions -from pants.util.logging import LogLevel -from pants.util.resources import read_resource -from pants.version import VERSION - -logger = logging.getLogger(__name__) - -_SCRIPTS_PACKAGE = "pants.backend.nfpm.native_libs.deb" -_DEB_SEARCH_FOR_SONAMES_SCRIPT = "search_for_sonames.py" -_PEX_NAME = "native_libs_scripts.pex" - - -@dataclass(frozen=True) -class DebSearchForSonamesRequest: - distro: str - distro_codename: str - debian_arch: str - sonames: tuple[str, ...] - from_best_so_files: bool - - def __init__( - self, - distro: str, - distro_codename: str, - debian_arch: str, - sonames: Iterable[str], - *, - from_best_so_files: bool = False, - ): - object.__setattr__(self, "distro", distro) - object.__setattr__(self, "distro_codename", distro_codename) - object.__setattr__(self, "debian_arch", debian_arch) - object.__setattr__(self, "sonames", tuple(sorted(sonames))) - object.__setattr__(self, "from_best_so_files", from_best_so_files) - - -@dataclass(frozen=True) -class DebPackagesPerSoFile: - so_file: str - packages: tuple[str, ...] - - def __init__(self, so_file: str, packages: Iterable[str]): - object.__setattr__(self, "so_file", so_file) - object.__setattr__(self, "packages", tuple(sorted(packages))) - - -_TYPICAL_LD_PATH_PATTERNS = ( - # platform specific system libs (like libc) get selected first - # "/usr/local/lib/*-linux-*/", - "/lib/*-linux-*/", - "/usr/lib/*-linux-*/", - # Then look for a generic system libs - # "/usr/local/lib/", - "/lib/", - "/usr/lib/", - # Anything else has to be added manually to dependencies. - # These rules cannot use symbols or shlibs metadata to inform package selection. -) - - -@dataclass(frozen=True) -class DebPackagesForSoname: - soname: str - packages_per_so_files: tuple[DebPackagesPerSoFile, ...] - - def __init__(self, soname: str, packages_per_so_files: Iterable[DebPackagesPerSoFile]): - object.__setattr__(self, "soname", soname) - object.__setattr__(self, "packages_per_so_files", tuple(packages_per_so_files)) - - @property - def from_best_so_files(self) -> DebPackagesForSoname: - """Pick best so_files from packages_for_so_files using a simplified ld.so-like algorithm. - - The most preferred is first. This is NOT a recursive match; Only match if direct child of - ld_path_patt dir. Anything that uses a subdir like /usr/lib//lib*.so.* uses rpath to - prefer the app's libs over system libs. If this vastly simplified form of ld.so-style - matching does not select the correct libs, then the package(s) that provide the shared lib - should be added manually to the nfpm requires field. - """ - if len(self.packages_per_so_files) <= 1: # shortcut; no filtering required for 0-1 results. - return self - - remaining = list(self.packages_per_so_files) - - packages_per_so_files = [] - for ld_path_patt in _TYPICAL_LD_PATH_PATTERNS: - for packages_per_so_file in remaining[:]: - if PurePath(packages_per_so_file.so_file).parent.match(ld_path_patt): - packages_per_so_files.append(packages_per_so_file) - remaining.remove(packages_per_so_file) - - return replace(self, packages_per_so_files=tuple(packages_per_so_files)) - - -@dataclass(frozen=True) -class DebPackagesForSonames: - packages_for_sonames: tuple[DebPackagesForSoname, ...] - - @classmethod - def from_dict(cls, raw: Mapping[str, Mapping[str, Iterable[str]]]) -> DebPackagesForSonames: - return cls( - tuple( - DebPackagesForSoname( - soname, - ( - DebPackagesPerSoFile(so_file, packages) - for so_file, packages in files_to_packages.items() - ), - ) - for soname, files_to_packages in raw.items() - ) - ) - - @property - def from_best_so_files(self) -> DebPackagesForSonames: - packages = [] - for packages_for_soname in self.packages_for_sonames: - packages.append(packages_for_soname.from_best_so_files) - return DebPackagesForSonames(tuple(packages)) - - -@rule -async def deb_search_for_sonames( - request: DebSearchForSonamesRequest, -) -> DebPackagesForSonames: - script = read_resource(_SCRIPTS_PACKAGE, _DEB_SEARCH_FOR_SONAMES_SCRIPT) - if not script: - raise ValueError( - f"Unable to find source of {_DEB_SEARCH_FOR_SONAMES_SCRIPT!r} in {_SCRIPTS_PACKAGE}" - ) - - script_content = FileContent( - path=_DEB_SEARCH_FOR_SONAMES_SCRIPT, content=script, is_executable=True - ) - - # Pull python and requirements versions from the pants venv since that is what the script is tested with. - pants_python = PythonExecutable.fingerprinted( - sys.executable, ".".join(map(str, sys.version_info[:3])).encode("utf8") - ) - distributions_in_pants_venv: list[importlib.metadata.Distribution] = list( - find_matching_distributions(None) - ) - constraints = tuple(f"{dist.name}=={dist.version}" for dist in distributions_in_pants_venv) - requirements = { # requirements (and transitive deps) are constrained to the versions in the pants venv - "aiohttp", - "aiohttp-retry", - "beautifulsoup4", - } - - script_digest, venv_pex = await concurrently( - create_digest(CreateDigest([script_content])), - create_venv_pex( - **implicitly( - PexRequest( - output_filename=_PEX_NAME, - internal_only=True, - python=pants_python, - requirements=PexRequirements( - requirements, - constraints_strings=constraints, - description_of_origin=f"Requirements for {_PEX_NAME}:{_DEB_SEARCH_FOR_SONAMES_SCRIPT}", - ), - ) - ) - ), - ) - - result: FallibleProcessResult = await execute_process( - **implicitly( - VenvPexProcess( - venv_pex, - argv=( - script_content.path, - f"--user-agent-suffix=pants/{VERSION}", - f"--distro={request.distro}", - f"--distro-codename={request.distro_codename}", - f"--arch={request.debian_arch}", - *request.sonames, - ), - input_digest=script_digest, - description=f"Search deb packages for sonames: {request.sonames}", - level=LogLevel.DEBUG, - ) - ) - ) - - if result.exit_code == 0: - packages = json.loads(result.stdout) - else: - # The search API returns 200 even if no results were found. - # A 4xx or 5xx error means we gave up retrying because the server is unavailable. - # TODO: Should this raise an error instead of just a warning? - logger.warning(result.stderr.decode("utf-8")) - packages = {} - - deb_packages_for_sonames = DebPackagesForSonames.from_dict(packages) - if request.from_best_so_files: - return deb_packages_for_sonames.from_best_so_files - return deb_packages_for_sonames @dataclass(frozen=True) From d8ef4e8123fda4dc870c42d2c63178d176fe29a6 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Sat, 11 Oct 2025 18:13:31 -0500 Subject: [PATCH 13/26] nfpm.native_libs: register rules after moving to deb package --- src/python/pants/backend/nfpm/native_libs/rules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/python/pants/backend/nfpm/native_libs/rules.py b/src/python/pants/backend/nfpm/native_libs/rules.py index cfc7a8a83f1..5fd14ffccca 100644 --- a/src/python/pants/backend/nfpm/native_libs/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/rules.py @@ -8,6 +8,7 @@ from pants.backend.nfpm.field_sets import NfpmRpmPackageFieldSet from pants.backend.nfpm.fields.rpm import NfpmRpmDependsField, NfpmRpmProvidesField +from pants.backend.nfpm.native_libs.deb.rules import rules as deb_rules from pants.backend.nfpm.native_libs.elfdeps.rules import RequestPexELFInfo, elfdeps_analyze_pex from pants.backend.nfpm.native_libs.elfdeps.rules import rules as elfdeps_rules from pants.backend.nfpm.util_rules.contents import ( @@ -117,6 +118,7 @@ async def inject_native_libs_dependencies_in_package_fields( def rules() -> Iterable[Rule | UnionRule]: return ( + *deb_rules(), *elfdeps_rules(), *collect_rules(), UnionRule(InjectNfpmPackageFieldsRequest, NativeLibsNfpmPackageFieldsRequest), From 0cf706548d26fa05d674b73cd2f3c70d9041736c Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Mon, 3 Nov 2025 16:00:47 -0600 Subject: [PATCH 14/26] nfpm.native_libs: regenerate user_reqs.lock to include new deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit __________________________________________________________________ Lockfile diff: 3rdparty/python/user_reqs.lock [python-default] __________________________________________________________________ == Added dependencies == ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ aiohappyeyeballs 2.6.1 aiohttp 3.12.15 aiohttp-retry 2.9.1 aiosignal 1.4.0 attrs 25.4.0 frozenlist 1.8.0 multidict 6.7.0 propcache 0.4.1 yarl 1.22.0 --- 3rdparty/python/user_reqs.lock | 522 ++++++++++++++++++++++++ 3rdparty/python/user_reqs.lock.metadata | 2 + 2 files changed, 524 insertions(+) diff --git a/3rdparty/python/user_reqs.lock b/3rdparty/python/user_reqs.lock index 5d3f5244ee1..79f26e7e56d 100644 --- a/3rdparty/python/user_reqs.lock +++ b/3rdparty/python/user_reqs.lock @@ -9,6 +9,165 @@ "locked_resolves": [ { "locked_requirements": [ + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", + "url": "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", + "url": "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz" + } + ], + "project_name": "aiohappyeyeballs", + "requires_dists": [], + "requires_python": ">=3.9", + "version": "2.6.1" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", + "url": "https://files.pythonhosted.org/packages/85/e0/444747a9455c5de188c0f4a0173ee701e2e325d4b2550e9af84abb20cdba/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", + "url": "https://files.pythonhosted.org/packages/12/8a/8b75f203ea7e5c21c0920d84dd24a5c0e971fe1e9b9ebbf29ae7e8e39790/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", + "url": "https://files.pythonhosted.org/packages/17/e5/fb779a05ba6ff44d7bc1e9d24c644e876bfff5abe5454f7b854cace1b9cc/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", + "url": "https://files.pythonhosted.org/packages/1b/96/784c785674117b4cb3877522a177ba1b5e4db9ce0fd519430b5de76eec90/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", + "url": "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", + "url": "https://files.pythonhosted.org/packages/28/e5/55a33b991f6433569babb56018b2fb8fb9146424f8b3a0c8ecca80556762/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", + "url": "https://files.pythonhosted.org/packages/37/4e/a22e799c2035f5d6a4ad2cf8e7c1d1bd0923192871dd6e367dafb158b14c/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", + "url": "https://files.pythonhosted.org/packages/47/0b/a1451543475bb6b86a5cfc27861e52b14085ae232896a2654ff1231c0992/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", + "url": "https://files.pythonhosted.org/packages/55/fd/793a23a197cc2f0d29188805cfc93aa613407f07e5f9da5cd1366afd9d7c/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", + "url": "https://files.pythonhosted.org/packages/57/4f/ed60a591839a9d85d40694aba5cef86dde9ee51ce6cca0bb30d6eb1581e7/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", + "url": "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", + "url": "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", + "url": "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", + "url": "https://files.pythonhosted.org/packages/c6/82/1ddf0ea4f2f3afe79dffed5e8a246737cff6cbe781887a6a170299e33204/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", + "url": "https://files.pythonhosted.org/packages/ca/bf/23a335a6670b5f5dfc6d268328e55a22651b440fca341a64fccf1eada0c6/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", + "url": "https://files.pythonhosted.org/packages/f8/6c/f766d0aaafcee0447fad0328da780d344489c042e25cd58fde566bf40aed/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + } + ], + "project_name": "aiohttp", + "requires_dists": [ + "Brotli; platform_python_implementation == \"CPython\" and extra == \"speedups\"", + "aiodns>=3.3.0; extra == \"speedups\"", + "aiohappyeyeballs>=2.5.0", + "aiosignal>=1.4.0", + "async-timeout<6.0,>=4.0; python_version < \"3.11\"", + "attrs>=17.3.0", + "brotlicffi; platform_python_implementation != \"CPython\" and extra == \"speedups\"", + "frozenlist>=1.1.1", + "multidict<7.0,>=4.5", + "propcache>=0.2.0", + "yarl<2.0,>=1.17.0" + ], + "requires_python": ">=3.9", + "version": "3.12.15" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54", + "url": "https://files.pythonhosted.org/packages/1a/99/84ba7273339d0f3dfa57901b846489d2e5c2cd731470167757f1935fffbd/aiohttp_retry-2.9.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1", + "url": "https://files.pythonhosted.org/packages/9d/61/ebda4d8e3d8cfa1fd3db0fb428db2dd7461d5742cea35178277ad180b033/aiohttp_retry-2.9.1.tar.gz" + } + ], + "project_name": "aiohttp-retry", + "requires_dists": [ + "aiohttp" + ], + "requires_python": ">=3.7", + "version": "2.9.1" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", + "url": "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", + "url": "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz" + } + ], + "project_name": "aiosignal", + "requires_dists": [ + "frozenlist>=1.1.0", + "typing-extensions>=4.2; python_version < \"3.13\"" + ], + "requires_python": ">=3.9", + "version": "1.4.0" + }, { "artifacts": [ { @@ -89,6 +248,24 @@ "requires_python": ">=3.9", "version": "4.11.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", + "url": "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", + "url": "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz" + } + ], + "project_name": "attrs", + "requires_dists": [], + "requires_python": ">=3.9", + "version": "25.4.0" + }, { "artifacts": [ { @@ -608,6 +785,89 @@ "requires_python": ">=3.8", "version": "1.5.5" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", + "url": "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", + "url": "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", + "url": "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", + "url": "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", + "url": "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", + "url": "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", + "url": "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", + "url": "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", + "url": "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", + "url": "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", + "url": "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", + "url": "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", + "url": "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", + "url": "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", + "url": "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl" + } + ], + "project_name": "frozenlist", + "requires_dists": [], + "requires_python": ">=3.9", + "version": "1.8.0" + }, { "artifacts": [ { @@ -895,6 +1155,101 @@ "requires_python": ">=3.9", "version": "1.8.5" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", + "url": "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", + "url": "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", + "url": "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", + "url": "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", + "url": "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", + "url": "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", + "url": "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", + "url": "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", + "url": "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", + "url": "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", + "url": "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", + "url": "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", + "url": "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", + "url": "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", + "url": "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", + "url": "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", + "url": "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl" + } + ], + "project_name": "multidict", + "requires_dists": [ + "typing-extensions>=4.1.0; python_version < \"3.11\"" + ], + "requires_python": ">=3.9", + "version": "6.7.0" + }, { "artifacts": [ { @@ -1016,6 +1371,84 @@ "requires_python": ">=3.9", "version": "1.6.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", + "url": "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", + "url": "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", + "url": "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", + "url": "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", + "url": "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", + "url": "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", + "url": "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", + "url": "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", + "url": "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", + "url": "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", + "url": "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", + "url": "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", + "url": "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", + "url": "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl" + } + ], + "project_name": "propcache", + "requires_dists": [], + "requires_python": ">=3.9", + "version": "0.4.1" + }, { "artifacts": [ { @@ -2243,6 +2676,93 @@ "requires_dists": [], "requires_python": ">=3.9", "version": "15.0.1" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", + "url": "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", + "url": "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", + "url": "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", + "url": "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", + "url": "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", + "url": "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", + "url": "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", + "url": "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", + "url": "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", + "url": "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", + "url": "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", + "url": "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", + "url": "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", + "url": "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", + "url": "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl" + } + ], + "project_name": "yarl", + "requires_dists": [ + "idna>=2.0", + "multidict>=4.0", + "propcache>=0.2.1" + ], + "requires_python": ">=3.9", + "version": "1.22.0" } ], "marker": null, @@ -2259,6 +2779,8 @@ "requirements": [ "PyGithub==2.8.1", "PyYAML<7.0,>=6.0", + "aiohttp-retry==2.9.1", + "aiohttp==3.12.15", "ansicolors==1.1.8", "beautifulsoup4==4.11.1", "chevron==0.14.0", diff --git a/3rdparty/python/user_reqs.lock.metadata b/3rdparty/python/user_reqs.lock.metadata index 54ff0916edc..1ee2e244769 100644 --- a/3rdparty/python/user_reqs.lock.metadata +++ b/3rdparty/python/user_reqs.lock.metadata @@ -6,6 +6,8 @@ "generated_with_requirements": [ "PyGithub==2.8.1", "PyYAML<7.0,>=6.0", + "aiohttp-retry==2.9.1", + "aiohttp==3.12.15", "ansicolors==1.1.8", "beautifulsoup4==4.11.1", "chevron==0.14.0", From 70f9619c6a8b0e642b60bb046edaee946e29f2fa Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Wed, 5 Nov 2025 21:38:36 -0600 Subject: [PATCH 15/26] nfpm.native_libs: Include native_libs.deb in docs/notes --- docs/notes/2.31.x.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/notes/2.31.x.md b/docs/notes/2.31.x.md index 7df044cf073..523b3fb28c9 100644 --- a/docs/notes/2.31.x.md +++ b/docs/notes/2.31.x.md @@ -74,6 +74,7 @@ So, this backend provides a simplified set of features from these native packagi - `rpm`: `elfdeps` analyzes ELF metadata and `rpmdeps` adds the requirements to the `.rpm` file. This backend should be platform-agnostic, allowing it to run wherever `pants` can run. To do this, it relies on the [`elfdeps`](https://github.com/python-wheel-build/elfdeps) [📦](https://pypi.org/project/elfdeps/) pure-python package (a "Python implementation of RPM `elfdeps`" with its pure-python dep [`pyelftools`](https://github.com/eliben/pyelftools) [📦](https://pypi.org/project/pyelftools/)) for analyzing `ELF` libraries. +Then, for `deb` packages, this backend queries official [`debian`](https://packages.debian.org/search) or [`ubuntu`](https://packages.ubuntu.com/search) API (over HTTPS) to lookup which package(s) contain required libraries. Using the package search API avoids the local package metadata stores that can only find dependency packages if they are installed (such stores are not just distribution-specific, they are specific to a single release of a distribution). Please provide feedback on this backend [here](https://github.com/pantsbuild/pants/discussions/22396). From d775ddb1665b2a000b8665f2e61d0d714a063a9d Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Thu, 20 Nov 2025 18:06:32 -0600 Subject: [PATCH 16/26] nfpm.native_libs.deb: add rules to prevent py deps on :scripts --- .../pants/backend/nfpm/native_libs/deb/BUILD | 19 ++++++++++++++++++- .../search_for_sonames_integration_test.py | 9 +++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/python/pants/backend/nfpm/native_libs/deb/BUILD b/src/python/pants/backend/nfpm/native_libs/deb/BUILD index bc8f9855ce8..88a473b3257 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/BUILD +++ b/src/python/pants/backend/nfpm/native_libs/deb/BUILD @@ -1,8 +1,25 @@ # Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). +# these _SCRIPTS should be both python_sources and resources +_SCRIPTS = ("search_for_sonames",) +__dependents_rules__( + ( + # the python targets of these scripts + tuple(f"[/{script}.py]" for script in _SCRIPTS), + # can be dependencies of these scripts and script tests + tuple(f"[/{script}*.py]" for script in _SCRIPTS), + # and nothing else + "!*", + ), + # fall back to parent rulesets in //src/python/pants/backend/BUILD + extend=True, +) + +resources(name="scripts", sources=[f"{script}.py" for script in _SCRIPTS]) + python_sources( - overrides={"rules.py": dict(dependencies=["./search_for_sonames.py"])}, + overrides={"rules.py": dict(dependencies=[":scripts"])}, ) python_tests( diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py index 42e0cc480cb..05cb25a555b 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py @@ -8,12 +8,17 @@ import pytest +from pants.backend.nfpm.native_libs.deb.rules import ( + DebPackagesForSonames, + DebSearchForSonamesRequest, +) +from pants.backend.nfpm.native_libs.deb.rules import rules as native_libs_deb_rules from pants.backend.python.util_rules import pex_from_targets from pants.engine.rules import QueryRule from pants.testutil.rule_runner import RuleRunner -from .rules import DebPackagesForSonames, DebSearchForSonamesRequest -from .rules import rules as native_libs_deb_rules +# The relative import emphasizes that `search_for_sonames` is a standalone script that runs +# from a sandbox root (thus avoiding a dependency on the pants code structure). from .search_for_sonames import deb_search_for_sonames _libldap_soname = "libldap-2.5.so.0" From 139e5954ddf9af9231a59b227c37255568102255 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Thu, 20 Nov 2025 21:12:14 -0600 Subject: [PATCH 17/26] Revert "nfpm.native_libs.deb: add rules to prevent py deps on :scripts" This reverts commit 44907754618524228661a6359b21ad4475ca74cd. --- .../pants/backend/nfpm/native_libs/deb/BUILD | 19 +------------------ .../search_for_sonames_integration_test.py | 9 ++------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/src/python/pants/backend/nfpm/native_libs/deb/BUILD b/src/python/pants/backend/nfpm/native_libs/deb/BUILD index 88a473b3257..bc8f9855ce8 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/BUILD +++ b/src/python/pants/backend/nfpm/native_libs/deb/BUILD @@ -1,25 +1,8 @@ # Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -# these _SCRIPTS should be both python_sources and resources -_SCRIPTS = ("search_for_sonames",) -__dependents_rules__( - ( - # the python targets of these scripts - tuple(f"[/{script}.py]" for script in _SCRIPTS), - # can be dependencies of these scripts and script tests - tuple(f"[/{script}*.py]" for script in _SCRIPTS), - # and nothing else - "!*", - ), - # fall back to parent rulesets in //src/python/pants/backend/BUILD - extend=True, -) - -resources(name="scripts", sources=[f"{script}.py" for script in _SCRIPTS]) - python_sources( - overrides={"rules.py": dict(dependencies=[":scripts"])}, + overrides={"rules.py": dict(dependencies=["./search_for_sonames.py"])}, ) python_tests( diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py index 05cb25a555b..42e0cc480cb 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py @@ -8,17 +8,12 @@ import pytest -from pants.backend.nfpm.native_libs.deb.rules import ( - DebPackagesForSonames, - DebSearchForSonamesRequest, -) -from pants.backend.nfpm.native_libs.deb.rules import rules as native_libs_deb_rules from pants.backend.python.util_rules import pex_from_targets from pants.engine.rules import QueryRule from pants.testutil.rule_runner import RuleRunner -# The relative import emphasizes that `search_for_sonames` is a standalone script that runs -# from a sandbox root (thus avoiding a dependency on the pants code structure). +from .rules import DebPackagesForSonames, DebSearchForSonamesRequest +from .rules import rules as native_libs_deb_rules from .search_for_sonames import deb_search_for_sonames _libldap_soname = "libldap-2.5.so.0" From 6789c4719006fec787a61d934afd7f65e8e2c97f Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Fri, 21 Nov 2025 00:08:31 -0600 Subject: [PATCH 18/26] nfpm.native_libs.deb: add usage comments --- .../backend/nfpm/native_libs/deb/search_for_sonames.py | 9 +++++++++ .../deb/search_for_sonames_integration_test.py | 9 +++++++-- .../nfpm/native_libs/deb/search_for_sonames_test.py | 2 ++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py index acd75bb0b27..c613aead211 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py @@ -1,6 +1,15 @@ # Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). +"""A standalone script that uses asyncio to find packages that contain a SONAME. + +Rule code should run this script as a subprocess in a sandboxed venv. Generally, that venv should +be a subset of the pants venv (using the version of packages that pants depends on). + +WARNING: This script relies on aiohttp and asyncio, so their are probably incompatibilities +with rule code in the pants engine. So, avoid importing this script in rule code. +""" + from __future__ import annotations import argparse diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py index 42e0cc480cb..05cb25a555b 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py @@ -8,12 +8,17 @@ import pytest +from pants.backend.nfpm.native_libs.deb.rules import ( + DebPackagesForSonames, + DebSearchForSonamesRequest, +) +from pants.backend.nfpm.native_libs.deb.rules import rules as native_libs_deb_rules from pants.backend.python.util_rules import pex_from_targets from pants.engine.rules import QueryRule from pants.testutil.rule_runner import RuleRunner -from .rules import DebPackagesForSonames, DebSearchForSonamesRequest -from .rules import rules as native_libs_deb_rules +# The relative import emphasizes that `search_for_sonames` is a standalone script that runs +# from a sandbox root (thus avoiding a dependency on the pants code structure). from .search_for_sonames import deb_search_for_sonames _libldap_soname = "libldap-2.5.so.0" diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_test.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_test.py index 4634317c387..6cfbf794e24 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_test.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_test.py @@ -3,6 +3,8 @@ from __future__ import annotations +# The relative import emphasizes that `search_for_sonames` is a standalone script that runs +# from a sandbox root (thus avoiding a dependency on the pants code structure). from .search_for_sonames import deb_packages_from_html_response # simplified for readability and to keep it focused From cc84f68660992f5d7e6572a76588a2f00be4cb25 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Thu, 27 Nov 2025 01:25:51 -0600 Subject: [PATCH 19/26] nfpm.native_libs.deb: drop /usr/local comments Though ld.so looks in /usr/local, packages from system package repos are not supposed to install anything in /usr/local, so searching for that does not make sense. Keeping it commented also adds no value, so drop the commented /usr/local entries. --- src/python/pants/backend/nfpm/native_libs/deb/rules.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/python/pants/backend/nfpm/native_libs/deb/rules.py b/src/python/pants/backend/nfpm/native_libs/deb/rules.py index 4e7d88c0cf6..d709a98eeff 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/rules.py @@ -68,11 +68,9 @@ def __init__(self, so_file: str, packages: Iterable[str]): _TYPICAL_LD_PATH_PATTERNS = ( # platform specific system libs (like libc) get selected first - # "/usr/local/lib/*-linux-*/", "/lib/*-linux-*/", "/usr/lib/*-linux-*/", # Then look for a generic system libs - # "/usr/local/lib/", "/lib/", "/usr/lib/", # Anything else has to be added manually to dependencies. From 7fbb73a2c773f851aca8a27cd7a42f3c2785166f Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Thu, 27 Nov 2025 10:09:22 -0600 Subject: [PATCH 20/26] nfpm.native_libs.deb: split tests into separate files --- .../pants/backend/nfpm/native_libs/deb/BUILD | 6 + .../native_libs/deb/rules_integration_test.py | 73 ++++++ .../search_for_sonames_integration_test.py | 247 +----------------- .../nfpm/native_libs/deb/test_utils.py | 188 +++++++++++++ 4 files changed, 268 insertions(+), 246 deletions(-) create mode 100644 src/python/pants/backend/nfpm/native_libs/deb/rules_integration_test.py create mode 100644 src/python/pants/backend/nfpm/native_libs/deb/test_utils.py diff --git a/src/python/pants/backend/nfpm/native_libs/deb/BUILD b/src/python/pants/backend/nfpm/native_libs/deb/BUILD index bc8f9855ce8..737f35a4b02 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/BUILD +++ b/src/python/pants/backend/nfpm/native_libs/deb/BUILD @@ -7,5 +7,11 @@ python_sources( python_tests( name="tests", + sources=["*_test.py"], overrides={"search_for_sonames_integration_test.py": dict(timeout=150)}, ) + +python_test_utils( + name="test_utils", + sources=["test_utils.py"], +) diff --git a/src/python/pants/backend/nfpm/native_libs/deb/rules_integration_test.py b/src/python/pants/backend/nfpm/native_libs/deb/rules_integration_test.py new file mode 100644 index 00000000000..68b6df190a8 --- /dev/null +++ b/src/python/pants/backend/nfpm/native_libs/deb/rules_integration_test.py @@ -0,0 +1,73 @@ +# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +import sys + +import pytest + +from pants.backend.nfpm.native_libs.deb.rules import ( + DebPackagesForSonames, + DebSearchForSonamesRequest, +) +from pants.backend.nfpm.native_libs.deb.rules import rules as native_libs_deb_rules +from pants.backend.nfpm.native_libs.deb.test_utils import TEST_CASES +from pants.backend.python.util_rules import pex_from_targets +from pants.engine.rules import QueryRule +from pants.testutil.rule_runner import RuleRunner + + +@pytest.fixture +def rule_runner() -> RuleRunner: + rule_runner = RuleRunner( + rules=[ + *pex_from_targets.rules(), + *native_libs_deb_rules(), + QueryRule(DebPackagesForSonames, (DebSearchForSonamesRequest,)), + ], + ) + + # The rule builds a pex with wheels for the pants venv. + _py_version = ".".join(map(str, sys.version_info[:3])) + + rule_runner.set_options( + [ + f"--python-interpreter-constraints=['CPython=={_py_version}']", + ], + env_inherit={"PATH", "PYENV_ROOT", "HOME"}, + ) + return rule_runner + + +@pytest.mark.parametrize( + "distro,distro_codename,debian_arch,sonames,expected_raw,expected_raw_from_best_so_files", + TEST_CASES, +) +def test_deb_search_for_sonames_rule( + distro: str, + distro_codename: str, + debian_arch: str, + sonames: tuple[str, ...], + expected_raw: dict[str, dict[str, list[str]]], + expected_raw_from_best_so_files: None | dict[str, dict[str, list[str]]], + rule_runner: RuleRunner, +) -> None: + for from_best_so_files, _expected_raw in ( + (False, expected_raw), + (True, expected_raw_from_best_so_files or expected_raw), + ): + expected = DebPackagesForSonames.from_dict(_expected_raw) + result = rule_runner.request( + DebPackagesForSonames, + [ + DebSearchForSonamesRequest( + distro, + distro_codename, + debian_arch, + sonames, + from_best_so_files=from_best_so_files, + ) + ], + ) + assert result == expected diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py index 05cb25a555b..55ed084137b 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py @@ -3,206 +3,16 @@ from __future__ import annotations -import sys from typing import Any import pytest -from pants.backend.nfpm.native_libs.deb.rules import ( - DebPackagesForSonames, - DebSearchForSonamesRequest, -) -from pants.backend.nfpm.native_libs.deb.rules import rules as native_libs_deb_rules -from pants.backend.python.util_rules import pex_from_targets -from pants.engine.rules import QueryRule -from pants.testutil.rule_runner import RuleRunner +from pants.backend.nfpm.native_libs.deb.test_utils import TEST_CASES # The relative import emphasizes that `search_for_sonames` is a standalone script that runs # from a sandbox root (thus avoiding a dependency on the pants code structure). from .search_for_sonames import deb_search_for_sonames -_libldap_soname = "libldap-2.5.so.0" -_libldap_so_file = "/usr/lib/{}-linux-gnu/" + _libldap_soname -_libldap_pkg = "libldap-2.5-0" - -_libc6_soname = "libc.so.6" - - -def _libc6_pkgs(_arch: str, prof: bool = True) -> dict[str, list[str]]: - pkgs = {} - if prof: - pkgs[f"/lib/libc6-prof/{_arch}-linux-gnu/{_libc6_soname}"] = ["libc6-prof"] - pkgs[f"/lib/{_arch}-linux-gnu/{_libc6_soname}"] = ["libc6"] - return dict(sorted(pkgs.items())) - - -_libc6_pkgs_amd64 = { # only x86_64 not aarch64 - "/lib32/" + _libc6_soname: ["libc6-i386"], - "/libx32/" + _libc6_soname: ["libc6-x32"], -} -_libc6_cross_pkgs = { - f"/usr/{cross_machine}-linux-{cross_os_lib}/lib{cross_bits}/{_libc6_soname}": [ - f"libc6-{cross_arch}-cross" - ] - for cross_machine, cross_os_lib, cross_bits, cross_arch in [ - ("aarch64", "gnu", "", "arm64"), - ("arm", "gnueabi", "", "armel"), - ("arm", "gnueabihf", "", "armhf"), - ("hppa", "gnu", "", "hppa"), - ("i686", "gnu", "", "i386"), - ("i686", "gnu", "64", "amd64-i386"), - ("i686", "gnu", "x32", "x32-i386"), - ("m68k", "gnu", "", "m68k"), - ("mips", "gnu", "", "mips"), - ("mips", "gnu", "32", "mipsn32-mips"), - ("mips", "gnu", "64", "mips64-mips"), - ("mips64", "gnuabi64", "", "mips64"), - ("mips64", "gnuabi64", "32", "mipsn32-mips64"), - ("mips64", "gnuabi64", "o32", "mips32-mips64"), - ("mips64", "gnuabin32", "", "mipsn32"), - ("mips64", "gnuabin32", "64", "mips64-mipsn32"), - ("mips64", "gnuabin32", "o32", "mips32-mipsn32"), - ("mips64el", "gnuabi64", "", "mips64el"), - ("mips64el", "gnuabi64", "32", "mipsn32-mips64el"), - ("mips64el", "gnuabi64", "o32", "mips32-mips64el"), - ("mips64el", "gnuabin32", "", "mipsn32el"), - ("mips64el", "gnuabin32", "64", "mips64-mipsn32el"), - ("mips64el", "gnuabin32", "o32", "mips32-mipsn32el"), - ("mipsel", "gnu", "", "mipsel"), - ("mipsel", "gnu", "32", "mipsn32-mipsel"), - ("mipsel", "gnu", "64", "mips64-mipsel"), - ("mipsisa32r6", "gnu", "", "mipsr6"), - ("mipsisa32r6", "gnu", "32", "mipsn32-mipsr6"), - ("mipsisa32r6", "gnu", "64", "mips64-mipsr6"), - ("mipsisa32r6el", "gnu", "", "mipsr6el"), - ("mipsisa32r6el", "gnu", "32", "mipsn32-mipsr6el"), - ("mipsisa32r6el", "gnu", "64", "mips64-mipsr6el"), - ("mipsisa64r6", "gnuabi64", "", "mips64r6"), - ("mipsisa64r6", "gnuabi64", "32", "mipsn32-mips64r6"), - ("mipsisa64r6", "gnuabi64", "o32", "mips32-mips64r6"), - ("mipsisa64r6", "gnuabin32", "", "mipsn32r6"), - ("mipsisa64r6", "gnuabin32", "64", "mips64-mipsn32r6"), - ("mipsisa64r6", "gnuabin32", "o32", "mips32-mipsn32r6"), - ("mipsisa64r6el", "gnuabi64", "", "mips64r6el"), - ("mipsisa64r6el", "gnuabi64", "32", "mipsn32-mips64r6el"), - ("mipsisa64r6el", "gnuabi64", "o32", "mips32-mips64r6el"), - ("mipsisa64r6el", "gnuabin32", "", "mipsn32r6el"), - ("mipsisa64r6el", "gnuabin32", "64", "mips64-mipsn32r6el"), - ("mipsisa64r6el", "gnuabin32", "o32", "mips32-mipsn32r6el"), - ("powerpc", "gnu", "", "powerpc"), - ("powerpc", "gnu", "64", "ppc64-powerpc"), - ("powerpc64", "gnu", "", "ppc64"), - ("powerpc64", "gnu", "32", "powerpc-ppc64"), - ("powerpc64le", "gnu", "", "ppc64el"), - ("riscv64", "gnu", "", "riscv64"), - ("s390x", "gnu", "", "s390x"), - ("s390x", "gnu", "32", "s390-s390x"), - ("sh4", "gnu", "", "sh4"), - ("sparc64", "gnu", "", "sparc64"), - ("sparc64", "gnu", "32", "sparc-sparc64"), - ("x86_64", "gnu", "", "amd64"), - ("x86_64", "gnu", "32", "i386-amd64"), - ("x86_64", "gnu", "x32", "x32-amd64"), - ("x86_64", "gnux32", "", "x32"), - ("x86_64", "gnux32", "32", "i386-x32"), - ("x86_64", "gnux32", "64", "amd64-x32"), - ] -} - -TEST_CASES = ( - pytest.param( - "debian", - "bookworm", - "amd64", - (_libldap_soname,), - {_libldap_soname: {_libldap_so_file.format("x86_64"): [_libldap_pkg]}}, - None, # from_best_so_files is the same result - id="debian-amd64-libldap", - ), - pytest.param( - "debian", - "bookworm", - "arm64", - (_libldap_soname,), - {_libldap_soname: {_libldap_so_file.format("aarch64"): [_libldap_pkg]}}, - None, # from_best_so_files is the same result - id="debian-arm64-libldap", - ), - pytest.param( - "ubuntu", - "jammy", - "amd64", - (_libldap_soname,), - {_libldap_soname: {_libldap_so_file.format("x86_64"): [_libldap_pkg]}}, - None, # from_best_so_files is the same result - id="ubuntu-amd64-libldap", - ), - pytest.param( - "ubuntu", - "jammy", - "arm64", - (_libldap_soname,), - {_libldap_soname: {_libldap_so_file.format("aarch64"): [_libldap_pkg]}}, - None, # from_best_so_files is the same result - id="ubuntu-arm64-libldap", - ), - pytest.param( - "ubuntu", "foobar", "amd64", (_libldap_soname,), {}, None, id="bad-distro_codename" - ), - pytest.param("ubuntu", "jammy", "foobar", (_libldap_soname,), {}, None, id="bad-debian_arch"), - pytest.param("ubuntu", "jammy", "amd64", ("foobarbaz-9.9.so.9",), {}, None, id="bad-soname"), - pytest.param( - "ubuntu", - "jammy", - "amd64", - ("libcurl.so",), # the search api returns a table like this: - # ------------------------------------------- | ----------------------------------------------------------- | - # File | Packages | - # ------------------------------------------- | ----------------------------------------------------------- | - # /usr/lib/cupt4-2/downloadmethods/libcurl.so | libcupt4-2-downloadmethod-curl | - # /usr/lib/x86_64-linux-gnu/libcurl.so | libcurl4-gnutls-dev, libcurl4-nss-dev, libcurl4-openssl-dev | - # ------------------------------------------- | ----------------------------------------------------------- | - { - "libcurl.so": { - "/usr/lib/cupt4-2/downloadmethods/libcurl.so": ["libcupt4-2-downloadmethod-curl"], - "/usr/lib/x86_64-linux-gnu/libcurl.so": [ - "libcurl4-gnutls-dev", - "libcurl4-nss-dev", - "libcurl4-openssl-dev", - ], - } - }, - { # from_best_so_files is NOT the same result - "libcurl.so": { - "/usr/lib/x86_64-linux-gnu/libcurl.so": [ - "libcurl4-gnutls-dev", - "libcurl4-nss-dev", - "libcurl4-openssl-dev", - ], - } - }, - id="same-file-in-multiple-packages", - ), - pytest.param( - "ubuntu", - "jammy", - "amd64", - (_libc6_soname,), - {_libc6_soname: _libc6_pkgs("x86_64") | _libc6_pkgs_amd64 | _libc6_cross_pkgs}, - {_libc6_soname: _libc6_pkgs("x86_64", prof=False)}, - id="ubuntu-amd64-libc6", - ), - pytest.param( - "ubuntu", - "jammy", - "arm64", - (_libc6_soname,), - {_libc6_soname: _libc6_pkgs("aarch64") | _libc6_cross_pkgs}, - {_libc6_soname: _libc6_pkgs("aarch64", prof=False)}, - id="ubuntu-arm64-libc6", - ), -) - @pytest.mark.parametrize("distro,distro_codename,debian_arch,sonames,expected,_", TEST_CASES) async def test_deb_search_for_sonames( @@ -215,58 +25,3 @@ async def test_deb_search_for_sonames( ): result = await deb_search_for_sonames(distro, distro_codename, debian_arch, sonames) assert result == expected - - -@pytest.fixture -def rule_runner() -> RuleRunner: - rule_runner = RuleRunner( - rules=[ - *pex_from_targets.rules(), - *native_libs_deb_rules(), - QueryRule(DebPackagesForSonames, (DebSearchForSonamesRequest,)), - ], - ) - - # The rule builds a pex with wheels for the pants venv. - _py_version = ".".join(map(str, sys.version_info[:3])) - - rule_runner.set_options( - [ - f"--python-interpreter-constraints=['CPython=={_py_version}']", - ], - env_inherit={"PATH", "PYENV_ROOT", "HOME"}, - ) - return rule_runner - - -@pytest.mark.parametrize( - "distro,distro_codename,debian_arch,sonames,expected_raw,expected_raw_from_best_so_files", - TEST_CASES, -) -def test_deb_search_for_sonames_rule( - distro: str, - distro_codename: str, - debian_arch: str, - sonames: tuple[str, ...], - expected_raw: dict[str, dict[str, list[str]]], - expected_raw_from_best_so_files: None | dict[str, dict[str, list[str]]], - rule_runner: RuleRunner, -) -> None: - for from_best_so_files, _expected_raw in ( - (False, expected_raw), - (True, expected_raw_from_best_so_files or expected_raw), - ): - expected = DebPackagesForSonames.from_dict(_expected_raw) - result = rule_runner.request( - DebPackagesForSonames, - [ - DebSearchForSonamesRequest( - distro, - distro_codename, - debian_arch, - sonames, - from_best_so_files=from_best_so_files, - ) - ], - ) - assert result == expected diff --git a/src/python/pants/backend/nfpm/native_libs/deb/test_utils.py b/src/python/pants/backend/nfpm/native_libs/deb/test_utils.py new file mode 100644 index 00000000000..99ecabe4ddd --- /dev/null +++ b/src/python/pants/backend/nfpm/native_libs/deb/test_utils.py @@ -0,0 +1,188 @@ +# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +import pytest + +_libldap_soname = "libldap-2.5.so.0" +_libldap_so_file = "/usr/lib/{}-linux-gnu/" + _libldap_soname +_libldap_pkg = "libldap-2.5-0" + +_libc6_soname = "libc.so.6" + + +def _libc6_pkgs(_arch: str, prof: bool = True) -> dict[str, list[str]]: + pkgs = {} + if prof: + pkgs[f"/lib/libc6-prof/{_arch}-linux-gnu/{_libc6_soname}"] = ["libc6-prof"] + pkgs[f"/lib/{_arch}-linux-gnu/{_libc6_soname}"] = ["libc6"] + return dict(sorted(pkgs.items())) + + +_libc6_pkgs_amd64 = { # only x86_64 not aarch64 + "/lib32/" + _libc6_soname: ["libc6-i386"], + "/libx32/" + _libc6_soname: ["libc6-x32"], +} +_libc6_cross_pkgs = { + f"/usr/{cross_machine}-linux-{cross_os_lib}/lib{cross_bits}/{_libc6_soname}": [ + f"libc6-{cross_arch}-cross" + ] + for cross_machine, cross_os_lib, cross_bits, cross_arch in [ + ("aarch64", "gnu", "", "arm64"), + ("arm", "gnueabi", "", "armel"), + ("arm", "gnueabihf", "", "armhf"), + ("hppa", "gnu", "", "hppa"), + ("i686", "gnu", "", "i386"), + ("i686", "gnu", "64", "amd64-i386"), + ("i686", "gnu", "x32", "x32-i386"), + ("m68k", "gnu", "", "m68k"), + ("mips", "gnu", "", "mips"), + ("mips", "gnu", "32", "mipsn32-mips"), + ("mips", "gnu", "64", "mips64-mips"), + ("mips64", "gnuabi64", "", "mips64"), + ("mips64", "gnuabi64", "32", "mipsn32-mips64"), + ("mips64", "gnuabi64", "o32", "mips32-mips64"), + ("mips64", "gnuabin32", "", "mipsn32"), + ("mips64", "gnuabin32", "64", "mips64-mipsn32"), + ("mips64", "gnuabin32", "o32", "mips32-mipsn32"), + ("mips64el", "gnuabi64", "", "mips64el"), + ("mips64el", "gnuabi64", "32", "mipsn32-mips64el"), + ("mips64el", "gnuabi64", "o32", "mips32-mips64el"), + ("mips64el", "gnuabin32", "", "mipsn32el"), + ("mips64el", "gnuabin32", "64", "mips64-mipsn32el"), + ("mips64el", "gnuabin32", "o32", "mips32-mipsn32el"), + ("mipsel", "gnu", "", "mipsel"), + ("mipsel", "gnu", "32", "mipsn32-mipsel"), + ("mipsel", "gnu", "64", "mips64-mipsel"), + ("mipsisa32r6", "gnu", "", "mipsr6"), + ("mipsisa32r6", "gnu", "32", "mipsn32-mipsr6"), + ("mipsisa32r6", "gnu", "64", "mips64-mipsr6"), + ("mipsisa32r6el", "gnu", "", "mipsr6el"), + ("mipsisa32r6el", "gnu", "32", "mipsn32-mipsr6el"), + ("mipsisa32r6el", "gnu", "64", "mips64-mipsr6el"), + ("mipsisa64r6", "gnuabi64", "", "mips64r6"), + ("mipsisa64r6", "gnuabi64", "32", "mipsn32-mips64r6"), + ("mipsisa64r6", "gnuabi64", "o32", "mips32-mips64r6"), + ("mipsisa64r6", "gnuabin32", "", "mipsn32r6"), + ("mipsisa64r6", "gnuabin32", "64", "mips64-mipsn32r6"), + ("mipsisa64r6", "gnuabin32", "o32", "mips32-mipsn32r6"), + ("mipsisa64r6el", "gnuabi64", "", "mips64r6el"), + ("mipsisa64r6el", "gnuabi64", "32", "mipsn32-mips64r6el"), + ("mipsisa64r6el", "gnuabi64", "o32", "mips32-mips64r6el"), + ("mipsisa64r6el", "gnuabin32", "", "mipsn32r6el"), + ("mipsisa64r6el", "gnuabin32", "64", "mips64-mipsn32r6el"), + ("mipsisa64r6el", "gnuabin32", "o32", "mips32-mipsn32r6el"), + ("powerpc", "gnu", "", "powerpc"), + ("powerpc", "gnu", "64", "ppc64-powerpc"), + ("powerpc64", "gnu", "", "ppc64"), + ("powerpc64", "gnu", "32", "powerpc-ppc64"), + ("powerpc64le", "gnu", "", "ppc64el"), + ("riscv64", "gnu", "", "riscv64"), + ("s390x", "gnu", "", "s390x"), + ("s390x", "gnu", "32", "s390-s390x"), + ("sh4", "gnu", "", "sh4"), + ("sparc64", "gnu", "", "sparc64"), + ("sparc64", "gnu", "32", "sparc-sparc64"), + ("x86_64", "gnu", "", "amd64"), + ("x86_64", "gnu", "32", "i386-amd64"), + ("x86_64", "gnu", "x32", "x32-amd64"), + ("x86_64", "gnux32", "", "x32"), + ("x86_64", "gnux32", "32", "i386-x32"), + ("x86_64", "gnux32", "64", "amd64-x32"), + ] +} + +TEST_CASES = ( + pytest.param( + "debian", + "bookworm", + "amd64", + (_libldap_soname,), + {_libldap_soname: {_libldap_so_file.format("x86_64"): [_libldap_pkg]}}, + None, # from_best_so_files is the same result + id="debian-amd64-libldap", + ), + pytest.param( + "debian", + "bookworm", + "arm64", + (_libldap_soname,), + {_libldap_soname: {_libldap_so_file.format("aarch64"): [_libldap_pkg]}}, + None, # from_best_so_files is the same result + id="debian-arm64-libldap", + ), + pytest.param( + "ubuntu", + "jammy", + "amd64", + (_libldap_soname,), + {_libldap_soname: {_libldap_so_file.format("x86_64"): [_libldap_pkg]}}, + None, # from_best_so_files is the same result + id="ubuntu-amd64-libldap", + ), + pytest.param( + "ubuntu", + "jammy", + "arm64", + (_libldap_soname,), + {_libldap_soname: {_libldap_so_file.format("aarch64"): [_libldap_pkg]}}, + None, # from_best_so_files is the same result + id="ubuntu-arm64-libldap", + ), + pytest.param( + "ubuntu", "foobar", "amd64", (_libldap_soname,), {}, None, id="bad-distro_codename" + ), + pytest.param("ubuntu", "jammy", "foobar", (_libldap_soname,), {}, None, id="bad-debian_arch"), + pytest.param("ubuntu", "jammy", "amd64", ("foobarbaz-9.9.so.9",), {}, None, id="bad-soname"), + pytest.param( + "ubuntu", + "jammy", + "amd64", + ("libcurl.so",), # the search api returns a table like this: + # ------------------------------------------- | ----------------------------------------------------------- | + # File | Packages | + # ------------------------------------------- | ----------------------------------------------------------- | + # /usr/lib/cupt4-2/downloadmethods/libcurl.so | libcupt4-2-downloadmethod-curl | + # /usr/lib/x86_64-linux-gnu/libcurl.so | libcurl4-gnutls-dev, libcurl4-nss-dev, libcurl4-openssl-dev | + # ------------------------------------------- | ----------------------------------------------------------- | + { + "libcurl.so": { + "/usr/lib/cupt4-2/downloadmethods/libcurl.so": ["libcupt4-2-downloadmethod-curl"], + "/usr/lib/x86_64-linux-gnu/libcurl.so": [ + "libcurl4-gnutls-dev", + "libcurl4-nss-dev", + "libcurl4-openssl-dev", + ], + } + }, + { # from_best_so_files is NOT the same result + "libcurl.so": { + "/usr/lib/x86_64-linux-gnu/libcurl.so": [ + "libcurl4-gnutls-dev", + "libcurl4-nss-dev", + "libcurl4-openssl-dev", + ], + } + }, + id="same-file-in-multiple-packages", + ), + pytest.param( + "ubuntu", + "jammy", + "amd64", + (_libc6_soname,), + {_libc6_soname: _libc6_pkgs("x86_64") | _libc6_pkgs_amd64 | _libc6_cross_pkgs}, + {_libc6_soname: _libc6_pkgs("x86_64", prof=False)}, + id="ubuntu-amd64-libc6", + ), + pytest.param( + "ubuntu", + "jammy", + "arm64", + (_libc6_soname,), + {_libc6_soname: _libc6_pkgs("aarch64") | _libc6_cross_pkgs}, + {_libc6_soname: _libc6_pkgs("aarch64", prof=False)}, + id="ubuntu-arm64-libc6", + ), +) From ae56961087b3edea7781a1bd5a486ca5299e0002 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Mon, 1 Dec 2025 12:49:31 -0600 Subject: [PATCH 21/26] typo fix --- .../pants/backend/nfpm/native_libs/deb/search_for_sonames.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py index c613aead211..3290eec3d07 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py @@ -6,7 +6,7 @@ Rule code should run this script as a subprocess in a sandboxed venv. Generally, that venv should be a subset of the pants venv (using the version of packages that pants depends on). -WARNING: This script relies on aiohttp and asyncio, so their are probably incompatibilities +WARNING: This script relies on aiohttp and asyncio, so there are probably incompatibilities with rule code in the pants engine. So, avoid importing this script in rule code. """ From 78dd63c4328ac9ea92a5efb7649e8561d9a55f05 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Mon, 1 Dec 2025 13:14:41 -0600 Subject: [PATCH 22/26] nfpm.native_libs.deb: refactor script error handling The API returns success (200) when returning an empty result set. A not-found result is not a failure, as it means that the dep has to be provided by a custom package; it just means the dep is not in one of the distro-provided packages. So, now the script also returns success (0) when an empty result-set is returned. --- .../pants/backend/nfpm/native_libs/deb/rules.py | 16 ++++++---------- .../nfpm/native_libs/deb/search_for_sonames.py | 5 ++++- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/python/pants/backend/nfpm/native_libs/deb/rules.py b/src/python/pants/backend/nfpm/native_libs/deb/rules.py index d709a98eeff..566d0586d7b 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/rules.py @@ -17,8 +17,8 @@ from pants.engine.fs import CreateDigest, FileContent from pants.engine.internals.native_engine import UnionRule from pants.engine.internals.selectors import concurrently -from pants.engine.intrinsics import create_digest, execute_process -from pants.engine.process import FallibleProcessResult +from pants.engine.intrinsics import create_digest +from pants.engine.process import ProcessResult, execute_process_or_raise from pants.engine.rules import Rule, collect_rules, implicitly, rule from pants.init.import_util import find_matching_distributions from pants.util.logging import LogLevel @@ -185,7 +185,8 @@ async def deb_search_for_sonames( ), ) - result: FallibleProcessResult = await execute_process( + # Raising an error means we gave up retrying because the server is unavailable. + result: ProcessResult = await execute_process_or_raise( **implicitly( VenvPexProcess( venv_pex, @@ -204,14 +205,9 @@ async def deb_search_for_sonames( ) ) - if result.exit_code == 0: - packages = json.loads(result.stdout) - else: - # The search API returns 200 even if no results were found. - # A 4xx or 5xx error means we gave up retrying because the server is unavailable. - # TODO: Should this raise an error instead of just a warning? + packages = json.loads(result.stdout) + if result.stderr: logger.warning(result.stderr.decode("utf-8")) - packages = {} deb_packages_for_sonames = DebPackagesForSonames.from_dict(packages) if request.from_best_so_files: diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py index 3290eec3d07..624e4aafd5c 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py @@ -192,13 +192,16 @@ def main() -> int: ) if not packages: + # The search API returns 200 even if no results were found (which is not an error). + # If the service is unavailable, an error should be raised before getting here. print("{}") print( f"No {options.distro} {options.distro_codename} ({options.arch}) packages" f" found for sonames: {options.sonames}", file=sys.stderr, ) - return 1 + # If needed, add a flag to this script to say "fail if no results found". + return 0 print(json.dumps(packages, indent=None, separators=(",", ":"))) From cfb4fa7c8865b34f457563fc11abb7d31308cfdd Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Tue, 2 Dec 2025 15:34:58 -0600 Subject: [PATCH 23/26] nfpm.native_libs.deb: new deb subsystem for configurable search URLs --- .../nfpm/native_libs/deb/_constants.py | 12 +++++++ .../backend/nfpm/native_libs/deb/rules.py | 10 +++++- .../native_libs/deb/search_for_sonames.py | 10 +++--- .../search_for_sonames_integration_test.py | 6 ++-- .../backend/nfpm/native_libs/deb/subsystem.py | 32 +++++++++++++++++++ 5 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 src/python/pants/backend/nfpm/native_libs/deb/_constants.py create mode 100644 src/python/pants/backend/nfpm/native_libs/deb/subsystem.py diff --git a/src/python/pants/backend/nfpm/native_libs/deb/_constants.py b/src/python/pants/backend/nfpm/native_libs/deb/_constants.py new file mode 100644 index 00000000000..cfaadd2a340 --- /dev/null +++ b/src/python/pants/backend/nfpm/native_libs/deb/_constants.py @@ -0,0 +1,12 @@ +# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +"""Constants for use in rule code and script tests.""" + + +DEFAULT_DEB_DISTRO_PACKAGE_SEARCH_URLS: dict[str, str] = { + "debian": "https://packages.debian.org/search", + "ubuntu": "https://packages.ubuntu.com/search", +} diff --git a/src/python/pants/backend/nfpm/native_libs/deb/rules.py b/src/python/pants/backend/nfpm/native_libs/deb/rules.py index 566d0586d7b..6434bfc4814 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/rules.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/rules.py @@ -11,6 +11,7 @@ from dataclasses import dataclass, replace from pathlib import PurePath +from pants.backend.nfpm.native_libs.deb.subsystem import DebSubsystem from pants.backend.python.util_rules.pex import PexRequest, VenvPexProcess, create_venv_pex from pants.backend.python.util_rules.pex_environment import PythonExecutable from pants.backend.python.util_rules.pex_requirements import PexRequirements @@ -142,6 +143,7 @@ def from_best_so_files(self) -> DebPackagesForSonames: @rule async def deb_search_for_sonames( request: DebSearchForSonamesRequest, + deb_subsystem: DebSubsystem, ) -> DebPackagesForSonames: script = read_resource(_NATIVE_LIBS_DEB_PACKAGE, _SEARCH_FOR_SONAMES_SCRIPT) if not script: @@ -185,6 +187,8 @@ async def deb_search_for_sonames( ), ) + distro_package_search_url = deb_subsystem.options.distro_package_search_urls[request.distro] + # Raising an error means we gave up retrying because the server is unavailable. result: ProcessResult = await execute_process_or_raise( **implicitly( @@ -193,6 +197,7 @@ async def deb_search_for_sonames( argv=( script_content.path, f"--user-agent-suffix=pants/{VERSION}", + f"--search-url={distro_package_search_url}", f"--distro={request.distro}", f"--distro-codename={request.distro_codename}", f"--arch={request.debian_arch}", @@ -216,4 +221,7 @@ async def deb_search_for_sonames( def rules() -> Iterable[Rule | UnionRule]: - return collect_rules() + return ( + *DebSubsystem.rules(), + *collect_rules(), + ) diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py index 624e4aafd5c..990965a5fc2 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py @@ -32,7 +32,7 @@ async def deb_search_for_sonames( - distro: str, + search_url: str, distro_codename: str, debian_arch: str, sonames: Iterable[str], @@ -44,7 +44,6 @@ async def deb_search_for_sonames( version. This code, however, should be able to run on any host even non-debian and non-ubuntu hosts. So, it uses an API call instead of local tooling. """ - search_url = DISTRO_PACKAGE_SEARCH_URL[distro] # tasks are IO bound async with ( @@ -168,9 +167,8 @@ def deb_packages_from_html_response( def main() -> int: arg_parser = argparse.ArgumentParser() arg_parser.add_argument("--user-agent-suffix") - arg_parser.add_argument( - "--distro", default="ubuntu", choices=tuple(DISTRO_PACKAGE_SEARCH_URL.keys()) - ) + arg_parser.add_argument("--search-url", default="https://packages.debian.org/search") + arg_parser.add_argument("--distro", default="debian") arg_parser.add_argument("--distro-codename", required=True) arg_parser.add_argument("--arch", default="amd64") arg_parser.add_argument("sonames", nargs="+") @@ -183,7 +181,7 @@ def main() -> int: packages = asyncio.get_event_loop().run_until_complete( deb_search_for_sonames( - distro=options.distro, + search_url=options.search_url, distro_codename=options.distro_codename, debian_arch=options.arch, sonames=tuple(options.sonames), diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py index 55ed084137b..19a0a15160f 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_integration_test.py @@ -7,6 +7,7 @@ import pytest +from pants.backend.nfpm.native_libs.deb._constants import DEFAULT_DEB_DISTRO_PACKAGE_SEARCH_URLS from pants.backend.nfpm.native_libs.deb.test_utils import TEST_CASES # The relative import emphasizes that `search_for_sonames` is a standalone script that runs @@ -21,7 +22,8 @@ async def test_deb_search_for_sonames( debian_arch: str, sonames: tuple[str, ...], expected: dict[str, dict[str, list[str]]], - _: Any, # unused. This is for the next test. + _: Any, # unused. This is for the rule integration test. ): - result = await deb_search_for_sonames(distro, distro_codename, debian_arch, sonames) + search_url = DEFAULT_DEB_DISTRO_PACKAGE_SEARCH_URLS[distro] + result = await deb_search_for_sonames(search_url, distro_codename, debian_arch, sonames) assert result == expected diff --git a/src/python/pants/backend/nfpm/native_libs/deb/subsystem.py b/src/python/pants/backend/nfpm/native_libs/deb/subsystem.py new file mode 100644 index 00000000000..82171aed5c9 --- /dev/null +++ b/src/python/pants/backend/nfpm/native_libs/deb/subsystem.py @@ -0,0 +1,32 @@ +# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +from pants.backend.nfpm.native_libs.deb._constants import DEFAULT_DEB_DISTRO_PACKAGE_SEARCH_URLS +from pants.option.option_types import DictOption +from pants.option.subsystem import Subsystem +from pants.util.strutil import help_text + + +class DebSubsystem(Subsystem): + options_scope = "deb" # not "debian" to avoid conflicting with debian backend + help = "Options for deb scripts in the nfpm.native_libs backend." + + distro_package_search_urls = DictOption[str]( + default=DEFAULT_DEB_DISTRO_PACKAGE_SEARCH_URLS, + help=help_text( + """ + A mapping of distro names to package search URLs. + + This is used when inspecting packaged native_libs to inject nfpm package deps. + + The key is a distro name and should be lowercase. + + Each value is a fully-qualified URL with scheme (`https://`), domain, and path + (path is typically `/search`). The URL must point to an instance of the debian + package search service, returning the API results in HTML format. + """ + ), + advanced=True, + ) From 606163c537a7d9f7b03fdcedadd28a6d150fa04f Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Tue, 2 Dec 2025 16:54:09 -0600 Subject: [PATCH 24/26] nfpm.native_libs.deb: protect against "Client Challenge" response --- .../pants/backend/nfpm/native_libs/deb/search_for_sonames.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py index 990965a5fc2..ee65215ec1f 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py @@ -127,6 +127,10 @@ def deb_packages_from_html_response( soup = BeautifulSoup(html_doc, "html.parser") + # Fail if the API is requesting javascript-based validation + if soup.title.get_text() == "Client Challenge": + raise ValueError("API service is blocking the request") + # .table means 'search for a tag'. The response should only have one. # In xmlpath, descending would look like one of these: # /html/body/div[1]/div[3]/div[2]/table From 38b122c42477e52644b1c2c5bdc8d830a2fe9188 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Tue, 2 Dec 2025 21:25:49 -0600 Subject: [PATCH 25/26] nfpm.native_libs.deb: drop unused dict (moved to subsystem) --- .../pants/backend/nfpm/native_libs/deb/search_for_sonames.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py index ee65215ec1f..117da41fe32 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames.py @@ -25,11 +25,6 @@ from aiohttp_retry.types import ClientType from bs4 import BeautifulSoup -DISTRO_PACKAGE_SEARCH_URL = { - "debian": "https://packages.debian.org/search", - "ubuntu": "https://packages.ubuntu.com/search", -} - async def deb_search_for_sonames( search_url: str, From 57e0845e134e1c023cdcd8770441ba50d0f05e14 Mon Sep 17 00:00:00 2001 From: Jacob Floyd Date: Tue, 2 Dec 2025 21:44:00 -0600 Subject: [PATCH 26/26] nfpm.native_libs.deb: comment out tests w/ blocked API requests It seems debian added some kind of policy that requires a javascript-enabled browser to get search results. Ubuntu has not done the same. Effectively, that means that this script can only support ubuntu. Also fix add to the HTML parsing unit test since that is expected now. --- .../deb/search_for_sonames_test.py | 5 ++- .../nfpm/native_libs/deb/test_utils.py | 37 ++++++++++--------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_test.py b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_test.py index 6cfbf794e24..275bbd5be1b 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_test.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/search_for_sonames_test.py @@ -11,7 +11,10 @@ SAMPLE_HTML_RESPONSE = """ <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html lang="en"> - <head><!-- ... --></head> + <head> + <title>Debian -- Package Contents Search Results -- libldap-2.5.so.0 + +
...
diff --git a/src/python/pants/backend/nfpm/native_libs/deb/test_utils.py b/src/python/pants/backend/nfpm/native_libs/deb/test_utils.py index 99ecabe4ddd..0fe935b926a 100644 --- a/src/python/pants/backend/nfpm/native_libs/deb/test_utils.py +++ b/src/python/pants/backend/nfpm/native_libs/deb/test_utils.py @@ -94,24 +94,25 @@ def _libc6_pkgs(_arch: str, prof: bool = True) -> dict[str, list[str]]: } TEST_CASES = ( - pytest.param( - "debian", - "bookworm", - "amd64", - (_libldap_soname,), - {_libldap_soname: {_libldap_so_file.format("x86_64"): [_libldap_pkg]}}, - None, # from_best_so_files is the same result - id="debian-amd64-libldap", - ), - pytest.param( - "debian", - "bookworm", - "arm64", - (_libldap_soname,), - {_libldap_soname: {_libldap_so_file.format("aarch64"): [_libldap_pkg]}}, - None, # from_best_so_files is the same result - id="debian-arm64-libldap", - ), + # debian is blocking non-javascript-enabled search requests + # pytest.param( + # "debian", + # "bookworm", + # "amd64", + # (_libldap_soname,), + # {_libldap_soname: {_libldap_so_file.format("x86_64"): [_libldap_pkg]}}, + # None, # from_best_so_files is the same result + # id="debian-amd64-libldap", + # ), + # pytest.param( + # "debian", + # "bookworm", + # "arm64", + # (_libldap_soname,), + # {_libldap_soname: {_libldap_so_file.format("aarch64"): [_libldap_pkg]}}, + # None, # from_best_so_files is the same result + # id="debian-arm64-libldap", + # ), pytest.param( "ubuntu", "jammy",