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

pantsbuild / pants / 20147226056

11 Dec 2025 08:58PM UTC coverage: 78.827% (-1.5%) from 80.293%
20147226056

push

github

web-flow
Forwarded the `style` and `complete-platform` args from pants.toml to PEX (#22910)

## Context

After Apple switched to the `arm64` architecture, some package
publishers stopped releasing `x86_64` variants of their packages for
`darwin`. As a result, generating a universal lockfile now fails because
no single package version is compatible with both `x86_64` and `arm64`
on `darwin`.

The solution is to use the `--style` and `--complete-platform` flags
with PEX. For example:
```
pex3 lock create \
    --style strict \
    --complete-platform 3rdparty/platforms/manylinux_2_28_aarch64.json \
    --complete-platform 3rdparty/platforms/macosx_26_0_arm64.json \
    -r 3rdparty/python/requirements_pyarrow.txt \
    -o python-pyarrow.lock
```

See the Slack discussion here:
https://pantsbuild.slack.com/archives/C046T6T9U/p1760098582461759

## Reproduction

* `BUILD`
```
python_requirement(
    name="awswrangler",
    requirements=["awswrangler==3.12.1"],
    resolve="awswrangler",
)
```
* Run `pants generate-lockfiles --resolve=awswrangler` on macOS with an
`arm64` CPU
```
pip: ERROR: Cannot install awswrangler==3.12.1 because these package versions have conflicting dependencies.
pip: ERROR: ResolutionImpossible: for help visit https://pip.pypa.io/en/latest/topics/dependency-resolution/#dealing-with-dependency-conflicts
pip:  
pip:  The conflict is caused by:
pip:      awswrangler 3.12.1 depends on pyarrow<18.0.0 and >=8.0.0; sys_platform == "darwin" and platform_machine == "x86_64"
pip:      awswrangler 3.12.1 depends on pyarrow<21.0.0 and >=18.0.0; sys_platform != "darwin" or platform_machine != "x86_64"
pip:  
pip:  Additionally, some packages in these conflicts have no matching distributions available for your environment:
pip:      pyarrow
pip:  
pip:  To fix this you could try to:
pip:  1. loosen the range of package versions you've specified
pip:  2. remove package versions to allow pip to attempt to solve the dependency conflict
```

## Implementation
... (continued)

77 of 100 new or added lines in 6 files covered. (77.0%)

868 existing lines in 42 files now uncovered.

74471 of 94474 relevant lines covered (78.83%)

3.18 hits per line

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

0.0
/src/python/pants/backend/go/util_rules/coverage_html.py
1
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
UNCOV
3
from __future__ import annotations
×
4

UNCOV
5
import io
×
UNCOV
6
import math
×
UNCOV
7
from collections.abc import Sequence
×
UNCOV
8
from dataclasses import dataclass
×
UNCOV
9
from pathlib import PurePath
×
10

UNCOV
11
import chevron
×
12

UNCOV
13
from pants.backend.go.util_rules.coverage import GoCoverMode
×
UNCOV
14
from pants.backend.go.util_rules.coverage_profile import (
×
15
    GoCoverageBoundary,
16
    GoCoverageProfile,
17
    parse_go_coverage_profiles,
18
)
UNCOV
19
from pants.engine.internals.native_engine import Digest
×
UNCOV
20
from pants.engine.intrinsics import get_digest_contents
×
UNCOV
21
from pants.engine.rules import collect_rules, rule
×
22

23
# Adapted from Go toolchain.
24
# See https://github.com/golang/go/blob/a0441c7ae3dea57a0553c9ea77e184c34b7da40f/src/cmd/cover/html.go
25
#
26
# Note: `go tool cover` could not be used for the HTML support because it attempts to find the source files
27
# on its own using go list.
28
# See https://github.com/golang/go/blob/a0441c7ae3dea57a0553c9ea77e184c34b7da40f/src/cmd/cover/func.go#L200-L222.
29
#
30
# The Go rules have been engineered to avoid `go list` due to it needing, among other things, all transitive
31
# third-party dependencies available to it when analyzing first-party sources. Thus, the use of `go list` by
32
# `go tool cover` in this case means we cannot use `go tool cover` to generate the HTML.
33
#
34
# Original copyright:
35
#  // Copyright 2013 The Go Authors. All rights reserved.
36
#  // Use of this source code is governed by a BSD-style
37
#  // license that can be found in the LICENSE file.
38

39

UNCOV
40
@dataclass(frozen=True)
×
UNCOV
41
class RenderGoCoverageProfileToHtmlRequest:
×
UNCOV
42
    raw_coverage_profile: bytes
×
UNCOV
43
    description_of_origin: str
×
UNCOV
44
    sources_digest: Digest
×
UNCOV
45
    sources_dir_path: str
×
46

47

UNCOV
48
@dataclass(frozen=True)
×
UNCOV
49
class RenderGoCoverageProfileToHtmlResult:
×
UNCOV
50
    html_output: bytes
×
51

52

UNCOV
53
@dataclass(frozen=True)
×
UNCOV
54
class RenderedFile:
×
UNCOV
55
    name: str
×
UNCOV
56
    body: str
×
UNCOV
57
    coverage: float
×
58

59

UNCOV
60
def _get_pkg_name(filename: str) -> str | None:
×
61
    elems = filename.split("/")
×
62
    i = len(elems) - 2
×
63
    while i >= 0:
×
64
        if elems[i] != "":
×
65
            return elems[i]
×
66
        i -= 1
×
67
    return None
×
68

69

UNCOV
70
def _percent_covered(profile: GoCoverageProfile) -> float:
×
71
    covered = 0
×
72
    total = 0
×
73
    for block in profile.blocks:
×
74
        total += block.num_stmt
×
75
        if block.count > 0:
×
76
            covered += block.num_stmt
×
77
    if total == 0:
×
78
        return 0.0
×
79
    return float(covered) / float(total) * 100.0
×
80

81

UNCOV
82
def _render_source_file(content: bytes, boundaries: Sequence[GoCoverageBoundary]) -> str:
×
83
    rendered = io.StringIO()
×
84
    for i in range(len(content)):
×
85
        while boundaries and boundaries[0].offset == i:
×
86
            b = boundaries[0]
×
87
            if b.start:
×
88
                n = 0
×
89
                if b.count > 0:
×
90
                    n = int(math.floor(b.norm * 9)) + 1
×
91
                rendered.write(f'<span class="cov{n}" title="{b.count}">')
×
92
            else:
93
                rendered.write("</span>")
×
94
            boundaries = boundaries[1:]
×
95
        c = content[i]
×
96
        if c == ord(">"):
×
97
            rendered.write("&gt;")
×
98
        elif c == ord("<"):
×
99
            rendered.write("&lt;")
×
100
        elif c == ord("&"):
×
101
            rendered.write("&amp;")
×
102
        elif c == ord("\t"):
×
103
            rendered.write("        ")
×
104
        else:
105
            rendered.write(chr(c))
×
106
    return rendered.getvalue()
×
107

108

UNCOV
109
@rule
×
UNCOV
110
async def render_go_coverage_profile_to_html(
×
111
    request: RenderGoCoverageProfileToHtmlRequest,
112
) -> RenderGoCoverageProfileToHtmlResult:
113
    digest_contents = await get_digest_contents(request.sources_digest)
×
114
    profiles = parse_go_coverage_profiles(
×
115
        request.raw_coverage_profile, description_of_origin=request.description_of_origin
116
    )
117

118
    files: list[RenderedFile] = []
×
119
    pkg_name: str | None = None
×
120
    cover_mode_set = False
×
121
    for profile in profiles:
×
122
        if pkg_name is None:
×
123
            pkg_name = _get_pkg_name(profile.filename)
×
124
        if profile.cover_mode == GoCoverMode.SET:
×
125
            cover_mode_set = True
×
126

127
        name = PurePath(profile.filename).name
×
128

129
        file_contents: bytes | None = None
×
130
        full_file_path = str(PurePath(request.sources_dir_path, name))
×
131
        for entry in digest_contents:
×
132
            if entry.path == full_file_path:
×
133
                file_contents = entry.content
×
134
                break
×
135

136
        if file_contents is None:
×
137
            continue
×
138

139
        files.append(
×
140
            RenderedFile(
141
                name=name,
142
                body=_render_source_file(file_contents, profile.boundaries(file_contents)),
143
                coverage=_percent_covered(profile),
144
            )
145
        )
146

147
    rendered = chevron.render(
×
148
        template=_HTML_TEMPLATE,
149
        data={
150
            "pkg_name": pkg_name or "",
151
            "set": cover_mode_set,
152
            "files": [
153
                {
154
                    "i": i,
155
                    "name": file.name,
156
                    "coverage": f"{file.coverage:.1f}",
157
                    "body": file.body,
158
                }
159
                for i, file in enumerate(files)
160
            ],
161
        },
162
    )
163

164
    return RenderGoCoverageProfileToHtmlResult(rendered.encode())
×
165

166

UNCOV
167
_HTML_TEMPLATE = """\
×
168
<!DOCTYPE html>
169
<html>
170
    <head>
171
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
172
        <title>{{#pkg_name}}{{pkg_name}}: {{/pkg_name}}Go Coverage Report</title>
173
        <style>
174
            body {
175
                background: black;
176
                color: rgb(80, 80, 80);
177
            }
178
            body, pre, #legend span {
179
                font-family: Menlo, monospace;
180
                font-weight: bold;
181
            }
182
            #topbar {
183
                background: black;
184
                position: fixed;
185
                top: 0; left: 0; right: 0;
186
                height: 42px;
187
                border-bottom: 1px solid rgb(80, 80, 80);
188
            }
189
            #content {
190
                margin-top: 50px;
191
            }
192
            #nav, #legend {
193
                float: left;
194
                margin-left: 10px;
195
            }
196
            #legend {
197
                margin-top: 12px;
198
            }
199
            #nav {
200
                margin-top: 10px;
201
            }
202
            #legend span {
203
                margin: 0 5px;
204
            }
205
            <!--
206
            Colors generated by:
207
            def rgb(n):
208
                if n == 0:
209
                    return "rgb(192, 0, 0)" # Red
210
                # Gradient from gray to green.
211
                r = 128 - 12*(n-1)
212
                g = 128 + 12*(n-1)
213
                b = 128 + 3*(n-1)
214
                return f"rgb({r}, {g}, {b})"
215

216
            def colors():
217
                for i in range(11):
218
                    print(f".cov{i} {{ color: {rgb(i)} }}")
219
            -->
220
            .cov0 { color: rgb(192, 0, 0) }
221
            .cov1 { color: rgb(128, 128, 128) }
222
            .cov2 { color: rgb(116, 140, 131) }
223
            .cov3 { color: rgb(104, 152, 134) }
224
            .cov4 { color: rgb(92, 164, 137) }
225
            .cov5 { color: rgb(80, 176, 140) }
226
            .cov6 { color: rgb(68, 188, 143) }
227
            .cov7 { color: rgb(56, 200, 146) }
228
            .cov8 { color: rgb(44, 212, 149) }
229
            .cov9 { color: rgb(32, 224, 152) }
230
            .cov10 { color: rgb(20, 236, 155) }
231
        </style>
232
    </head>
233
    <body>
234
        <div id="topbar">
235
            <div id="nav">
236
                <select id="files">
237
                    {{#files}}
238
                    <option value="file{{i}}">{{name}} ({{coverage}}%)</option>
239
                    {{/files}}
240
                </select>
241
            </div>
242
            <div id="legend">
243
                <span>not tracked</span>
244
                {{#set}}
245
                <span class="cov0">not covered</span>
246
                <span class="cov8">covered</span>
247
                {{/set}}
248
                {{^set}}
249
                <span class="cov0">no coverage</span>
250
                <span class="cov1">low coverage</span>
251
                <span class="cov2">*</span>
252
                <span class="cov3">*</span>
253
                <span class="cov4">*</span>
254
                <span class="cov5">*</span>
255
                <span class="cov6">*</span>
256
                <span class="cov7">*</span>
257
                <span class="cov8">*</span>
258
                <span class="cov9">*</span>
259
                <span class="cov10">high coverage</span>
260
                {{/set}}
261
            </div>
262
        </div>
263
        <div id="content">
264
            {{#files}}
265
            <pre class="file" id="file{{i}}" style="display: none">{{{body}}}</pre>
266
            {{/files}}
267
        </div>
268
    </body>
269
    <script>
270
        (function() {
271
            var files = document.getElementById('files');
272
            var visible;
273
            files.addEventListener('change', onChange, false);
274
            function select(part) {
275
                if (visible)
276
                    visible.style.display = 'none';
277
                visible = document.getElementById(part);
278
                if (!visible)
279
                    return;
280
                files.value = part;
281
                visible.style.display = 'block';
282
                location.hash = part;
283
            }
284
            function onChange() {
285
                select(files.value);
286
                window.scrollTo(0, 0);
287
            }
288
            if (location.hash != "") {
289
                select(location.hash.substr(1));
290
            }
291
            if (!visible) {
292
                select("file0");
293
            }
294
        })();
295
    </script>
296
</html>
297
"""
298

299

UNCOV
300
def rules():
×
UNCOV
301
    return collect_rules()
×
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