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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

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

UNCOV
4
from __future__ import annotations
×
5

UNCOV
6
import os
×
UNCOV
7
import stat
×
UNCOV
8
from abc import ABCMeta
×
UNCOV
9
from collections.abc import Iterable
×
UNCOV
10
from enum import Enum
×
UNCOV
11
from typing import Any, ClassVar, cast
×
12

UNCOV
13
from pants.backend.nfpm.fields.all import NfpmDependencies
×
UNCOV
14
from pants.backend.nfpm.subsystem import MTIME_DEFAULT
×
UNCOV
15
from pants.core.target_types import RelocatedFiles
×
UNCOV
16
from pants.engine.addresses import Address
×
UNCOV
17
from pants.engine.target import (
×
18
    AsyncFieldMixin,
19
    ImmutableValue,
20
    IntField,
21
    InvalidFieldException,
22
    OptionalSingleSourceField,
23
    OverridesField,
24
    StringField,
25
    StringSequenceField,
26
    TupleSequenceField,
27
    ValidNumbers,
28
)
UNCOV
29
from pants.util.frozendict import FrozenDict
×
UNCOV
30
from pants.util.strutil import help_text
×
31

32
# -----------------------------------------------------------------------------------------------
33
# file_info fields
34
# -----------------------------------------------------------------------------------------------
35

36

UNCOV
37
class NfpmContentFileOwnerField(StringField):
×
UNCOV
38
    nfpm_alias = "contents.[].file_info.owner"
×
UNCOV
39
    alias: ClassVar[str] = "file_owner"
×
UNCOV
40
    default = "root"  # Make the nFPM default visible in help.
×
UNCOV
41
    help = help_text(
×
42
        """
43
        Username that should own this packaged file or directory.
44

45
        This is like the OWNER arg in chown: https://www.mankier.com/1/chown
46
        """
47
    )
48

49

UNCOV
50
class NfpmContentFileGroupField(StringField):
×
UNCOV
51
    nfpm_alias = "contents.[].file_info.group"
×
UNCOV
52
    alias: ClassVar[str] = "file_group"
×
UNCOV
53
    default = "root"  # Make the nFPM default visible in help.
×
UNCOV
54
    help = help_text(
×
55
        """
56
        Name of the group that should own this packaged file or directory.
57

58
        This is like the GROUP arg in chown: https://www.mankier.com/1/chown
59
        """
60
    )
61

62

63
# inlined private var from https://github.com/python/cpython/blob/3.9/Lib/stat.py#L128-L154
UNCOV
64
_FILEMODE_TABLE = (
×
65
    (),  # exclude irrelevant filetype bits that are in stat._filemode_table
66
    ((stat.S_IRUSR, "r"),),
67
    ((stat.S_IWUSR, "w"),),
68
    ((stat.S_IXUSR | stat.S_ISUID, "s"), (stat.S_ISUID, "S"), (stat.S_IXUSR, "x")),
69
    ((stat.S_IRGRP, "r"),),
70
    ((stat.S_IWGRP, "w"),),
71
    ((stat.S_IXGRP | stat.S_ISGID, "s"), (stat.S_ISGID, "S"), (stat.S_IXGRP, "x")),
72
    ((stat.S_IROTH, "r"),),
73
    ((stat.S_IWOTH, "w"),),
74
    ((stat.S_IXOTH | stat.S_ISVTX, "t"), (stat.S_ISVTX, "T"), (stat.S_IXOTH, "x")),
75
)
UNCOV
76
_FILEMODE_CHARS = frozenset("-rwxsStT")
×
77

78

UNCOV
79
def _parse_filemode(filemode: str) -> int:
×
80
    """Parse string filemode into octal representation.
81

82
    This is the opposite of stat.filemode, except that it does not support the filetype bits.
83
    """
UNCOV
84
    if len(filemode) != 9:
×
UNCOV
85
        raise ValueError(f"Symbolic file mode must be exactly 9 characters, not {len(filemode)}.")
