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

pantsbuild / pants / 22285099215

22 Feb 2026 08:52PM UTC coverage: 75.854% (-17.1%) from 92.936%
22285099215

Pull #23121

github

web-flow
Merge c7299df9c into ba8359840
Pull Request #23121: fix issue with optional fields in dependency validator

28 of 29 new or added lines in 2 files covered. (96.55%)

11174 existing lines in 400 files now uncovered.

53694 of 70786 relevant lines covered (75.85%)

1.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.76
/src/python/pants/bsp/util_rules/targets.py
1
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
from __future__ import annotations
1✔
4

5
import itertools
1✔
6
import logging
1✔
7
from collections import defaultdict
1✔
8
from collections.abc import Sequence
1✔
9
from dataclasses import dataclass
1✔
10
from pathlib import Path
1✔
11
from typing import ClassVar, Generic, TypeVar
1✔
12

13
import toml
1✔
14

15
from pants.base.build_root import BuildRoot
1✔
16
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
1✔
17
from pants.base.specs import RawSpecs, RawSpecsWithoutFileOwners
1✔
18
from pants.base.specs_parser import SpecsParser
1✔
19
from pants.bsp.goal import BSPGoal
1✔
20
from pants.bsp.protocol import BSPHandlerMapping
1✔
21
from pants.bsp.spec.base import (
1✔
22
    BSPData,
23
    BuildTarget,
24
    BuildTargetCapabilities,
25
    BuildTargetIdentifier,
26
    StatusCode,
27
    TaskId,
28
    Uri,
29
)
30
from pants.bsp.spec.targets import (
1✔
31
    DependencyModule,
32
    DependencyModulesItem,
33
    DependencyModulesParams,
34
    DependencyModulesResult,
35
    DependencySourcesItem,
36
    DependencySourcesParams,
37
    DependencySourcesResult,
38
    SourceItem,
39
    SourceItemKind,
40
    SourcesItem,
41
    SourcesParams,
42
    SourcesResult,
43
    WorkspaceBuildTargetsParams,
44
    WorkspaceBuildTargetsResult,
45
)
46
from pants.engine.environment import EnvironmentName
1✔
47
from pants.engine.fs import PathGlobs, Workspace
1✔
48
from pants.engine.internals.graph import resolve_source_paths, resolve_targets
1✔
49
from pants.engine.internals.native_engine import EMPTY_DIGEST, Digest, MergeDigests
1✔
50
from pants.engine.internals.selectors import concurrently
1✔
51
from pants.engine.intrinsics import get_digest_contents, merge_digests
1✔
52
from pants.engine.rules import _uncacheable_rule, collect_rules, implicitly, rule
1✔
53
from pants.engine.target import (
1✔
54
    Field,
55
    FieldDefaults,
56
    FieldSet,
57
    SourcesField,
58
    SourcesPathsRequest,
59
    Targets,
60
)
61
from pants.engine.unions import UnionMembership, UnionRule, union
1✔
62
from pants.source.source_root import SourceRootsRequest, get_source_roots
1✔
63
from pants.util.frozendict import FrozenDict
1✔
64
from pants.util.ordered_set import OrderedSet
1✔
65
from pants.util.strutil import bullet_list
1✔
66

67
_logger = logging.getLogger(__name__)
1✔
68

69
_FS = TypeVar("_FS", bound=FieldSet)
1✔
70

71

72
@union(in_scope_types=[EnvironmentName])
1✔
73
@dataclass(frozen=True)
1✔
74
class BSPBuildTargetsMetadataRequest(Generic[_FS]):
1✔
75
    """Hook to allow language backends to provide metadata for BSP build targets."""
76

77
    language_id: ClassVar[str]
1✔
78
    can_merge_metadata_from: ClassVar[tuple[str, ...]]
1✔
79
    field_set_type: ClassVar[type[_FS]]
1✔
80

81
    resolve_prefix: ClassVar[str]
1✔
82
    resolve_field: ClassVar[type[Field]]
1✔
83

84
    field_sets: tuple[_FS, ...]
1✔
85

86

