Skip to content

Commit 29a0ff6

Browse files
authored
external tool upgrade: fixed width + alphabetical sort for Platforms (#23095)
This implements the -- I think -- consensus --position from #23045, namely: * Lexicographic sort of platform * Fixed width platform: yes
1 parent 20e43d5 commit 29a0ff6

File tree

2 files changed

+102
-5
lines changed

2 files changed

+102
-5
lines changed

build-support/bin/external_tool_upgrade.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
replace_class_variables,
3636
)
3737
from packaging.specifiers import InvalidSpecifier, SpecifierSet
38-
from packaging.version import Version
38+
from packaging.version import InvalidVersion, Version
3939
from tqdm import tqdm
4040

4141
logger = logging.getLogger(__name__)
@@ -44,6 +44,13 @@
4444
# The ExternalToolVersion class is copied here to avoid depending on pants.
4545
# This makes it possible to run this as a standalone script with uv or use a
4646
# separate resolve in pants.
47+
48+
# Max platform name width for consistent formatting. Hardcoded rather than
49+
# computed from pants.engine.platform.Platform to maintain independent resolves.
50+
# If Platform gains values, update this constant.
51+
PLATFORM_WIDTH = 12
52+
53+
4754
@dataclass(frozen=True)
4855
class ExternalToolVersion:
4956
version: str
@@ -53,7 +60,8 @@ class ExternalToolVersion:
5360
url_override: str | None = None
5461

5562
def encode(self) -> str:
56-
parts = [self.version, self.platform, self.sha256, str(self.filesize)]
63+
padded_platform = self.platform.ljust(PLATFORM_WIDTH)
64+
parts = [self.version, padded_platform, self.sha256, str(self.filesize)]
5765
if self.url_override:
5866
parts.append(self.url_override)
5967
return "|".join(parts)
@@ -66,6 +74,29 @@ def decode(cls, version_str: str) -> ExternalToolVersion:
6674
return cls(version, platform, sha256, int(filesize), url_override=url_override)
6775

6876

77+
def _version_key(version_str: str) -> tuple[int, Version | str]:
78+
"""Return a sort key for version strings, handling non-PEP 440 versions.
79+
80+
Valid PEP 440 versions get priority (1, ...) so they sort first with reverse=True. Invalid
81+
versions get (0, ...) and fall back to string comparison.
82+
"""
83+
try:
84+
return (1, Version(version_str.lstrip("v")))
85+
except InvalidVersion:
86+
return (0, version_str)
87+
88+
89+
def sorted_by_version_and_platform(
90+
versions: list[ExternalToolVersion],
91+
) -> list[ExternalToolVersion]:
92+
"""Sort by version descending, then platform alphabetically.
93+
94+
See https://github.com/pantsbuild/pants/issues/23045
95+
"""
96+
by_platform = sorted(versions, key=lambda etv: etv.platform)
97+
return sorted(by_platform, key=lambda etv: _version_key(etv.version), reverse=True)
98+
99+
69100
def format_string_to_regex(format_string: str) -> re.Pattern:
70101
r"""Converts a format string to a regex.
71102
@@ -356,8 +387,7 @@ def main():
356387

357388
fetched_versions = set(external_versions)
358389

359-
known_versions = list(existing_versions | fetched_versions)
360-
known_versions.sort(key=lambda tu: (Version(tu.version), tu.platform), reverse=True)
390+
known_versions = sorted_by_version_and_platform(list(existing_versions | fetched_versions))
361391

362392
if args.version_constraint:
363393
# Only upgrade if the newest matching version is greater than current default

build-support/bin/external_tool_upgrade_test.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
import doctest
44

55
import external_tool_upgrade
6-
from external_tool_upgrade import ExternalToolVersion, filter_versions_by_constraint
6+
from external_tool_upgrade import (
7+
PLATFORM_WIDTH,
8+
ExternalToolVersion,
9+
filter_versions_by_constraint,
10+
sorted_by_version_and_platform,
11+
)
712
from packaging.version import Version
813

914

@@ -151,3 +156,65 @@ def test_version_constraint_with_v_prefix_upgrades_correctly() -> None:
151156

152157
result = _select_default_version_with_constraint(known_versions, current_default, constraint)
153158
assert result == "v5.0"
159+
160+
161+
def test_encode_pads_platform_to_fixed_width() -> None:
162+
v = ExternalToolVersion("1.0", "linux_arm64", "abc123", 100)
163+
encoded = v.encode()
164+
parts = encoded.split("|")
165+
assert parts[1] == "linux_arm64 "
166+
assert len(parts[1]) == PLATFORM_WIDTH
167+
168+
169+
def test_encode_no_padding_needed_for_max_width_platform() -> None:
170+
v = ExternalToolVersion("1.0", "macos_x86_64", "abc123", 100)
171+
encoded = v.encode()
172+
parts = encoded.split("|")
173+
assert parts[1] == "macos_x86_64"
174+
assert len(parts[1]) == PLATFORM_WIDTH
175+
176+
177+
def test_encode_decode_round_trip() -> None:
178+
original = ExternalToolVersion("2.0", "linux_arm64", "abc123def456", 999)
179+
decoded = ExternalToolVersion.decode(original.encode())
180+
assert decoded == original
181+
182+
183+
def test_sorted_by_version_and_platform() -> None:
184+
versions = [
185+
ExternalToolVersion("2.0", "macos_x86_64", "a", 1),
186+
ExternalToolVersion("1.0", "linux_arm64", "b", 2),
187+
ExternalToolVersion("2.0", "linux_arm64", "c", 3),
188+
ExternalToolVersion("1.0", "macos_x86_64", "d", 4),
189+
ExternalToolVersion("2.0", "linux_x86_64", "e", 5),
190+
]
191+
result = sorted_by_version_and_platform(versions)
192+
193+
expected_order = [
194+
("2.0", "linux_arm64"),
195+
("2.0", "linux_x86_64"),
196+
("2.0", "macos_x86_64"),
197+
("1.0", "linux_arm64"),
198+
("1.0", "macos_x86_64"),
199+
]
200+
actual_order = [(v.version, v.platform) for v in result]
201+
assert actual_order == expected_order
202+
203+
204+
def test_sorted_by_version_and_platform_with_non_pep440_versions() -> None:
205+
versions = [
206+
ExternalToolVersion("v2.1.0-M5-18-gfebf9838c", "linux_x86_64", "a", 1),
207+
ExternalToolVersion("v2.1.24", "linux_x86_64", "b", 2),
208+
ExternalToolVersion("v2.1.6", "linux_x86_64", "c", 3),
209+
ExternalToolVersion("v2.0.16-169-g194ebc55c", "linux_x86_64", "d", 4),
210+
]
211+
result = sorted_by_version_and_platform(versions)
212+
213+
expected_order = [
214+
"v2.1.24",
215+
"v2.1.6",
216+
"v2.1.0-M5-18-gfebf9838c",
217+
"v2.0.16-169-g194ebc55c",
218+
]
219+
actual_order = [v.version for v in result]
220+
assert actual_order == expected_order

0 commit comments

Comments
 (0)