×
UNCOV
86
    if not _FILEMODE_CHARS.issuperset(filemode):
×
UNCOV
87
        raise ValueError(
×
88
            "Cannot parse symbolic file mode with unknown symbols: "
89
            + "".join(set(filemode).difference(_FILEMODE_CHARS))
90
        )
91

UNCOV
92
    mode = 0
×
93
    # enumerate starting with index 1 to avoid irrelevant filetype bits.
UNCOV
94
    for i, symbol in enumerate(filemode, 1):
×
UNCOV
95
        if symbol == "-":
×
UNCOV
96
            continue
×
UNCOV
97
        for bit, char in _FILEMODE_TABLE[i]:
×
UNCOV
98
            if symbol == char:
×
UNCOV
99
                mode = mode | bit
×
UNCOV
100
                break
×
101
        else:
102
            # else none of the chars matched symbol (loop didn't hit break)
UNCOV
103
            raise ValueError(
×
104
                f"Symbol at position {i} is unknown: '{symbol}'. Valid symbols at "
105
                f"position {i}: {''.join(char for _, char in _FILEMODE_TABLE[i])}"
106
            )
UNCOV
107
    return mode
×
108

109

UNCOV
110
class NfpmContentFileModeField(IntField):
×
UNCOV
111
    nfpm_alias = "contents.[].file_info.mode"
×
UNCOV
112
    alias: ClassVar[str] = "file_mode"
×
UNCOV
113
    help = help_text(
×
114
        """
115
        A file mode as a numeric octal, an string octal, or a symbolic representation.
116

117
        NB: In most cases, you should set this field and not rely on the default value.
118
        Pants only tracks the executable bit for workspace files. So, this field defaults
119
        to 0o755 for executable files and 0o644 for files that are not executable.
120

121
        You may specify the file mode as: an octal, an octal string, or a symbolic string.
122
        If you specify a numeric octal (not as a string), make sure to include python's
123
        octal prefix: `0o` like in `0o644`. If you specify the octal as a string,
124
        the `Oo` prefix is optional (like `644`). If you specify a symbolic file mode string,
125
        you must provide 9 characters with "-" in place of any absent permissions
126
        (like `'rw-r--r--'`).
127

128
        For example to specify world readable/executable and user writable, these
129
        are equivalent:
130

131
          - `0o755`
132
          - `'755'`
133
          - `'rwxr-xr-x'`
134

135
        Another example for a file with read/write permissions for only the user:
136

137
          - `0o600`
138
          - `'600'`
139
          - `'rw-------'`
140

141
        Another example for a file with the group sticky bit set:
142

143
          - `0o2660`
144
          - `'2660'`
145
          - `'rw-rwS---'`
146

147
        WARNING: If you forget to include the `0o` prefix on a numeric octal, then
148
        it will be interpreted as an integer which is probably not what you want.
149
        For example, `755` (no quotes) will be processed as `0o1363` (symbolically
150
        that would be '-wxrw--wt') which is probably not what you intended. Pants
151
        cannot detect errors like this, so be careful to either use a string or
152
        include the `0o` octal prefix.
153
        """
154
    )
155

156
    # The octal should be between 0o0000 and 0o7777 (inclusive)
UNCOV
157
    valid_numbers = ValidNumbers.positive_and_zero
×
158

UNCOV
159
    @classmethod
×
UNCOV
160
    def compute_value(cls, raw_value: int | str | None, address: Address) -> int | None:
×
UNCOV
161
        if isinstance(raw_value, str):
×
UNCOV
162
            try:
×
UNCOV
163
                octal_value = int(raw_value, 8)
×
UNCOV
164
            except ValueError:
×
UNCOV
165
                try:
×
UNCOV
166
                    octal_value = _parse_filemode(raw_value)
×
UNCOV
167
                except ValueError as e:
×
UNCOV
168
                    raise InvalidFieldException(
×
169
                        f"The '{cls.alias}' field in target {address} must be "
170
                        "an octal (like 0o755 or 0o600), "
171
                        "an octal as a string (like '755' or '600'), "
172
                        "or a symbolic file mode (like 'rwxr-xr-x' or 'rw-------'). "
173
                        f"It is set to {repr(raw_value)}."
174
                    ) from e
