• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 19529437518

20 Nov 2025 07:44AM UTC coverage: 78.884% (-1.4%) from 80.302%
19529437518

push

github

web-flow
nfpm.native_libs: Add RPM package depends from packaged pex_binaries (#22899)

## PR Series Overview

This is the second in a series of PRs that introduces a new backend:
`pants.backend.npm.native_libs`
Initially, the backend will be available as:
`pants.backend.experimental.nfpm.native_libs`

I proposed this new backend (originally named `bindeps`) in discussion
#22396.

This backend will inspect ELF bin/lib files (like `lib*.so`) in packaged
contents (for this PR series, only in `pex_binary` targets) to identify
package dependency metadata and inject that metadata on the relevant
`nfpm_deb_package` or `nfpm_rpm_package` targets. Effectively, it will
provide an approximation of these native packager features:
- `rpm`: `rpmdeps` + `elfdeps`
- `deb`: `dh_shlibdeps` + `dpkg-shlibdeps` (These substitute
`${shlibs:Depends}` in debian control files have)

### Goal: Host-agnostic package builds

This pants backend is designed to be host-agnostic, like
[nFPM](https://nfpm.goreleaser.com/).

Native packaging tools are often restricted to a single release of a
single distro. Unlike native package builders, this new pants backend
does not use any of those distro-specific or distro-release-specific
utilities or local package databases. This new backend should be able to
run (help with building deb and rpm packages) anywhere that pants can
run (MacOS, rpm linux distros, deb linux distros, other linux distros,
docker, ...).

### Previous PRs in series

- #22873

## PR Overview

This PR adds rules in `nfpm.native_libs` to add package dependency
metadata to `nfpm_rpm_package`. The 2 new rules are:

- `inject_native_libs_dependencies_in_package_fields`:

    - An implementation of the polymorphic rule `inject_nfpm_package_fields`.
      This rule is low priority (`priority = 2`) so that in-repo plugins can
      override/augment what it injects. (See #22864)

    - Rule logic overview:
        - find any pex_binaries that will be packaged in an `nfpm_rpm_package`
   ... (continued)

96 of 118 new or added lines in 3 files covered. (81.36%)

910 existing lines in 53 files now uncovered.

73897 of 93678 relevant lines covered (78.88%)

3.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.38
/src/python/pants/testutil/python_interpreter_selection.py
1
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
10✔
5

6
import os
10✔
7
import subprocess
10✔
8
from collections.abc import Iterable
10✔
9
from functools import lru_cache
10✔
10
from unittest import skipIf
10✔
11

12
import _pytest.mark.structures
10✔
13
import pytest
10✔
14

15
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
10✔
16

17
PY_2 = "2"
10✔
18
PY_3 = "3"
10✔
19

20
PY_27 = "2.7"
10✔
21
PY_36 = "3.6"
10✔
22
PY_37 = "3.7"
10✔
23
PY_38 = "3.8"
10✔
24
PY_39 = "3.9"
10✔
25
PY_310 = "3.10"
10✔
26
PY_311 = "3.11"
10✔
27

28

29
def has_python_version(version):
10✔
30
    """Returns `True` if the current system has the specified version of python.
31

32
    :param string version: A python version string, such as 2.7, 3.
33
    """
34
    # TODO: Tests that skip unless a python interpreter is present often need the path to that
35
    # interpreter, and so end up calling python_interpreter_path again. Find a way to streamline this.
36
    return python_interpreter_path(version) is not None
10✔
37

38

39
@lru_cache
10✔
40
def python_interpreter_path(version):
10✔
41
    """Returns the interpreter path if the current system has the specified version of python.
42

43
    :param string version: A python version string, such as 2.7, 3.
44
    :returns: the normalized path to the interpreter binary if found; otherwise `None`
45
    :rtype: string
46
    """
47
    try:
10✔
48
        command = [f"python{version}", "-c", "import sys; print(sys.executable)"]
10✔
49
        py_path = subprocess.check_output(command).decode().strip()
10✔
50
        return os.path.realpath(py_path)
10✔
51
    except (subprocess.CalledProcessError, FileNotFoundError):
1✔
52
        return None
1✔
53

54

55
def skip_unless_all_pythons_present(*versions):
10✔
56
    """A decorator that only runs the decorated test method if all of the specified pythons are
57
    present.
58

59
    :param string *versions: Python version strings, such as 2.7, 3.
60
    """
61
    missing_versions = [v for v in versions if not has_python_version(v)]
9✔
62
    if len(missing_versions) == 1:
9✔
63
        return skipIf(True, f"Could not find python {missing_versions[0]} on system. Skipping.")
1✔
64
    elif len(missing_versions) > 1:
9✔
65
        return skipIf(
×
66
            True,
67
            "Skipping due to the following missing required pythons: {}".format(
68
                ", ".join(missing_versions)
69
            ),
70
        )
71
    else:
72
        return skipIf(False, "All required pythons present, continuing with test!")
9✔
73

74

75
def skip_unless_python27_present(func):
10✔
76
    """A test skip decorator that only runs a test method if python2.7 is present."""
77
    return skip_unless_all_pythons_present(PY_27)(func)
×
78

79

80
def skip_unless_python3_present(func):
10✔
81
    """A test skip decorator that only runs a test method if python3 is present."""
82
    return skip_unless_all_pythons_present(PY_3)(func)
×
83

84

85
def skip_unless_python36_present(func):
10✔
86
    """A test skip decorator that only runs a test method if python3.6 is present."""
87
    return skip_unless_all_pythons_present(PY_36)(func)
×
88

89

90
def skip_unless_python37_present(func):
10✔
91
    """A test skip decorator that only runs a test method if python3.7 is present."""
UNCOV
92
    return skip_unless_all_pythons_present(PY_37)(func)
×
93

94

95
def skip_unless_python38_present(func):
10✔
96
    """A test skip decorator that only runs a test method if python3.8 is present."""
97
    return skip_unless_all_pythons_present(PY_38)(func)
7✔
98

99

100
def skip_unless_python39_present(func):
10✔
101
    """A test skip decorator that only runs a test method if python3.9 is present."""
102
    return skip_unless_all_pythons_present(PY_39)(func)
7✔
103

104

105
def skip_unless_python310_present(func):
10✔
106
    """A test skip decorator that only runs a test method if python3.10 is present."""
107
    return skip_unless_all_pythons_present(PY_310)(func)
×
108

109

110
def skip_unless_python311_present(func):
10✔
111
    """A test skip decorator that only runs a test method if python3.11 is present."""
112
    return skip_unless_all_pythons_present(PY_311)(func)
×
113

114

115
def skip_unless_python310_and_python311_present(func):
10✔
116
    """A test skip decorator that only runs a test method if python3.8 and python3.9 are present."""
117
    return skip_unless_all_pythons_present(PY_310, PY_311)(func)
4✔
118

119

120
def skip_unless_python27_and_python3_present(func):
10✔
121
    """A test skip decorator that only runs a test method if python2.7 and python3 are present."""
122
    return skip_unless_all_pythons_present(PY_27, PY_3)(func)
×
123

124

125
def skip_unless_python27_and_python36_present(func):
10✔
126
    """A test skip decorator that only runs a test method if python2.7 and python3.6 are present."""
127
    return skip_unless_all_pythons_present(PY_27, PY_36)(func)
×
128

129

130
def skip_unless_python38_and_python39_present(func):
10✔
131
    """A test skip decorator that only runs a test method if python3.8 and python3.9 are present."""
132
    return skip_unless_all_pythons_present(PY_38, PY_39)(func)
1✔
133

134

135
def skip_unless_python37_and_python39_present(func):
10✔
136
    """A test skip decorator that only runs a test method if python3.7 and python3.9 are present."""
137
    return skip_unless_all_pythons_present(PY_37, PY_39)(func)
×
138

139

140
def all_major_minor_python_versions(
10✔
141
    constraints: Iterable[str],
142
) -> tuple[_pytest.mark.structures.ParameterSet, ...]:
143
    """All major.minor Python versions used by the interpreter constraints.
144

145
    This is intended to be used with `@pytest.mark.parametrize()` to run a test with every relevant
146
    Python interpreter.
147
    """
148
    versions = InterpreterConstraints(constraints).partition_into_major_minor_versions(
10✔
149
        # Please update this when new stable Python versions are released to CI.
150
        interpreter_universe=["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
151
    )
152
    return tuple(
10✔
153
        pytest.param(
154
            version,
155
            marks=pytest.mark.skipif(
156
                not has_python_version(version),
157
                reason=f"Could not find python {version} on system. Skipping.",
158
            ),
159
        )
160
        for version in versions
161
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc