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

pantsbuild / pants / 26080722777

19 May 2026 06:37AM UTC coverage: 52.106% (-11.5%) from 63.597%
26080722777

Pull #23250

github

web-flow
Merge 63ec06323 into 2693df832
Pull Request #23250: Feature: Add generic option to docker image

12 of 50 new or added lines in 3 files covered. (24.0%)

5382 existing lines in 201 files now uncovered.

32053 of 61515 relevant lines covered (52.11%)

1.04 hits per line

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

71.04
/src/python/pants/engine/internals/build_files.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
2✔
5

6
import ast
2✔
7
import builtins
2✔
8
import itertools
2✔
9
import logging
2✔
10
import os.path
2✔
11
import sys
2✔
12
import typing
2✔
13
from collections import defaultdict
2✔
14
from collections.abc import Coroutine, Sequence
2✔
15
from dataclasses import dataclass
2✔
16
from pathlib import PurePath
2✔
17
from typing import Any, cast
2✔
18

19
import typing_extensions
2✔
20

21
from pants.build_graph.address import (
2✔
22
    Address,
23
    AddressInput,
24
    BuildFileAddress,
25
    BuildFileAddressRequest,
26
    MaybeAddress,
27
    ResolveError,
28
)
29
from pants.core.util_rules.env_vars import environment_vars_subset
2✔
30
from pants.engine.engine_aware import EngineAwareParameter
2✔
31
from pants.engine.env_vars import CompleteEnvironmentVars, EnvironmentVars, EnvironmentVarsRequest
2✔
32
from pants.engine.fs import FileContent, GlobMatchErrorBehavior, PathGlobs
2✔
33
from pants.engine.internals.defaults import BuildFileDefaults, BuildFileDefaultsParserState
2✔
34
from pants.engine.internals.dep_rules import (
2✔
35
    BuildFileDependencyRules,
36
    DependencyRuleApplication,
37
    MaybeBuildFileDependencyRulesImplementation,
38
)
39
from pants.engine.internals.mapper import (
2✔
40
    DELETED_SPEC_PATH,
41
    AddressFamily,
42
    AddressMap,
43
    DuplicateNameError,
44
)
45
from pants.engine.internals.parser import (
2✔
46
    BuildFilePreludeSymbols,
47
    BuildFileSymbolsInfo,
48
    Parser,
49
    error_on_imports,
50
)
51
from pants.engine.internals.selectors import concurrently
2✔
52
from pants.engine.internals.session import SessionValues
2✔
53
from pants.engine.internals.synthetic_targets import (
2✔
54
    SyntheticAddressMapsRequest,
55
    get_synthetic_address_maps,
56
)
57
from pants.engine.internals.target_adaptor import TargetAdaptor, TargetAdaptorRequest
2✔
58
from pants.engine.intrinsics import get_digest_contents, path_globs_to_paths
2✔
59
from pants.engine.rules import QueryRule, collect_rules, implicitly, rule
2✔
60
from pants.engine.target import (
2✔
61
    DependenciesRuleApplication,
62
    DependenciesRuleApplicationRequest,
63
    InvalidTargetException,
64
    RegisteredTargetTypes,
65
)
66
from pants.engine.unions import UnionMembership
2✔
67
from pants.init.bootstrap_scheduler import BootstrapStatus
2✔
68
from pants.option.global_options import GlobalOptions
2✔
69
from pants.util.frozendict import FrozenDict
2✔
70
from pants.util.strutil import softwrap
2✔
71

72
logger = logging.getLogger(__name__)
2✔
73

74

75
# Singleton for use as a reference to the DeletedTarget pseudo-target.
76
# A backend can inject a dependency on DELETED_ADDRESS if it is capable of detecting
77
# (via naming conventions) that a file depends on some other, deleted file.
78
# The generic change detection mechanism will use that dependency to include the
79
# files in the transitive closure of dependents of changed files.
80
# This is somewhat limited - the backend doesn't get the content of the deleted file,
81
# and so cannot use it in the regular inference pipeline.
82
#
83
# This is not the most elegant solution, but the legacy design of dep inference and of
84
# change detection does not admit a simple one.
85
#
86
# A more global solution, that doesn't rely on backend specifics, would be to capture the
87
# state of the repo before and after the changes and apply dep inference to both.
88
# However that is a very substantial change from the status quo, so for now this
89
# incremental improvement will suffice.
90
DELETED_ADDRESS = Address(DELETED_SPEC_PATH)
2✔
91