UNCOV
175
            value = super().compute_value(octal_value, address)
×
176
        else:
UNCOV
177
            value = super().compute_value(raw_value, address)
×
UNCOV
178
        if value is not None and value > 0o7777:
×
UNCOV
179
            raise InvalidFieldException(
×
180
                f"The '{cls.alias} field in target {address} must be less than or equal to "
181
                f"0o7777, but was set to {repr(raw_value)} (ie: `{value:#o}`)."
182
            )
UNCOV
183
        return value
×
184

185

UNCOV
186
class NfpmContentFileMtimeField(StringField):
×
UNCOV
187
    nfpm_alias = "contents.[].file_info.mtime"
×
UNCOV
188
    alias: ClassVar[str] = "file_mtime"
×
UNCOV
189
    default = MTIME_DEFAULT
×
UNCOV
190
    help = help_text(
×
191
        f"""
192
        The file modification time as an RFC 3339 formatted string.
193

194
        For example: 2008-01-02T15:04:05Z
195

196
        The format is defined in RFC 3339: https://rfc-editor.org/rfc/rfc3339.html
197

198
        Though nFPM supports pulling mtime from the src file or directory in most
199
        cases, the pants nfpm backend does not support this. Reading the mtime from
200
        the filesystem is problematic because Pants does not track the mtime of files
201
        and does not propagate any file mtime into the sandboxes. Reasons for this
202
        include: git does not track mtime, timestamps like mtime cause many issues
203
        for reproducible packaging builds, and reproducible builds are required
204
        for pants to provide its fine-grained caches.
205

206
        The default value is {repr(MTIME_DEFAULT)}. You may also override
207
        the default value by setting `[nfpm].default_mtime` in `pants.toml`,
208
        or by setting the `SOURCE_DATE_EPOCH` environment variable.
209

210
        See also: https://reproducible-builds.org/docs/timestamps/
211
        """
212
    )
213

UNCOV
214
    def normalized_value(self, default_mtime: str | None):
×
215
        if self.value == self.default and default_mtime and self.value != default_mtime:
×
216
            return default_mtime
×
217
        return self.value
×
218

219

220
# -----------------------------------------------------------------------------------------------
221
# Internal generic parent class fields
222
# -----------------------------------------------------------------------------------------------
223

224

UNCOV
225
class _SrcDstSequenceField(TupleSequenceField):
×
UNCOV
226
    nfpm_alias = ""
×
227

UNCOV
228
    expected_element_type = str
×
UNCOV
229
    expected_element_count = 2
×
UNCOV
230
    expected_type_description = "an iterable of 2-string-tuples (tuple[tuple[str, str], ...])"
×
UNCOV
231
    expected_element_type_description = "2-string-tuples (tuple[str, str])"
×
UNCOV
232
    value: tuple[tuple[str, str], ...]
×
233

234
    # Subclasses must define these
UNCOV
235
    _dst_alias: ClassVar[str]
×
236

UNCOV
237
    @classmethod
×
UNCOV
238
    def compute_value(
×
239
        cls, raw_value: Iterable[Iterable[str]] | None, address: Address
240
    ) -> tuple[tuple[str, str], ...]:
241
        src_dst_map = super().compute_value(raw_value, address)
×
242
        if not src_dst_map:
×
243
            return ()
×
244

245
        # nFPM normalizes paths to be absolute, so "" is effectively the same as "/".
246
        # But, this does not validate the paths, leaving it up to nFPM to do any validation
247
        # that a specific packager might require.
248

249
        dst_seen = set()
×
250
        dst_dupes = set()
×
251
        for src, dst in src_dst_map:
×
252
            if dst in dst_seen:
×
253
                dst_dupes.add(dst)
×
254
            else:
255
                dst_seen.add(dst)
×
256
        if dst_dupes:
×
257
            raise InvalidFieldException(
×
258
                help_text(
259
                    f"""
260
                    '{cls._dst_alias}' must be unique in '{cls.alias}', but
261
                    found duplicate entries for: {repr(dst_dupes)}
262
                    """
263
                )
264
            )
265

266
        return cast("tuple[tuple[str, str], ...]", src_dst_map)
×
267

268

UNCOV
269
class _NfpmContentOverridesField(OverridesField):
×
UNCOV
270
    nfpm_alias = ""
×
UNCOV
271
    _disallow_overrides_for_field_aliases: tuple[str, ...] = ()
×
272

UNCOV
273
    @classmethod
×
UNCOV
274
    def compute_value(
×
275
        cls,
276
        raw_value: dict[str | tuple[str, ...], dict[str, Any]] | None,
277
        address: Address,
278
    ) -> FrozenDict[tuple[str, ...], FrozenDict[str, ImmutableValue]] | None:
279
        value = super().compute_value(raw_value, address)
×
280
        if not value:
×
281
            return None
×
282
        for dst, overrides in value.items():
×
283
            for field_alias in cls._disallow_overrides_for_field_aliases:
×
284
                if field_alias in overrides:
×
285
                    raise InvalidFieldException(
×
286
                        help_text(
287
                            f"""
288
                            '{cls.alias}' does not support overriding '{field_alias}'.
289
                            Please remove the '{field_alias}' override for: {dst}
290
                            """
291
                        )
292
                    )
293
        return value
×
294

295

296
# -----------------------------------------------------------------------------------------------
297
# File-specific fields
298
# -----------------------------------------------------------------------------------------------
299

300

UNCOV
301
class NfpmContentFileSourceField(OptionalSingleSourceField):
×
UNCOV
302
    nfpm_alias = ""
×
UNCOV
303
    none_is_valid_value = True
×
UNCOV
304
    help = help_text(
×
305
        lambda: f"""
306
        A file that should be copied into an nFPM package (optional).
307

308
        Either specify a file with '{NfpmContentFileSourceField.alias}', or use
309
        '{NfpmDependencies.alias}' to add a dependency on the target that owns
310
        the file.
311

312
        If both '{NfpmContentSrcField.alias}' and '{NfpmContentFileSourceField.alias}'
313
        are populated, then the file in '{NfpmContentFileSourceField.alias}' will be
314
        placed in the sandbox at the '{NfpmContentSrcField.alias}' path (similar to
315
        how the '{RelocatedFiles.alias}' target works).
316
        """
317
    )
318

UNCOV
319
    @classmethod
×
UNCOV
320
    def compute_value(cls, raw_value: str | None, address: Address) -> str | None:
×
UNCOV
321
        value_or_default = super().compute_value(raw_value, address)
×
322
        # This field should either have a path to a file or it should be None.
323
        # If it is a path to a file, we rely on standard glob_match_error_behavior
324
        # to inform the user of any issues finding the file.
UNCOV
325
        if value_or_default == "":
×
326
            # avoid ambiguity so we can use "is None" checks with this field.
327
            raise InvalidFieldException(
×
328
                help_text(
329
                    f"""\
330
                    The '{cls.alias}' field in target {address} should not be an empty string.
331
                    Did you mean to set it to None?
332
                    """
333
                )
334
            )
UNCOV
335
        return value_or_default
×
336

337

UNCOV
338
class NfpmContentSrcField(AsyncFieldMixin, StringField):
×
UNCOV
339
    nfpm_alias = "contents.[].src"
×
UNCOV
340
    alias: ClassVar[str] = "src"
