• 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

0.0
/src/python/pants/backend/k8s/goals/deploy.py
1
# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
UNCOV
3
from __future__ import annotations
×
4

UNCOV
5
import logging
×
UNCOV
6
from dataclasses import dataclass
×
7

UNCOV
8
from pants.backend.docker.goals.package_image import DockerPackageFieldSet
×
UNCOV
9
from pants.backend.k8s.k8s_subsystem import K8sSubsystem
×
UNCOV
10
from pants.backend.k8s.kubectl_subsystem import Kubectl
×
UNCOV
11
from pants.backend.k8s.target_types import (
×
12
    K8sBundleContextField,
13
    K8sBundleDependenciesField,
14
    K8sBundleSourcesField,
15
    K8sSourceField,
16
)
UNCOV
17
from pants.core.goals.deploy import DeployFieldSet, DeployProcess
×
UNCOV
18
from pants.core.util_rules.env_vars import environment_vars_subset
×
UNCOV
19
from pants.core.util_rules.external_tool import download_external_tool
×
UNCOV
20
from pants.engine.env_vars import EnvironmentVarsRequest
×
UNCOV
21
from pants.engine.fs import MergeDigests
×
UNCOV
22
from pants.engine.internals.graph import hydrate_sources, resolve_targets
×
UNCOV
23
from pants.engine.internals.native_engine import Digest
×
UNCOV
24
from pants.engine.intrinsics import digest_to_snapshot
×
UNCOV
25
from pants.engine.platform import Platform
×
UNCOV
26
from pants.engine.process import InteractiveProcess, Process, ProcessCacheScope
×
UNCOV
27
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
×
UNCOV
28
from pants.engine.target import DependenciesRequest, HydrateSourcesRequest, SourcesField
×
UNCOV
29
from pants.engine.unions import UnionRule
×
UNCOV
30
from pants.util.frozendict import FrozenDict
×
UNCOV
31
from pants.util.logging import LogLevel
×
32

UNCOV
33
logger = logging.getLogger(__name__)
×
34

35

UNCOV
36
@dataclass(frozen=True)
×
UNCOV
37
class DeployK8sBundleFieldSet(DeployFieldSet):
×
UNCOV
38
    required_fields = (
×
39
        K8sBundleSourcesField,
40
        K8sBundleContextField,
41
        K8sBundleDependenciesField,
42
    )
UNCOV
43
    sources: K8sBundleSourcesField
×
UNCOV
44
    context: K8sBundleContextField
×
UNCOV
45
    dependencies: K8sBundleDependenciesField
×
46

47

UNCOV
48
@dataclass(frozen=True)
×
UNCOV
49
class KubectlApply:
×
UNCOV
50
    paths: tuple[str, ...]
×
UNCOV
51
    input_digest: Digest
×
UNCOV
52
    platform: Platform
×
UNCOV
53
    env: FrozenDict[str, str] | None = None
×
UNCOV
54
    context: str | None = None
×
55

56

UNCOV
57
@rule
×
UNCOV
58
async def kubectl_apply_process(
×
59
    request: KubectlApply, platform: Platform, kubectl: Kubectl
60
) -> Process:
61
    tool_relpath = "__kubectl"
×
62
    argv: tuple[str, ...] = (f"{tool_relpath}/kubectl",)
×
63

64
    if request.context is not None:
×
65
        argv += ("--context", request.context)
×
66

67
    argv += ("apply", "-o", "yaml")
×
68

69
    for path in request.paths:
×
70
        argv += ("-f", path)
×
71

72
    kubectl_tool = await download_external_tool(kubectl.get_request(platform))
×
73

74
    immutable_input_digests = {
×
75
        tool_relpath: kubectl_tool.digest,
76
    }
77

78
    return Process(
×
79
        argv=argv,
80
        input_digest=request.input_digest,
81
        cache_scope=ProcessCacheScope.PER_SESSION,
82
        description=f"Applying kubernetes config {request.paths}",
83
        env=request.env,
84
        immutable_input_digests=immutable_input_digests,
85
    )
86

87

UNCOV
88
@rule(desc="Run k8s deploy process", level=LogLevel.DEBUG)
×
UNCOV
89
async def run_k8s_deploy(
×
90
    field_set: DeployK8sBundleFieldSet,
91
    kubectl: Kubectl,
92
    k8s_subsystem: K8sSubsystem,
93
    platform: Platform,
94
) -> DeployProcess:
95
    context = field_set.context.value
×
96
    if context is None:
×
97
        raise ValueError(
×
98
            f"Missing `{K8sBundleContextField.alias}` field on target `{field_set.address.spec}`"
99
        )
100

101
    context = context if kubectl.pass_context else None
×
102
    if context is not None and context not in k8s_subsystem.available_contexts:
×
103
        raise ValueError(
×
104
            f"Context `{context}` is not listed in `[{K8sSubsystem.options_scope}].available_contexts`"
105
        )
106

107
    dependencies = await resolve_targets(
×
108
        **implicitly(field_set.sources.to_unparsed_address_inputs())
109
    )
110
    file_sources = await concurrently(
×
111
        hydrate_sources(
112
            HydrateSourcesRequest(
113
                t.get(SourcesField),
114
                for_sources_types=(K8sSourceField,),
115
                enable_codegen=True,
116
            ),
117
            **implicitly(),
118
        )
119
        for t in dependencies
120
    )
121
    snapshot, target_dependencies = await concurrently(
×
122
        digest_to_snapshot(
123
            **implicitly(MergeDigests((*(sources.snapshot.digest for sources in file_sources),)))
124
        ),
125
        resolve_targets(**implicitly(DependenciesRequest(field_set.dependencies))),
126
    )
127

128
    publish_targets = [
×
129
        tgt for tgt in target_dependencies if DockerPackageFieldSet.is_applicable(tgt)
130
    ]
131

132
    env = await environment_vars_subset(
×
133
        **implicitly(
134
            {EnvironmentVarsRequest(requested=kubectl.extra_env_vars): EnvironmentVarsRequest}
135
        )
136
    )
137

138
    process = InteractiveProcess.from_process(
×
139
        await kubectl_apply_process(
140
            KubectlApply(
141
                snapshot.files,
142
                platform=platform,
143
                input_digest=snapshot.digest,
144
                env=env,
145
                context=context,
146
            ),
147
            **implicitly(),
148
        )
149
    )
150

151
    description = f"context {context}" if context is not None else None
×
152

153
    return DeployProcess(
×
154
        name=field_set.address.spec,
155
        publish_dependencies=tuple(publish_targets),
156
        process=process,
157
        description=description,
158
    )
159

160

UNCOV
161
def rules():
×
UNCOV
162
    return [
×
163
        *collect_rules(),
164
        UnionRule(DeployFieldSet, DeployK8sBundleFieldSet),
165
    ]
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