92

93
class BuildFileSyntaxError(SyntaxError):
2✔
94
    """An error parsing a BUILD file."""
95

96
    def from_syntax_error(error: SyntaxError) -> BuildFileSyntaxError:
2✔
UNCOV
97
        return BuildFileSyntaxError(
×
98
            error.msg,
99
            (
100
                error.filename,
101
                error.lineno,
102
                error.offset,
103
                error.text,
104
            ),
105
        )
106

107
    def __str__(self) -> str:
2✔
UNCOV
108
        first_line = f"Error parsing BUILD file {self.filename}:{self.lineno}: {self.msg}"
×
109
        # These two fields are optional per the spec, so we can't rely on them being set.
UNCOV
110
        if self.text is not None and self.offset is not None:
×
UNCOV
111
            second_line = f"  {self.text.rstrip()}"
×
UNCOV
112
            third_line = f"  {' ' * (self.offset - 1)}^"
×
UNCOV
113
            return f"{first_line}\n{second_line}\n{third_line}"
×
114

115
        return first_line
×
116

117

118
@dataclass(frozen=True)
2✔
119
class BuildFileOptions:
2✔
120
    patterns: tuple[str, ...]
2✔
121
    ignores: tuple[str, ...] = ()
2✔
122
    prelude_globs: tuple[str, ...] = ()
2✔
123

124

125
@rule
2✔
126
async def extract_build_file_options(
2✔
127
    global_options: GlobalOptions,
128
    bootstrap_status: BootstrapStatus,
129
) -> BuildFileOptions:
130
    return BuildFileOptions(
2✔
131
        patterns=global_options.build_patterns,
132
        ignores=global_options.build_ignore,
133
        prelude_globs=(
134
            () if bootstrap_status.in_progress else global_options.build_file_prelude_globs
135
        ),
136
    )
137

138

139
@rule(desc="Expand macros")
2✔
140
async def evaluate_preludes(
2✔
141
    build_file_options: BuildFileOptions,
142
    parser: Parser,
143
) -> BuildFilePreludeSymbols:
144
    prelude_digest_contents = await get_digest_contents(
2✔
145
        **implicitly(
146
            PathGlobs(
147
                build_file_options.prelude_globs,
148
                glob_match_error_behavior=GlobMatchErrorBehavior.ignore,
149
            )
150
        )
151
    )
152
    globals: dict[str, Any] = {
2✔
153
        # Later entries have precendence replacing conflicting keys from previous entries, so we
154
        # start with typing_extensions as the lowest prio source for global values.
155
        **{name: getattr(typing_extensions, name) for name in typing_extensions.__all__},
156
        **{name: getattr(typing, name) for name in typing.__all__},
157
        **{name: getattr(builtins, name) for name in dir(builtins) if name.endswith("Error")},
158
        # Ensure the globals for each prelude includes the builtin symbols (E.g. `python_sources`)
159
        # and any build file aliases (e.g. from plugins)
160
        **parser.symbols,
161
    }
162
    locals: dict[str, Any] = {}
2✔
163
    env_vars: set[str] = set()
2✔
164
    for file_content in prelude_digest_contents:
2✔
UNCOV
165
        try:
×
UNCOV
166
            file_content_str = file_content.content.decode()
×
UNCOV
167
            content = compile(file_content_str, file_content.path, "exec", dont_inherit=True)
×
UNCOV
168
            exec(content, globals, locals)
×
UNCOV
169
        except Exception as e:
×
UNCOV
170
            raise Exception(f"Error parsing prelude file {file_content.path}: {e}")
×
UNCOV
171
        error_on_imports(file_content_str, file_content.path)
×
UNCOV
172
        env_vars.update(BUILDFileEnvVarExtractor.get_env_vars(file_content))
×
173
    # __builtins__ is a dict, so isn't hashable, and can't be put in a FrozenDict.
174
    # Fortunately, we don't care about it - preludes should not be able to override builtins, so we just pop it out.
175
    # TODO: Give a nice error message if a prelude tries to set and expose a non-hashable value.
176
    locals.pop("__builtins__", None)
2✔
177
    # Ensure preludes can reference each other by populating the shared globals object with references
178
    # to the other symbols
179
    globals.update(locals)
