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

pantsbuild / pants / 25403087079

05 May 2026 09:23PM UTC coverage: 92.903% (-0.04%) from 92.944%
25403087079

Pull #23319

github

web-flow
Merge 17479f77c into f46dc7805
Pull Request #23319: [pants_ng] Scaffolding for a pants_ng mode.

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

10 existing lines in 4 files now uncovered.

91968 of 98994 relevant lines covered (92.9%)

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