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

pantsbuild / pants / 25951830241

16 May 2026 03:39AM UTC coverage: 92.89% (+0.001%) from 92.889%
25951830241

push

github

web-flow
Fix: IntrinsicError when a non-directory PATH entry is used with `system_binary` (Cherry-pick of #23327) (#23353)

Closes #21990

## Problem

Pants crashed with `IntrinsicError: Not a directory (os error 20)` when
a
`system_binary` target (or any binary lookup) included a non-directory
entry in
its search path. Two distinct shapes of the same underlying bug:

1. **`foo/bar` on PATH where `foo` is a file**. Calling
`path_metadata_request`
on `foo/bar` hits `ENOTDIR` in the OS. That error was not caught in the
Rust
   layer, so it surfaced as an uncaught `IntrinsicError`.

2. **A symlink-to-file on PATH** — the case from the issue report. The
first
`path_metadata_request` succeeds and returns
`PathMetadataKind::SYMLINK`,
but the Python code then tried to look up `symlink/binary_name`, which
also
   hits `ENOTDIR`.

## Fix

**`src/rust/fs/src/posixfs.rs`** — `path_metadata` now accepts a
`follow_symlinks`
flag. When `true`, it calls `tokio::fs::metadata` (which follows
symlinks) instead
of `tokio::fs::symlink_metadata`, so a symlink-to-directory appears as a
`Directory` rather than a `Symlink`. It also continues to treat
`ENOTDIR` as absent
(`Ok(None)`), fixing the file-component-on-PATH crash.

**`src/rust/fs/src/lib.rs`** — The `Vfs` trait's `path_metadata`
signature is
updated to accept `follow_symlinks: bool`. The in-memory `DigestTrie`
implementation
accepts but ignores the flag.

**`src/rust/engine/src/nodes/path_metadata.rs`** — `PathMetadataNode`
now carries
`follow_symlinks` and passes it through to the VFS call.

**`src/rust/engine/src/intrinsics/digests.rs`** — The
`path_metadata_request`
intrinsic reads `follow_symlinks` from the Python `PathMetadataRequest`
object and
passes it to `PathMetadataNode`.

**`src/python/pants/engine/fs.py`** — `PathMetadataRequest` gains a
`follow_symlinks: bool = False` field.

**`src/python/pants/core/util_rules/system_binaries.py`** — The initial
lookup of
each PATH entry uses `follow_symlinks=True`. A symlink... (continued)

23 of 23 new or added lines in 3 files covered. (100.0%)

1 existing line in 1 file now uncovered.

92104 of 99154 relevant lines covered (92.89%)

4.04 hits per line

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

96.15
/src/python/pants/backend/tools/preamble/subsystem.py
1
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
from __future__ import annotations
1✔
4

5
from collections.abc import Sequence
1✔
6

7
from pants.option.option_types import DictOption, SkipOption
1✔
8
from pants.option.subsystem import Subsystem
1✔
9
from pants.source.filespec import FilespecMatcher
1✔
10
from pants.util.strutil import help_text, softwrap
1✔
11

12

13
class PreambleSubsystem(Subsystem):
1✔
14
    options_scope = "preamble"
1✔
15
    name = "preamble"
1✔
16
    help = help_text(
1✔
17
        """
18
        Formats files with a preamble, with the preamble looked up based on path.
19

20
        This is useful for things such as copyright headers or shebang lines.
21

22
        Pants substitutes the following identifiers (following Python's `string.Template` substitutions):
23
        - $year: The current year (only used when actually writing the year to the file).
24
        """
25
    )
26

27
    skip = SkipOption("fmt")
1✔
28

29
    _template_by_globs = DictOption[str](
1✔
30
        help=softwrap(
31
            """
32
            Which preamble template to use based on the path globs (relative to the build root).
33

34
            Example:
35

36
                {
37
                    '*.rs': '// Copyright (c) $year\\n// Line 2\\n'
38
                    '*.py:!__init__.py': '# Copyright (c) $year\\n# Line 2\\n',
39
                }
40

41
            It might be helpful to load this config from a JSON or YAML file. To do that, set
42
            `[preamble].config = '@path/to/config.yaml'`, for example.
43
            """
44
        ),
45
        fromfile=True,
46
    )
47

48
    @property
1✔
49
    def template_by_globs(self) -> dict[tuple[str, ...], str]:
1✔
50
        return {tuple(key.split(":")): value for key, value in self._template_by_globs.items()}
1✔
51

52
    def get_template_by_path(self, filepaths: Sequence[str]) -> dict[str, str]:
1✔
53
        """Returns a mapping from path to (most-relevant) template."""
54
        filepaths_to_test = set(filepaths)
1✔
55
        template_by_path = {}
1✔
56
        for globs, template in self.template_by_globs.items():
1✔
57
            if not filepaths_to_test:
1✔
UNCOV
58
                break
×
59

60
            matched_filepaths = FilespecMatcher(
1✔
61
                includes=[
62
                    (glob[2:] if glob.startswith(r"\\!") else glob)
63
                    for glob in globs
64
                    if not glob.startswith("!")
65
                ],
66
                excludes=[glob[1:] for glob in globs if glob.startswith("!")],
67
            ).matches(tuple(filepaths_to_test))
68
            filepaths_to_test -= set(matched_filepaths)
1✔
69
            for filepath in matched_filepaths:
1✔
70
                template_by_path[filepath] = template
1✔
71

72
        return template_by_path
1✔
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