2✔
180
    return BuildFilePreludeSymbols.create(locals, env_vars)
2✔
181

182

183
@rule
2✔
184
async def get_all_build_file_symbols_info(
2✔
185
    parser: Parser, prelude_symbols: BuildFilePreludeSymbols
186
) -> BuildFileSymbolsInfo:
187
    return BuildFileSymbolsInfo.from_info(
×
188
        parser.symbols_info.info.values(), prelude_symbols.info.values()
189
    )
190

191

192
@rule
2✔
193
async def maybe_resolve_address(address_input: AddressInput) -> MaybeAddress:
2✔
194
    if address_input.path_component == DELETED_ADDRESS.spec_path:
2✔
195
        return MaybeAddress(DELETED_ADDRESS)
×
196
    # Determine the type of the path_component of the input.
197
    if address_input.path_component:
2✔
198
        paths = await path_globs_to_paths(PathGlobs(globs=(address_input.path_component,)))
2✔
199
        is_file, is_dir = bool(paths.files), bool(paths.dirs)
2✔
200
    else:
201
        # It is an address in the root directory.
202
        is_file, is_dir = False, True
2✔
203

204
    if is_file:
2✔
205
        return MaybeAddress(address_input.file_to_address())
2✔
206
    if is_dir:
2✔
207
        return MaybeAddress(address_input.dir_to_address())
2✔
UNCOV
208
    spec = address_input.path_component
×
UNCOV
209
    if address_input.target_component:
×
210
        spec += f":{address_input.target_component}"
×
UNCOV
211
    return MaybeAddress(
×
212
        ResolveError(
213
            softwrap(
214
                f"""
215
                The file or directory '{address_input.path_component}' does not exist on disk in
216
                the workspace, so the address '{spec}' from {address_input.description_of_origin}
217
                cannot be resolved.
218
                """
219
            )
220
        )
221
    )
222

223

224
@rule
2✔
225
async def resolve_address(maybe_address: MaybeAddress) -> Address:
2✔
226
    if isinstance(maybe_address.val, ResolveError):
2✔
UNCOV
227
        raise maybe_address.val
×
228
    return maybe_address.val
2✔
229

230

231
@dataclass(frozen=True)
2✔
232
class AddressFamilyDir(EngineAwareParameter):
2✔
233
    """The directory to find addresses for.
234

235
    This does _not_ recurse into subdirectories.
236
    """
237

238
    path: str
2✔
239

240
    def debug_hint(self) -> str:
2✔
241
        return self.path
2✔
242

243

244
@dataclass(frozen=True)
2✔
245
class OptionalAddressFamily:
2✔
246
    path: str
2✔
247
    address_family: AddressFamily | None = None
2✔
248

249
    def ensure(self) -> AddressFamily:
2✔
250
        if self.address_family is not None:
2✔
251
            return self.address_family
2✔
UNCOV
252
        raise ResolveError(f"Directory '{self.path}' does not contain any BUILD files.")
×
253

254

255
_DELETED_OPTIONAL_ADDRESS_FAMILY = OptionalAddressFamily(
2✔
256
    DELETED_ADDRESS.spec_path, AddressFamily.create(DELETED_ADDRESS.spec_path, [])
257
)
258

259

260
@rule
2✔
261
async def ensure_address_family(request: OptionalAddressFamily) -> AddressFamily:
2✔
262
    return request.ensure()
2✔
263

264

265
class BUILDFileEnvVarExtractor(ast.NodeVisitor):
2✔
266
    def __init__(self, filename: str):
2✔
267
        super().__init__()
2✔
268
        self.env_vars: set[str] = set()
2✔
269
        self.filename = filename
2✔
270

271
    @classmethod
2✔
272
    def get_env_vars(cls, file_content: FileContent) -> Sequence[str]:
2✔
273
        obj = cls(file_content.path)
2✔
274
        try:
2✔
275
            obj.visit(ast.parse(file_content.content, file_content.path))
2✔
UNCOV
276
        except SyntaxError as e:
×
UNCOV
277
            raise BuildFileSyntaxError.from_syntax_error(e).with_traceback(e.__traceback__)
×
278

279
        return tuple(obj.env_vars)
2✔
280

281
    def visit_Call(self, node: ast.Call):
2✔
282
        is_env = isinstance(node.func, ast.Name) and node.func.id == "env"
2✔
283
        for arg in node.args:
2✔
UNCOV
284
            if not is_env:
×
UNCOV
285
                self.visit(arg)
×
UNCOV
286
                continue
×
287

288
            # Only first arg may be checked as env name
UNCOV
289
            is_env = False
×
290

UNCOV
291
            if sys.version_info[0:2] < (3, 8):
×
292
                value = arg.s if isinstance(arg, ast.Str) else None
×
293
            else:
UNCOV
294
                value = arg.value if isinstance(arg, ast.Constant) else None
×
UNCOV
295
            if value:
×
296
                # Found env name in this call, we're done here.
UNCOV
297
                self.env_vars.add(value)  # type: ignore[arg-type]
×
UNCOV
298
                return
×
299
            else:
UNCOV
300
                logger.warning(
×
301
                    f"{self.filename}:{arg.lineno}: Only constant string values as variable name to "
302
                    f"`env()` is currently supported. This `env()` call will always result in "
303
                    "the default value only."
304
                )
305

306
        for kwarg in node.keywords:
2✔
307
            self.visit(kwarg)
2✔
308

309

310
@rule(desc="Search for addresses in BUILD files")
2✔
311
async def parse_address_family(
2✔
312
    directory: AddressFamilyDir,
313
    parser: Parser,
314
    bootstrap_status: BootstrapStatus,
315
    build_file_options: BuildFileOptions,
316
    prelude_symbols: BuildFilePreludeSymbols,
317
    registered_target_types: RegisteredTargetTypes,
318
    union_membership: UnionMembership,
319
    maybe_build_file_dependency_rules_implementation: MaybeBuildFileDependencyRulesImplementation,
320
    session_values: SessionValues,
321
) -> OptionalAddressFamily:
322
    """Given an AddressMapper and a directory, return an AddressFamily.
323

324
    The AddressFamily may be empty, but it will not be None.
325
    """
326
    if directory.path == _DELETED_OPTIONAL_ADDRESS_FAMILY.path:
2✔
327
        return _DELETED_OPTIONAL_ADDRESS_FAMILY
×
328

329
    digest_contents, all_synthetic_address_maps = await concurrently(
2✔
330
        get_digest_contents(
331
            **implicitly(
332
                PathGlobs(
333
                    globs=(
334
                        *(os.path.join(directory.path, p) for p in build_file_options.patterns),
335
                        *(f"!{p}" for p in build_file_options.ignores),
336
                    )
337
                )
338
            ),
339
        ),
340
        get_synthetic_address_maps(SyntheticAddressMapsRequest(directory.path), **implicitly()),
341
    )
342
    synthetic_address_maps = tuple(itertools.chain(all_synthetic_address_maps))
2✔
343
    if not digest_contents and not synthetic_address_maps:
2✔
344
        return OptionalAddressFamily(directory.path)
2✔
345

346
    defaults = BuildFileDefaults({})
2✔
347
    dependents_rules: BuildFileDependencyRules | None = None
2✔
348
    dependencies_rules: BuildFileDependencyRules | None = None
2✔
349
    parent_dirs = tuple(PurePath(directory.path).parents)
2✔
350
    if parent_dirs:
2✔
351
        maybe_parents = await concurrently(
2✔
352
            parse_address_family(AddressFamilyDir(str(parent_dir)), **implicitly())
353
            for parent_dir in parent_dirs
354
        )
355
        for maybe_parent in maybe_parents:
2✔
356
            if maybe_parent.address_family is not None:
2✔
357
                family = maybe_parent.address_family
2✔
358
                defaults = family.defaults
2✔
359
                dependents_rules = family.dependents_rules
2✔
360
                dependencies_rules = family.dependencies_rules
2✔
361
                break
2✔
362

363
    defaults_parser_state = BuildFileDefaultsParserState.create(
2✔
364
        directory.path, defaults, registered_target_types, union_membership
365
    )
366
    build_file_dependency_rules_class = (
2✔
367
        maybe_build_file_dependency_rules_implementation.build_file_dependency_rules_class
368
    )
369
    if build_file_dependency_rules_class is not None:
2✔
370
        dependents_rules_parser_state = build_file_dependency_rules_class.create_parser_state(
×
371
            directory.path,
372
            dependents_rules,
373
        )
374
        dependencies_rules_parser_state = build_file_dependency_rules_class.create_parser_state(
×
375
            directory.path,
376
            dependencies_rules,
377
        )
