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

pantsbuild / pants / 26260209689

21 May 2026 11:59PM UTC coverage: 75.453% (-15.7%) from 91.156%
26260209689

Pull #23365

github

web-flow
Merge 5fe873b58 into 7ea655ba0
Pull Request #23365: uv.lock -> pex optimization

5 of 16 new or added lines in 1 file covered. (31.25%)

10118 existing lines in 378 files now uncovered.

54669 of 72454 relevant lines covered (75.45%)

2.31 hits per line

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

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

4
from __future__ import annotations
3✔
5

6
import logging
3✔
7
import os
3✔
8
from collections.abc import Iterable
3✔
9
from dataclasses import dataclass
3✔
10
from pathlib import PurePath
3✔
11

12
from pants.engine.engine_aware import EngineAwareParameter
3✔
13
from pants.engine.fs import (
3✔
14
    AddPrefix,
15
    CreateDigest,
16
    Digest,
17
    Directory,
18
    FileContent,
19
    MergeDigests,
20
    RemovePrefix,
21
)
22
from pants.engine.intrinsics import add_prefix, create_digest, merge_digests, remove_prefix
3✔
23
from pants.engine.process import execute_process_or_raise
3✔
24
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
3✔
25
from pants.jvm.jdk_rules import InternalJdk, JvmProcess
3✔
26
from pants.jvm.resolve.coursier_fetch import ToolClasspathRequest, materialize_classpath_for_tool
3✔
27
from pants.jvm.resolve.jvm_tool import GenerateJvmLockfileFromTool
3✔
28
from pants.jvm.shading import jarjar
3✔
29
from pants.jvm.shading.jarjar import JarJar, MisplacedClassStrategy
3✔
30
from pants.jvm.target_types import JvmShadingRule, _shading_validate_rules
3✔
31
from pants.util.logging import LogLevel
3✔
32

33
logger = logging.getLogger(__name__)
3✔
34

35

36
@dataclass(frozen=True)
3✔
37
class ShadeJarRequest(EngineAwareParameter):
3✔
38
    path: PurePath
3✔
39
    digest: Digest
3✔
40
    rules: tuple[JvmShadingRule, ...]
3✔
41

42
    # JarJar configuration options
43
    skip_manifest: bool | None
3✔
44
    misplaced_class_strategy: MisplacedClassStrategy | None
3✔
45

46
    def __init__(
3✔
47
        self,
48
        *,
49
        path: str | PurePath,
50
        digest: Digest,
51
        rules: Iterable[JvmShadingRule] | None = None,
52
        skip_manifest: bool | None = None,
53
        misplaced_class_strategy: MisplacedClassStrategy | None = None,
54
    ) -> None:
55
        object.__setattr__(self, "path", path if isinstance(path, PurePath) else PurePath(path))
2✔
56
        object.__setattr__(self, "digest", digest)
2✔
57
        object.__setattr__(self, "rules", tuple(rules or ()))
2✔
58
        object.__setattr__(self, "skip_manifest", skip_manifest)
2✔
59
        object.__setattr__(self, "misplaced_class_strategy", misplaced_class_strategy)
2✔
60

61
        self.__post_init__()
2✔
62

63
    def __post_init__(self):
3✔
64
        validation_errors = _shading_validate_rules(self.rules)
2✔
65
        if validation_errors:
2✔
UNCOV
66
            raise ValueError("\n".join(["Invalid rules provided:\n", *validation_errors]))
×
67

68
    def debug_hint(self) -> str | None:
3✔
UNCOV
69
        return str(self.path)
×
70

71

72
@dataclass(frozen=True)
3✔
73
class ShadedJar:
3✔
74
    path: str
3✔
75
    digest: Digest
3✔
76

77

78
_JARJAR_MAIN_CLASS = "com.eed3si9n.jarjar.Main"
3✔
79
_JARJAR_RULE_CONFIG_FILENAME = "rules"
3✔
80

81

