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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

0.0
/src/python/pants/jvm/target_types.py
1
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

UNCOV
4
from __future__ import annotations
×
5

UNCOV
6
import dataclasses
×
UNCOV
7
import re
×
UNCOV
8
import xml.etree.ElementTree as ET
×
UNCOV
9
from abc import ABC, ABCMeta, abstractmethod
×
UNCOV
10
from collections.abc import Callable, Iterable, Iterator
×
UNCOV
11
from dataclasses import dataclass
×
UNCOV
12
from typing import ClassVar
×
13

UNCOV
14
from pants.build_graph.build_file_aliases import BuildFileAliases
×
UNCOV
15
from pants.core.goals.generate_lockfiles import UnrecognizedResolveNamesError
×
UNCOV
16
from pants.core.goals.package import OutputPathField
×
UNCOV
17
from pants.core.goals.run import RestartableField, RunFieldSet, RunInSandboxBehavior
×
UNCOV
18
from pants.core.goals.test import TestExtraEnvVarsField, TestTimeoutField
×
UNCOV
19
from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
×
UNCOV
20
from pants.engine.addresses import Address
×
UNCOV
21
from pants.engine.intrinsics import get_digest_contents
×
UNCOV
22
from pants.engine.rules import collect_rules, rule
×
UNCOV
23
from pants.engine.target import (
×
24
    COMMON_TARGET_FIELDS,
25
    AsyncFieldMixin,
26
    BoolField,
27
    Dependencies,
28
    DictStringToStringSequenceField,
29
    FieldDefaultFactoryRequest,
30
    FieldDefaultFactoryResult,
31
    GeneratedTargets,
32
    GenerateTargetsRequest,
33
    InvalidFieldException,
34
    InvalidTargetException,
35
    OptionalSingleSourceField,
36
    SequenceField,
37
    SingleSourceField,
38
    SpecialCasedDependencies,
39
    StringField,
40
    StringSequenceField,
41
    Target,
42
    TargetGenerator,
43
)
UNCOV
44
from pants.engine.unions import UnionMembership, UnionRule
×
UNCOV
45
from pants.jvm.resolve.coordinate import Coordinate
×
UNCOV
46
from pants.jvm.subsystems import JvmSubsystem
×
UNCOV
47
from pants.util.docutil import git_url
×
UNCOV
48
from pants.util.frozendict import FrozenDict
×
UNCOV
49
from pants.util.logging import LogLevel
×
UNCOV
50
from pants.util.strutil import bullet_list, help_text, pluralize, softwrap
×
51

52
# -----------------------------------------------------------------------------------------------
53
# Generic resolve support fields
54
# -----------------------------------------------------------------------------------------------
55

56

UNCOV
57
class JvmDependenciesField(Dependencies):
×
UNCOV
58
    pass
×
59

60

UNCOV
61
class JvmResolveField(StringField, AsyncFieldMixin):
×
UNCOV
62
    alias = "resolve"
×
UNCOV
63
    required = False
×
UNCOV
64
    help = help_text(
×
65
        """
66
        The resolve from `[jvm].resolves` to use when compiling this target.
67

68
        If not defined, will default to `[jvm].default_resolve`.
69
        """
70
        # TODO: Document expectations for dependencies once we validate that.
71
    )
72

UNCOV
73
    def normalized_value(self, jvm_subsystem: JvmSubsystem) -> str:
×
74
        """Get the value after applying the default and validating that the key is recognized."""
75
        resolve = self.value or jvm_subsystem.default_resolve
×
76
        if resolve not in jvm_subsystem.resolves:
×
77
            raise UnrecognizedResolveNamesError(
×
78
                [resolve],
79
                jvm_subsystem.resolves.keys(),
80
                description_of_origin=f"the field `{self.alias}` in the target {self.address}",
81
            )
82
        return resolve
×
83

84

UNCOV
85
class JvmJdkField(StringField):
×
UNCOV
86
    alias = "jdk"
×
UNCOV
87
    required = False
×
UNCOV
88
    help = help_text(
×
89
        """
90
        The major version of the JDK that this target should be built with. If not defined,
91
        will default to `[jvm].default_source_jdk`.
92
        """
93
    )
94

95

UNCOV
96
class PrefixedJvmJdkField(JvmJdkField):
×
UNCOV
97
    alias = "jvm_jdk"
×
98

99

UNCOV
100
class PrefixedJvmResolveField(JvmResolveField):
×
UNCOV
101
    alias = "jvm_resolve"
×
102

103

104
# -----------------------------------------------------------------------------------------------
105
# Targets that can be called with `./pants run` or `experimental_run_in_sandbox`
106
# -----------------------------------------------------------------------------------------------
UNCOV
107
NO_MAIN_CLASS = "org.pantsbuild.meta.no.main.class"
×
108

109

UNCOV
110
class JvmMainClassNameField(StringField):
×
UNCOV
111
    alias = "main"
×
UNCOV
112
    required = False
×
UNCOV
113
    default = None