87
@dataclass(frozen=True)
1✔
88
class BSPBuildTargetsMetadataResult:
1✔
89
    """Response type for a BSPBuildTargetsMetadataRequest."""
90

91
    # Metadata for the `data` field of the final `BuildTarget`.
92
    metadata: BSPData | None = None
1✔
93

94
    # Output to write into `.pants.d/bsp` for access by IDE.
95
    digest: Digest = EMPTY_DIGEST
1✔
96

97

98
@rule(polymorphic=True)
1✔
99
async def get_bsp_build_targets_metadata(
1✔
100
    req: BSPBuildTargetsMetadataRequest, env_name: EnvironmentName
101
) -> BSPBuildTargetsMetadataResult:
102
    raise NotImplementedError()
×
103

104

105
@dataclass(frozen=True)
1✔
106
class BSPTargetDefinition:
1✔
107
    display_name: str | None
1✔
108
    base_directory: str | None
1✔
109
    addresses: tuple[str, ...]
1✔
110
    resolve_filter: str | None
1✔
111

112

113
@dataclass(frozen=True)
1✔
114
class BSPBuildTargetInternal:
1✔
115
    name: str
1✔
116
    specs: RawSpecs
1✔
117
    definition: BSPTargetDefinition
1✔
118

119
    @property
1✔
120
    def bsp_target_id(self) -> BuildTargetIdentifier:
1✔
UNCOV
121
        return BuildTargetIdentifier(f"pants:{self.name}")
×
122

123

124
@dataclass(frozen=True)
1✔
125
class BSPBuildTargetSourcesInfo:
1✔
126
    """Source files and roots for a BSP build target.
127

128
    It is a separate class so that it is computed lazily only when called for by an RPC call.
129
    """
130

131
    source_files: frozenset[str]
1✔
132
    source_roots: frozenset[str]
1✔
133

134

135
@dataclass(frozen=True)
1✔
136
class BSPBuildTargets:
1✔
137
    targets_mapping: FrozenDict[str, BSPBuildTargetInternal]
1✔
138

139

140
@dataclass(frozen=True)
1✔
141
class _ParseOneBSPMappingRequest:
1✔
142
    name: str
1✔
143
    definition: BSPTargetDefinition
1✔
144

145

146
@rule
1✔
147
async def parse_one_bsp_mapping(request: _ParseOneBSPMappingRequest) -> BSPBuildTargetInternal:
1✔
148
    specs_parser = SpecsParser()
1✔
149
    specs = specs_parser.parse_specs(
1✔
150
        request.definition.addresses, description_of_origin=f"the BSP mapping {request.name}"
151
    ).includes
152
    return BSPBuildTargetInternal(request.name, specs, request.definition)
1✔
153

154

155
@rule
1✔
156
async def materialize_bsp_build_targets(bsp_goal: BSPGoal) -> BSPBuildTargets:
1✔
157
    definitions: dict[str, BSPTargetDefinition] = {}
1✔
158
    for config_file in bsp_goal.groups_config_files:
1✔
159
        config_contents = await get_digest_contents(
1✔
160
            **implicitly(
161
                PathGlobs(
162
                    [config_file],
163
                    glob_match_error_behavior=GlobMatchErrorBehavior.error,
164
                    description_of_origin=f"BSP config file `{config_file}`",
165
                )
166
            )
167
        )
168
        if len(config_contents) == 0:
1✔
169
            raise ValueError(f"BSP targets config file `{config_file}` does not exist.")
×
170
        elif len(config_contents) > 1:
1✔
171
            raise ValueError(
×
172
                f"BSP targets config file specified as `{config_file}` matches multiple files. "
173
                "Please do not use wildcards in config file paths."
174
            )
175

176
        config = toml.loads(config_contents[0].content.decode())
1✔
177

178
        groups = config.get("groups")
1✔
179
        if groups is None:
1✔
180
            raise ValueError(
×
181
                f"BSP targets config file `{config_file}` is missing the `groups` table."
182
            )
183
        if not isinstance(groups, dict):
1✔
184
            raise ValueError(
×
185
                f"BSP targets config file `{config_file}` contains a `groups` key that is not a TOML table."
186
            )
