diff --git a/docs/docs/using-pants/key-concepts/backends.mdx b/docs/docs/using-pants/key-concepts/backends.mdx index 829fa81469e..4895770faf2 100644 --- a/docs/docs/using-pants/key-concepts/backends.mdx +++ b/docs/docs/using-pants/key-concepts/backends.mdx @@ -65,6 +65,7 @@ The list of all backends (both stable and experimental) is also available via `p | Backend | What it does | Docs | | :----------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | | `pants.backend.experimental.adhoc` | Enables support for executing arbitrary runnable targets. | [Integrating new tools without plugins](../../ad-hoc-tools/integrating-new-tools-without-plugins.mdx) | +| `pants.backend.experimental.bsp` | Enables core Build Server Protocol ("BSP") support. | | | `pants.backend.experimental.cc` | Enables core C and C++ support. | | | `pants.backend.experimental.cc.lint.clangformat` | Enables clang-format, a C and C++ autoformatter: [https://clang.llvm.org/docs/ClangFormat.html](https://clang.llvm.org/docs/ClangFormat.html) | | | `pants.backend.experimental.codegen.avro.java` | Enables generating Java from Avro | | @@ -83,6 +84,7 @@ The list of all backends (both stable and experimental) is also available via `p | `pants.backend.experimental.helm` | Enables core Helm support: [https://helm.sh](https://helm.sh) | [Helm overview](../../helm/index.mdx) | | `pants.backend.experimental.helm.check.kubeconfirm` | Enables Kubeconform, a fast Kubernetes manifest validator: [https://github.com/yannh/kubeconform](https://github.com/yannh/kubeconform) | [Helm overview](../../helm/index.mdx) | | `pants.backend.experimental.java` | Enables core Java support. | [Java & Scala overview](../../jvm/java-and-scala.mdx) | +| `pants.backend.experimental.java.bsp` | Enable Java-specific support for Build Server Protocol | [Java & Scala overview](../../jvm/java-and-scala.mdx) | | `pants.backend.experimental.java.debug_goals` | Enable additional goals for introspecting Java targets | [Java & Scala overview](../../jvm/java-and-scala.mdx) | | `pants.backend.experimental.java.lint.google_java_format` | Enables Google Java Format. | [Java & Scala overview](../../jvm/java-and-scala.mdx) | | `pants.backend.experimental.javascript` | Enables core JavaScript support. | | @@ -105,6 +107,7 @@ The list of all backends (both stable and experimental) is also available via `p | `pants.backend.experimental.python.typecheck.pytype` | Enables Pytype, a Python type checker: [https://google.github.io/pytype/](https://google.github.io/pytype/) | | | `pants.backend.experimental.rust` | Enables core Rust support. | | | `pants.backend.experimental.scala` | Enables core Scala support. | [Java & Scala overview](../../jvm/java-and-scala.mdx) | +| `pants.backend.experimental.scala.bsp` | Enables Scala-specific support for Build Server Protocol | [Java & Scala overview](../../jvm/java-and-scala.mdx) | | `pants.backend.experimental.scala.debug_goals` | Enables additional goals for introspecting Scala targets | [Java & Scala overview](../../jvm/java-and-scala.mdx) | | `pants.backend.experimental.scala.lint.scalafmt` | Enables the Scalafmt formatter. | [Java & Scala overview](../../jvm/java-and-scala.mdx) | | `pants.backend.experimental.swift` | Enables core Swift support. | | diff --git a/docs/notes/2.23.x.md b/docs/notes/2.23.x.md index 3f9ee500c95..1dadc441875 100644 --- a/docs/notes/2.23.x.md +++ b/docs/notes/2.23.x.md @@ -16,6 +16,8 @@ We offer [formal sponsorship tiers for companies](https://www.pantsbuild.org/spo The deprecations for the `--changed-dependees` option and the `dependees` goal have expired. Use the equivalent [`--changed-dependents` option](https://www.pantsbuild.org/2.23/reference/subsystems/changed#dependents) or [`dependents` goal](https://www.pantsbuild.org/2.23/reference/goals/dependents) instead. +BSP support has been moved out of core rules into separate core and language-specific backends. + ### Test goal A new option `--experimental-report-test-result-info` is added to the `[test]` config section. Enabling this option will @@ -73,6 +75,15 @@ The deprecation for `crossversion="partial"` on `scala_artifact` has expired. Us The Scala dependency inference now understand usages of the `_root_` package name as a marker for disambiguating between colliding dependencies and will try to resolve those symbols as absolute. For instance, `import _root_.io.circe.syntax` will now be understood as an import of `io.circie.syntax`. +##### BSP (Build Server Protocol) + +The BSP (Build Server Protocol) support has been moved out of the Pants core into several new backends to faciliate disabling this support if it is not needed. The new backends are: + +- `pants.backend.experimental.bsp` (core) +- `pants.backend.experimental.java.bsp` (Java support) +- `pants.backend.experimental.scala.bsp` (Scala support) + +Enable the core `pants.backend.experimental.bsp` backend and one or more of the language-specific backends to enable BSP support. Scala dependency inference now also understands types refered to only by pattern matching cases such as `case MyType() =>`. These used to require manually adding a dependency if the type was defined in a separate file even if it was in the same package. This is now inferred. #### JVM @@ -221,6 +232,8 @@ Fixed bug where files larger than 512KB were being materialized to a process's s Fixed bug where using `RuleRunner` in plugin tests caused `ValueError` that complains about not finding build root sentinel files. Note that you may have to adjust your tests to account for the new `BUILDROOT` file that `RuleRunner` now injects in the sandbox it creates for each test. For example, a test that uses a `**` glob might have to add `!BUILDROOT` to exclude the `BUILDROOT` file, or otherwise account for its presence when inspecting a sandbox or its digest. +Plugins may now provide "auxiliary" goals by implememting the `auxiliary_goals` function in their plugin registration module and returning one or more subclasses of `pants.goal.auxiliary_goal.AuxiliaryGoal`. An auxiliary goal is a special kind of goal which is invoked outside of the engine. The BSP (Build Server Protocol) support now uses this mechanism to move the `experimental-bsp` goal out of the Pants core rules. (The BSP rules used this support since running the BSP server cannot be done from within execution of the rules engine.) + ### Other minor tweaks The "Provided by" information in the documentation now correctly reflects the proper backend to enable to activate a certain feature. diff --git a/src/python/pants/backend/experimental/bsp/BUILD b/src/python/pants/backend/experimental/bsp/BUILD new file mode 100644 index 00000000000..3928f7e3ba8 --- /dev/null +++ b/src/python/pants/backend/experimental/bsp/BUILD @@ -0,0 +1,4 @@ +# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +python_sources() diff --git a/src/python/pants/backend/experimental/bsp/__init__.py b/src/python/pants/backend/experimental/bsp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/python/pants/backend/experimental/bsp/register.py b/src/python/pants/backend/experimental/bsp/register.py new file mode 100644 index 00000000000..405345d3f31 --- /dev/null +++ b/src/python/pants/backend/experimental/bsp/register.py @@ -0,0 +1,13 @@ +# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from pants.bsp.goal import BSPGoal +from pants.bsp.rules import rules as bsp_rules + + +def auxiliary_goals(): + return (BSPGoal,) + + +def rules(): + return (*bsp_rules(),) diff --git a/src/python/pants/backend/experimental/java/bsp/BUILD b/src/python/pants/backend/experimental/java/bsp/BUILD new file mode 100644 index 00000000000..3928f7e3ba8 --- /dev/null +++ b/src/python/pants/backend/experimental/java/bsp/BUILD @@ -0,0 +1,4 @@ +# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +python_sources() diff --git a/src/python/pants/backend/experimental/java/bsp/__init__.py b/src/python/pants/backend/experimental/java/bsp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/python/pants/backend/experimental/java/bsp/register.py b/src/python/pants/backend/experimental/java/bsp/register.py new file mode 100644 index 00000000000..9fb8eb0f912 --- /dev/null +++ b/src/python/pants/backend/experimental/java/bsp/register.py @@ -0,0 +1,29 @@ +# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from pants.backend.experimental.bsp.register import auxiliary_goals as bsp_auxiliary_goals +from pants.backend.experimental.bsp.register import rules as bsp_rules +from pants.backend.experimental.java.register import build_file_aliases as java_build_file_aliases +from pants.backend.experimental.java.register import rules as java_rules +from pants.backend.experimental.java.register import target_types as java_target_types +from pants.backend.java.bsp.rules import rules as java_bsp_rules + + +def auxiliary_goals(): + return bsp_auxiliary_goals() + + +def target_types(): + return java_target_types() + + +def rules(): + return ( + *java_rules(), + *bsp_rules(), + *java_bsp_rules(), + ) + + +def build_file_aliases(): + return java_build_file_aliases() diff --git a/src/python/pants/backend/experimental/java/register.py b/src/python/pants/backend/experimental/java/register.py index a3fbed53ab3..0cd9d1729a4 100644 --- a/src/python/pants/backend/experimental/java/register.py +++ b/src/python/pants/backend/experimental/java/register.py @@ -1,7 +1,6 @@ # Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -from pants.backend.java.bsp import rules as java_bsp_rules from pants.backend.java.compile import javac from pants.backend.java.dependency_inference import java_parser from pants.backend.java.dependency_inference import rules as dependency_inference_rules @@ -39,7 +38,6 @@ def rules(): *java_parser.rules(), *dependency_inference_rules.rules(), *tailor.rules(), - *java_bsp_rules.rules(), *archive.rules(), *target_types_rules(), *jvm_common.rules(), diff --git a/src/python/pants/backend/experimental/scala/bsp/BUILD b/src/python/pants/backend/experimental/scala/bsp/BUILD new file mode 100644 index 00000000000..3928f7e3ba8 --- /dev/null +++ b/src/python/pants/backend/experimental/scala/bsp/BUILD @@ -0,0 +1,4 @@ +# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +python_sources() diff --git a/src/python/pants/backend/experimental/scala/bsp/__init__.py b/src/python/pants/backend/experimental/scala/bsp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/python/pants/backend/experimental/scala/bsp/register.py b/src/python/pants/backend/experimental/scala/bsp/register.py new file mode 100644 index 00000000000..2ffa216cba0 --- /dev/null +++ b/src/python/pants/backend/experimental/scala/bsp/register.py @@ -0,0 +1,28 @@ +# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). +from pants.backend.experimental.bsp.register import auxiliary_goals as bsp_auxiliary_goals +from pants.backend.experimental.bsp.register import rules as bsp_rules +from pants.backend.experimental.scala.register import build_file_aliases as scala_build_file_aliases +from pants.backend.experimental.scala.register import rules as scala_rules +from pants.backend.experimental.scala.register import target_types as scala_target_types +from pants.backend.scala.bsp.rules import rules as scala_bsp_rules + + +def auxiliary_goals(): + return bsp_auxiliary_goals() + + +def target_types(): + return scala_target_types() + + +def rules(): + return ( + *scala_rules(), + *bsp_rules(), + *scala_bsp_rules(), + ) + + +def build_file_aliases(): + return scala_build_file_aliases() diff --git a/src/python/pants/backend/experimental/scala/register.py b/src/python/pants/backend/experimental/scala/register.py index 14683063736..315f04f40b5 100644 --- a/src/python/pants/backend/experimental/scala/register.py +++ b/src/python/pants/backend/experimental/scala/register.py @@ -1,6 +1,5 @@ # Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). -from pants.backend.scala.bsp.rules import rules as bsp_rules from pants.backend.scala.compile import scalac from pants.backend.scala.dependency_inference import rules as dep_inf_rules from pants.backend.scala.goals import check, repl, tailor @@ -50,7 +49,6 @@ def rules(): *dep_inf_rules.rules(), *target_types_rules(), *scala_lockfile_rules(), - *bsp_rules(), *jvm_common.rules(), *wrap_scala.rules, ] diff --git a/src/python/pants/bin/BUILD b/src/python/pants/bin/BUILD index 66e86801a8b..8e8874b6524 100644 --- a/src/python/pants/bin/BUILD +++ b/src/python/pants/bin/BUILD @@ -24,6 +24,7 @@ target( "src/python/pants/backend/docker", "src/python/pants/backend/docker/lint/hadolint", "src/python/pants/backend/experimental/adhoc", + "src/python/pants/backend/experimental/bsp", "src/python/pants/backend/experimental/cc", "src/python/pants/backend/experimental/cc/lint/clangformat", "src/python/pants/backend/experimental/codegen/avro/java", @@ -43,6 +44,7 @@ target( "src/python/pants/backend/experimental/helm", "src/python/pants/backend/experimental/helm/check/kubeconform", "src/python/pants/backend/experimental/java", + "src/python/pants/backend/experimental/java/bsp", "src/python/pants/backend/experimental/java/debug_goals", "src/python/pants/backend/experimental/java/lint/google_java_format", "src/python/pants/backend/experimental/javascript", @@ -69,6 +71,7 @@ target( "src/python/pants/backend/experimental/python/typecheck/pytype", "src/python/pants/backend/experimental/rust", "src/python/pants/backend/experimental/scala", + "src/python/pants/backend/experimental/scala/bsp", "src/python/pants/backend/experimental/scala/debug_goals", "src/python/pants/backend/experimental/scala/lint/scalafix", "src/python/pants/backend/experimental/scala/lint/scalafmt", diff --git a/src/python/pants/bin/auxiliary_goal_integration_test.py b/src/python/pants/bin/auxiliary_goal_integration_test.py new file mode 100644 index 00000000000..766c0e1cd11 --- /dev/null +++ b/src/python/pants/bin/auxiliary_goal_integration_test.py @@ -0,0 +1,24 @@ +# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from pants.testutil.pants_integration_test import run_pants + + +def test_auxiliary_goal_invocation() -> None: + result1 = run_pants( + [ + "experimental-bsp", + ] + ) + result1.assert_failure() + assert "Unknown goal: experimental-bsp" in result1.stdout + + result2 = run_pants( + [ + "--backend-packages=pants.backend.experimental.bsp", + "experimental-bsp", + ] + ) + result2.assert_success() + assert "Wrote BSP runner script" in result2.stderr + assert "Wrote BSP connection file" in result2.stderr diff --git a/src/python/pants/bin/local_pants_runner.py b/src/python/pants/bin/local_pants_runner.py index c7e5550d464..6057c96184d 100644 --- a/src/python/pants/bin/local_pants_runner.py +++ b/src/python/pants/bin/local_pants_runner.py @@ -6,6 +6,7 @@ import logging import sys from dataclasses import dataclass +from typing import Any from pants.base.exiter import PANTS_FAILED_EXIT_CODE, PANTS_SUCCEEDED_EXIT_CODE, ExitCode from pants.base.specs import Specs @@ -25,6 +26,7 @@ WorkunitsCallbackFactories, ) from pants.engine.unions import UnionMembership +from pants.goal.auxiliary_goal import AuxiliaryGoal, AuxiliaryGoalContext from pants.goal.builtin_goal import BuiltinGoal from pants.goal.run_tracker import RunTracker from pants.init.engine_initializer import EngineInitializer, GraphScheduler, GraphSession @@ -211,13 +213,28 @@ def _get_workunits_callbacks(self) -> tuple[WorkunitsCallback, ...]: ) return tuple(filter(bool, (wcf.callback_factory() for wcf in workunits_callback_factories))) - def _run_builtin_goal(self, builtin_goal: str) -> ExitCode: - scope_info = self.options.known_scope_to_info[builtin_goal] + def _run_builtin_or_auxiliary_goal(self, goal_name: str) -> ExitCode: + scope_info = self.options.known_scope_to_info[goal_name] assert scope_info.subsystem_cls - scoped_options = self.options.for_scope(builtin_goal) + + scoped_options = self.options.for_scope(goal_name) goal = scope_info.subsystem_cls(scoped_options) - assert isinstance(goal, BuiltinGoal) - return goal.run( + + def _run_builtin_goal(context: AuxiliaryGoalContext, goal: Any) -> ExitCode: + assert isinstance(goal, BuiltinGoal) + return goal.run( + build_config=context.build_config, + graph_session=context.graph_session, + options=context.options, + specs=context.specs, + union_membership=context.union_membership, + ) + + def _run_auxiliary_goal(context: AuxiliaryGoalContext, goal: Any) -> ExitCode: + assert isinstance(goal, AuxiliaryGoal) + return goal.run(context) + + context = AuxiliaryGoalContext( build_config=self.build_config, graph_session=self.graph_session, options=self.options, @@ -225,9 +242,19 @@ def _run_builtin_goal(self, builtin_goal: str) -> ExitCode: union_membership=self.union_membership, ) + if scope_info.is_builtin: + return _run_builtin_goal(context, goal) + elif scope_info.is_auxiliary: + return _run_auxiliary_goal(context, goal) + else: + raise AssertionError( + f"Probable builtin or auxiliary goal `{goal_name}` is not configured correctly. " + "Please report this error to the Pants team at https://github.com/pantsbuild/pants/issues/new/choose." + ) + def _run_inner(self) -> ExitCode: - if self.options.builtin_goal: - return self._run_builtin_goal(self.options.builtin_goal) + if self.options.builtin_or_auxiliary_goal: + return self._run_builtin_or_auxiliary_goal(self.options.builtin_or_auxiliary_goal) goals = tuple(self.options.goals) if not goals: diff --git a/src/python/pants/bsp/goal.py b/src/python/pants/bsp/goal.py index 95a5d944877..2bae132f495 100644 --- a/src/python/pants/bsp/goal.py +++ b/src/python/pants/bsp/goal.py @@ -12,19 +12,16 @@ from pants.base.build_root import BuildRoot from pants.base.exiter import PANTS_FAILED_EXIT_CODE, PANTS_SUCCEEDED_EXIT_CODE, ExitCode -from pants.base.specs import Specs from pants.bsp.context import BSPContext from pants.bsp.protocol import BSPConnection from pants.bsp.util_rules.lifecycle import BSP_VERSION, BSPLanguageSupport -from pants.build_graph.build_configuration import BuildConfiguration from pants.engine.env_vars import CompleteEnvironmentVars from pants.engine.internals.session import SessionValues from pants.engine.unions import UnionMembership -from pants.goal.builtin_goal import BuiltinGoal +from pants.goal.auxiliary_goal import AuxiliaryGoal, AuxiliaryGoalContext from pants.init.engine_initializer import GraphSession from pants.option.option_types import BoolOption, FileListOption, StrListOption from pants.option.option_value_container import OptionValueContainer -from pants.option.options import Options from pants.util.docutil import bin_name from pants.util.strutil import softwrap from pants.version import VERSION @@ -32,7 +29,7 @@ _logger = logging.getLogger(__name__) -class BSPGoal(BuiltinGoal): +class BSPGoal(AuxiliaryGoal): name = "experimental-bsp" help = "Setup repository for Build Server Protocol (https://build-server-protocol.github.io/)." @@ -102,23 +99,18 @@ class BSPGoal(BuiltinGoal): def run( self, - *, - build_config: BuildConfiguration, - graph_session: GraphSession, - options: Options, - specs: Specs, - union_membership: UnionMembership, + context: AuxiliaryGoalContext, ) -> ExitCode: - goal_options = options.for_scope(self.name) + goal_options = context.options.for_scope(self.name) if goal_options.server: return self._run_server( - graph_session=graph_session, - union_membership=union_membership, + graph_session=context.graph_session, + union_membership=context.union_membership, ) - current_session_values = graph_session.scheduler_session.py_session.session_values + current_session_values = context.graph_session.scheduler_session.py_session.session_values env = current_session_values[CompleteEnvironmentVars] return self._setup_bsp_connection( - union_membership=union_membership, env=env, options=goal_options + union_membership=context.union_membership, env=env, options=goal_options ) def _setup_bsp_connection( diff --git a/src/python/pants/build_graph/build_configuration.py b/src/python/pants/build_graph/build_configuration.py index 003c5b58fec..109f0a4af1b 100644 --- a/src/python/pants/build_graph/build_configuration.py +++ b/src/python/pants/build_graph/build_configuration.py @@ -269,6 +269,24 @@ def register_target_types( def register_remote_auth_plugin(self, remote_auth_plugin: Callable) -> None: self._remote_auth_plugin = remote_auth_plugin + def register_auxiliary_goals(self, plugin_or_backend: str, auxiliary_goals: Iterable[type]): + """Registers the given auxiliary goals.""" + if not isinstance(auxiliary_goals, Iterable): + raise TypeError( + f"The entrypoint `auxiliary_goals` must return an iterable. " + f"Given {repr(auxiliary_goals)}" + ) + # Import `AuxiliaryGoal` here to avoid import cycle. + from pants.goal.auxiliary_goal import AuxiliaryGoal + + bad_elements = [goal for goal in auxiliary_goals if not issubclass(goal, AuxiliaryGoal)] + if bad_elements: + raise TypeError( + "Every element of the entrypoint `auxiliary_goals` must be a subclass of " + f"{AuxiliaryGoal.__name__}. Bad elements: {bad_elements}." + ) + self.register_subsystems(plugin_or_backend, auxiliary_goals) + def allow_unknown_options(self, allow: bool = True) -> None: """Allows overriding whether Options parsing will fail for unrecognized Options. diff --git a/src/python/pants/core/register.py b/src/python/pants/core/register.py index 69f322e0d7c..7419b10e800 100644 --- a/src/python/pants/core/register.py +++ b/src/python/pants/core/register.py @@ -6,7 +6,6 @@ These are always activated and cannot be disabled. """ from pants.backend.codegen import export_codegen_goal -from pants.bsp.rules import rules as bsp_rules from pants.build_graph.build_file_aliases import BuildFileAliases from pants.core.goals import ( check, @@ -86,7 +85,6 @@ def rules(): *run.rules(), *tailor.rules(), *test.rules(), - *bsp_rules(), # util_rules *adhoc_binaries.rules(), *anonymous_telemetry.rules(), diff --git a/src/python/pants/goal/auxiliary_goal.py b/src/python/pants/goal/auxiliary_goal.py new file mode 100644 index 00000000000..66912467d89 --- /dev/null +++ b/src/python/pants/goal/auxiliary_goal.py @@ -0,0 +1,58 @@ +# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import ClassVar + +from pants.base.exiter import ExitCode +from pants.base.specs import Specs +from pants.build_graph.build_configuration import BuildConfiguration +from pants.engine.goal import GoalSubsystem +from pants.engine.unions import UnionMembership +from pants.init.engine_initializer import GraphSession +from pants.option.options import Options +from pants.option.scope import ScopeInfo + + +@dataclass +class AuxiliaryGoalContext: + """Context passed to a `AuxiliaryGoal.run` implementation.""" + + build_config: BuildConfiguration + graph_session: GraphSession + options: Options + specs: Specs + union_membership: UnionMembership + + +class AuxiliaryGoal(ABC, GoalSubsystem): + """Configure an "auxiliary" goal which allows rules to "take over" Pants client execution in + lieu of executing an ordinary goal. + + Only a single auxiliary goal is executed per run, any remaining goals/arguments are passed + unaltered to the auxiliary goal. Auxiliary goals have precedence over regular goals. + + When multiple auxiliary goals are presented, the first auxiliary goal will be used unless there is a + auxiliary goal that begin with a hyphen (`-`), in which case the last such "option goal" will be + prioritized. This is to support things like `./pants some-builtin-goal --help`. + + The intended use for this API is rule code which runs a server (for example, a BSP server) + which provides an alternate interface to the Pants rule engine, or other kinds of goals + which must run "outside" of the usual engine processing to function. + """ + + # Used by `pants.option.arg_splitter.ArgSplitter()` to optionally allow aliasing auxiliary goals. + aliases: ClassVar[tuple[str, ...]] = () + + @classmethod + def create_scope_info(cls, **scope_info_kwargs) -> ScopeInfo: + return super().create_scope_info(is_auxiliary=True, **scope_info_kwargs) + + @abstractmethod + def run( + self, + context: AuxiliaryGoalContext, + ) -> ExitCode: + pass diff --git a/src/python/pants/goal/builtins.py b/src/python/pants/goal/builtins.py index 9b5140d0ca2..8d242b28823 100644 --- a/src/python/pants/goal/builtins.py +++ b/src/python/pants/goal/builtins.py @@ -3,7 +3,6 @@ from __future__ import annotations -from pants.bsp.goal import BSPGoal from pants.build_graph.build_configuration import BuildConfiguration from pants.goal import help from pants.goal.builtin_goal import BuiltinGoal @@ -18,7 +17,6 @@ def register_builtin_goals(build_configuration: BuildConfiguration.Builder) -> N def builtin_goals() -> tuple[type[BuiltinGoal], ...]: return ( - BSPGoal, CompletionBuiltinGoal, ExplorerBuiltinGoal, MigrateCallByNameBuiltinGoal, diff --git a/src/python/pants/init/extension_loader.py b/src/python/pants/init/extension_loader.py index 2fecae4ba86..5a71f2028fe 100644 --- a/src/python/pants/init/extension_loader.py +++ b/src/python/pants/init/extension_loader.py @@ -107,6 +107,9 @@ def load_plugins( f"register remote auth function {remote_auth_func.__module__}.{remote_auth_func.__name__} from plugin: {plugin}" ) build_configuration.register_remote_auth_plugin(remote_auth_func) + if "auxiliary_goals" in entries: + auxiliary_goals = entries["auxiliary_goals"].load()() + build_configuration.register_auxiliary_goals(req.key, auxiliary_goals) loaded[dist.as_requirement().key] = dist @@ -168,3 +171,6 @@ def invoke_entrypoint(name: str): f"register remote auth function {remote_auth_func.__module__}.{remote_auth_func.__name__} from backend: {backend_package}" ) build_configuration.register_remote_auth_plugin(remote_auth_func) + auxiliary_goals = invoke_entrypoint("auxiliary_goals") + if auxiliary_goals: + build_configuration.register_auxiliary_goals(backend_package, auxiliary_goals) diff --git a/src/python/pants/option/arg_splitter.py b/src/python/pants/option/arg_splitter.py index e6c07120b65..beb4c479b52 100644 --- a/src/python/pants/option/arg_splitter.py +++ b/src/python/pants/option/arg_splitter.py @@ -22,7 +22,7 @@ class ArgSplitterError(Exception): class SplitArgs: """The result of splitting args.""" - builtin_goal: str | None # Requested builtin goal (explicitly or implicitly). + builtin_or_auxiliary_goal: str | None # Requested builtin goal (explicitly or implicitly). goals: list[str] # Explicitly requested goals. unknown_goals: list[str] # Any unknown goals. scope_to_flags: dict[str, list[str]] # Scope name -> list of flags in that scope. @@ -135,7 +135,7 @@ def split_args(self, args: Sequence[str]) -> SplitArgs: specs: list[str] = [] passthru: list[str] = [] unknown_scopes: list[str] = [] - builtin_goal: str | None = None + builtin_or_auxiliary_goal: str | None = None def add_scope(s: str) -> None: # Force the scope to appear, even if empty. @@ -150,19 +150,21 @@ def add_goal(scope: str) -> str: add_scope(scope) return scope - nonlocal builtin_goal - if scope_info.is_builtin and (not builtin_goal or scope.startswith("-")): - if builtin_goal: - goals.add(builtin_goal) + nonlocal builtin_or_auxiliary_goal + if (scope_info.is_builtin or scope_info.is_auxiliary) and ( + not builtin_or_auxiliary_goal or scope.startswith("-") + ): + if builtin_or_auxiliary_goal: + goals.add(builtin_or_auxiliary_goal) - # Get scope from info in case we hit an aliased builtin goal. - builtin_goal = scope_info.scope + # Get scope from info in case we hit an aliased builtin/auxiliary goal. + builtin_or_auxiliary_goal = scope_info.scope else: goals.add(scope_info.scope) add_scope(scope_info.scope) - # Use builtin goal as default scope for args. - return builtin_goal or scope_info.scope + # Use builtin/auxiliary goal as default scope for args. + return builtin_or_auxiliary_goal or scope_info.scope self._unconsumed_args = list(reversed(args)) # The first token is the binary name, so skip it. @@ -198,11 +200,11 @@ def assign_flag_to_scope(flg: str, default_scope: str) -> None: else: add_goal(arg) - if not builtin_goal: + if not builtin_or_auxiliary_goal: if unknown_scopes and UNKNOWN_GOAL_NAME in self._known_goal_scopes: - builtin_goal = UNKNOWN_GOAL_NAME + builtin_or_auxiliary_goal = UNKNOWN_GOAL_NAME elif not goals and NO_GOAL_NAME in self._known_goal_scopes: - builtin_goal = NO_GOAL_NAME + builtin_or_auxiliary_goal = NO_GOAL_NAME if self._at_standalone_double_dash(): self._unconsumed_args.pop() @@ -223,7 +225,7 @@ def assign_flag_to_scope(flg: str, default_scope: str) -> None: ) return SplitArgs( - builtin_goal=builtin_goal, + builtin_or_auxiliary_goal=builtin_or_auxiliary_goal, goals=list(goals), unknown_goals=unknown_scopes, scope_to_flags=dict(scope_to_flags), diff --git a/src/python/pants/option/arg_splitter_test.py b/src/python/pants/option/arg_splitter_test.py index ae9230d0c5b..a20ed4bb2b4 100644 --- a/src/python/pants/option/arg_splitter_test.py +++ b/src/python/pants/option/arg_splitter_test.py @@ -54,16 +54,16 @@ def assert_valid_split( assert expected_passthru == split_args.passthru assert expected_is_help == ( - split_args.builtin_goal + split_args.builtin_or_auxiliary_goal in ("help", "help-advanced", "help-all", UNKNOWN_GOAL_NAME, NO_GOAL_NAME) ) - assert expected_help_advanced == ("help-advanced" == split_args.builtin_goal) - assert expected_help_all == ("help-all" == split_args.builtin_goal) + assert expected_help_advanced == ("help-advanced" == split_args.builtin_or_auxiliary_goal) + assert expected_help_all == ("help-all" == split_args.builtin_or_auxiliary_goal) def assert_unknown_goal(splitter: ArgSplitter, args_str: str, unknown_goals: list[str]) -> None: split_args = splitter.split_args(shlex.split(args_str)) - assert UNKNOWN_GOAL_NAME == split_args.builtin_goal + assert UNKNOWN_GOAL_NAME == split_args.builtin_or_auxiliary_goal assert set(unknown_goals) == set(split_args.unknown_goals) @@ -424,7 +424,7 @@ def test_help_detection(splitter: ArgSplitter, command_line: str, expected: dict def test_version_request_detection(splitter: ArgSplitter) -> None: def assert_version_request(args_str: str) -> None: split_args = splitter.split_args(shlex.split(args_str)) - assert "version" == split_args.builtin_goal + assert "version" == split_args.builtin_or_auxiliary_goal assert_version_request("./pants -v") assert_version_request("./pants -V") @@ -452,7 +452,7 @@ def test_unknown_goal_detection( @pytest.mark.parametrize("extra_args", ("", "foo/bar:baz", "f.ext", "-linfo", "--arg")) def test_no_goal_detection(extra_args: str, splitter: ArgSplitter) -> None: split_args = splitter.split_args(shlex.split(f"./pants {extra_args}")) - assert NO_GOAL_NAME == split_args.builtin_goal + assert NO_GOAL_NAME == split_args.builtin_or_auxiliary_goal def test_subsystem_scope_is_unknown_goal(splitter: ArgSplitter) -> None: diff --git a/src/python/pants/option/options.py b/src/python/pants/option/options.py index e0dbaac06ad..6d77dd096a9 100644 --- a/src/python/pants/option/options.py +++ b/src/python/pants/option/options.py @@ -172,7 +172,7 @@ def create( ) return cls( - builtin_goal=split_args.builtin_goal, + builtin_or_auxiliary_goal=split_args.builtin_or_auxiliary_goal, goals=split_args.goals, unknown_goals=split_args.unknown_goals, scope_to_flags=split_args.scope_to_flags, @@ -188,7 +188,7 @@ def create( def __init__( self, - builtin_goal: str | None, + builtin_or_auxiliary_goal: str | None, goals: list[str], unknown_goals: list[str], scope_to_flags: dict[str, list[str]], @@ -205,7 +205,7 @@ def __init__( Dependents should use `Options.create` instead. """ - self._builtin_goal = builtin_goal + self._builtin_or_auxiliary_goal = builtin_or_auxiliary_goal self._goals = goals self._unknown_goals = unknown_goals self._scope_to_flags = scope_to_flags @@ -227,12 +227,12 @@ def specs(self) -> list[str]: return self._specs @property - def builtin_goal(self) -> str | None: - """The requested builtin goal, if any. + def builtin_or_auxiliary_goal(self) -> str | None: + """The requested builtin or auxiliary goal, if any. :API: public """ - return self._builtin_goal + return self._builtin_or_auxiliary_goal @property def goals(self) -> list[str]: diff --git a/src/python/pants/option/scope.py b/src/python/pants/option/scope.py index c9b9c46f5ec..7945a15406e 100644 --- a/src/python/pants/option/scope.py +++ b/src/python/pants/option/scope.py @@ -39,6 +39,9 @@ class ScopeInfo: # Builtin goals, such as `help` and `version` etc. is_builtin: bool = False + # Auxiliary goals, such as the `experimental-bsp` goal. + is_auxiliary: bool = False + @property def description(self) -> str: return cast(str, self._subsystem_cls_attr("help")) diff --git a/src/python/pants/testutil/rule_runner.py b/src/python/pants/testutil/rule_runner.py index 2a73858ea33..4540a4a67f9 100644 --- a/src/python/pants/testutil/rule_runner.py +++ b/src/python/pants/testutil/rule_runner.py @@ -53,6 +53,7 @@ from pants.engine.rules import QueryRule as QueryRule from pants.engine.target import AllTargets, Target, WrappedTarget, WrappedTargetRequest from pants.engine.unions import UnionMembership, UnionRule +from pants.goal.auxiliary_goal import AuxiliaryGoal from pants.init.engine_initializer import EngineInitializer from pants.init.logging import initialize_stdio, initialize_stdio_raw, stdio_destination from pants.option.global_options import ( @@ -261,6 +262,7 @@ def __init__( max_workunit_verbosity: LogLevel = LogLevel.DEBUG, inherent_environment: EnvironmentName | None = EnvironmentName(None), is_bootstrap: bool = False, + auxiliary_goals: Iterable[type[AuxiliaryGoal]] | None = None, ) -> None: bootstrap_args = [*bootstrap_args] @@ -308,6 +310,7 @@ def rewrite_rule_for_inherent_environment(rule): build_config_builder.register_rules("_dummy_for_test_", all_rules) build_config_builder.register_target_types("_dummy_for_test_", target_types or ()) + build_config_builder.register_auxiliary_goals("_dummy_for_test_", auxiliary_goals or ()) self.build_config = build_config_builder.create() self.environment = CompleteEnvironmentVars({})