×
UNCOV
114
    help = help_text(
×
115
        """
116
        `.`-separated name of the JVM class containing the `main()` method to be called when
117
        executing this target. If not supplied, this will be calculated automatically, either by
118
        inspecting the existing manifest (for 3rd-party JARs), or by inspecting the classes inside
119
        the JAR, looking for a valid `main` method.  If a value cannot be calculated automatically,
120
        you must supply a value for `run` to succeed.
121
        """
122
    )
123

124

UNCOV
125
@dataclass(frozen=True)
×
UNCOV
126
class JvmRunnableSourceFieldSet(RunFieldSet):
×
UNCOV
127
    run_in_sandbox_behavior = RunInSandboxBehavior.RUN_REQUEST_HERMETIC
×
UNCOV
128
    jdk_version: JvmJdkField
×
UNCOV
129
    main_class: JvmMainClassNameField
×
130

131

UNCOV
132
@dataclass(frozen=True)
×
UNCOV
133
class GenericJvmRunRequest:
×
134
    """Allows the use of a generic rule to return a `RunRequest` based on the field set."""
135

UNCOV
136
    field_set: JvmRunnableSourceFieldSet
×
137

138

139
# -----------------------------------------------------------------------------------------------
140
# `jvm_artifact` targets
141
# -----------------------------------------------------------------------------------------------
142

UNCOV
143
_DEFAULT_PACKAGE_MAPPING_URL = git_url(
×
144
    "src/python/pants/jvm/dependency_inference/jvm_artifact_mappings.py"
145
)
146

147

UNCOV
148
class JvmArtifactGroupField(StringField):
×
UNCOV
149
    alias = "group"
×
UNCOV
150
    required = True
×
UNCOV
151
    value: str
×
UNCOV
152
    help = help_text(
×
153
        """
154
        The 'group' part of a Maven-compatible coordinate to a third-party JAR artifact.
155

156
        For the JAR coordinate `com.google.guava:guava:30.1.1-jre`, the group is `com.google.guava`.
157
        """
158
    )
159

160

UNCOV
161
class JvmArtifactArtifactField(StringField):
×
UNCOV
162
    alias = "artifact"
×
UNCOV
163
    required = True
×
UNCOV
164
    value: str
×
UNCOV
165
    help = help_text(
×
166
        """
167
        The 'artifact' part of a Maven-compatible coordinate to a third-party JAR artifact.
168

169
        For the JAR coordinate `com.google.guava:guava:30.1.1-jre`, the artifact is `guava`.
170
        """
171
    )
172

173

UNCOV
174
class JvmArtifactVersionField(StringField):
×
UNCOV
175
    alias = "version"
×
UNCOV
176
    required = True
×
UNCOV
177
    value: str
×
UNCOV
178
    help = help_text(
×
179
        """
180
        The 'version' part of a Maven-compatible coordinate to a third-party JAR artifact.
181

182
        For the JAR coordinate `com.google.guava:guava:30.1.1-jre`, the version is `30.1.1-jre`.
183
        """
184
    )
185

186

UNCOV
187
class JvmArtifactUrlField(StringField):
×
UNCOV
188
    alias = "url"
×
UNCOV
189
    required = False
×
UNCOV
190
    help = help_text(
×
191
        """
192
        A URL that points to the location of this artifact.
193

194
        If specified, Pants will not fetch this artifact from default Maven repositories, and
195
        will instead fetch the artifact from this URL. To use default maven
196
        repositories, do not set this value.
197

198
        Note that `file:` URLs are not supported. Instead, use the `jar` field for local
199
        artifacts.
200
        """
201
    )
202

203

UNCOV
204
class JvmArtifactJarSourceField(OptionalSingleSourceField):
×
UNCOV
205
    alias = "jar"
×
UNCOV
206
    expected_file_extensions = (".jar",)
×
UNCOV
207
    help = help_text(
×
208
        """
209
        A local JAR file that provides this artifact to the lockfile resolver, instead of a
210
        Maven repository.
211

212
        Path is relative to the BUILD file.
213

214
        Use the `url` field for remote artifacts.
215
        """
216
    )
217

UNCOV
218
    @classmethod
×
UNCOV
219
    def compute_value(cls, raw_value: str | None, address: Address) -> str | None:
×
UNCOV
220
        value_or_default = super().compute_value(raw_value, address)
×
UNCOV
221
        if value_or_default and value_or_default.startswith("file:"):
×
222
            raise InvalidFieldException(
×
223
                softwrap(
224
                    f"""
225
                    The `{cls.alias}` field does not support `file:` URLS, but the target
226
                    {address} sets the field to `{value_or_default}`.
227

228
                    Instead, use the `jar` field to specify the relative path to the local jar file.
229
                    """
230
                )
231
            )
UNCOV
232
        return value_or_default
×
233

234

UNCOV
235
class JvmArtifactPackagesField(StringSequenceField):
×
UNCOV
236
    alias = "packages"