82
@rule(desc="Applies shading rules to a JAR file")
3✔
83
async def shade_jar(request: ShadeJarRequest, jdk: InternalJdk, jarjar: JarJar) -> ShadedJar:
3✔
84
    if not request.rules:
2✔
85
        return ShadedJar(path=str(request.path), digest=request.digest)
×
86

87
    output_prefix = "__out"
2✔
88
    output_filename = os.path.join(output_prefix, request.path.name)
2✔
89

90
    rule_config_content = "\n".join([rule.encode() for rule in request.rules]) + "\n"
2✔
91
    logger.debug(f"Using JarJar rule file with following contents:\n{rule_config_content}")
2✔
92

93
    conf_digest, output_digest = await concurrently(
2✔
94
        create_digest(
95
            CreateDigest(
96
                [
97
                    FileContent(
98
                        path=_JARJAR_RULE_CONFIG_FILENAME,
99
                        content=rule_config_content.encode("utf-8"),
100
                    ),
101
                ]
102
            )
103
        ),
104
        create_digest(CreateDigest([Directory(output_prefix)])),
105
    )
106

107
    tool_classpath, input_digest = await concurrently(
2✔
108
        materialize_classpath_for_tool(
109
            ToolClasspathRequest(lockfile=GenerateJvmLockfileFromTool.create(jarjar))
110
        ),
111
        merge_digests(MergeDigests([request.digest, output_digest])),
112
    )
113

114
    toolcp_prefix = "__toolcp"
2✔
115
    conf_prefix = "__conf"
2✔
116
    immutable_input_digests = {
2✔
117
        toolcp_prefix: tool_classpath.digest,
118
        conf_prefix: conf_digest,
119
    }
120

121
    def should_skip_manifest() -> bool:
2✔
122
        if request.skip_manifest is not None:
2✔
123
            return request.skip_manifest
1✔
124
        return jarjar.skip_manifest
1✔
125

126
    system_properties: dict[str, str] = {
2✔
127
        "verbose": str(logger.isEnabledFor(LogLevel.DEBUG.level)).lower(),
128
        "skipManifest": str(should_skip_manifest()).lower(),
129
    }
130
    misplaced_class_strategy = request.misplaced_class_strategy or jarjar.misplaced_class_strategy
2✔
131
    if misplaced_class_strategy:
2✔
132
        system_properties["misplacedClassStrategy"] = misplaced_class_strategy.value
×
133

134
    result = await execute_process_or_raise(
2✔
135
        **implicitly(
136
            JvmProcess(
137
                jdk=jdk,
138
                argv=[
139
                    _JARJAR_MAIN_CLASS,
140
                    "process",
141
                    os.path.join(conf_prefix, _JARJAR_RULE_CONFIG_FILENAME),
142
                    str(request.path),
143
                    output_filename,
144
                ],
145
                classpath_entries=tool_classpath.classpath_entries(toolcp_prefix),
146
                input_digest=input_digest,
147
                extra_immutable_input_digests=immutable_input_digests,
148
                extra_jvm_options=[
149
                    *jarjar.jvm_options,
150
                    *[f"-D{prop}={value}" for prop, value in system_properties.items()],
151
                ],
152
                description=f"Shading JAR {request.path}",
153
                output_directories=(output_prefix,),
154
                level=LogLevel.DEBUG,
155
            )
156
        )
157
    )
158

159
    shaded_jar_digest = await remove_prefix(RemovePrefix(result.output_digest, output_prefix))
2✔
160
    if request.path.parents:
2✔
161
        # Restore the folder structure of the original path in the output digest
162
        shaded_jar_digest = await add_prefix(AddPrefix(shaded_jar_digest, str(request.path.parent)))
2✔
163

164
    return ShadedJar(path=str(request.path), digest=shaded_jar_digest)
2✔
165

166

167
def rules():
3✔
168
    return [*collect_rules(), *jarjar.rules()]
3✔
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