187

188
        for id, group in groups.items():
1✔
189
            if not isinstance(group, dict):
1✔
190
                raise ValueError(
×
191
                    f"BSP targets config file `{config_file}` contains an entry for "
192
                    "`groups` array that is not a dictionary (index={i})."
193
                )
194

195
            base_directory = group.get("base_directory")
1✔
196
            display_name = group.get("display_name")
1✔
197
            addresses = group.get("addresses", [])
1✔
198
            if not addresses:
1✔
199
                raise ValueError(
×
200
                    f"BSP targets config file `{config_file}` contains group ID `{id}` which has "
201
                    "no address specs defined via the `addresses` key. Please specify at least "
202
                    "one address spec."
203
                )
204

205
            resolve_filter = group.get("resolve")
1✔
206

207
            definitions[id] = BSPTargetDefinition(
1✔
208
                display_name=display_name,
209
                base_directory=base_directory,
210
                addresses=tuple(addresses),
211
                resolve_filter=resolve_filter,
212
            )
213

214
    bsp_internal_targets = await concurrently(
1✔
215
        parse_one_bsp_mapping(_ParseOneBSPMappingRequest(name, definition))
216
        for name, definition in definitions.items()
217
    )
218
    target_mapping = dict(zip(definitions.keys(), bsp_internal_targets))
1✔
219
    return BSPBuildTargets(FrozenDict(target_mapping))
1✔
220

221

222
@rule
1✔
223
async def resolve_bsp_build_target_identifier(
1✔
224
    bsp_target_id: BuildTargetIdentifier, bsp_build_targets: BSPBuildTargets
225
) -> BSPBuildTargetInternal:
226
    scheme, _, target_name = bsp_target_id.uri.partition(":")
1✔
227
    if scheme != "pants":
1✔
228
        raise ValueError(f"Unknown BSP scheme `{scheme}` for BSP target ID `{bsp_target_id}.")
×
229

230
    target_internal = bsp_build_targets.targets_mapping.get(target_name)
1✔
231
    if not target_internal:
1✔
232
        raise ValueError(f"Unknown BSP target name: {target_name}")
×
233

234
    return target_internal
1✔
235

236

237
@rule
1✔
238
async def resolve_bsp_build_target_addresses(
1✔
239
    bsp_target: BSPBuildTargetInternal,
240
    union_membership: UnionMembership,
241
    field_defaults: FieldDefaults,
242
) -> Targets:
243
    # NB: Using `RawSpecs` directly rather than `RawSpecsWithoutFileOwners` results in a rule graph cycle.
244
    targets = await resolve_targets(
1✔
245
        **implicitly(RawSpecsWithoutFileOwners.from_raw_specs(bsp_target.specs))
246
    )
247
    if bsp_target.definition.resolve_filter is None:
1✔
UNCOV
248
        return targets
×
249

250
    resolve_filter = bsp_target.definition.resolve_filter
1✔
251
    resolve_prefix, matched, resolve_value = resolve_filter.partition(":")
1✔
252
    if not resolve_prefix or not matched:
1✔
253
        raise ValueError(
×
254
            f"The `resolve` filter for `{bsp_target}` must have a platform or language specific "
255
            f"prefix like `$lang:$filter`, but the configured value: `{resolve_filter}` did not."
256
        )
257

258
    resolve_fields = {
1✔
259
        impl.resolve_field
260
        for impl in union_membership.get(BSPBuildTargetsMetadataRequest)
261
        if impl.resolve_prefix == resolve_prefix
262
    }
263

264
    return Targets(
1✔
265
        t
266
        for t in targets
267
        if any(
268
            t.has_field(field) and field_defaults.value_or_default(t[field]) == resolve_value
269
            for field in resolve_fields
270
        )
271
    )
272

273

274
@rule
1✔
275
async def resolve_bsp_build_target_source_roots(
1✔
276
    bsp_target: BSPBuildTargetInternal,
277
) -> BSPBuildTargetSourcesInfo:
UNCOV
278
    targets = await resolve_bsp_build_target_addresses(bsp_target, **implicitly())
