• 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

80.0
/src/python/pants/jvm/resolve/common.py
1
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
11✔
5

6
import dataclasses
11✔
7
from collections.abc import Iterable
11✔
8
from dataclasses import dataclass
11✔
9
from urllib.parse import quote_plus as url_quote_plus
11✔
10

11
from pants.engine.collection import DeduplicatedCollection
11✔
12
from pants.engine.target import Target
11✔
13
from pants.jvm.resolve.coordinate import Coordinate
11✔
14
from pants.jvm.target_types import (
11✔
15
    JvmArtifactArtifactField,
16
    JvmArtifactExclusionsField,
17
    JvmArtifactFieldSet,
18
    JvmArtifactForceVersionField,
19
    JvmArtifactGroupField,
20
    JvmArtifactJarSourceField,
21
    JvmArtifactUrlField,
22
    JvmArtifactVersionField,
23
)
24
from pants.util.ordered_set import FrozenOrderedSet
11✔
25

26

27
@dataclass(frozen=True)
11✔
28
class ArtifactRequirement:
11✔
29
    """A single Maven-style coordinate for a JVM dependency, along with information of how to fetch
30
    the dependency if it is not to be fetched from a Maven repository."""
31

32
    coordinate: Coordinate
11✔
33

34
    url: str | None = None
11✔
35
    jar: JvmArtifactJarSourceField | None = None
11✔
36
    excludes: frozenset[str] | None = None
11✔
37
    force_version: bool = False
11✔
38

39
    @classmethod
11✔
40
    def from_jvm_artifact_target(cls, target: Target) -> ArtifactRequirement:
11✔
41
        if not JvmArtifactFieldSet.is_applicable(target):
×
42
            raise AssertionError(
×
43
                "`ArtifactRequirement.from_jvm_artifact_target()` only works on targets with "
44
                "`JvmArtifactFieldSet` fields present."
45
            )
46

47
        exclusions = target[JvmArtifactExclusionsField].value or ()
×
48
        return ArtifactRequirement(
×
49
            coordinate=Coordinate(
50
                group=target[JvmArtifactGroupField].value,
51
                artifact=target[JvmArtifactArtifactField].value,
52
                version=target[JvmArtifactVersionField].value,
53
            ),
54
            url=target[JvmArtifactUrlField].value,
55
            jar=(
56
                target[JvmArtifactJarSourceField]
57
                if target[JvmArtifactJarSourceField].value
58
                else None
59
            ),
60
            excludes=frozenset([*(exclusion.to_coord_str() for exclusion in exclusions)]) or None,
61
            force_version=target[JvmArtifactForceVersionField].value,
62
        )
63

64
    def with_extra_excludes(self, *excludes: str) -> ArtifactRequirement:
11✔
65
        """Creates a copy of this `ArtifactRequirement` with `excludes` provided.
66

67
        Mostly useful for testing (`Coordinate(...).as_requirement().with_extra_excludes(...)`).
68
        """
69

70
        return dataclasses.replace(
×
71
            self, excludes=self.excludes.union(excludes) if self.excludes else frozenset(excludes)
72
        )
73

74
    def to_coord_arg_str(self) -> str:
11✔
75
        return self.coordinate.to_coord_arg_str(
×
76
            {"url": url_quote_plus(self.url)} if self.url else {}
77
        )
78

79
    def to_metadata_str(self) -> str:
11✔
80
        attrs = {
9✔
81
            "url": self.url or "not_provided",
82
            "jar": self.jar.address.spec if self.jar else "not_provided",
83
        }
84
        if self.excludes:
9✔
85
            attrs["excludes"] = ",".join(sorted(self.excludes))
×
86

87
        return self.coordinate.to_coord_arg_str(attrs)
9✔
88

89

90
# TODO: Consider whether to carry classpath scope in some fashion via ArtifactRequirements.
91
class ArtifactRequirements(DeduplicatedCollection[ArtifactRequirement]):
11✔
92
    """An ordered list of Coordinates used as requirements."""
93

94
    @classmethod
11✔
95
    def from_coordinates(cls, coordinates: Iterable[Coordinate]) -> ArtifactRequirements:
11✔
UNCOV
96
        return ArtifactRequirements(ArtifactRequirement(coord) for coord in coordinates)
×
97

98

99
@dataclass(frozen=True)
11✔
100
class GatherJvmCoordinatesRequest:
11✔
101
    """A request to turn strings of coordinates (`group:artifact:version`) and/or addresses to
102
    `jvm_artifact` targets into `ArtifactRequirements`."""
103

104
    artifact_inputs: FrozenOrderedSet[str]
11✔
105
    option_name: str
11✔
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