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

pantsbuild / pants / 25405422172

05 May 2026 10:18PM UTC coverage: 92.879% (-0.07%) from 92.944%
25405422172

Pull #23319

github

web-flow
Merge c82d0f333 into e8b784f89
Pull Request #23319: [pants_ng] Scaffolding for a pants_ng mode.

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

209 existing lines in 15 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