×
UNCOV
279
    targets_with_sources = [tgt for tgt in targets if tgt.has_field(SourcesField)]
×
UNCOV
280
    sources_paths = await concurrently(
×
281
        resolve_source_paths(SourcesPathsRequest(tgt[SourcesField]), **implicitly())
282
        for tgt in targets_with_sources
283
    )
UNCOV
284
    merged_source_files: set[str] = set()
×
UNCOV
285
    for sp in sources_paths:
×
UNCOV
286
        merged_source_files.update(sp.files)
×
UNCOV
287
    source_roots_result = await get_source_roots(SourceRootsRequest.for_files(merged_source_files))
×
UNCOV
288
    source_root_paths = {x.path for x in source_roots_result.path_to_root.values()}
×
UNCOV
289
    return BSPBuildTargetSourcesInfo(
×
290
        source_files=frozenset(merged_source_files),
291
        source_roots=frozenset(source_root_paths),
292
    )
293

294

295
# -----------------------------------------------------------------------------------------------
296
# Workspace Build Targets Request
297
# See https://build-server-protocol.github.io/docs/specification.html#workspace-build-targets-request
298
# -----------------------------------------------------------------------------------------------
299

300

301
class WorkspaceBuildTargetsHandlerMapping(BSPHandlerMapping):
1✔
302
    method_name = "workspace/buildTargets"
1✔
303
    request_type = WorkspaceBuildTargetsParams
1✔
304
    response_type = WorkspaceBuildTargetsResult
1✔
305

306

307
@dataclass(frozen=True)
1✔
308
class GenerateOneBSPBuildTargetRequest:
1✔
309
    bsp_target: BSPBuildTargetInternal
1✔
310

311

312
@dataclass(frozen=True)
1✔
313
class GenerateOneBSPBuildTargetResult:
1✔
314
    build_target: BuildTarget
1✔
315
    digest: Digest = EMPTY_DIGEST
1✔
316

317

318
def merge_metadata(
1✔
319
    metadata_results_by_request_type: Sequence[
320
        tuple[type[BSPBuildTargetsMetadataRequest], BSPBuildTargetsMetadataResult]
321
    ],
322
) -> BSPData | None:
UNCOV
323
    if not metadata_results_by_request_type:
×
324
        return None
×
UNCOV
325
    if len(metadata_results_by_request_type) == 1:
×
326
        return metadata_results_by_request_type[0][1].metadata
×
327

328
    # Naive algorithm (since we only support Java and Scala backends), find the metadata request type that cannot
329
    # merge from another and use that one.
UNCOV
330
    if len(metadata_results_by_request_type) != 2:
×
331
        raise AssertionError(
×
332
            "BSP core rules only support naive ordering of language-backend metadata. Contact Pants developers."
333
        )
UNCOV
334
    if not metadata_results_by_request_type[0][0].can_merge_metadata_from:
×
335
        metadata_index = 1
×
UNCOV
336
    elif not metadata_results_by_request_type[1][0].can_merge_metadata_from:
×
UNCOV
337
        metadata_index = 0
×
338
    else:
339
        raise AssertionError(
×
340
            "BSP core rules only support naive ordering of language-backend metadata. Contact Pants developers."
341
        )
342

343
    # Pretend to merge the metadata into a single piece of metadata, but really just choose the metadata
344
    # from the selected provider.
UNCOV
345
    return metadata_results_by_request_type[metadata_index][1].metadata
×
346

347

348
@rule
1✔
349
async def generate_one_bsp_build_target_request(
1✔
350
    request: GenerateOneBSPBuildTargetRequest,
351
    union_membership: UnionMembership,
352
    build_root: BuildRoot,
353
) -> GenerateOneBSPBuildTargetResult:
354
    # Find all Pants targets that are part of this BSP build target.
UNCOV
355
    targets = await resolve_bsp_build_target_addresses(request.bsp_target, **implicitly())
×
356

357
    # Determine whether the targets are compilable.
UNCOV
358
    can_compile = any(
×
359
        req_type.field_set_type.is_applicable(t)  # type: ignore[misc]
360
        for req_type in union_membership[BSPCompileRequest]
361
        for t in targets
362
    )