×
UNCOV
237
    help = help_text(
×
238
        f"""
239
        The JVM packages this artifact provides for the purposes of dependency inference.
240

241
        For example, the JVM artifact `junit:junit` might provide `["org.junit.**"]`.
242

243
        Usually you can leave this field off. If unspecified, Pants will fall back to the
244
        `[java-infer].third_party_import_mapping`, then to a built in mapping
245
        ({_DEFAULT_PACKAGE_MAPPING_URL}), and then finally it will default to
246
        the normalized `group` of the artifact. For example, in the absence of any other mapping
247
        the artifact `io.confluent:common-config` would default to providing
248
        `["io.confluent.**"]`.
249

250
        The package path may be made recursive to match symbols in subpackages
251
        by adding `.**` to the end of the package path. For example, specify `["org.junit.**"]`
252
        to infer a dependency on the artifact for any file importing a symbol from `org.junit` or
253
        its subpackages.
254
        """
255
    )
256

257

UNCOV
258
class JvmArtifactForceVersionField(BoolField):
×
UNCOV
259
    alias = "force_version"
×
UNCOV
260
    default = False
×
UNCOV
261
    help = help_text(
×
262
        """
263
        Force artifact version during resolution.
264

265
        If set, pants will pass `--force-version` argument to `coursier fetch` for this artifact.
266
        """
267
    )
268

269

UNCOV
270
class JvmProvidesTypesField(StringSequenceField):
×
UNCOV
271
    alias = "experimental_provides_types"
×
UNCOV
272
    help = help_text(
×
273
        """
274
        Signals that the specified types should be fulfilled by these source files during
275
        dependency inference.
276

277
        This allows for specific types within packages that are otherwise inferred as
278
        belonging to `jvm_artifact` targets to be unambiguously inferred as belonging
279
        to this first-party source.
280

281
        If a given type is defined, at least one source file captured by this target must
282
        actually provide that symbol.
283
        """
284
    )
285

286

UNCOV
287
@dataclass(frozen=True)
×
UNCOV
288
class JvmArtifactExclusion:
×
UNCOV
289
    alias: ClassVar[str] = "jvm_exclude"
×
UNCOV
290
    help: ClassVar[str | Callable[[], str]] = help_text(
×
291
        """
292
        Exclude the given `artifact` and `group`, or all artifacts from the given `group`.
293
        """
294
    )
295

UNCOV
296
    group: str
×
UNCOV
297
    artifact: str | None = None
×
298

UNCOV
299
    def validate(self, _: Address) -> set[str]:
×
UNCOV
300
        return set()
×
301

UNCOV
302
    def to_coord_str(self) -> str:
×
UNCOV
303
        result = self.group
×
UNCOV
304
        if self.artifact:
×
305
            result += f":{self.artifact}"
×
306
        else:
UNCOV
307
            result += ":*"
×
UNCOV
308
        return result
×
309

310

UNCOV
311
def _jvm_artifact_exclusions_field_help(
×
312
    supported_exclusions: Callable[[], Iterable[type[JvmArtifactExclusion]]],
313
) -> str | Callable[[], str]:
UNCOV
314
    return help_text(
×
315
        lambda: f"""
316
        A list of exclusions for unversioned coordinates that should be excluded
317
        as dependencies when this artifact is resolved.
318

319
        This does not prevent this artifact from being included in the resolve as a dependency
320
        of other artifacts that depend on it, and is currently intended as a way to resolve
321
        version conflicts in complex resolves.
322

323
        Supported exclusions are:
324
        {bullet_list(f"`{exclusion.alias}`: {exclusion.help}" for exclusion in supported_exclusions())}
325
        """
326
    )
327

328

UNCOV
329
class JvmArtifactExclusionsField(SequenceField[JvmArtifactExclusion]):
×
UNCOV
330
    alias = "exclusions"
×
UNCOV
331
    help = _jvm_artifact_exclusions_field_help(
×
332
        lambda: JvmArtifactExclusionsField.supported_exclusion_types
333
    )
334

UNCOV
335
    supported_exclusion_types: ClassVar[tuple[type[JvmArtifactExclusion], ...]] = (
×
336
        JvmArtifactExclusion,
337
    )
UNCOV
338
    expected_element_type = JvmArtifactExclusion
×
UNCOV
339
    expected_type_description = "an iterable of JvmArtifactExclusionRule"
×
340

UNCOV
341
    @classmethod
×
UNCOV
342
    def compute_value(
×
343
        cls, raw_value: Iterable[JvmArtifactExclusion] | None, address: Address
344
    ) -> tuple[JvmArtifactExclusion, ...] | None:
UNCOV
345
        computed_value = super().compute_value(raw_value, address)
×
346

UNCOV
347
        if computed_value:
×
UNCOV
348
            errors: list[str] = []
×
UNCOV
349
            for exclusion_rule in computed_value:
×
UNCOV
350
                err = exclusion_rule.validate(address)
×
UNCOV
351
                if err:
×
352
                    errors.extend(err)
×
353

UNCOV
354
            if errors:
×
355
                raise InvalidFieldException(
×
356
                    softwrap(
357
                        f"""
358
                        Invalid value for `{JvmArtifactExclusionsField.alias}` field at target
359
                        {address}. Found following errors:
360

361
                        {bullet_list(errors)}
362
                        """
363
                    )
364
                )
UNCOV
365
        return computed_value
×
366

367

