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

pantsbuild / pants / 20332790708

18 Dec 2025 09:48AM UTC coverage: 64.992% (-15.3%) from 80.295%
20332790708

Pull #22949

github

web-flow
Merge f730a56cd into 407284c67
Pull Request #22949: Add experimental uv resolver for Python lockfiles

54 of 97 new or added lines in 5 files covered. (55.67%)

8270 existing lines in 295 files now uncovered.

48990 of 75379 relevant lines covered (64.99%)

1.81 hits per line

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

87.78
/src/python/pants/backend/nfpm/fields/rpm.py
1
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
4✔
5

6
from enum import Enum
4✔
7
from typing import ClassVar
4✔
8

9
from pants.backend.nfpm.config import NfpmContent
4✔
10
from pants.backend.nfpm.fields._relationships import NfpmPackageRelationshipsField
4✔
11
from pants.backend.nfpm.fields.all import NfpmDependencies
4✔
12
from pants.backend.nfpm.fields.scripts import NfpmPackageScriptsField
4✔
13
from pants.engine.addresses import Address
4✔
14
from pants.engine.target import InvalidFieldException, StringField, StringSequenceField
4✔
15
from pants.util.frozendict import FrozenDict
4✔
16
from pants.util.strutil import help_text
4✔
17

18
# These fields are used by the `nfpm_rpm_package` target
19
# Some fields will be duplicated by other package types which
20
# allows the help string to be packager-specific.
21

22

23
# Internally, nFPM just uses this to populate the default for 'packager'.
24
# So, there is no point exposing both in our UX.
25
# class NfpmRpmMaintainerField(StringField):
26
#     alias = "maintainer"
27

28

29
class NfpmRpmPackagerField(StringField):
4✔
30
    nfpm_alias = "rpm.packager"  # nFPM uses value of 'maintainer' as default.
4✔
31
    alias: ClassVar[str] = "packager"
4✔
32
    help = help_text(
4✔
33
        # based in part on the docs at:
34
        # https://nfpm.goreleaser.com/configuration/#reference
35
        lambda: f"""
36
        The name and email address of the packager or packager organization.
37

38
        The '{NfpmRpmPackagerField.alias}' is used to identify who actually
39
        packaged the software, as opposed to the author of the software.
40

41
        The name is first, then the email address inside angle brackets `<>`
42
        (in RFC5322 format). For example: "Foo Bar <maintainer@example.com>"
43
        This format is the conventional format; it not a hard requirement.
44

45
        See: https://ftp.osuosl.org/pub/rpm/max-rpm/s1-rpm-inside-tags.html#S3-RPM-INSIDE-PACKAGER-TAG
46

47
        N.B.: Packages distributed by Fedora do not use this field.
48
        https://docs.fedoraproject.org/en-US/packaging-guidelines/#_tags_and_sections
49
        """
50
    )
51
    # nFPM embeds this string as-is in the rpm package.
52

53

54
class NfpmRpmVendorField(StringField):
4✔
55
    nfpm_alias = "vendor"
4✔
56
    alias: ClassVar[str] = nfpm_alias
4✔
57
    help = help_text(
4✔
58
        """
59
        The entity responsible for packaging (typically an organization).
60

61
        See: https://ftp.osuosl.org/pub/rpm/max-rpm/s1-rpm-inside-tags.html#S3-RPM-INSIDE-VENDOR-TAG
62

63
        N.B.: Packages distributed by Fedora do not use this field.
64
        https://docs.fedoraproject.org/en-US/packaging-guidelines/#_tags_and_sections
65
        """
66
    )
67

68

69
class NfpmRpmGroupField(StringField):
4✔
70
    nfpm_alias = "rpm.group"
4✔
71
    alias: ClassVar[str] = "group"
4✔
72
    help = help_text(
4✔
73
        lambda: f"""
74
        For older rpm-based distros, this groups packages by their functionality.
75

76
        '{NfpmRpmGroupField.alias}' is a path-like string to allow for hierarchical
77
        grouping of applications like "Applications/Editors".
78

79
        See: https://ftp.osuosl.org/pub/rpm/max-rpm/s1-rpm-inside-tags.html#S3-RPM-INSIDE-GROUP-TAG
80

81
        N.B.: This field is only useful when packaging for old distros (EL 5 or earlier).
82
        All newer rpm-based distros have deprecated--and do not use--this field.
83
        https://docs.fedoraproject.org/en-US/packaging-guidelines/#_tags_and_sections
84
        """
85
    )