363

364
    # Classify the targets by the language backends that claim to provide metadata for them.
UNCOV
365
    field_sets_by_request_type: dict[type[BSPBuildTargetsMetadataRequest], OrderedSet[FieldSet]] = (
×
366
        defaultdict(OrderedSet)
367
    )
UNCOV
368
    metadata_request_types: Sequence[type[BSPBuildTargetsMetadataRequest]] = union_membership.get(
×
369
        BSPBuildTargetsMetadataRequest
370
    )
UNCOV
371
    metadata_request_types_by_lang_id: dict[str, type[BSPBuildTargetsMetadataRequest]] = {}
×
UNCOV
372
    for metadata_request_type in metadata_request_types:
×
UNCOV
373
        previous = metadata_request_types_by_lang_id.get(metadata_request_type.language_id)
×
UNCOV
374
        if previous:
×
375
            raise ValueError(
×
376
                f"Multiple implementations claim to support `{metadata_request_type.language_id}`:"
377
                f"{bullet_list([previous.__name__, metadata_request_type.__name__])}"
378
                "\n"
379
                "Do you have conflicting language support backends enabled?"
380
            )
UNCOV
381
        metadata_request_types_by_lang_id[metadata_request_type.language_id] = metadata_request_type
×
382

UNCOV
383
    for tgt in targets:
×
UNCOV
384
        for metadata_request_type in metadata_request_types:
×
UNCOV
385
            field_set_type: type[FieldSet] = metadata_request_type.field_set_type
×
UNCOV
386
            if field_set_type.is_applicable(tgt):
×
UNCOV
387
                field_sets_by_request_type[metadata_request_type].add(field_set_type.create(tgt))
×
388

389
    # Request each language backend to provide metadata for the BuildTarget, and then merge it.
UNCOV
390
    metadata_results = await concurrently(
×
391
        get_bsp_build_targets_metadata(
392
            **implicitly(
393
                {request_type(field_sets=tuple(field_sets)): BSPBuildTargetsMetadataRequest}
394
            )
395
        )
396
        for request_type, field_sets in field_sets_by_request_type.items()
397
    )
UNCOV
398
    metadata = merge_metadata(list(zip(field_sets_by_request_type.keys(), metadata_results)))
×
399

UNCOV
400
    digest = await merge_digests(MergeDigests([r.digest for r in metadata_results]))
×
401

402
    # Determine "base directory" for this build target using source roots.
403
    # TODO: This actually has nothing to do with source roots. It should probably be computed as an ancestor
404
    # directory or else be configurable by the user. It is used as a hint in IntelliJ for where to place the
405
    # corresponding IntelliJ module.
UNCOV
406
    source_info = await resolve_bsp_build_target_source_roots(request.bsp_target)
×
UNCOV
407
    if source_info.source_roots:
×
UNCOV
408
        roots = [build_root.pathlib_path.joinpath(p) for p in source_info.source_roots]
×
409
    else:
410
        roots = []
×
411

UNCOV
412
    base_directory: Path | None = None
×
UNCOV
413
    if request.bsp_target.definition.base_directory:
×
414
        base_directory = build_root.pathlib_path.joinpath(
×
415
            request.bsp_target.definition.base_directory
416
        )
UNCOV
417
    elif roots:
×
UNCOV
418
        base_directory = roots[0]
×
419

UNCOV
420
    return GenerateOneBSPBuildTargetResult(
×
421
        build_target=BuildTarget(
422
            id=BuildTargetIdentifier(f"pants:{request.bsp_target.name}"),
423
            display_name=request.bsp_target.name,
424
            base_directory=base_directory.as_uri() if base_directory else None,
425
            tags=(),
426
            capabilities=BuildTargetCapabilities(
427
                can_compile=can_compile,
428
                can_debug=False,
429
                # TODO: See https://github.com/pantsbuild/pants/issues/15050.
430
                can_run=False,
431
                can_test=False,
432
            ),
433
            language_ids=tuple(sorted(req.language_id for req in field_sets_by_request_type)),
434
            dependencies=(),
435
            data=metadata,
436
        ),
437
        digest=digest,
438
    )
