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

pantsbuild / pants / 25443604553

06 May 2026 03:05PM UTC coverage: 92.879% (-0.04%) from 92.915%
25443604553

push

github

web-flow
[pants_ng] Scaffolding for a pants_ng mode. (#23319)

In this mode the command line is parsed as an
NG invocation, and dispatched appropriately.

Of course at the moment there are no
implementations to dispatch to. That will follow.

This does expose a new option, `pants_ng` to users. 
There is a big warning not to set it, but we're not trying
to hide that we're working on a new thing, so I am
comfortable with this.

25 of 76 new or added lines in 9 files covered. (32.89%)

1294 existing lines in 76 files now uncovered.

92234 of 99306 relevant lines covered (92.88%)

4.05 hits per line

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

81.6
/src/python/pants/init/engine_initializer.py
1
# Copyright 2016 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
12✔
5

6
import dataclasses
12✔
7
import logging
12✔
8
from collections.abc import Iterable, Mapping
12✔
9
from dataclasses import dataclass
12✔
10
from pathlib import Path
12✔
11
from typing import Any, ClassVar, cast
12✔
12

13
from pants.base.build_environment import get_buildroot
12✔
14
from pants.base.build_root import BuildRoot
12✔
15
from pants.base.exiter import PANTS_SUCCEEDED_EXIT_CODE
12✔
16
from pants.base.specs import Specs
12✔
17
from pants.build_graph.build_configuration import BuildConfiguration
12✔
18
from pants.core.environments import rules as environments_rules
12✔
19
from pants.core.environments.rules import determine_bootstrap_environment
12✔
20
from pants.core.util_rules import system_binaries
12✔
21
from pants.engine import desktop, download_file, fs, intrinsics, process
12✔
22
from pants.engine.console import Console
12✔
23
from pants.engine.environment import EnvironmentName
12✔
24
from pants.engine.fs import PathGlobs, Snapshot, Workspace
12✔
25
from pants.engine.goal import CurrentExecutingGoals, Goal
12✔
26
from pants.engine.internals import (
12✔
27
    build_files,
28
    dep_rules,
29
    graph,
30
    options_parsing,
31
    platform_rules,
32
    specs_rules,
33
    synthetic_targets,
34
)
35
from pants.engine.internals.native_engine import PyExecutor, PySessionCancellationLatch
12✔
36
from pants.engine.internals.parser import Parser
12✔
37
from pants.engine.internals.scheduler import Scheduler, SchedulerSession
12✔
38
from pants.engine.internals.selectors import Params
12✔
39
from pants.engine.internals.session import SessionValues
12✔
40
from pants.engine.rules import QueryRule, Rule, collect_rules, rule
12✔
41
from pants.engine.streaming_workunit_handler import rules as streaming_workunit_handler_rules
12✔
42
from pants.engine.target import RegisteredTargetTypes
12✔
43
from pants.engine.unions import UnionMembership, UnionRule
12✔
44
from pants.init import specs_calculator
12✔
45
from pants.init.bootstrap_scheduler import BootstrapStatus
12✔
46
from pants.ng.goal import GoalNg, GoalSubsystemNg
12✔
47
from pants.option.bootstrap_options import (
12✔
48
    DEFAULT_EXECUTION_OPTIONS,
49
    DynamicRemoteOptions,
50
    ExecutionOptions,
51
    LocalStoreOptions,
52
)
53
from pants.option.global_options import GlobalOptions
12✔
54
from pants.option.option_value_container import OptionValueContainer
12✔
55
from pants.option.subsystem import Subsystem
12✔
56
from pants.util.docutil import bin_name
12✔
57
from pants.util.logging import LogLevel
12✔
58
from pants.util.ordered_set import FrozenOrderedSet
12✔
59
from pants.util.strutil import softwrap
12✔
60
from pants.vcs.changed import rules as changed_rules
12✔
61
from pants.vcs.git import rules as git_rules
12✔
62

63
logger = logging.getLogger(__name__)
12✔
64

65

66
@dataclass(frozen=True)
12✔
67
class GraphScheduler:
12✔
68
    """A thin wrapper around a Scheduler configured with @rules."""
69

70
    scheduler: Scheduler
12✔
71
    goal_map: Any
12✔
72

73
    def new_session(
12✔
74
        self,
75
        build_id,
76
        dynamic_ui: bool = False,
77
        ui_use_prodash: bool = False,
78
        use_colors=True,
79
        max_workunit_level: LogLevel = LogLevel.DEBUG,
80
        session_values: SessionValues | None = None,
81
        cancellation_latch: PySessionCancellationLatch | None = None,
82
    ) -> GraphSession:
83
        session = self.scheduler.new_session(
×
84
            build_id,
85
            dynamic_ui,
86
            ui_use_prodash,
87
            max_workunit_level=max_workunit_level,
88
            session_values=session_values,
89
            cancellation_latch=cancellation_latch,
90
        )
91
        console = Console(use_colors=use_colors, session=session if dynamic_ui else None)
×
92
        return GraphSession(session, console, self.goal_map)
×
93

94

95
@dataclass(frozen=True)
12✔
96
class GraphSession:
12✔
97
    """A thin wrapper around a SchedulerSession configured with @rules."""
98

99
    scheduler_session: SchedulerSession
12✔
100
    console: Console
12✔
101
    goal_map: Any
12✔
102

103
    # NB: Keep this in sync with the method `run_goal_rules`.
104
    goal_param_types: ClassVar[tuple[type, ...]] = (Specs, Console, Workspace, EnvironmentName)
12✔
105

106
    def goal_consumed_subsystem_scopes(self, goal_name: str) -> tuple[str, ...]:
12✔
107
        """Return the scopes of subsystems that could be consumed while running the given goal."""
108
        goal_product = self.goal_map.get(goal_name)
×
109
        if not goal_product:
×
110
            return tuple()
×
111
        consumed_types = self.goal_consumed_types(goal_product)
×
112
        return tuple(
×
113
            sorted({typ.options_scope for typ in consumed_types if issubclass(typ, Subsystem)})
114
        )
115

116
    def goal_consumed_types(self, goal_product: type) -> set[type]:
12✔
117
        """Return the set of types that could possibly be consumed while running the given goal."""
118
        return set(
×
119
            self.scheduler_session.scheduler.rule_graph_consumed_types(
120
                self.goal_param_types, goal_product
121
            )
122
        )
123

124
    def run_goal_rules(
12✔
125
        self,
126
        *,
127
        union_membership: UnionMembership,
128
        goals: Iterable[str],
129
        specs: Specs,
130
        poll: bool = False,
131
        poll_delay: float | None = None,
132
    ) -> int:
133
        """Runs @goal_rules sequentially and interactively by requesting their implicit Goal
134
        products.
135

136
        For retryable failures, raises scheduler.ExecutionError.
137

138
        :returns: An exit code.
139
        """
140

141
        workspace = Workspace(self.scheduler_session)
×
142
        env_name = determine_bootstrap_environment(self.scheduler_session)
×
143

144
        for goal in goals:
×
NEW
145
            goal_product = self.goal_map.get(goal)
×
NEW
146
            if goal_product is None:
×
147
                # This must be an ng command, since the og goals have already
148
                # been validated by the options parser.
NEW
149
                raise UnknownCommand(goal)
×
NEW
150
            pants_ng = issubclass(goal_product.subsystem_cls, GoalSubsystemNg)
×
NEW
151
            if not pants_ng and not goal_product.subsystem_cls.activated(union_membership):
×
UNCOV
152
                raise GoalNotActivatedException(goal)
×
153
            # NB: Keep this in sync with the property `goal_param_types`.
154
            params = Params(specs, self.console, workspace, env_name)
×
155
            logger.debug(f"requesting {goal_product} to satisfy execution of `{goal}` goal")
×
156
            try:
×
157
                exit_code = self.scheduler_session.run_goal_rule(
×
158
                    goal_product, params, poll=poll, poll_delay=poll_delay
159
                )
160
            finally:
161
                self.console.flush()
×
162

163
            if exit_code != PANTS_SUCCEEDED_EXIT_CODE:
×
164
                return exit_code
×
165

166
        return PANTS_SUCCEEDED_EXIT_CODE
×
167

168

169
class EngineInitializer:
12✔
170
    """Constructs the components necessary to run the engine."""
171

172
    class GoalMappingError(Exception):
12✔
173
        """Raised when a goal cannot be mapped to an @rule."""
174

175
    @staticmethod
12✔
176
    def _make_goal_map_from_rules(rules, pants_ng: bool) -> Mapping[str, type[Goal]]:
12✔
177
        goal_map: dict[str, type[Goal]] = {}
12✔
178
        for r in rules:
12✔
179
            output_type = getattr(r, "output_type", None)
12✔
180
            if (
12✔
181
                not output_type
182
                or not issubclass(output_type, Goal)
183
                or issubclass(output_type, GoalNg) != pants_ng
184
            ):
185
                continue
12✔
186

187
            goal = r.output_type.name
12✔
188
            deprecated_goal = (
12✔
189
                None if pants_ng else r.output_type.subsystem_cls.deprecated_options_scope
190
            )
191
            for goal_name in [goal, deprecated_goal] if deprecated_goal else [goal]:
12✔
192
                if goal_name in goal_map:
12✔
193
                    raise EngineInitializer.GoalMappingError(
×
194
                        f"could not map goal `{goal_name}` to rule `{r}`: already claimed by product "
195
                        f"`{goal_map[goal_name]}`"
196
                    )
197
                goal_map[goal_name] = r.output_type
12✔
198
        return goal_map
12✔
199

200
    @staticmethod
12✔
201
    def setup_graph(
12✔
202
        bootstrap_options: OptionValueContainer,
203
        build_configuration: BuildConfiguration,
204
        dynamic_remote_options: DynamicRemoteOptions,
205
        executor: PyExecutor,
206
        is_bootstrap: bool = False,
207
    ) -> GraphScheduler:
208
        build_root = get_buildroot()
4✔
209
        executor = executor or GlobalOptions.create_py_executor(bootstrap_options)
4✔
210
        execution_options = ExecutionOptions.from_options(bootstrap_options, dynamic_remote_options)
4✔
211
        if is_bootstrap:
4✔
212
            # Don't spawn a single-use sandboxer process that will then get preempted by a
213
            # post-bootstrap one created by pantsd. This is fine: our plugin resolving sequence
214
            # isn't susceptible to the race condition that the sandboxer solves.
215
            # TODO: Are we sure? In any case we plan to replace the plugin resolver and
216
            #  get rid of the bootstrap scheduler, so this should be moot soon enough.
217
            execution_options = dataclasses.replace(execution_options, use_sandboxer=False)
4✔
218
        local_store_options = LocalStoreOptions.from_options(bootstrap_options)
4✔
219
        return EngineInitializer.setup_graph_extended(
4✔
220
            build_configuration,
221
            execution_options,
222
            executor=executor,
223
            pants_ignore_patterns=GlobalOptions.compute_pants_ignore(build_root, bootstrap_options),
224
            use_gitignore=bootstrap_options.pants_ignore_use_gitignore,
225
            local_store_options=local_store_options,
226
            local_execution_root_dir=bootstrap_options.local_execution_root_dir,
227
            named_caches_dir=bootstrap_options.named_caches_dir,
228
            ca_certs_path=bootstrap_options.ca_certs_path,
229
            build_root=build_root,
230
            pants_workdir=bootstrap_options.pants_workdir,
231
            include_trace_on_error=bootstrap_options.print_stacktrace,
232
            engine_visualize_to=bootstrap_options.engine_visualize_to,
233
            watch_filesystem=bootstrap_options.watch_filesystem,
234
            is_bootstrap=is_bootstrap,
235
            pants_ng=bootstrap_options.pants_ng,
236
        )
237

238
    @staticmethod
12✔
239
    def setup_graph_extended(
12✔
240
        build_configuration: BuildConfiguration,
241
        execution_options: ExecutionOptions,
242
        *,
243
        executor: PyExecutor,
244
        pants_ignore_patterns: list[str],
245
        use_gitignore: bool,
246
        local_store_options: LocalStoreOptions,
247
        local_execution_root_dir: str,
248
        named_caches_dir: str,
249
        pants_workdir: str,
250
        ca_certs_path: str | None = None,
251
        build_root: str | None = None,
252
        include_trace_on_error: bool = True,
253
        engine_visualize_to: str | None = None,
254
        watch_filesystem: bool = True,
255
        is_bootstrap: bool = False,
256
        pants_ng: bool = False,
257
    ) -> GraphScheduler:
258
        build_root_path = build_root or get_buildroot()
12✔
259

260
        rules = build_configuration.rules
12✔
261
        union_membership: UnionMembership
262
        registered_target_types = RegisteredTargetTypes.create(build_configuration.target_types)
12✔
263

264
        execution_options = execution_options or DEFAULT_EXECUTION_OPTIONS
12✔
265

266
        @rule
12✔
267
        async def parser_singleton() -> Parser:
12✔
268
            return Parser(
12✔
269
                build_root=build_root_path,
270
                registered_target_types=registered_target_types,
271
                union_membership=union_membership,
272
                object_aliases=build_configuration.registered_aliases,
273
                ignore_unrecognized_symbols=is_bootstrap,
274
            )
275

276
        @rule
12✔
277
        async def bootstrap_status() -> BootstrapStatus:
12✔
278
            return BootstrapStatus(is_bootstrap)
12✔
279

280
        @rule
12✔
281
        async def build_configuration_singleton() -> BuildConfiguration:
12✔
282
            return build_configuration
12✔
283

284
        @rule
12✔
285
        async def registered_target_types_singleton() -> RegisteredTargetTypes:
12✔
286
            return registered_target_types
12✔
287

288
        @rule
12✔
289
        async def union_membership_singleton() -> UnionMembership:
12✔
290
            return union_membership
12✔
291

292
        @rule
12✔
293
        async def build_root_singleton() -> BuildRoot:
12✔
294
            return cast(BuildRoot, BuildRoot.instance)
12✔
295

296
        @rule
12✔
297
        async def current_executing_goals(session_values: SessionValues) -> CurrentExecutingGoals:
12✔
298
            return session_values.get(CurrentExecutingGoals) or CurrentExecutingGoals()
3✔
299

300
        # Create a Scheduler containing graph and filesystem rules, with no installed goals.
301
        rules = FrozenOrderedSet(
12✔
302
            (
303
                *collect_rules(locals()),
304
                *intrinsics.rules(),
305
                *build_files.rules(),
306
                *fs.rules(),
307
                *dep_rules.rules(),
308
                *desktop.rules(),
309
                *download_file.rules(),
310
                *git_rules(),
311
                *graph.rules(),
312
                *specs_rules.rules(),
313
                *options_parsing.rules(),
314
                *process.rules(),
315
                *environments_rules.rules(),
316
                *system_binaries.rules(),
317
                *platform_rules.rules(),
318
                *changed_rules(),
319
                *streaming_workunit_handler_rules(),
320
                *specs_calculator.rules(),
321
                *synthetic_targets.rules(),
322
                *rules,
323
            )
324
        )
325

326
        def is_pertinent_rule(rule: Rule | UnionRule, ng: bool) -> bool:
12✔
327
            output_type = getattr(rule, "output_type", None)
12✔
328
            if not output_type:  # This is a UnionRule, so keep it.
12✔
329
                return True
12✔
330
            if issubclass(output_type, Goal) and issubclass(output_type, GoalNg) != ng:
12✔
NEW
331
                return False
×
332
            return True
12✔
333

334
        # Strip out all the goal_rules that don't pertain to our current ng/og state. This allows
335
        # ng commands to use the same names as og goals, without collision.
336
        rules = FrozenOrderedSet(rule for rule in rules if is_pertinent_rule(rule, pants_ng))
12✔
337

338
        goal_map = EngineInitializer._make_goal_map_from_rules(rules, pants_ng)
12✔
339

340
        union_membership = UnionMembership.from_rules(
12✔
341
            (
342
                *build_configuration.union_rules,
343
                *(r for r in rules if isinstance(r, UnionRule)),
344
            )
345
        )
346

347
        # param types for goals with the `USES_ENVIRONMENT` behaviour (see `goal.py`)
348
        environment_selecting_goal_param_types = [
12✔
349
            t for t in GraphSession.goal_param_types if t != EnvironmentName
350
        ]
351
        rules = FrozenOrderedSet(
12✔
352
            (
353
                *rules,
354
                # Install queries for each Goal.
355
                *(
356
                    QueryRule(
357
                        goal_type,
358
                        (
359
                            environment_selecting_goal_param_types
360
                            if goal_type._selects_environments()
361
                            else GraphSession.goal_param_types
362
                        ),
363
                    )
364
                    for goal_type in goal_map.values()
365
                ),
366
                QueryRule(Snapshot, [PathGlobs]),  # Used by the SchedulerService.
367
            )
368
        )
369

370
        def ensure_absolute_path(v: str) -> str:
12✔
371
            return Path(v).resolve().as_posix()
12✔
372

373
        def ensure_optional_absolute_path(v: str | None) -> str | None:
12✔
374
            if v is None:
12✔
375
                return None
12✔
376
            return ensure_absolute_path(v)
1✔
377

378
        scheduler = Scheduler(
12✔
379
            ignore_patterns=pants_ignore_patterns,
380
            use_gitignore=use_gitignore,
381
            build_root=build_root_path,
382
            pants_workdir=pants_workdir,
383
            local_execution_root_dir=ensure_absolute_path(local_execution_root_dir),
384
            named_caches_dir=ensure_absolute_path(named_caches_dir),
385
            ca_certs_path=ensure_optional_absolute_path(ca_certs_path),
386
            rules=rules,
387
            union_membership=union_membership,
388
            executor=executor,
389
            execution_options=execution_options,
390
            local_store_options=local_store_options,
391
            include_trace_on_error=include_trace_on_error,
392
            visualize_to_dir=engine_visualize_to,
393
            watch_filesystem=watch_filesystem,
394
        )
395

396
        return GraphScheduler(scheduler, goal_map)
12✔
397

398

399
class UnknownCommand(Exception):
12✔
400
    def __init__(self, goal: str) -> None:
12✔
NEW
401
        super().__init__(f"Unknown command `{goal.replace('.', ' ')}`")
×
402

403

404
class GoalNotActivatedException(Exception):
12✔
405
    def __init__(self, goal_name: str) -> None:
12✔
406
        super().__init__(
×
407
            softwrap(
408
                f"""
409
                No relevant backends activate the `{goal_name}` goal, so the goal would do
410
                nothing.
411

412
                This usually means that you have not yet set the option
413
                `[GLOBAL].backend_packages` in `pants.toml`, which is how Pants knows
414
                which languages and tools to support. Run `{bin_name()} help backends`.
415
                """
416
            )
417
        )
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