86

87

88
class NfpmRpmSummaryField(StringField):
4✔
89
    nfpm_alias = "rpm.summary"
4✔
90
    alias: ClassVar[str] = "summary"
4✔
91
    help = help_text(
4✔
92
        lambda: f"""
93
        A one-line description of the packaged software.
94

95
        If unset, nFPM will use the first line of 'description' for
96
        the '{NfpmRpmSummaryField.alias}'.
97

98
        See: https://ftp.osuosl.org/pub/rpm/max-rpm/s1-rpm-inside-tags.html#S3-RPM-INSIDE-SUMMARY-TAG
99
        """
100
    )
101

102

103
class NfpmRpmPrefixesField(StringSequenceField):
4✔
104
    nfpm_alias = "rpm.prefixes"
4✔
105
    alias: ClassVar[str] = "prefixes"
4✔
106
    help = help_text(
4✔
107
        lambda: f"""
108
        A list of relocatable prefixes (to support relocatable rpms).
109

110
        Defining '{NfpmRpmPrefixesField.alias}' allows rpm to install your package
111
        at an alternative prefix if the user requests that using the `--prefix` flag.
112
        This list is the default list of prefixes used by this package.
113
        If rpm relocates an installation of this package, it will strip off each of
114
        these prefixes, replacing them with the `--prefix` requested by the user.
115

116
        Before using this, ensure that any packaged software can actually run
117
        from alternate prefixes, and watch out for any absolute symlinks targets which
118
        might not be relocated correctly.
119

120
        RPM specs use the `Prefix:` tag to list each prefix in this list.
121

122
        See:
123
        https://rpm-software-management.github.io/rpm/manual/tags.html#packages-with-files
124
        https://ftp.osuosl.org/pub/rpm/max-rpm/ch-rpm-reloc.html
125
        https://ftp.osuosl.org/pub/rpm/max-rpm/s1-rpm-inside-tags.html#S3-RPM-INSIDE-PREFIX-TAG
126
        """
127
    )
128

129

130
class NfpmRpmReplacesField(NfpmPackageRelationshipsField):
4✔
131
    nfpm_alias = "replaces"
4✔
132
    alias: ClassVar[str] = nfpm_alias
4✔
133
    help = help_text(
4✔
134
        lambda: f"""
135
        A list of packages that this package obsoletes (replaces).
136

137
        When a pacakge name changes or splits, rpm uses "obsoletes" (ie the
138
        '{NfpmRpmReplacesField.alias}' field) on the new package to list the old
139
        package name(s) that can be upgraded to this package.
140

141
        The rpm file header uses the term "obsoletes" for this. This field is
142
        named "{NfpmRpmReplacesField.alias}" because that is the term used by nFPM.
143

144
        See:
145
        https://rpm-software-management.github.io/rpm/manual/dependencies.html#obsoletes
146
        """
147
    )
148

149

150
class NfpmRpmProvidesField(NfpmPackageRelationshipsField):
4✔
151
    nfpm_alias = "provides"
4✔
152
    alias: ClassVar[str] = nfpm_alias
4✔
153
    help = help_text(
4✔
154
        lambda: f"""
155
        A list of virtual packages or file paths that this package provides.
156

157
        This is used so that multiple packages can be be alternatives for each other.
158
        The list can include virtual package names and/or file paths. For example
159
        the `bash` package includes these in '{NfpmRpmProvidesField.alias}':
160

161
          - "bash"
162
          - "/bin/sh"
163
          - "/bin/bash"
164

165
        This means another package could also provide alternative implementations for
166
        the "bash" package name and could provide "/bin/sh" and/or "/bin/bash".
167

168
        N.B.: Virtual package names must not include any version numbers.
169

170
        See:
171
        https://rpm-software-management.github.io/rpm/manual/dependencies.html#provides
172
        https://ftp.osuosl.org/pub/rpm/max-rpm/s1-rpm-depend-manual-dependencies.html#S2-RPM-DEPEND-PROVIDES-TAG
173
        """
174
    )