439

440

441
@_uncacheable_rule
1✔
442
async def bsp_workspace_build_targets(
1✔
443
    _: WorkspaceBuildTargetsParams,
444
    bsp_build_targets: BSPBuildTargets,
445
    workspace: Workspace,
446
) -> WorkspaceBuildTargetsResult:
UNCOV
447
    bsp_target_results = await concurrently(
×
448
        generate_one_bsp_build_target_request(
449
            GenerateOneBSPBuildTargetRequest(target_internal), **implicitly()
450
        )
451
        for target_internal in bsp_build_targets.targets_mapping.values()
452
    )
UNCOV
453
    digest = await merge_digests(MergeDigests([r.digest for r in bsp_target_results]))
×
UNCOV
454
    if digest != EMPTY_DIGEST:
×
UNCOV
455
        workspace.write_digest(digest, path_prefix=".pants.d/bsp")
×
456

UNCOV
457
    return WorkspaceBuildTargetsResult(
×
458
        targets=tuple(r.build_target for r in bsp_target_results),
459
    )
460

461

462
# -----------------------------------------------------------------------------------------------
463
# Build Target Sources Request
464
# See https://build-server-protocol.github.io/docs/specification.html#build-target-sources-request
465
# -----------------------------------------------------------------------------------------------
466

467

468
class BuildTargetSourcesHandlerMapping(BSPHandlerMapping):
1✔
469
    method_name = "buildTarget/sources"
1✔
470
    request_type = SourcesParams
1✔
471
    response_type = SourcesResult
1✔
472

473

474
@dataclass(frozen=True)
1✔
475
class MaterializeBuildTargetSourcesRequest:
1✔
476
    bsp_target_id: BuildTargetIdentifier
1✔
477

478

479
@dataclass(frozen=True)
1✔
480
class MaterializeBuildTargetSourcesResult:
1✔
481
    sources_item: SourcesItem
1✔
482

483

484
@rule
1✔
485
async def materialize_bsp_build_target_sources(
1✔
486
    request: MaterializeBuildTargetSourcesRequest,
487
    build_root: BuildRoot,
488
) -> MaterializeBuildTargetSourcesResult:
UNCOV
489
    bsp_target = await resolve_bsp_build_target_identifier(request.bsp_target_id, **implicitly())
×
UNCOV
490
    source_info = await resolve_bsp_build_target_source_roots(bsp_target)
×
491

UNCOV
492
    if source_info.source_roots:
×
UNCOV
493
        roots = [build_root.pathlib_path.joinpath(p) for p in source_info.source_roots]
×
494
    else:
495
        roots = [build_root.pathlib_path]
×
496

UNCOV
497
    sources_item = SourcesItem(
×
498
        target=request.bsp_target_id,
499
        sources=tuple(
500
            SourceItem(
501
                uri=build_root.pathlib_path.joinpath(filename).as_uri(),
502
                kind=SourceItemKind.FILE,
503
                generated=False,
504
            )
505
            for filename in sorted(source_info.source_files)
506
        ),
507
        roots=tuple(r.as_uri() for r in roots),
508
    )
509

UNCOV
510
    return MaterializeBuildTargetSourcesResult(sources_item)
×
511

512

513
@rule
1✔
514
async def bsp_build_target_sources(request: SourcesParams) -> SourcesResult:
1✔
UNCOV
515
    sources_items = await concurrently(
×
516
        materialize_bsp_build_target_sources(
517
            MaterializeBuildTargetSourcesRequest(btgt), **implicitly()
518
        )
519
        for btgt in request.targets
520
    )
UNCOV
521
    return SourcesResult(items=tuple(si.sources_item for si in sources_items))
×
522

523

524
# -----------------------------------------------------------------------------------------------
525
# Dependency Sources Request
526
# See https://build-server-protocol.github.io/docs/specification.html#dependency-sources-request
527
# -----------------------------------------------------------------------------------------------
528

529

530
class DependencySourcesHandlerMapping(BSPHandlerMapping):
1✔
531
    method_name = "buildTarget/dependencySources"
