• 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

56.94
/src/python/pants/backend/docker/lint/hadolint/rules_integration_test.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
2✔
5

6
from textwrap import dedent
2✔
7
from typing import Any
2✔
8

9
import pytest
2✔
10

11
from pants.backend.docker.lint.hadolint.rules import HadolintFieldSet, HadolintRequest
2✔
12
from pants.backend.docker.lint.hadolint.rules import rules as hadolint_rules
2✔
13
from pants.backend.docker.rules import rules as docker_rules
2✔
14
from pants.backend.docker.target_types import DockerImageTarget
2✔
15
from pants.core.goals import package
2✔
16
from pants.core.goals.lint import LintResult, Partitions
2✔
17
from pants.core.util_rules import config_files, external_tool, source_files
2✔
18
from pants.engine.addresses import Address
2✔
19
from pants.engine.target import Target
2✔
20
from pants.testutil.rule_runner import QueryRule, RuleRunner
2✔
21

22

23
@pytest.fixture
2✔
24
def rule_runner() -> RuleRunner:
2✔
25
    return RuleRunner(
2✔
26
        rules=[
27
            *config_files.rules(),
28
            *docker_rules(),
29
            *external_tool.rules(),
30
            *hadolint_rules(),
31
            *package.rules(),
32
            *source_files.rules(),
33
            QueryRule(Partitions, [HadolintRequest.PartitionRequest]),
34
            QueryRule(LintResult, [HadolintRequest.Batch]),
35
        ],
36
        target_types=[DockerImageTarget],
37
    )
38

39

40
GOOD_FILE = "FROM python:3.8"
2✔
41
BAD_FILE = "FROM python"
2✔
42

43

44
def run_hadolint(
2✔
45
    rule_runner: RuleRunner, targets: list[Target], *, extra_args: list[str] | None = None
46
) -> tuple[LintResult, ...]:
47
    rule_runner.set_options(
2✔
48
        extra_args or (),
49
        env_inherit={"PATH"},
50
    )
51
    partitions = rule_runner.request(
2✔
52
        Partitions[HadolintFieldSet, Any],
53
        [HadolintRequest.PartitionRequest(tuple(HadolintFieldSet.create(tgt) for tgt in targets))],
54
    )
55
    results = []
2✔
56
    for partition in partitions:
2✔
57
        result = rule_runner.request(
2✔
58
            LintResult,
59
            [HadolintRequest.Batch("", partition.elements, partition.metadata)],
60
        )
61
        results.append(result)
2✔
62
    return tuple(results)
2✔
63

64

65
def assert_success(
2✔
66
    rule_runner: RuleRunner, target: Target, *, extra_args: list[str] | None = None
67
) -> None:
UNCOV
68
    result = run_hadolint(rule_runner, [target], extra_args=extra_args)
×
UNCOV
69
    assert len(result) == 1
×
UNCOV
70
    assert result[0].exit_code == 0
×
UNCOV
71
    assert not result[0].stdout
×
UNCOV
72
    assert not result[0].stderr
×
73

74

75
def test_passing(rule_runner: RuleRunner) -> None:
2✔
UNCOV
76
    rule_runner.write_files({"Dockerfile": GOOD_FILE, "BUILD": "docker_image(name='t')"})
×
UNCOV
77
    tgt = rule_runner.get_target(Address("", target_name="t"))
×
UNCOV
78
    assert_success(rule_runner, tgt)
×
79

80

81
@pytest.mark.platform_specific_behavior
2✔
82
def test_failing(rule_runner: RuleRunner) -> None:
2✔
83
    rule_runner.write_files({"Dockerfile": BAD_FILE, "BUILD": "docker_image(name='t')"})
2✔
84
    tgt = rule_runner.get_target(Address("", target_name="t"))
2✔
85
    result = run_hadolint(rule_runner, [tgt])
2✔
86
    assert len(result) == 1
2✔
87
    assert result[0].exit_code == 1
2✔
88
    assert "Dockerfile:1 " in result[0].stdout
2✔
89

90

91
def test_multiple_targets(rule_runner: RuleRunner) -> None:
2✔
UNCOV
92
    rule_runner.write_files(
×
93
        {
94
            "Dockerfile.good": GOOD_FILE,
95
            "Dockerfile.bad": BAD_FILE,
96
            "BUILD": dedent(
97
                """
98
                docker_image(name="good", source="Dockerfile.good")
99
                docker_image(name="bad", source="Dockerfile.bad")
100
                """
101
            ),
102
        }
103
    )
UNCOV
104
    tgts = [
×
105
        rule_runner.get_target(
106
            Address("", target_name="good", relative_file_path="Dockerfile.good")
107
        ),
108
        rule_runner.get_target(Address("", target_name="bad", relative_file_path="Dockerfile.bad")),
109
    ]
UNCOV
110
    result = run_hadolint(rule_runner, tgts)
×
UNCOV
111
    assert len(result) == 1
×
UNCOV
112
    assert result[0].exit_code == 1
×
UNCOV
113
    assert "Dockerfile.good" not in result[0].stdout
×
UNCOV
114
    assert "Dockerfile.bad:1 " in result[0].stdout
×
115

116

117
def test_config_files(rule_runner: RuleRunner) -> None:
2✔
UNCOV
118
    rule_runner.write_files(
×
119
        {
120
            ".hadolint.yaml": "ignored: [DL3006, DL3061, DL3011]",
121
            "a/Dockerfile": BAD_FILE,
122
            "a/BUILD": "docker_image()",
123
            "b/Dockerfile": BAD_FILE,
124
            "b/BUILD": "docker_image()",
125
            "c/BUILD": "docker_image()",
126
            "c/Dockerfile": "EXPOSE 123456",
127
        }
128
    )
UNCOV
129
    tgts = [
×
130
        rule_runner.get_target(Address("a")),
131
        rule_runner.get_target(Address("b")),
132
    ]
UNCOV
133
    result = run_hadolint(rule_runner, tgts)
×
UNCOV
134
    assert len(result) == 1
×
UNCOV
135
    assert result[0].exit_code == 0
×
UNCOV
136
    assert "a/Dockerfile" not in result[0].stdout
×
UNCOV
137
    assert "b/Dockerfile " not in result[0].stdout
×
138

UNCOV
139
    tgt = rule_runner.get_target(Address("c"))
×
UNCOV
140
    assert_success(rule_runner, tgt, extra_args=["--hadolint-config=.hadolint.yaml"])
×
141

142

143
def test_passthrough_args(rule_runner: RuleRunner) -> None:
2✔
UNCOV
144
    rule_runner.write_files({"Dockerfile": BAD_FILE, "BUILD": "docker_image(name='t')"})
×
UNCOV
145
    tgt = rule_runner.get_target(Address("", target_name="t"))
×
UNCOV
146
    assert_success(rule_runner, tgt, extra_args=["--hadolint-args=--ignore DL3006"])
×
147

148

149
def test_skip(rule_runner: RuleRunner) -> None:
2✔
UNCOV
150
    rule_runner.write_files({"Dockerfile": BAD_FILE, "BUILD": "docker_image(name='t')"})
×
UNCOV
151
    tgt = rule_runner.get_target(Address("", target_name="t"))
×
UNCOV
152
    result = run_hadolint(rule_runner, [tgt], extra_args=["--hadolint-skip"])
×
UNCOV
153
    assert not result
×
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

© 2026 Coveralls, Inc