175

176

177
class NfpmRpmDependsField(NfpmPackageRelationshipsField):
4✔
178
    nfpm_alias = "depends"
4✔
179
    alias: ClassVar[str] = nfpm_alias
4✔
180
    help = help_text(
4✔
181
        lambda: f"""
182
        List of package requirements (for package installers).
183

184
        The '{NfpmRpmDependsField.alias}' field has install-time requirements that can
185
        use version selectors (with one of `<`, `<=`, `=`, `>=`, `>` surrounded by
186
        spaces), where the version is formatted: `[epoch:]version[-release]`
187

188
          - "git"
189
          - "bash < 5"
190
          - "perl >= 9:5.00502-3"
191

192
        The rpm file header uses the term "requires" for this. This field is
193
        named "{NfpmRpmDependsField.alias}" because that is the term used by nFPM.
194

195
        WARNING: This is NOT the same as the 'dependencies' field!
196
        It does not accept pants-style dependencies like target addresses.
197

198
        See:
199
        https://rpm-software-management.github.io/rpm/manual/dependencies.html#requires
200
        https://rpm-software-management.github.io/rpm/manual/dependencies.html#versioning
201
        https://ftp.osuosl.org/pub/rpm/max-rpm/s1-rpm-inside-tags.html#S3-RPM-INSIDE-REQUIRES-TAG
202
        https://ftp.osuosl.org/pub/rpm/max-rpm/s1-rpm-depend-manual-dependencies.html#S2-RPM-DEPEND-REQUIRES-TAG
203
        """
204
    )
205

206

207
class NfpmRpmRecommendsField(NfpmPackageRelationshipsField):
4✔
208
    nfpm_alias = "recommends"
4✔
209
    alias: ClassVar[str] = nfpm_alias
4✔
210
    help = help_text(
4✔
211
        lambda: f"""
212
        List of weak package requirements (for package installers).
213

214
        This is like the '{NfpmRpmDependsField.alias}' field, but the package resolver
215
        can ignore the requirement if it cannot resolve the packages with it included.
216
        If an entry in '{NfpmRpmRecommendsField.alias}' is ignored, no error or warning gets
217
        reported.
218

219
        The '{NfpmRpmRecommendsField.alias}' field has the same syntax as the
220
        '{NfpmRpmDependsField.alias}' field.
221

222
        See:
223
        https://rpm-software-management.github.io/rpm/manual/dependencies.html#weak-dependencies
224
        """
225
    )
226

227

228
class NfpmRpmSuggestsField(NfpmPackageRelationshipsField):
4✔
229
    nfpm_alias = "suggests"
4✔
230
    alias: ClassVar[str] = nfpm_alias
4✔
231
    help = help_text(
4✔
232
        lambda: f"""
233
        List of very weak package requirements (for package installers).
234

235
        These suggestions are ignored by the package resolver. They are merely shown
236
        to the user as optional packages that the user might want to also install.
237

238
        The '{NfpmRpmSuggestsField.alias}' field has the same syntax as the
239
        '{NfpmRpmDependsField.alias}' field.
240

241
        See:
242
        https://rpm-software-management.github.io/rpm/manual/dependencies.html#weak-dependencies
243
        """
244
    )
245

246

247
class NfpmRpmConflictsField(NfpmPackageRelationshipsField):
4✔
248
    nfpm_alias = "conflicts"
4✔
249
    alias: ClassVar[str] = nfpm_alias
4✔
250
    help = help_text(
4✔
251
        lambda: f"""
252
        A list of packages that this package conflicts with.
253

254
        Packages that conflict with each other cannot be installed at the same time.
255

256
        The '{NfpmRpmConflictsField.alias}' field has the same syntax as the
257
        '{NfpmRpmDependsField.alias}' field.
258

259
        See:
260
        https://rpm-software-management.github.io/rpm/manual/dependencies.html#conflicts
261
        https://docs.fedoraproject.org/en-US/packaging-guidelines/Conflicts/
262
        https://ftp.osuosl.org/pub/rpm/max-rpm/s1-rpm-inside-tags.html#S3-RPM-INSIDE-CONFLICTS-TAG
263
        https://ftp.osuosl.org/pub/rpm/max-rpm/s1-rpm-depend-manual-dependencies.html#S2-RPM-DEPEND-CONFLICTS-TAG
264
        """
265
    )