1✔
532
    request_type = DependencySourcesParams
1✔
533
    response_type = DependencySourcesResult
1✔
534

535

536
@rule
1✔
537
async def bsp_dependency_sources(request: DependencySourcesParams) -> DependencySourcesResult:
1✔
538
    # TODO: This is a stub.
UNCOV
539
    return DependencySourcesResult(
×
540
        tuple(DependencySourcesItem(target=tgt, sources=()) for tgt in request.targets)
541
    )
542

543

544
# -----------------------------------------------------------------------------------------------
545
# Dependency Modules Request
546
# See https://build-server-protocol.github.io/docs/specification.html#dependency-modules-request
547
# -----------------------------------------------------------------------------------------------
548

549

550
@union(in_scope_types=[EnvironmentName])
1✔
551
@dataclass(frozen=True)
1✔
552
class BSPDependencyModulesRequest(Generic[_FS]):
1✔
553
    """Hook to allow language backends to provide dependency modules."""
554

555
    field_set_type: ClassVar[type[_FS]]
1✔
556

557
    field_sets: tuple[_FS, ...]
1✔
558

559

560
@dataclass(frozen=True)
1✔
561
class BSPDependencyModulesResult:
1✔
562
    modules: tuple[DependencyModule, ...]
1✔
563
    digest: Digest = EMPTY_DIGEST
1✔
564

565

566
@rule(polymorphic=True)
1✔
567
async def get_bsp_dependency_modules(
1✔
568
    req: BSPDependencyModulesRequest, env_name: EnvironmentName
569
) -> BSPDependencyModulesResult:
570
    raise NotImplementedError()
×
571

572

573
class DependencyModulesHandlerMapping(BSPHandlerMapping):
1✔
574
    method_name = "buildTarget/dependencyModules"
1✔
575
    request_type = DependencyModulesParams
1✔
576
    response_type = DependencyModulesResult
1✔
577

578

579
@dataclass(frozen=True)
1✔
580
class ResolveOneDependencyModuleRequest:
1✔
581
    bsp_target_id: BuildTargetIdentifier
1✔
582

583

584
@dataclass(frozen=True)
1✔
585
class ResolveOneDependencyModuleResult:
1✔
586
    bsp_target_id: BuildTargetIdentifier
1✔
587
    modules: tuple[DependencyModule, ...] = ()
1✔
588
    digest: Digest = EMPTY_DIGEST
1✔
589

590

591
@rule
1✔
592
async def resolve_one_dependency_module(
1✔
593
    request: ResolveOneDependencyModuleRequest,
594
    union_membership: UnionMembership,
595
) -> ResolveOneDependencyModuleResult:
596
    targets = await resolve_bsp_build_target_addresses(**implicitly(request.bsp_target_id))
×
597

598
    field_sets_by_request_type: dict[type[BSPDependencyModulesRequest], list[FieldSet]] = (
×
599
        defaultdict(list)
600
    )
601
    dep_module_request_types: Sequence[type[BSPDependencyModulesRequest]] = union_membership.get(
×
602
        BSPDependencyModulesRequest
603
    )
604
    for tgt in targets:
×
605
        for dep_module_request_type in dep_module_request_types:
×
606
            field_set_type = dep_module_request_type.field_set_type
×
607
            if field_set_type.is_applicable(tgt):
×
608
                field_set = field_set_type.create(tgt)
×
609
                field_sets_by_request_type[dep_module_request_type].append(field_set)
×
610

611
    if not field_sets_by_request_type:
×
612
        return ResolveOneDependencyModuleResult(bsp_target_id=request.bsp_target_id)
×
613

614
    responses = await concurrently(
×
615
        get_bsp_dependency_modules(
616
            **implicitly(
617
                {dep_module_request_type(field_sets=tuple(field_sets)): BSPDependencyModulesRequest}
618
            )
619
        )
620
        for dep_module_request_type, field_sets in field_sets_by_request_type.items()
621
    )
622

623
    modules = set(itertools.chain.from_iterable([r.modules for r in responses]))
×
624
    digest = await merge_digests(MergeDigests([r.digest for r in responses]))
