• 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

35.57
/src/python/pants/bin/local_pants_runner.py
1
# Copyright 2015 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 logging
12✔
7
import sys
12✔
8
from dataclasses import dataclass
12✔
9
from typing import Any
12✔
10

11
from pants.base.exiter import PANTS_FAILED_EXIT_CODE, PANTS_SUCCEEDED_EXIT_CODE, ExitCode
12✔
12
from pants.base.specs import Specs
12✔
13
from pants.build_graph.build_configuration import BuildConfiguration
12✔
14
from pants.core.environments.rules import determine_bootstrap_environment
12✔
15
from pants.engine.env_vars import CompleteEnvironmentVars
12✔
16
from pants.engine.goal import CurrentExecutingGoals
12✔
17
from pants.engine.internals import native_engine
12✔
18
from pants.engine.internals.native_engine import (
12✔
19
    PyExecutor,
20
    PyNgInvocation,
21
    PyNgOptions,
22
    PySessionCancellationLatch,
23
)
24
from pants.engine.internals.scheduler import ExecutionError
12✔
25
from pants.engine.internals.selectors import Params
12✔
26
from pants.engine.internals.session import SessionValues
12✔
27
from pants.engine.streaming_workunit_handler import (
12✔
28
    StreamingWorkunitHandler,
29
    WorkunitsCallback,
30
    WorkunitsCallbackFactories,
31
)
32
from pants.engine.unions import UnionMembership
12✔
33
from pants.goal.auxiliary_goal import AuxiliaryGoal, AuxiliaryGoalContext
12✔
34
from pants.goal.builtin_goal import BuiltinGoal
12✔
35
from pants.goal.run_tracker import RunTracker
12✔
36
from pants.init.engine_initializer import EngineInitializer, GraphScheduler, GraphSession
12✔
37
from pants.init.logging import stdio_destination_use_color
12✔
38
from pants.init.options_initializer import OptionsInitializer
12✔
39
from pants.init.specs_calculator import calculate_specs
12✔
40
from pants.option.bootstrap_options import DynamicRemoteOptions
12✔
41
from pants.option.global_options import DynamicUIRenderer, GlobalOptions
12✔
42
from pants.option.options import Options
12✔
43
from pants.option.options_bootstrapper import OptionsBootstrapper
12✔
44
from pants.util.logging import LogLevel
12✔
45

46
logger = logging.getLogger(__name__)
12✔
47

48

49
@dataclass
12✔
50
class LocalPantsRunner:
12✔
51
    """Handles a single pants invocation running in the process-local context.
52

53
    LocalPantsRunner is used both for single runs of Pants without `pantsd` (where a Scheduler is
54
    created at the beginning of the run and destroyed at the end), and also for runs of Pants in
55
    `pantsd` (where a Scheduler is borrowed from `pantsd` creation time, and left running at the
56
    end).
57
    """
58

59
    options: Options
12✔
60
    options_bootstrapper: OptionsBootstrapper
12✔
61
    session_end_tasks_timeout: float
12✔
62
    build_config: BuildConfiguration
12✔
63
    run_tracker: RunTracker
12✔
64
    specs: Specs
12✔
65
    graph_session: GraphSession
12✔
66
    executor: PyExecutor
12✔
67
    union_membership: UnionMembership
12✔
68
    is_pantsd_run: bool
12✔
69
    working_dir: str
12✔
70
    ng_invocation: PyNgInvocation | None
12✔
71

72
    @classmethod
12✔
73
    def create(
12✔
74
        cls,
75
        env: CompleteEnvironmentVars,
76
        working_dir: str,
77
        options_bootstrapper: OptionsBootstrapper,
78
        options_initializer: OptionsInitializer | None = None,
79
        scheduler: GraphScheduler | None = None,
80
        cancellation_latch: PySessionCancellationLatch | None = None,
81
        ng_invocation: PyNgInvocation | None = None,
82
    ) -> LocalPantsRunner:
83
        """Creates a new LocalPantsRunner instance by parsing options.
84

85
        By the time this method runs, logging will already have been initialized in either
86
        PantsRunner or DaemonPantsRunner.
87

88
        :param env: The environment for this run.
89
        :param options_bootstrapper: The OptionsBootstrapper instance to reuse.
90
        :param scheduler: If being called from the daemon, a warmed scheduler to use.
91
        """
92
        global_bootstrap_options = options_bootstrapper.bootstrap_options.for_global_scope()
×
93
        executor = (
×
94
            scheduler.scheduler.py_executor
95
            if scheduler
96
            else GlobalOptions.create_py_executor(global_bootstrap_options)
97
        )
98
        options_initializer = options_initializer or OptionsInitializer(
×
99
            options_bootstrapper,
100
            executor,
101
        )
102
        build_config = options_initializer.build_config(options_bootstrapper, env)
×
103
        union_membership = UnionMembership.from_rules(build_config.union_rules)
×
104
        options = options_initializer.options(
×
105
            options_bootstrapper, env, build_config, union_membership, raise_=True
106
        )