378
    else:
379
        dependents_rules_parser_state = None
2✔
380
        dependencies_rules_parser_state = None
2✔
381

382
    def _extract_env_vars(
2✔
383
        file_content: FileContent, extra_env: Sequence[str], env: CompleteEnvironmentVars
384
    ) -> Coroutine[Any, Any, EnvironmentVars]:
385
        """For BUILD file env vars, we only ever consult the local systems env."""
386
        env_vars = (*BUILDFileEnvVarExtractor.get_env_vars(file_content), *extra_env)
2✔
387
        return environment_vars_subset(EnvironmentVarsRequest(env_vars), env)
2✔
388

389
    all_env_vars = await concurrently(
2✔
390
        _extract_env_vars(
391
            fc, prelude_symbols.referenced_env_vars, session_values[CompleteEnvironmentVars]
392
        )
393
        for fc in digest_contents
394
    )
395

396
    declared_address_maps = [
2✔
397
        AddressMap.parse(
398
            fc.path,
399
            fc.content.decode(),
400
            parser,
401
            prelude_symbols,
402
            env_vars,
403
            bootstrap_status.in_progress,
404
            defaults_parser_state,
405
            dependents_rules_parser_state,
406
            dependencies_rules_parser_state,
407
        )
408
        for fc, env_vars in zip(digest_contents, all_env_vars)
409
    ]
410
    declared_address_maps.sort(key=lambda x: x.path)
2✔
411

412
    # Freeze defaults and dependency rules
413
    frozen_defaults = defaults_parser_state.get_frozen_defaults()
2✔
414
    frozen_dependents_rules = cast(
2✔
415
        "BuildFileDependencyRules | None",
416
        dependents_rules_parser_state
417
        and dependents_rules_parser_state.get_frozen_dependency_rules(),
418
    )
419
    frozen_dependencies_rules = cast(
2✔
420
        "BuildFileDependencyRules | None",
421
        dependencies_rules_parser_state
422
        and dependencies_rules_parser_state.get_frozen_dependency_rules(),
423
    )
424

425
    # Process synthetic targets.
426

427
    def apply_defaults(tgt: TargetAdaptor) -> TargetAdaptor:
2✔
UNCOV
428
        default_values = frozen_defaults.get(tgt.type_alias)
×
UNCOV
429
        if default_values is None:
×
UNCOV
430
            return tgt
×
UNCOV
431
        return tgt.with_new_kwargs(**{**default_values, **tgt.kwargs})
×
432

433
    name_to_path_and_synthetic_target: dict[str, tuple[str, TargetAdaptor]] = {}
2✔
434
    for synthetic_address_map in synthetic_address_maps:
2✔
UNCOV
435
        for name, target in synthetic_address_map.name_to_target_adaptor.items():
×
UNCOV
436
            name_to_path_and_synthetic_target[name] = (
×
437
                synthetic_address_map.path,
438
                apply_defaults(target),
439
            )
440

441
    name_to_path_and_declared_target: dict[str, tuple[str, TargetAdaptor]] = {}
2✔
442
    for declared_address_map in declared_address_maps:
2✔
443
        for name, target in declared_address_map.name_to_target_adaptor.items():
2✔
444
            if name in name_to_path_and_declared_target:
2✔
445
                # This is a duplicate declared name, raise an exception.
UNCOV
446
                duplicate_path = name_to_path_and_declared_target[name][0]
×
UNCOV
447
                raise DuplicateNameError(
×
448
                    f"A target already exists at `{duplicate_path}` with name `{name}` and target type "
449
                    f"`{target.type_alias}`. The `{name}` target in `{declared_address_map.path}` "
450
                    "cannot use the same name."
451
                )
452

453
            name_to_path_and_declared_target[name] = (declared_address_map.path, target)
2✔
454

455
    # We copy the dict so we can modify the original in the loop.
456
    for name, (
2✔
457
        declared_target_path,
458
        declared_target,
459
    ) in name_to_path_and_declared_target.copy().items():
460
        # Pop the synthetic target to let the declared target take precedence.
461
        synthetic_target_path, synthetic_target = name_to_path_and_synthetic_target.pop(
2✔
462
            name, (None, None)
463
        )
464
        if "_extend_synthetic" not in declared_target.kwargs:
2✔
465
            # The explicitly declared target should replace the synthetic one.
466
            continue
2✔
467

