Skip to content

Commit

Permalink
initial bindings of BSP specification (#14325)
Browse files Browse the repository at this point in the history
Adds an incomplete initial binding of the [Build Server Protocol](https://build-server-protocol.github.io/docs/specification.html). The goal of this PR is to unblock the initial BSP protocol implementation which is being done as a separate PR.

[ci skip-rust]
  • Loading branch information
Tom Dyas authored Feb 1, 2022
1 parent f529095 commit 6f7fcfc
Show file tree
Hide file tree
Showing 3 changed files with 363 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/python/pants/bsp/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

python_sources()
Empty file.
359 changes: 359 additions & 0 deletions src/python/pants/bsp/spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import annotations

from dataclasses import dataclass
from typing import Any

# -----------------------------------------------------------------------------------------------
# Base types
# -----------------------------------------------------------------------------------------------

Uri = str


@dataclass(frozen=True)
class BuildTargetIdentifier:
"""A unique identifier for a target, can use any URI-compatible encoding as long as it is unique
within the workspace.
Clients should not infer metadata out of the URI structure such as the path or query parameters,
use BuildTarget instead.
"""

# The target’s Uri
uri: Uri

@classmethod
def from_json_dict(cls, d):
return cls(uri=d["uri"])


@dataclass(frozen=True)
class BuildTargetCapabilities:
# This target can be compiled by the BSP server.
can_compile: bool

# This target can be tested by the BSP server.
can_test: bool

# This target can be run by the BSP server.
can_run: bool

# This target can be debugged by the BSP server.
can_debug: bool

@classmethod
def from_json_dict(cls, d):
return cls(
can_compile=d["canCompile"],
can_test=d["canTest"],
can_run=d["canRun"],
can_debug=d["canDebug"],
)


@dataclass(frozen=True)
class BuildTarget:
"""Build target contains metadata about an artifact (for example library, test, or binary
artifact)"""

# The target’s unique identifier
id: BuildTargetIdentifier

# A human readable name for this target.
# May be presented in the user interface.
# Should be unique if possible.
# The id.uri is used if None.
display_name: str | None

# The directory where this target belongs to. Multiple build targets are allowed to map
# to the same base directory, and a build target is not required to have a base directory.
# A base directory does not determine the sources of a target, see buildTarget/sources. */
base_directory: Uri | None

# Free-form string tags to categorize or label this build target.
# For example, can be used by the client to:
# - customize how the target should be translated into the client's project model.
# - group together different but related targets in the user interface.
# - display icons or colors in the user interface.
# Pre-defined tags are listed in `BuildTargetTag` but clients and servers
# are free to define new tags for custom purposes.
tags: tuple[str, ...]

# The capabilities of this build target.
capabilities: BuildTargetCapabilities

# The set of languages that this target contains.
# The ID string for each language is defined in the LSP.
language_ids: tuple[str, ...]

# The direct upstream build target dependencies of this build target
dependencies: tuple[BuildTargetIdentifier, ...]

# Kind of data to expect in the `data` field. If this field is not set, the kind of data is not specified.
data_kind: str | None

# Language-specific metadata about this target.
# See ScalaBuildTarget as an example.
data: Any | None

@classmethod
def from_json_dict(cls, d):
return cls(
id=BuildTargetIdentifier.from_json_dict(d["id"]),
display_name=d.get("displayName"),
base_directory=d["baseDirectory"],
tags=tuple(d.get("tags", [])),
capabilities=BuildTargetCapabilities.from_json_dict(d["capabilities"]),
language_ids=tuple(d.get("languageIds", [])),
dependencies=tuple(
BuildTargetIdentifier.from_json_dict(x) for x in d.get("dependencies", [])
),
data_kind=d.get("dataKind"),
data=d.get("data"),
)


class BuildTargetDataKind:
# The `data` field contains a `ScalaBuildTarget` object.
SCALA = "scala"

# The `data` field contains a `SbtBuildTarget` object.
SBT = "sbt"


class BuildTargetTag:
# Target contains re-usable functionality for downstream targets. May have any
# combination of capabilities.
LIBRARY = "library"

# Target contains source code for producing any kind of application, may have
# but does not require the `canRun` capability.
APPLICATION = "application"

# Target contains source code for testing purposes, may have but does not
# require the `canTest` capability.
TEST = "test"

# Target contains source code for integration testing purposes, may have
# but does not require the `canTest` capability.
# The difference between "test" and "integration-test" is that
# integration tests traditionally run slower compared to normal tests
# and require more computing resources to execute.
INTEGRATION_TEST = "integration-test"

# Target contains source code to measure performance of a program, may have
# but does not require the `canRun` build target capability.
BENCHMARK = "benchmark"

# Target should be ignored by IDEs. */
NO_IDE = "no-ide"

# Actions on the target such as build and test should only be invoked manually
# and explicitly. For example, triggering a build on all targets in the workspace
# should by default not include this target.
#
# The original motivation to add the "manual" tag comes from a similar functionality
# that exists in Bazel, where targets with this tag have to be specified explicitly
# on the command line.
MANUAL = "manual"


@dataclass(frozen=True)
class TaskId:
"""The Task Id allows clients to uniquely identify a BSP task and establish a client-parent
relationship with another task id."""

# A unique identifier
id: str

# The parent task ids, if any. A non-empty parents field means
# this task is a sub-task of every parent task id. The child-parent
# relationship of tasks makes it possible to render tasks in
# a tree-like user interface or inspect what caused a certain task
# execution.
parents: tuple[str, ...] | None

@classmethod
def from_json_dict(cls, d):
return cls(
id=d["id"],
parents=tuple(d["parents"]) if "parents" in d else None,
)


class StatusCode:
# Execution was successful.
OK = 1

# Execution failed.
ERROR = 2

# Execution was cancelled.
CANCELLED = 3


# -----------------------------------------------------------------------------------------------
# Protocol types
# -----------------------------------------------------------------------------------------------


@dataclass(frozen=True)
class BuildClientCapabilities:
# The languages that this client supports.
# The ID strings for each language is defined in the LSP.
# The server must never respond with build targets for other
# languages than those that appear in this list.
language_ids: tuple[str, ...]

@classmethod
def from_json_dict(cls, d):
return cls(language_ids=tuple(d.get("languageIds", [])))


@dataclass(frozen=True)
class InitializeBuildParams:
# Name of the client
display_name: str

# The version of the client
version: str

# The BSP version that the client speaks
bsp_version: str

# The rootUri of the workspace
root_uri: Uri

# The capabilities of the client
capabilities: BuildClientCapabilities

# Additional metadata about the client
data: Any | None

@classmethod
def from_json_dict(cls, d):
return cls(
display_name=d["displayName"],
version=d["version"],
bsp_version=d["bspVersion"],
root_uri=d["rootUri"],
capabilities=BuildClientCapabilities.from_json_dict(d["capabilities"]),
data=d.get("data"),
)


@dataclass(frozen=True)
class CompileProvider:
language_ids: tuple[str, ...]

@classmethod
def from_json_dict(cls, d):
return cls(language_ids=tuple(d.get("languageIds", [])))


@dataclass(frozen=True)
class RunProvider:
language_ids: tuple[str, ...]

@classmethod
def from_json_dict(cls, d):
return cls(language_ids=tuple(d.get("languageIds", [])))


@dataclass(frozen=True)
class DebugProvider:
language_ids: tuple[str, ...]

@classmethod
def from_json_dict(cls, d):
return cls(language_ids=tuple(d.get("languageIds", [])))


@dataclass(frozen=True)
class TestProvider:
language_ids: tuple[str, ...]

@classmethod
def from_json_dict(cls, d):
return cls(language_ids=tuple(d.get("languageIds", [])))


@dataclass(frozen=True)
class BuildServerCapabilities:
# The languages the server supports compilation via method buildTarget/compile.
compile_provider: CompileProvider | None

# The languages the server supports test execution via method buildTarget/test
test_provider: TestProvider | None

# The languages the server supports run via method buildTarget/run
run_provider: RunProvider | None

# The languages the server supports debugging via method debugSession/start
debug_provider: DebugProvider | None

# The server can provide a list of targets that contain a
# single text document via the method buildTarget/inverseSources
inverse_sources_provider: bool | None

# The server provides sources for library dependencies
# via method buildTarget/dependencySources
dependency_sources_provider: bool | None

# The server cam provide a list of dependency modules (libraries with meta information)
# via method buildTarget/dependencyModules
dependency_modules_provider: bool | None

# The server provides all the resource dependencies
# via method buildTarget/resources
resources_provider: bool | None

# Reloading the build state through workspace/reload is supported
can_reload: bool | None

# The server sends notifications to the client on build
# target change events via buildTarget/didChange
build_target_changed_provider: bool | None

@classmethod
def from_json_dict(cls, d):
return cls(
compile_provider=CompileProvider.from_json_dict(d["compileProvider"])
if "compileProvider" in d
else None,
test_provider=TestProvider.from_json_dict(d["testProvider"])
if "testProvider" in d
else None,
run_provider=RunProvider.from_json_dict(d["runProvider"])
if "runProvider" in d
else None,
debug_provider=DebugProvider.from_json_dict(d["debugProvider"])
if "debugProvider" in d
else None,
inverse_sources_provider=d.get("inverseSourcesProvider"),
dependency_sources_provider=d.get("dependencySourcesProvider"),
dependency_modules_provider=d.get("dependencyModulesProvider"),
resources_provider=d.get("resourcesProvider"),
can_reload=d.get("canReload"),
build_target_changed_provider=d.get("buildTargetChangedProvider"),
)


@dataclass(frozen=True)
class InitializeBuildResult:
# Name of the server
display_name: str

# The version of the server
version: str

# The BSP version that the server speaks
bsp_version: str

# The capabilities of the build server
capabilities: BuildServerCapabilities

# Additional metadata about the server
data: Any | None

0 comments on commit 6f7fcfc

Please sign in to comment.