×
UNCOV
341
    help = help_text(
×
342
        lambda: f"""
343
        A file path that should be included in the package.
344

345
        When the package gets installed, the file from '{NfpmContentSrcField.alias}'
346
        will be installed using the absolute path in '{NfpmContentDstField.alias}'.
347

348
        This path should be relative to the sandbox. The path should point to a
349
        generated file or a real file sourced from the workspace.
350

351
        Either '{NfpmContentSrcField.alias}' or '{NfpmContentFileSourceField.alias}'
352
        is required. If the '{NfpmContentFileSourceField.alias}' field is provided,
353
        then the '{NfpmContentSrcField.alias}' defaults to the file referenced in the
354
        '{NfpmContentFileSourceField.alias}' field.
355

356
        If both '{NfpmContentSrcField.alias}' and '{NfpmContentFileSourceField.alias}'
357
        are populated, then the file in '{NfpmContentFileSourceField.alias}' will be
358
        placed in the sandbox at the '{NfpmContentSrcField.alias}' path (similar to
359
        how the '{RelocatedFiles.alias}' target works).
360
        """
361
    )
362

UNCOV
363
    @property
×
UNCOV
364
    def file_path(self) -> str | None:
×
365
        """The path to the file, relative to the build root.
366

367
        The return type is optional because it's possible to have 0-1 files.
368
        """
369
        if self.value is None:
×
370
            return None
×
371
        return os.path.join(self.address.spec_path, self.value)
×
372

373

UNCOV
374
class NfpmContentDstBaseField(StringField, metaclass=ABCMeta):
×
UNCOV
375
    nfpm_alias = "contents.[].dst"
×
UNCOV
376
    alias: ClassVar[str] = "dst"
×
377

378

UNCOV
379
class NfpmContentDstField(NfpmContentDstBaseField):
×
UNCOV
380
    required = True
×
UNCOV
381
    value: str
×
UNCOV
382
    help = help_text(
×
383
        lambda: f"""
384
        The absolute install path for a packaged file.
385

386
        When the package gets installed, the file from '{NfpmContentSrcField.alias}'
387
        will be installed using the absolute path in '{NfpmContentDstField.alias}'.
388

389
        This path is an absolute path on the file system where the package
390
        will be installed.
391
        """
392
    )
393

394

UNCOV
395
class NfpmContentFileType(Enum):
×
396
    # This is a subset of the nFPM content types that apply to individual files.
397
    # Do not include these:
398
    # - dir (not a file: handled by nfpm_content_dir target)
399
    # - tree (not a file: pants needs more explicit config to build sandbox)
400
    # - ghost (handled by ghost_contents field on nfpm_rpm_package target)
401
    # - symlink (handled by a nfpm_content_symlink target)
UNCOV
402
    file = ""
×
UNCOV
403
    config = "config"
×
404
    # config|noreplace is only used by rpm.
405
    # For other packagers (deb, apk, archlinux) this is the same as "config".
UNCOV
406
    config_noreplace = "config|noreplace"
×
407
    # These are undocumented, but present in nFPM code for rpm.
408
    # If used, this changes the type used in the rpm header.
409
    # For other packagers (deb, apk, archlinux) this is the same as "file".
UNCOV
410
    doc = "doc"
×
UNCOV
411
    license = "license"  # nFPM also supports "licence"
×
UNCOV
412
    readme = "readme"
×
413

414

UNCOV
415
class NfpmContentTypeField(StringField):
×
UNCOV
416
    nfpm_alias = "contents.[].type"
×
UNCOV
417
    alias: ClassVar[str] = "content_type"
×
UNCOV
418
    valid_choices = NfpmContentFileType
×
UNCOV
419
    default = NfpmContentFileType.file.value
×
UNCOV
420
    value: str  # will not be None due to enum + default