468
        # The _extend_synthetic kwarg was explicitly provided, so we must strip it.
UNCOV
469
        declared_target_kwargs = dict(declared_target.kwargs)
×
UNCOV
470
        extend_synthetic = declared_target_kwargs.pop("_extend_synthetic")
×
UNCOV
471
        if extend_synthetic:
×
UNCOV
472
            if synthetic_target is None:
×
UNCOV
473
                raise InvalidTargetException(
×
474
                    softwrap(
475
                        f"""
476
                            The `{declared_target.type_alias}` target {name!r} in {declared_target_path} has
477
                            `_extend_synthetic=True` but there is no synthetic target to extend.
478
                            """
479
                    )
480
                )
481

UNCOV
482
            if synthetic_target.type_alias != declared_target.type_alias:
×
UNCOV
483
                raise InvalidTargetException(
×
484
                    softwrap(
485
                        f"""
486
                        The `{declared_target.type_alias}` target {name!r} in {declared_target_path} is
487
                        of a different type than the synthetic target
488
                        `{synthetic_target.type_alias}` from {synthetic_target_path}.
489

490
                        When `_extend_synthetic` is true the target types must match, set this to
491
                        false if you want to replace the synthetic target with the target from your
492
                        BUILD file.
493
                        """
494
                    )
495
                )
496

497
            # Preserve synthetic field values not overriden by the declared target from the BUILD.
UNCOV
498
            kwargs = {**synthetic_target.kwargs, **declared_target_kwargs}
×
499
        else:
500
            kwargs = declared_target_kwargs
×
UNCOV
501
        name_to_path_and_declared_target[name] = (
×
502
            declared_target_path,
503
            declared_target.with_new_kwargs(**kwargs),
504
        )
505

506
    # Now reconstitute into AddressMaps, to pass into AddressFamily.create().
507
    # We no longer need to distinguish between synthetic and declared AddressMaps.
508
    # TODO: We might want to move the validation done by AddressFamily.create() to here, since
509
    #  we're already iterating over the AddressMap data, and simplify AddressFamily.
510
    path_to_targets = defaultdict(list)
2✔
511
    for name_to_path_and_target in [
2✔
512
        name_to_path_and_declared_target,
513
        name_to_path_and_synthetic_target,
514
    ]:
515
        for path_and_target in name_to_path_and_target.values():
2✔
516
            path_to_targets[path_and_target[0]].append(path_and_target[1])
2✔
517
    address_maps = [AddressMap.create(path, targets) for path, targets in path_to_targets.items()]
2✔
518

519
    return OptionalAddressFamily(
2✔
520
        directory.path,
521
        AddressFamily.create(
522
            spec_path=directory.path,
523
            address_maps=address_maps,
524
            defaults=frozen_defaults,
525
            dependents_rules=frozen_dependents_rules,
526
            dependencies_rules=frozen_dependencies_rules,
527
        ),
528
    )
529

530

531
@rule
2✔
532
async def find_build_file(request: BuildFileAddressRequest) -> BuildFileAddress:
2✔
533
    address = request.address
2✔
534
    address_family = await ensure_address_family(**implicitly(AddressFamilyDir(address.spec_path)))
2✔
535
    owning_address = address.maybe_convert_to_target_generator()
2✔
536
    if address_family.get_target_adaptor(owning_address) is None:
2✔
537
        raise ResolveError.did_you_mean(
×
538
            owning_address,
539
            description_of_origin=request.description_of_origin,
540
            known_names=address_family.target_names,
541
            namespace=address_family.namespace,
542
        )
543
    bfa = next(
2✔
544
        build_file_address
545
        for build_file_address in address_family.build_file_addresses
546
        if build_file_address.address == owning_address
547
    )
548
    return BuildFileAddress(address, bfa.rel_path) if address.is_generated_target else bfa
2✔
549

550

551
def _get_target_adaptor(
2✔
552
    address: Address, address_family: AddressFamily, description_of_origin: str
553
) -> TargetAdaptor:
554
    target_adaptor = address_family.get_target_adaptor(address)
2✔
555
    if target_adaptor is None:
2✔
UNCOV
556
        raise ResolveError.did_you_mean(
×
557
            address,
558
            description_of_origin=description_of_origin,
559
            known_names=address_family.target_names,
560
            namespace=address_family.namespace,
561
        )
562
    return target_adaptor