UNCOV
368
class JvmArtifactResolveField(JvmResolveField):
×
UNCOV
369
    help = help_text(
×
370
        """
371
        The resolve from `[jvm].resolves` that this artifact should be included in.
372

373
        If not defined, will default to `[jvm].default_resolve`.
374

375
        When generating a lockfile for a particular resolve via the `coursier-resolve` goal,
376
        it will include all artifacts that are declared compatible with that resolve. First-party
377
        targets like `java_source` and `scala_source` also declare which resolve they use
378
        via the `resolve` field; so, for your first-party code to use
379
        a particular `jvm_artifact` target, that artifact must be included in the resolve
380
        used by that code.
381
        """
382
    )
383

384

UNCOV
385
@dataclass(frozen=True)
×
UNCOV
386
class JvmArtifactFieldSet(JvmRunnableSourceFieldSet):
×
UNCOV
387
    group: JvmArtifactGroupField
×
UNCOV
388
    artifact: JvmArtifactArtifactField
×
UNCOV
389
    version: JvmArtifactVersionField
×
UNCOV
390
    packages: JvmArtifactPackagesField
×
UNCOV
391
    url: JvmArtifactUrlField
×
UNCOV
392
    force_version: JvmArtifactForceVersionField
×
393

UNCOV
394
    required_fields = (
×
395
        JvmArtifactGroupField,
396
        JvmArtifactArtifactField,
397
        JvmArtifactVersionField,
398
        JvmArtifactPackagesField,
399
        JvmArtifactForceVersionField,
400
    )
401

402

UNCOV
403
class JvmArtifactTarget(Target):
×
UNCOV
404
    alias = "jvm_artifact"
×
UNCOV
405
    core_fields = (
×
406
        *COMMON_TARGET_FIELDS,
407
        *JvmArtifactFieldSet.required_fields,
408
        JvmArtifactUrlField,  # TODO: should `JvmArtifactFieldSet` have an `all_fields` field?
409
        JvmArtifactJarSourceField,
410
        JvmArtifactResolveField,
411
        JvmArtifactExclusionsField,
412
        JvmJdkField,
413
        JvmMainClassNameField,
414
    )
UNCOV
415
    help = help_text(
×
416
        """
417
        A third-party JVM artifact, as identified by its Maven-compatible coordinate.
418

419
        That is, an artifact identified by its `group`, `artifact`, and `version` components.
420

421
        Each artifact is associated with one or more resolves (a logical name you give to a
422
        lockfile). For this artifact to be used by your first-party code, it must be
423
        associated with the resolve(s) used by that code. See the `resolve` field.
424
        """
425
    )
426

UNCOV
427
    def validate(self) -> None:
×
UNCOV
428
        if self[JvmArtifactJarSourceField].value and self[JvmArtifactUrlField].value:
×
429
            raise InvalidTargetException(
×
430
                f"You cannot specify both the `url` and `jar` fields, but both were set on the "
431
                f"`{self.alias}` target {self.address}."
432
            )
433

434

435
# -----------------------------------------------------------------------------------------------
436
# Generate `jvm_artifact` targets from pom.xml
437
# -----------------------------------------------------------------------------------------------
438

439

UNCOV
440
class PomXmlSourceField(SingleSourceField):
×
UNCOV
441
    default = "pom.xml"
×
UNCOV
442
    required = False
×
443

444

UNCOV
445
class JvmArtifactsPackageMappingField(DictStringToStringSequenceField):
×
UNCOV
446
    alias = "package_mapping"
×
UNCOV
447
    help = help_text(
×
448
        f"""
449
        A mapping of jvm artifacts to a list of the packages they provide.
450

451
        For example, `{{"com.google.guava:guava": ["com.google.common.**"]}}`.
452

453
        Any unspecified jvm artifacts will use a default. See the
454
        `{JvmArtifactPackagesField.alias}` field from the `{JvmArtifactTarget.alias}`
455
        target for more information.
456
        """
457
    )
UNCOV
458
    value: FrozenDict[str, tuple[str, ...]]
×
UNCOV
459
    default: ClassVar[FrozenDict[str, tuple[str, ...]] | None] = FrozenDict()
×
460

UNCOV
461
    @classmethod
×
UNCOV
462
    def compute_value(  # type: ignore[override]
×
463
        cls, raw_value: dict[str, Iterable[str]], address: Address
464
    ) -> FrozenDict[tuple[str, str], tuple[str, ...]]:
465
        value_or_default = super().compute_value(raw_value, address)
×
466
        assert value_or_default is not None
×
467
        return FrozenDict(
×
468
            {
469
                cls._parse_coord(coord): tuple(packages)
470
                for coord, packages in value_or_default.items()
471
            }
472
        )
473

UNCOV
474
    @classmethod
×
UNCOV
475
    def _parse_coord(cls, coord: str) -> tuple[str, str]:
×
476
        group, artifact = coord.split(":")
×
477
        return group, artifact
×
478

479

UNCOV
480
class JvmArtifactsTargetGenerator(TargetGenerator):
×
UNCOV
481
    alias = "jvm_artifacts"
×
UNCOV
482
    core_fields = (
×
483
        PomXmlSourceField,
484
        JvmArtifactsPackageMappingField,
485
        *COMMON_TARGET_FIELDS,
486
    )
UNCOV
487
    generated_target_cls = JvmArtifactTarget
×
UNCOV
488
    copied_fields = COMMON_TARGET_FIELDS