×
UNCOV
421
    help = help_text(
×
422
        lambda: f"""
423
        The nFPM content type for the packaged file.
424

425
        The content type can be either
426
          - {repr(NfpmContentFileType.file.value)}: a normal file (the default), or
427
          - {repr(NfpmContentFileType.config.value)}: a config file.
428

429
        For RPM packaged files, the content type can also be one of:
430
          - {repr(NfpmContentFileType.config_noreplace.value)},
431
          - {repr(NfpmContentFileType.doc.value)},
432
          - {repr(NfpmContentFileType.license.value)}, and
433
          - {repr(NfpmContentFileType.readme.value)}.
434

435
        The {repr(NfpmContentFileType.config_noreplace.value)} type is used for RPM's
436
        `%config(noreplace)` option. For packagers other than RPM, using
437
        {repr(NfpmContentFileType.config_noreplace.value)} is the same as
438
        {repr(NfpmContentFileType.config.value)} and the remaining RPM-specific
439
        types are the same as {repr(NfpmContentFileType.file.value)}, a normal file.
440

441
        This field only supports file-specific nFPM content types.
442
        Please use these targets for non-file content:
443
          - For 'dir' content, use targets `nfpm_content_dir` and `nfpm_content_dirs`.
444
          - For 'symlink' content, use targets `nfpm_content_symlink` and `nfpm_content_symlinks`.
445
          - For RPM 'ghost' content, use field 'ghost_contents' on target `nfpm_rpm_package`.
446

447
        The nFPM 'tree' content type is not supported. Before passing the list of
448
        package contents to nFPM, pants expands target generators and any globs,
449
        so the 'tree' content type does not make sense.
450
        """
451
    )
452

453

UNCOV
454
class NfpmContentFilesField(_SrcDstSequenceField):
×
UNCOV
455
    alias: ClassVar[str] = "files"
×
UNCOV
456
    required = True
×
UNCOV
457
    help = help_text(
×
458
        lambda: f"""
459
        A list of 2-tuples ('{NfpmContentSrcField.alias}', '{NfpmContentDstField.alias}').
460

461
        The second part, `{NfpmContentDstField.alias}', must be unique across all entries.
462
        """
463
    )
UNCOV
464
    dst_alias = NfpmContentDstField.alias
×
465

466

UNCOV
467
class NfpmContentFilesOverridesField(_NfpmContentOverridesField):
×
UNCOV
468
    help = help_text(
×
469
        f"""
470
        Override the field values for generated `nfpm_content_file` targets.
471

472
        This expects a dictionary of '{NfpmContentDstField.alias}' files to a dictionary for the overrides.
473
        """
474
    )
UNCOV
475
    _disallow_overrides_for_field_aliases = (
×
476
        NfpmContentFileSourceField.alias,
477
        NfpmContentSrcField.alias,
478
        NfpmContentDstField.alias,
479
    )
480

481

482
# -----------------------------------------------------------------------------------------------
483
# Symlink-specific fields
484
# -----------------------------------------------------------------------------------------------
485

486

UNCOV
487
class NfpmContentSymlinkSrcField(NfpmContentSrcField):
×
UNCOV
488
    required = True
×
UNCOV
489
    value: str
×
UNCOV
490
    help = help_text(
×
491
        lambda: f"""
492
        The symlink target path (on package install).
493

494
        When the package gets installed, a symlink will be installed at the
495
        '{NfpmContentSymlinkDstField.alias}' path. The symlink will point to the
496
        '{NfpmContentSymlinkSrcField.alias}' path (the symlink target).
497

498
        This path is a path on the file system where the package will be installed.
499
        If this path is absolute, it is the absolute path to the symlink's target path.
500
        If this path is relative, it is relative to the '{NfpmContentSymlinkDstField.alias}'
501
        path, which is where the symlink will be created.
502
        """
503
    )
504

505

UNCOV
506
class NfpmContentSymlinkDstField(NfpmContentDstBaseField):
×
UNCOV
507
    required = True
×
UNCOV
508
    value: str
×
UNCOV
509
    help = help_text(
×
510
        lambda: f"""
511
        The symlink path (on package install).
512

513
        When the package gets installed, a symlink will be installed at the
514
        '{NfpmContentSymlinkDstField.alias}' path. The symlink will point to the
515
        '{NfpmContentSymlinkSrcField.alias}' path (the symlink target).
516

517
        This path is an absolute path on the file system where the package
518
        will be installed.
519
        """
520
    )
521

522

UNCOV
523
class NfpmContentSymlinksField(_SrcDstSequenceField):
×
UNCOV
524
    alias: ClassVar[str] = "symlinks"