2✔
563

564

565
@rule
2✔
566
async def find_target_adaptor(request: TargetAdaptorRequest) -> TargetAdaptor:
2✔
567
    """Hydrate a TargetAdaptor so that it may be converted into the Target API."""
568
    address = request.address
2✔
569
    if address.is_generated_target:
2✔
570
        raise AssertionError(
×
571
            "Generated targets are not defined in BUILD files, and so do not have "
572
            f"TargetAdaptors: {request}"
573
        )
574
    address_family = await ensure_address_family(**implicitly(AddressFamilyDir(address.spec_path)))
2✔
575
    target_adaptor = _get_target_adaptor(address, address_family, request.description_of_origin)
2✔
576
    return target_adaptor
2✔
577

578

579
def _rules_path(address: Address) -> str:
2✔
580
    if address.is_file_target and os.path.sep in address.relative_file_path:  # type: ignore[operator]
×
581
        # The file is in a subdirectory of spec_path
582
        return os.path.dirname(address.filename)
×
583
    else:
584
        return address.spec_path
×
585

586

587
async def _get_target_family_and_adaptor_for_dep_rules(
2✔
588
    *addresses: Address, description_of_origin: str
589
) -> tuple[tuple[AddressFamily, TargetAdaptor], ...]:
590
    # Fetch up to 2 sets of address families per address, as we want the rules from the directory
591
    # the file is in rather than the directory where the target generator was declared, if not the
592
    # same.
593
    rules_paths = set(
×
594
        itertools.chain.from_iterable(
595
            {address.spec_path, _rules_path(address)} for address in addresses
596
        )
597
    )
598
    maybe_address_families = await concurrently(
×
599
        parse_address_family(AddressFamilyDir(rules_path), **implicitly())
600
        for rules_path in rules_paths
601
    )
602
    maybe_families = {maybe.path: maybe for maybe in maybe_address_families}
×
603

604
    return tuple(
×
605
        (
606
            (
607
                maybe_families[_rules_path(address)].address_family
608
                or maybe_families[address.spec_path].ensure()
609
            ),
610
            _get_target_adaptor(
611
                address,
612
                maybe_families[address.spec_path].ensure(),
613
                description_of_origin,
614
            ),
615
        )
616
        for address in addresses
617
    )
618

619

620
@rule
2✔
621
async def get_dependencies_rule_application(
2✔
622
    request: DependenciesRuleApplicationRequest,
623
    maybe_build_file_rules_implementation: MaybeBuildFileDependencyRulesImplementation,
624
) -> DependenciesRuleApplication:
625
    build_file_dependency_rules_class = (
×
626
        maybe_build_file_rules_implementation.build_file_dependency_rules_class
627
    )
628
    if build_file_dependency_rules_class is None:
×
629
        return DependenciesRuleApplication.allow_all()
×
630

631
    (
×
632
        (
633
            origin_rules_family,
634
            origin_target,
635
        ),
636
        *dependencies_family_adaptor,
637
    ) = await _get_target_family_and_adaptor_for_dep_rules(
638
        request.address,
639
        *request.dependencies,
640
        description_of_origin=request.description_of_origin,
641
    )
642

643
    dependencies_rule: dict[Address, DependencyRuleApplication] = {}
×
644
    for dependency_address, (dependency_rules_family, dependency_target) in zip(
×
645
        request.dependencies, dependencies_family_adaptor
646
    ):
647
        dependencies_rule[dependency_address] = (
×
648
            build_file_dependency_rules_class.check_dependency_rules(
649
                origin_address=request.address,
650
                origin_adaptor=origin_target,
651
                dependencies_rules=origin_rules_family.dependencies_rules,
652
                dependency_address=dependency_address,
653
                dependency_adaptor=dependency_target,
654
                dependents_rules=dependency_rules_family.dependents_rules,
655
            )
656
        )
657
    return DependenciesRuleApplication(request.address, FrozenDict(dependencies_rule))
×
658

659

660
def rules():
2✔
661
    return (
2✔
662
        *collect_rules(),
663
        # The `BuildFileSymbolsInfo` is consumed by the `HelpInfoExtracter` and uses the scheduler
664
        # session `product_request()` directly so we need an explicit QueryRule to provide this type
665
        # as an valid entrypoint into the rule graph.
666
        QueryRule(BuildFileSymbolsInfo, ()),
667
    )
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