107
        stdio_destination_use_color(options.for_global_scope().colors)
×
108

109
        run_tracker = RunTracker(options_bootstrapper.args, options)
×
110
        native_engine.maybe_set_panic_handler()
×
111

112
        with options_initializer.handle_unknown_flags(options_bootstrapper, env, raise_=True):
×
113
            # Verify CLI flags.
114
            if not build_config.allow_unknown_options:
×
115
                options.verify_args()
×
116

117
        # Verify configs.
118
        if global_bootstrap_options.verify_config:
×
119
            options.verify_configs()
×
120

121
        # If we're running with the daemon, we'll be handed a warmed Scheduler, which we use
122
        # to initialize a session here.
123
        is_pantsd_run = scheduler is not None
×
124
        if scheduler is None:
×
125
            dynamic_remote_options, _ = DynamicRemoteOptions.from_options(
×
126
                options, env, remote_auth_plugin_func=build_config.remote_auth_plugin_func
127
            )
128
            bootstrap_options = options_bootstrapper.bootstrap_options.for_global_scope()
×
129
            assert bootstrap_options is not None
×
130
            scheduler = EngineInitializer.setup_graph(
×
131
                bootstrap_options, build_config, dynamic_remote_options, executor
132
            )
133
        with options_initializer.handle_unknown_flags(options_bootstrapper, env, raise_=True):
×
134
            global_options = options.for_global_scope()
×
NEW
135
            session_values_dict = {
×
136
                OptionsBootstrapper: options_bootstrapper,
137
                CompleteEnvironmentVars: env,
138
                CurrentExecutingGoals: CurrentExecutingGoals(),
139
            }
NEW
140
            if ng_invocation is not None:
×
NEW
141
                session_values_dict[PyNgInvocation] = ng_invocation
×
NEW
142
                session_values_dict[PyNgOptions] = PyNgOptions(
×
143
                    ng_invocation, dict(env.items()), include_derivation=False
144
                )
UNCOV
145
        graph_session = scheduler.new_session(
×
146
            build_id=run_tracker.run_id,
147
            dynamic_ui=global_options.dynamic_ui,
148
            ui_use_prodash=global_options.dynamic_ui_renderer
149
            == DynamicUIRenderer.experimental_prodash,
150
            use_colors=global_options.get("colors", True),
151
            max_workunit_level=max(
152
                global_options.streaming_workunits_level,
153
                global_options.level,
154
                *(
155
                    LogLevel[level.upper()]
156
                    for level in global_options.log_levels_by_target.values()
157
                ),
158
            ),
159
            session_values=SessionValues(session_values_dict),
160
            cancellation_latch=cancellation_latch,
161
        )
162

NEW
163
        if ng_invocation:
×
NEW
164
            specs_strs = ng_invocation.specs()
×
165
        else:
NEW
166
            specs_strs = tuple(options.specs)
×
167

UNCOV
168
        specs = calculate_specs(
×
169
            specs_strs=specs_strs,
170
            options_bootstrapper=options_bootstrapper,
171
            options=options,
172
            session=graph_session.scheduler_session,
173
            working_dir=working_dir,
174
        )
175

176
        return cls(
×
177
            options=options,
178
            options_bootstrapper=options_bootstrapper,
179
            session_end_tasks_timeout=global_bootstrap_options.session_end_tasks_timeout,
180
            build_config=build_config,
181
            run_tracker=run_tracker,
182
            specs=specs,
183
            graph_session=graph_session,
184
            executor=executor,
185
            union_membership=union_membership,
186
            is_pantsd_run=is_pantsd_run,
187
            working_dir=working_dir,
188
            ng_invocation=ng_invocation,
189
        )
190

191
    def _perform_run(self, goals: tuple[str, ...]) -> ExitCode:
12✔
192
        global_options = self.options.for_global_scope()
×
193
        if not global_options.get("loop", False):
×
194
            return self._perform_run_body(goals, poll=False)
×
195

196
        iterations = global_options.loop_max
×
197
        exit_code = PANTS_SUCCEEDED_EXIT_CODE
×
198
        while iterations:
×
199
            # NB: We generate a new "run id" per iteration of the loop in order to allow us to
200
            # observe fresh values for Goals. See notes in `scheduler.rs`.
201
            self.graph_session.scheduler_session.new_run_id()
×
202
            try:
×
203
                exit_code = self._perform_run_body(goals, poll=True)
×
204
            except ExecutionError as e:
×
205
                logger.error(e)
×
206
            iterations -= 1
×
207

208
        return exit_code
×
209

210
    def _perform_run_body(self, goals: tuple[str, ...], poll: bool) -> ExitCode:
12✔
211
        return self.graph_session.run_goal_rules(
×
212
            union_membership=self.union_membership,
213
            goals=goals,
214
            specs=self.specs,
215
            poll=poll,
216
            poll_delay=(0.1 if poll else None),
217
        )
218

219
    def _get_workunits_callbacks(self) -> tuple[WorkunitsCallback, ...]:
12✔
220
        # Load WorkunitsCallbacks by requesting WorkunitsCallbackFactories, and then constructing
221
        # a per-run instance of each WorkunitsCallback.
222
        params = Params(
×
223
            self.union_membership,
224
            determine_bootstrap_environment(self.graph_session.scheduler_session),
225
        )
226
        (workunits_callback_factories,) = self.graph_session.scheduler_session.product_request(
×
227
            WorkunitsCallbackFactories, params
228
        )
229
        return tuple(filter(bool, (wcf.callback_factory() for wcf in workunits_callback_factories)))
×
230

231
    def _run_builtin_or_auxiliary_goal(self, goal_name: str) -> ExitCode:
12✔
232
        scope_info = self.options.known_scope_to_info[goal_name]
×
233
        assert scope_info.subsystem_cls
×
234

235
        scoped_options = self.options.for_scope(goal_name)
×
236
        goal = scope_info.subsystem_cls(scoped_options)
×
237

238
        def _run_builtin_goal(context: AuxiliaryGoalContext, goal: Any) -> ExitCode:
×
239
            assert isinstance(goal, BuiltinGoal)
×
240
            return goal.run(
×
241
                build_config=context.build_config,
242
                graph_session=context.graph_session,
243
                options=context.options,
244
                specs=context.specs,
245
                union_membership=context.union_membership,
246
            )
247

248
        def _run_auxiliary_goal(context: AuxiliaryGoalContext, goal: Any) -> ExitCode:
×
249
            assert isinstance(goal, AuxiliaryGoal)
×
250
            return goal.run(context)
×
251

252
        context = AuxiliaryGoalContext(
×
253
            build_config=self.build_config,
254
            graph_session=self.graph_session,
255
            options=self.options,
256
            specs=self.specs,
257
            union_membership=self.union_membership,
258
        )
259

260
        if scope_info.is_builtin:
×
261
            return _run_builtin_goal(context, goal)
×
262
        elif scope_info.is_auxiliary:
×
263
            return _run_auxiliary_goal(context, goal)
×
264
        else:
265
            raise AssertionError(
×
266
                f"Probable builtin or auxiliary goal `{goal_name}` is not configured correctly. "
267
                "Please report this error to the Pants team at https://github.com/pantsbuild/pants/issues/new/choose."
268
            )
269

270
    def _run_inner(self) -> ExitCode:
12✔
NEW
271
        if self.ng_invocation:
×
NEW
272
            goals = self.ng_invocation.goals()
×
NEW
273
        elif self.options.builtin_or_auxiliary_goal:
×
UNCOV
274
            return self._run_builtin_or_auxiliary_goal(self.options.builtin_or_auxiliary_goal)
×
275
        else:
NEW
276
            goals = tuple(self.options.goals)
×
277
        if not goals:
×
278
            return PANTS_SUCCEEDED_EXIT_CODE
×
279

280
        try:
×
281
            return self._perform_run(goals)
×
282
        except Exception as e:
×
283
            logger.error(e)
×
284
            return PANTS_FAILED_EXIT_CODE
×
285
        except KeyboardInterrupt:
×
286
            print("Interrupted by user.\n", file=sys.stderr)
×
287
            return PANTS_FAILED_EXIT_CODE
×
288

289
    def run(self, start_time: float) -> ExitCode:
12✔
NEW
290
        specs_strs = list(self.ng_invocation.specs()) if self.ng_invocation else self.options.specs
×
NEW
291
        self.run_tracker.start(run_start_time=start_time, specs=specs_strs)
×
UNCOV
292
        global_options = self.options.for_global_scope()
×
293

294
        streaming_reporter = StreamingWorkunitHandler(
×
295
            self.graph_session.scheduler_session,
296
            run_tracker=self.run_tracker,
297
            specs=self.specs,
298
            options_bootstrapper=self.options_bootstrapper,
299
            callbacks=self._get_workunits_callbacks(),
300
            report_interval_seconds=global_options.streaming_workunits_report_interval,
301
            allow_async_completion=(
302
                global_options.pantsd and global_options.streaming_workunits_complete_async
303
            ),
304
            max_workunit_verbosity=global_options.streaming_workunits_level,
305
        )
306
        try:
×
307
            with streaming_reporter:
×
308
                engine_result = PANTS_FAILED_EXIT_CODE
×
309
                try:
×
310
                    engine_result = self._run_inner()
×
311
                finally:
312
                    self.graph_session.scheduler_session.wait_for_tail_tasks(
×
313
                        self.session_end_tasks_timeout
314
                    )
315
                    metrics = self.graph_session.scheduler_session.metrics()
×
316
                    self.run_tracker.set_pantsd_scheduler_metrics(metrics)
×
317
                    self.run_tracker.end_run(engine_result)
×
318

319
                return engine_result
×
320
        finally:
321
            if not self.is_pantsd_run:
×
322
                # Tear down the executor. See #16105.
323
                self.executor.shutdown(3)
×
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