×
UNCOV
525
    required = True
×
UNCOV
526
    help = help_text(
×
527
        lambda: f"""
528
        A list of 2-tuples ('{NfpmContentSymlinkSrcField.alias}', '{NfpmContentSymlinkDstField.alias}').
529

530
        The second part, `{NfpmContentSymlinkDstField.alias}', must be unique across all entries.
531
        """
532
    )
UNCOV
533
    _dst_alias = NfpmContentSymlinkDstField.alias
×
534

535

UNCOV
536
class NfpmContentSymlinksOverridesField(_NfpmContentOverridesField):
×
UNCOV
537
    help = help_text(
×
538
        f"""
539
        Override the field values for generated `nfpm_content_symlink` targets.
540

541
        This expects a dictionary of '{NfpmContentSymlinkDstField.alias}' files
542
        to a dictionary for the overrides.
543
        """
544
    )
UNCOV
545
    _disallow_overrides_for_field_aliases = (
×
546
        NfpmContentSymlinkSrcField.alias,
547
        NfpmContentSymlinkDstField.alias,
548
    )
549

550

551
# -----------------------------------------------------------------------------------------------
552
# Dir-specific fields
553
# -----------------------------------------------------------------------------------------------
554

555

UNCOV
556
class NfpmContentDirDstField(NfpmContentDstBaseField):
×
UNCOV
557
    required = True
×
UNCOV
558
    value: str
×
UNCOV
559
    help = help_text(
×
560
        lambda: f"""
561
        The absolute install path for a directory.
562

563
        When the package gets installed, a directory will be created at the
564
        '{NfpmContentDirDstField.alias}' path.
565

566
        This path is an absolute path on the file system where the package
567
        will be installed.
568
        """
569
    )
570

571

UNCOV
572
class NfpmContentDirsField(StringSequenceField):
×
UNCOV
573
    nfpm_alias = ""
×
UNCOV
574
    alias: ClassVar[str] = "dirs"
×
UNCOV
575
    required = True
×
UNCOV
576
    value: tuple[str, ...]
×
UNCOV
577
    help = help_text(
×
578
        lambda: f"""
579
        A list of install path for '{NfpmContentDirDstField.alias}' directories.
580

581
        When the package gets installed, each directory will be created.
582

583
        Each path is an absolute path on the file system where the package
584
        will be installed.
585
        """
586
    )
587

UNCOV
588
    @classmethod
×
UNCOV
589
    def compute_value(cls, raw_value: Iterable[str] | None, address: Address) -> tuple[str, ...]:
×
590
        dst_dirs = super().compute_value(raw_value, address)
×
591
        assert dst_dirs is not None  # it is required
×
592
        # nFPM normalizes paths to be absolute, so "" is effectively the same as "/".
593
        # But this does not validate the paths, leaving it up to nFPM to do any validation
594
        # that a specific packager might require.
595

596
        dst_seen = set()
×
597
        dst_dupes = set()
×
598
        for dst in dst_dirs:
×
599
            if dst in dst_seen:
×
600
                dst_dupes.add(dst)
×
601
            else:
602
                dst_seen.add(dst)
×
603
        if dst_dupes:
×
604
            raise InvalidFieldException(
×
605
                help_text(
606
                    lambda: f"""
607
                    '{NfpmContentDirDstField.alias}' must be unique in '{cls.alias}', but
608
                    found duplicate entries for: {repr(dst_dupes)}
609
                    """
610
                )
611
            )
612

613
        return dst_dirs
×
614

615

UNCOV
616
class NfpmContentDirsOverridesField(_NfpmContentOverridesField):
×
UNCOV
617
    help = help_text(
×
618
        f"""
619
        Override the field values for generated `nfpm_content_dir` targets.
620

621
        This expects a dictionary of '{NfpmContentDirDstField.alias}' files
622
        to a dictionary for the overrides.
623
        """
624
    )
UNCOV
625
    _disallow_overrides_for_field_aliases = (NfpmContentDirDstField.alias,)
×
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