×
625

626
    return ResolveOneDependencyModuleResult(
×
627
        bsp_target_id=request.bsp_target_id,
628
        modules=tuple(modules),
629
        digest=digest,
630
    )
631

632

633
# Note: VSCode expects this endpoint to exist even if the capability bit for it is set `false`.
634
@_uncacheable_rule
1✔
635
async def bsp_dependency_modules(
1✔
636
    request: DependencyModulesParams, workspace: Workspace
637
) -> DependencyModulesResult:
638
    responses = await concurrently(
×
639
        resolve_one_dependency_module(ResolveOneDependencyModuleRequest(btgt), **implicitly())
640
        for btgt in request.targets
641
    )
642
    output_digest = await merge_digests(MergeDigests([r.digest for r in responses]))
×
643
    workspace.write_digest(output_digest, path_prefix=".pants.d/bsp")
×
644
    return DependencyModulesResult(
×
645
        tuple(DependencyModulesItem(target=r.bsp_target_id, modules=r.modules) for r in responses)
646
    )
647

648

649
# -----------------------------------------------------------------------------------------------
650
# Compile request.
651
# See https://build-server-protocol.github.io/docs/specification.html#compile-request
652
# -----------------------------------------------------------------------------------------------
653

654

655
@union(in_scope_types=[EnvironmentName])
1✔
656
@dataclass(frozen=True)
1✔
657
class BSPCompileRequest(Generic[_FS]):
1✔
658
    """Hook to allow language backends to compile targets."""
659

660
    field_set_type: ClassVar[type[_FS]]
1✔
661

662
    bsp_target: BSPBuildTargetInternal
1✔
663
    field_sets: tuple[_FS, ...]
1✔
664
    task_id: TaskId
1✔
665

666

667
@dataclass(frozen=True)
1✔
668
class BSPCompileResult:
1✔
669
    """Result of compilation of a target capable of target compilation."""
670

671
    status: StatusCode
1✔
672
    output_digest: Digest
1✔
673

674

675
@rule(polymorphic=True)
1✔
676
async def bsp_compile(req: BSPCompileRequest, env_name: EnvironmentName) -> BSPCompileResult:
1✔
677
    raise NotImplementedError()
×
678

679

680
# -----------------------------------------------------------------------------------------------
681
# Resources request.
682
# See https://build-server-protocol.github.io/docs/specification.html#resources-request
683
#
684
# NB: This method is used only for the _indexing_ of resources, and not to add them to the
685
# classpath (in the case of JVM targets). BSPCompileRequest implementations need to handle
686
# movement of resources to accessible classpath entries.
687
# -----------------------------------------------------------------------------------------------
688

689

690
@union(in_scope_types=[EnvironmentName])
1✔
691
@dataclass(frozen=True)
1✔
692
class BSPResourcesRequest(Generic[_FS]):
1✔
693
    """Hook to allow language backends to provide resources for targets."""
694

695
    field_set_type: ClassVar[type[_FS]]
1✔
696

697
    bsp_target: BSPBuildTargetInternal
1✔
698
    field_sets: tuple[_FS, ...]
1✔
699

700

701
@dataclass(frozen=True)
1✔
702
class BSPResourcesResult:
1✔
703
    """Resources for a target."""
704

705
    resources: tuple[Uri, ...]
1✔
706
    output_digest: Digest
1✔
707

708

709
@rule(polymorphic=True)
1✔
710
async def get_bsp_resources(
1✔
711
    req: BSPResourcesRequest, env_name: EnvironmentName
712
) -> BSPResourcesResult:
713
    raise NotImplementedError()
×
714

715

716
def rules():
1✔
717
    return (
1✔
718
        *collect_rules(),
719
        UnionRule(BSPHandlerMapping, WorkspaceBuildTargetsHandlerMapping),
720
        UnionRule(BSPHandlerMapping, BuildTargetSourcesHandlerMapping),
721
        UnionRule(BSPHandlerMapping, DependencySourcesHandlerMapping),
722
        UnionRule(BSPHandlerMapping, DependencyModulesHandlerMapping),
723
    )
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