×
UNCOV
489
    moved_fields = (JvmArtifactResolveField,)
×
UNCOV
490
    help = help_text(
×
491
        """
492
        Generate a `jvm_artifact` target for each dependency in pom.xml file.
493
        """
494
    )
495

496

UNCOV
497
class GenerateFromPomXmlRequest(GenerateTargetsRequest):
×
UNCOV
498
    generate_from = JvmArtifactsTargetGenerator
×
499

500

UNCOV
501
@rule(
×
502
    desc=("Generate `jvm_artifact` targets from pom.xml"),
503
    level=LogLevel.DEBUG,
504
)
UNCOV
505
async def generate_from_pom_xml(
×
506
    request: GenerateFromPomXmlRequest,
507
    union_membership: UnionMembership,
508
) -> GeneratedTargets:
509
    generator = request.generator
×
510
    pom_xml = await determine_source_files(SourceFilesRequest([generator[PomXmlSourceField]]))
×
511
    files = await get_digest_contents(pom_xml.snapshot.digest)
×
512
    if not files:
×
513
        raise FileNotFoundError(f"pom.xml not found: {generator[PomXmlSourceField].value}")
×
514

515
    mapping = request.generator[JvmArtifactsPackageMappingField].value
×
516
    coordinates = parse_pom_xml(files[0].content, pom_xml_path=pom_xml.snapshot.files[0])
×
517
    targets = (
×
518
        JvmArtifactTarget(
519
            unhydrated_values={
520
                "group": coord.group,
521
                "artifact": coord.artifact,
522
                "version": coord.version,
523
                "packages": mapping.get((coord.group, coord.artifact)),
524
                **request.template,
525
            },
526
            address=request.template_address.create_generated(coord.artifact),
527
        )
528
        for coord in coordinates
529
    )
530
    return GeneratedTargets(request.generator, targets)
×
531

532

UNCOV
533
def parse_pom_xml(content: bytes, pom_xml_path: str) -> Iterator[Coordinate]:
×
534
    root = ET.fromstring(content.decode("utf-8"))
×
535
    match = re.match(r"^(\{.*\})project$", root.tag)
×
536
    if not match:
×
537
        raise ValueError(
×
538
            f"Unexpected root tag `{root.tag}` in {pom_xml_path}, expected tag `project`"
539
        )
540

541
    namespace = match.group(1)
×
542
    for dependency in root.iter(f"{namespace}dependency"):
×
543
        yield Coordinate(
×
544
            group=get_child_text(dependency, f"{namespace}groupId"),
545
            artifact=get_child_text(dependency, f"{namespace}artifactId"),
546
            version=get_child_text(dependency, f"{namespace}version"),
547
        )
548

549

UNCOV
550
def get_child_text(parent: ET.Element, child: str) -> str:
×
551
    tag = parent.find(child)
×
552
    if tag is None:
×
553
        raise ValueError(f"missing element: {child}")
×
554
    text = tag.text
×
555
    if text is None:
×
556
        raise ValueError(f"empty element: {child}")
×
557
    return text
×
558

559

560
# -----------------------------------------------------------------------------------------------
561
# JUnit test support field(s)
562
# -----------------------------------------------------------------------------------------------
563

564

UNCOV
565
class JunitTestSourceField(SingleSourceField, metaclass=ABCMeta):
×
566
    """A marker that indicates that a source field represents a JUnit test."""
567

568

UNCOV
569
class JunitTestTimeoutField(TestTimeoutField):
×
UNCOV
570
    pass
×
571

572

UNCOV
573
class JunitTestExtraEnvVarsField(TestExtraEnvVarsField):
×
UNCOV
574
    pass
×
575

576

577
# -----------------------------------------------------------------------------------------------
578
# JAR support fields
579
# -----------------------------------------------------------------------------------------------
580

581

UNCOV
582
class JvmRequiredMainClassNameField(JvmMainClassNameField):
×
UNCOV
583
    required = True
×
UNCOV
584
    default = None
×
UNCOV
585
    help = help_text(
×
586
        """
587
        `.`-separated name of the JVM class containing the `main()` method to be called when
588
        executing this JAR.
589
        """
590
    )
591

592

UNCOV
593
class JvmShadingRule(ABC):
×
594
    """Base class for defining JAR shading rules as valid aliases in BUILD files.
595

596
    Subclasses need to provide with an `alias` and a `help` message. The `alias` represents
597
    the name that will be used in BUILD files to instantiate the given subclass.
598

599
    Set the `help` class property with a description, which will be used in `./pants help`. For the
600
    best rendering, use soft wrapping (e.g. implicit string concatenation) within paragraphs, but
601
    hard wrapping (`\n`) to separate distinct paragraphs and/or lists.
602
    """
603

UNCOV
604
    alias: ClassVar[str]
×
UNCOV
605
    help: ClassVar[str | Callable[[], str]]
×
606

UNCOV
607
    @abstractmethod
×
UNCOV
608
    def encode(self) -> str:
×
609
        pass
×
610

UNCOV
611
    @abstractmethod
×
UNCOV
612
    def validate(self) -> set[str]:
×
613
        pass
×
614