266

267

268
class NfpmRpmCompressionAlgorithm(Enum):
4✔
269
    # This is what nFPM implements.
270
    gzip = "gzip"
4✔
271
    lzma = "lzma"
4✔
272
    xz = "xz"
4✔
273
    zstd = "zstd"
4✔
274

275

276
class NfpmRpmCompressionField(StringField):
4✔
277
    nfpm_alias = "rpm.compression"
4✔
278
    alias: ClassVar[str] = "compression"
4✔
279
    valid_choices = NfpmRpmCompressionAlgorithm
4✔
280
    default = f"{NfpmRpmCompressionAlgorithm.gzip.value}:-1"  # same default as nFPM
4✔
281
    help = help_text(
4✔
282
        lambda: f"""
283
        The compression algorithm to use on the rpm package.
284

285
        This takes a compression algorithm and optionally a compression level.
286
        To specify a level, use 'algorithm:level'. Specifying a compression level
287
        is only valid for '{NfpmRpmCompressionAlgorithm.gzip.value}' or
288
        '{NfpmRpmCompressionAlgorithm.zstd.value}'.
289

290
        Here are several gzip examples with and without the optional compression level
291
        (-1 means use the default level which is 5; 9 is the max).
292

293
          - '{NfpmRpmCompressionAlgorithm.gzip.value}:9'
294
          - '{NfpmRpmCompressionAlgorithm.gzip.value}:0'
295
          - '{NfpmRpmCompressionAlgorithm.gzip.value}:-1'
296
          - '{NfpmRpmCompressionAlgorithm.gzip.value}:5'
297
          - '{NfpmRpmCompressionAlgorithm.gzip.value}'
298

299
        Here are several zstd examples. Note that nFPM uses a library that  only
300
        defines four named compression levels, and then maps the zstd integer
301
        levels to those. You may specify the zstd level as an integer, or using
302
        these names: https://github.com/klauspost/compress/tree/master/zstd#status
303

304
          - '{NfpmRpmCompressionAlgorithm.zstd.value}:fastest'
305
          - '{NfpmRpmCompressionAlgorithm.zstd.value}:default'
306
          - '{NfpmRpmCompressionAlgorithm.zstd.value}:better'
307
          - '{NfpmRpmCompressionAlgorithm.zstd.value}:best'
308
          - '{NfpmRpmCompressionAlgorithm.zstd.value}:3'
309
          - '{NfpmRpmCompressionAlgorithm.zstd.value}:9'
310
          - '{NfpmRpmCompressionAlgorithm.zstd.value}'
311
        """
312
    )
313

314
    @classmethod
4✔
315
    def compute_value(cls, raw_value: str | None, address: Address) -> str | None:
4✔
316
        if raw_value is None:
1✔
317
            # valid_choices has only algorithms, not compression level.
318
            # So, return default value, skipping check for default in valid_choices.
319
            return cls.default
1✔
320

321
        # We only need to do custom computation if raw_value has the optional level.
322
        if not (isinstance(raw_value, cls.expected_type) and ":" in raw_value):
×
323
            # If defined, only algorithm was provided, not level.
324
            # If not defined, this will apply the default.
325
            return super().compute_value(raw_value, address)
×
326

327
        # ":" is in raw_value so both an algorithm and level were provided.
328
        raw_algorithm, level, *unknown = raw_value.split(":")
×
329

330
        # This will check algorithm against the algorithm Enum.
331
        computed_algorithm = super().compute_value(raw_algorithm, address)
×
332
        if computed_algorithm not in ("gzip", "zstd"):
×
333
            raise InvalidFieldException(
×
334
                f"Values for the '{cls.alias}' field in target {address} "
335
                "may only specify a compression level for gzip or zstd compression, "
336
                f"but {repr(computed_algorithm)} was provided as {repr(raw_value)}."
337
            )