UNCOV
615
    @staticmethod
×
UNCOV
616
    def _validate_field(value: str, *, name: str, invalid_chars: str) -> set[str]:
×
UNCOV
617
        errors = []
×
UNCOV
618
        for ch in invalid_chars:
×
UNCOV
619
            if ch in value:
×
UNCOV
620
                errors.append(f"`{name}` can not contain the character `{ch}`.")
×
UNCOV
621
        return set(errors)
×
622

UNCOV
623
    def __repr__(self) -> str:
×
624
        fields = [f"{fld.name}={repr(getattr(self, fld.name))}" for fld in dataclasses.fields(self)]  # type: ignore[arg-type]
×
625
        return f"{self.alias}({', '.join(fields)})"
×
626

627

UNCOV
628
@dataclass(frozen=True, repr=False)
×
UNCOV
629
class JvmShadingRenameRule(JvmShadingRule):
×
UNCOV
630
    alias = "shading_rename"
×
UNCOV
631
    help = "Renames all occurrences of the given `pattern` by the `replacement`."
×
632

UNCOV
633
    pattern: str
×
UNCOV
634
    replacement: str
×
635

UNCOV
636
    def encode(self) -> str:
×
637
        return f"rule {self.pattern} {self.replacement}"
×
638

UNCOV
639
    def validate(self) -> set[str]:
×
UNCOV
640
        errors: list[str] = []
×
UNCOV
641
        errors.extend(
×
642
            JvmShadingRule._validate_field(self.pattern, name="pattern", invalid_chars="/")
643
        )
UNCOV
644
        errors.extend(
×
645
            JvmShadingRule._validate_field(self.replacement, name="replacement", invalid_chars="/")
646
        )
UNCOV
647
        return set(errors)
×
648

649

UNCOV
650
@dataclass(frozen=True, repr=False)
×
UNCOV
651
class JvmShadingRelocateRule(JvmShadingRule):
×
UNCOV
652
    alias = "shading_relocate"
×
UNCOV
653
    help = help_text(
×
654
        """
655
        Relocates the classes under the given `package` into the new package name.
656
        The default target package is `__shaded_by_pants__` if none provided in
657
        the `into` parameter.
658
        """
659
    )
660

UNCOV
661
    package: str
×
UNCOV
662
    into: str | None = None
×
663

UNCOV
664
    def encode(self) -> str:
×
665
        if not self.into:
×
666
            target_suffix = "__shaded_by_pants__"
×
667
        else:
668
            target_suffix = self.into
×
669
        return f"rule {self.package}.** {target_suffix}.@1"
×
670

UNCOV
671
    def validate(self) -> set[str]:
×
UNCOV
672
        errors: list[str] = []
×
UNCOV
673
        errors.extend(
×
674
            JvmShadingRule._validate_field(self.package, name="package", invalid_chars="/*")
675
        )
UNCOV
676
        if self.into:
×
UNCOV
677
            errors.extend(
×
678
                JvmShadingRule._validate_field(self.into, name="into", invalid_chars="/*")
679
            )
UNCOV
680
        return set(errors)
×
681

682

UNCOV
683
@dataclass(frozen=True, repr=False)
×
UNCOV
684
class JvmShadingZapRule(JvmShadingRule):
×
UNCOV
685
    alias = "shading_zap"
×
UNCOV
686
    help = "Removes from the final artifact the occurrences of the `pattern`."
×
687

UNCOV
688
    pattern: str
×
689

UNCOV
690
    def encode(self) -> str:
×
691
        return f"zap {self.pattern}"
×
692

UNCOV
693
    def validate(self) -> set[str]:
×
UNCOV
694
        return JvmShadingRule._validate_field(self.pattern, name="pattern", invalid_chars="/")
×
695

696

UNCOV
697
@dataclass(frozen=True, repr=False)
×
UNCOV
698
class JvmShadingKeepRule(JvmShadingRule):
×
UNCOV
699
    alias = "shading_keep"
×
UNCOV
700
    help = help_text(
×
701
        """
702
        Keeps in the final artifact the occurrences of the `pattern`
703
        (and removes anything else).
704
        """
705
    )
706

UNCOV
707
    pattern: str
×
708

UNCOV
709
    def encode(self) -> str:
×
710
        return f"keep {self.pattern}"
×
711

UNCOV
712
    def validate(self) -> set[str]:
×
UNCOV
713
        return JvmShadingRule._validate_field(self.pattern, name="pattern", invalid_chars="/")
×
714

715

UNCOV
716
JVM_SHADING_RULE_TYPES: list[type[JvmShadingRule]] = [
×
717
    JvmShadingRelocateRule,
718
    JvmShadingRenameRule,
719
    JvmShadingZapRule,
720
    JvmShadingKeepRule,
721
]
722

723

UNCOV
724
def _shading_rules_field_help(intro: str) -> str:
×
UNCOV
725
    return softwrap(
×
726
        f"""
727
        {intro}
728

729
        There are {pluralize(len(JVM_SHADING_RULE_TYPES), "possible shading rule")} available,
730
        which are as follows:
731
        {bullet_list([f"`{rule.alias}`: {rule.help}" for rule in JVM_SHADING_RULE_TYPES])}
732

733
        When defining shading rules, just add them in this field using the previously listed rule
734
        alias and passing along the required parameters.
735
        """
736
    )