338

339
        if unknown:
×
340
            raise InvalidFieldException(
×
341
                f"Values for the '{cls.alias}' field in target {address} "
342
                f"must not have more than one ':', but got {len(unknown) + 1}."
343
                "Only use ':' to specify an optional compression level after "
344
                "the compression algorithm (ie '<algorithm>[:<level>]')."
345
            )
346

347
        # Pass level as-is w/o sanitization or type checking because
348
        # there are too many possible levels to check it sanely here.
349
        return f"{computed_algorithm}:{level}"
×
350

351

352
class NfpmRpmScriptsField(NfpmPackageScriptsField):
4✔
353
    nfpm_aliases: ClassVar[FrozenDict[str, str]] = FrozenDict(
4✔
354
        {
355
            **NfpmPackageScriptsField.nfpm_aliases,
356
            "pretrans": "rpm.scripts.pretrans",
357
            "posttrans": "rpm.scripts.posttrans",
358
            "verify": "rpm.scripts.verify",
359
        }
360
    )
361
    help = help_text(
4✔
362
        f"""
363
        Map of install scriptlet source files for the deb package.
364

365
        This maps the script type (key) to the script source file (value).
366
        Each of the script source file(s) must be provided via '{NfpmDependencies.alias}'.
367
        The script types are the names used by nFPM. For reference, RPM
368
        uses the following scriptlet tag names instead and runs them before/after
369
        the indicated phase:
370

371
        | nFPM term   | RPM scriptlet | RPM phase   |
372
        +-------------+---------------+-------------+
373
        | preinstall  | %pre          | install     |
374
        | postinstall | %post         | install     |
375
        | preremove   | %preun        | uninstall   |
376
        | postremove  | %postun       | uninstall   |
377
        | pretrans    | %pretrans     | transaction |
378
        | posttrans   | %posttrans    | transaction |
379
        | verify      | %verifyscript | verify      |
380

381
        Please consult the RPM docs to understand what is required of these scripts.
382

383
        See:
384
        https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/
385
        https://rpm-software-management.github.io/rpm/manual/tags.html#scriptlets
386
        https://ftp.osuosl.org/pub/rpm/max-rpm/s1-rpm-inside-scripts.html#S2-RPM-INSIDE-ERASE-TIME-SCRIPTS
387
        """
388
    )
389

390

391
class NfpmRpmGhostContents(StringSequenceField):
4✔
392
    nfpm_alias = ""  # does not map directly to a nfpm.yaml field
4✔
393
    alias = "ghost_contents"
4✔
394
    help = help_text(
4✔
395
        """
396
        A list of files that this package owns, but that this package does not include.
397

398
        Examples of ghosted files include:
399
          - A log file or a state file that does not exist until runtime.
400
          - A binary that is managed by 'alternatives'.
401

402
        RPM specs use the `%ghost` directive to list these ghosted files.
403

404
        Each file in this list gets passed to nFPM via the 'contents' field with
405
        'type=ghost'. Then nFPM translates that into the appropriate RPM header.
406
        The file does not need to exist in your pants workspace as nFPM directly
407
        adds it to the RPM header.
408

409
        See: https://ftp.osuosl.org/pub/rpm/max-rpm/s1-rpm-inside-files-list-directives.html#S3-RPM-INSIDE-FLIST-GHOST-DIRECTIVE
410

411
        N.B.: Packages distributed by Fedora must use this if they provide 'alternatives'.
412
        https://docs.fedoraproject.org/en-US/packaging-guidelines/Alternatives/#_how_to_use_alternatives
413
        """
414
    )
415
    # nFPM normalizes paths to be absolute, so "" is effectively the same as "/".
416
    # But this does not validate the paths, leaving it up to nFPM to do any validation
417
    # that a specific packager might require.
418

419
    @property
4✔
420
    def nfpm_contents(self) -> list[NfpmContent]:
4✔
UNCOV
421
        contents = [NfpmContent(type="ghost", dst=ghost) for ghost in self.value or ()]
×
UNCOV
422
        return contents
×
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