737

738

UNCOV
739
def _shading_validate_rules(shading_rules: Iterable[JvmShadingRule]) -> set[str]:
×
UNCOV
740
    validation_errors = []
×
UNCOV
741
    for shading_rule in shading_rules:
×
UNCOV
742
        found_errors = shading_rule.validate()
×
UNCOV
743
        if found_errors:
×
UNCOV
744
            validation_errors.append(
×
745
                "\n".join(
746
                    [
747
                        f"In rule `{shading_rule.alias}`:",
748
                        bullet_list(found_errors),
749
                        "",
750
                    ]
751
                )
752
            )
UNCOV
753
    return set(validation_errors)
×
754

755

UNCOV
756
class JvmShadingRulesField(SequenceField[JvmShadingRule], metaclass=ABCMeta):
×
UNCOV
757
    alias = "shading_rules"
×
UNCOV
758
    required = False
×
UNCOV
759
    expected_element_type = JvmShadingRule
×
UNCOV
760
    expected_type_description = "an iterable of JvmShadingRule"
×
761

UNCOV
762
    @classmethod
×
UNCOV
763
    def compute_value(
×
764
        cls, raw_value: Iterable[JvmShadingRule] | None, address: Address
765
    ) -> tuple[JvmShadingRule, ...] | None:
766
        computed_value = super().compute_value(raw_value, address)
×
767

768
        if computed_value:
×
769
            validation_errors = _shading_validate_rules(computed_value)
×
770
            if validation_errors:
×
771
                raise InvalidFieldException(
×
772
                    "\n".join(
773
                        [
774
                            f"Invalid shading rules assigned to `{cls.alias}` field in target {address}:\n",
775
                            *validation_errors,
776
                        ]
777
                    )
778
                )
779

780
        return computed_value
×
781

782

783
# -----------------------------------------------------------------------------------------------
784
# `deploy_jar` target
785
# -----------------------------------------------------------------------------------------------
786

787

UNCOV
788
@dataclass(frozen=True)
×
UNCOV
789
class DeployJarDuplicateRule:
×
UNCOV
790
    alias: ClassVar[str] = "duplicate_rule"
×
UNCOV
791
    valid_actions: ClassVar[tuple[str, ...]] = ("skip", "replace", "concat", "concat_text", "throw")
×
792

UNCOV
793
    pattern: str
×
UNCOV
794
    action: str
×
795

UNCOV
796
    def validate(self) -> str | None:
×
797
        if self.action not in DeployJarDuplicateRule.valid_actions:
×
798
            return softwrap(
×
799
                f"""
800
                Value '{self.action}' for `action` associated with pattern
801
                '{self.pattern}' is not valid.
802

803
                It must be one of {list(DeployJarDuplicateRule.valid_actions)}.
804
                """
805
            )
806
        return None
×
807

UNCOV
808
    def __repr__(self) -> str:
×
809
        return f"{self.alias}(pattern='{self.pattern}', action='{self.action}')"
×
810

811

UNCOV
812
class DeployJarDuplicatePolicyField(SequenceField[DeployJarDuplicateRule]):
×
UNCOV
813
    alias = "duplicate_policy"
×
UNCOV
814
    help = help_text(
×
815
        f"""
816
        A list of the rules to apply when duplicate file entries are found in the final
817
        assembled JAR file.
818

819
        When defining a duplicate policy, just add `duplicate_rule` directives to this
820
        field as follows:
821

822
        Example:
823

824
            duplicate_policy=[
825
                duplicate_rule(pattern="^META-INF/services", action="concat_text"),
826
                duplicate_rule(pattern="^reference\\.conf", action="concat_text"),
827
                duplicate_rule(pattern="^org/apache/commons", action="throw"),
828
            ]
829

830
        Where:
831

832
        * The `pattern` field is treated as a regular expression
833
        * The `action` field must be one of `{list(DeployJarDuplicateRule.valid_actions)}`.
834

835
        Note that the order in which the rules are listed is relevant.
836
        """
837
    )
UNCOV
838
    required = False
×
839

UNCOV
840
    expected_element_type = DeployJarDuplicateRule
×
UNCOV
841
    expected_type_description = "a list of JAR duplicate rules"
×
842

UNCOV
843
    default = (
×
844
        DeployJarDuplicateRule(pattern="^META-INF/services/", action="concat_text"),
845
        DeployJarDuplicateRule(pattern="^META-INF/LICENSE", action="skip"),
846
    )
847

UNCOV
848
    @classmethod
×
UNCOV
849
    def compute_value(
×
850
        cls, raw_value: Iterable[DeployJarDuplicateRule] | None, address: Address
851
    ) -> tuple[DeployJarDuplicateRule, ...] | None:
852
        value = super().compute_value(raw_value, address)
×
853
        if value:
×
854
            errors = []
×
855
            for duplicate_rule in value:
×
856
                err = duplicate_rule.validate()
×
857
                if err:
×
858
                    errors.append(err)
×
859

860
            if errors:
×
861
                raise InvalidFieldException(
×
862
                    softwrap(
863
                        f"""
864
                        Invalid value for `{DeployJarDuplicatePolicyField.alias}` field at target:
865
                        {address}. Found following errors:
866

867
                        {bullet_list(errors)}
868
                        """
869
                    )
870
                )
871
        return value
×
872

UNCOV
873
    def value_or_default(self) -> tuple[DeployJarDuplicateRule, ...]:
×
874
        if self.value is not None:
×
875
            return self.value
×
876
        return self.default
×
877

878

UNCOV
879
class DeployJarShadingRulesField(JvmShadingRulesField):
×
UNCOV
880
    help = _shading_rules_field_help("Shading rules to be applied to the final JAR artifact.")
×
881

882

UNCOV
883
class DeployJarExcludeFilesField(StringSequenceField):
×
UNCOV
884
    alias = "exclude_files"
×
UNCOV
885
    help = help_text(
×
886
        """
887
        A list of patterns to exclude from the final jar.
888
        """
889
    )
890

891

UNCOV
892
class DeployJarTarget(Target):
×
UNCOV
893
    alias = "deploy_jar"
×
UNCOV
894
    core_fields = (
×
895
        *COMMON_TARGET_FIELDS,
896
        RestartableField,
897
        OutputPathField,
898
        JvmDependenciesField,
899
        JvmRequiredMainClassNameField,
900
        JvmJdkField,
901
        JvmResolveField,
902
        DeployJarDuplicatePolicyField,
903
        DeployJarShadingRulesField,
904
        DeployJarExcludeFilesField,
905
    )
UNCOV
906
    help = help_text(
×
907
        """
908
        A `jar` file with first and third-party code bundled for deploys.
909

910
        The JAR will contain class files for both first-party code and
911
        third-party dependencies, all in a common directory structure.
912
        """
913
    )
914

915

916
# -----------------------------------------------------------------------------------------------
917
# `jvm_war` targets
918
# -----------------------------------------------------------------------------------------------
919

920

UNCOV
921
class JvmWarDependenciesField(Dependencies):
×
UNCOV
922
    pass
×
923

924

UNCOV
925
class JvmWarDescriptorAddressField(SingleSourceField):
×
UNCOV
926
    alias = "descriptor"
×
UNCOV
927
    default = "web.xml"
×
UNCOV
928
    help = "Path to a file containing the descriptor (i.e., `web.xml`) for this WAR file. Defaults to `web.xml`."
×
929

930

UNCOV
931
class JvmWarContentField(SpecialCasedDependencies):
×
UNCOV
932
    alias = "content"
×
UNCOV
933
    help = help_text(
×
934
        """
935
        A list of addresses to `resources` and `files` targets with content to place in the
936
        document root of this WAR file.
937
        """
938
    )
939

940

UNCOV
941
class JvmWarShadingRulesField(JvmShadingRulesField):
×
UNCOV
942
    help = _shading_rules_field_help(
×
943
        "Shading rules to be applied to the individual JAR artifacts embedded in the `WEB-INF/lib` folder."
944
    )
945

946

UNCOV
947
class JvmWarTarget(Target):
×
UNCOV
948
    alias = "jvm_war"
×
UNCOV
949
    core_fields = (
×
950
        *COMMON_TARGET_FIELDS,
951
        JvmResolveField,
952
        JvmWarContentField,
953
        JvmWarDependenciesField,
954
        JvmWarDescriptorAddressField,
955
        JvmWarShadingRulesField,
956
        OutputPathField,
957
    )
UNCOV
958
    help = help_text(
×
959
        """
960
        A JSR 154 "web application archive" (or "war") with first-party and third-party code bundled for
961
        deploys in Java Servlet containers.
962
        """
963
    )
964

965

966
# -----------------------------------------------------------------------------------------------
967
# Dynamic Field defaults
968
# -----------------------------------------------------------------------------------------------#
969

970

UNCOV
971
class JvmResolveFieldDefaultFactoryRequest(FieldDefaultFactoryRequest):
×
UNCOV
972
    field_type = JvmResolveField
×
973

974

UNCOV
975
@rule
×
UNCOV
976
async def jvm_resolve_field_default_factory(
×
977
    request: JvmResolveFieldDefaultFactoryRequest,
978
    jvm: JvmSubsystem,
979
) -> FieldDefaultFactoryResult:
980
    return FieldDefaultFactoryResult(lambda f: f.normalized_value(jvm))
×
981

982

UNCOV
983
def rules():
×
UNCOV
984
    return [
×
985
        *collect_rules(),
986
        UnionRule(GenerateTargetsRequest, GenerateFromPomXmlRequest),
987
        UnionRule(FieldDefaultFactoryRequest, JvmResolveFieldDefaultFactoryRequest),
988
    ]
989

990

UNCOV
991
def build_file_aliases():
×
992
    return BuildFileAliases(
×
993
        objects={
994
            JvmArtifactExclusion.alias: JvmArtifactExclusion,
995
            DeployJarDuplicateRule.alias: DeployJarDuplicateRule,
996
            **{rule.alias: rule for rule in JVM_SHADING_RULE_TYPES},
997
        }
998
    )
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

© 2025 Coveralls, Inc