Skip to content

Commit 4990dbb

Browse files
authored
Add command line flag to avoid overwriting grpc wheels (#318)
* Add command line flag that avoid overwite of grpc wheel Add a flag --skip_exists that can let us avoid overiting a grpc wheel built by home assistant builder with a grpc wheel from pypi. * Use old python3.8 typing * Make comparisions using set to avoid ordering issues * Address lint errors found by tox * Address black format issues * Revert change to combine arguments to make code review easier * Apply black format chagnes * Remove --skip_exists and always use --skip_binary to remove local existing wheels
1 parent c7ae562 commit 4990dbb

File tree

3 files changed

+141
-28
lines changed

3 files changed

+141
-28
lines changed

builder/__main__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
check_available_binary,
1616
create_wheels_folder,
1717
create_wheels_index,
18+
remove_local_wheels,
1819
)
1920
from builder.pip import (
2021
build_wheels_local,
@@ -186,6 +187,17 @@ def builder(
186187
run_auditwheel(wheels_dir)
187188

188189
fix_wheels_name(wheels_dir)
190+
191+
if skip_binary != ":none:":
192+
# Some wheels that already exist should not be overwritten in case we replace with
193+
# a wheel that came from pypi rather than a wheel built from source with extra flags.
194+
# When --skip-binary and --skip-exists are set a wheel is only built from source once.
195+
packages = extract_packages(requirement, requirement_diff)
196+
constraints = parse_requirements(constraint) if constraint else []
197+
remove_local_wheels(
198+
wheels_index, skip_binary.split(","), packages + constraints, wheels_dir
199+
)
200+
189201
if not test:
190202
run_upload(upload, output, remote)
191203

builder/infra.py

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Create folder structure for index."""
22
from pathlib import Path
33
import re
4-
from typing import List, Set
4+
from typing import List, Set, Dict
55

66
import requests
77

@@ -23,6 +23,29 @@ def create_wheels_index(base_index: str) -> str:
2323
return f"{base_index}/{alpine_version()}/{build_arch()}/"
2424

2525

26+
def create_package_map(packages: List[str]) -> Dict[str, str]:
27+
"""Create a dictionary from package base name to package and version string."""
28+
results = {}
29+
for package in packages.copy():
30+
find = _RE_REQUIREMENT.match(package)
31+
if not find:
32+
continue
33+
package = find["package"]
34+
version = find["version"]
35+
results[package] = f"{package}-{version}"
36+
return results
37+
38+
39+
def check_existing_packages(index: str, package_map: Dict[str, str]) -> Set[str]:
40+
"""Return the set of package names that already exist in the index."""
41+
available_data = requests.get(index, allow_redirects=True).text
42+
found: Set[str] = set({})
43+
for (binary, package) in package_map.items():
44+
if package in available_data:
45+
found.add(binary)
46+
return found
47+
48+
2649
def check_available_binary(
2750
index: str, skip_binary: str, packages: List[str], constraints: List[str]
2851
) -> str:
@@ -31,33 +54,50 @@ def check_available_binary(
3154
return skip_binary
3255

3356
list_binary = skip_binary.split(",")
34-
available_data = requests.get(index, allow_redirects=True).text
35-
36-
list_needed: Set[str] = set()
37-
for binary in list_binary:
38-
for package in packages + constraints:
39-
if not package.startswith(binary):
40-
continue
4157

42-
# Check more details
43-
find = _RE_REQUIREMENT.match(package)
44-
if not find:
45-
raise ValueError(f"package requirement malformed: {package}")
58+
# Map of package basename to the desired package version
59+
package_map = create_package_map(packages + constraints)
4660

47-
# Check full name
48-
if binary != find["package"]:
49-
continue
50-
51-
# Process packages
52-
name = f"{binary}-{find['version']}"
53-
if name in available_data:
54-
continue
55-
56-
# Ignore binary
57-
print(f"Ignore Binary {package}: {name}", flush=True)
58-
list_needed.add(binary)
61+
# View of package map limited to packages in --skip-binary
62+
binary_package_map = {}
63+
for binary in list_binary:
64+
if not (package := package_map.get(binary)):
65+
print(
66+
f"Skip binary '{binary}' not in packages/constraints; Can't determine desired version",
67+
flush=True,
68+
)
69+
continue
70+
binary_package_map[binary] = package
71+
72+
print(f"Checking if binaries already exist for packages {binary_package_map}")
73+
list_found: Set[str] = check_existing_packages(index, binary_package_map)
74+
print(f"Packages already exist: {list_found}")
75+
list_needed = binary_package_map.keys() - list_found
5976

6077
# Generate needed list of skip binary
6178
if not list_needed:
6279
return ":none:"
80+
81+
print(f"Will force binary build for {list_needed}")
6382
return ",".join(list_needed)
83+
84+
85+
def remove_local_wheels(
86+
index: str,
87+
skip_exists: List[str],
88+
packages: List[str],
89+
wheels_dir: Path,
90+
) -> str:
91+
"""Remove existing wheels if they already exist in the index to avoid syncing."""
92+
package_map = create_package_map(packages)
93+
binary_package_map = {
94+
name: package_map[name] for name in skip_exists if name in package_map
95+
}
96+
print(f"Checking if binaries already exist for packages {binary_package_map}")
97+
exists = check_existing_packages(index, binary_package_map)
98+
for binary in exists:
99+
package = binary_package_map[binary]
100+
print(f"Found existing wheels for {binary}, removing local copy {package}")
101+
for wheel in wheels_dir.glob(f"{package}-*.whl"):
102+
print(f"Removing local wheel {wheel}")
103+
wheel.unlink()

tests/test_infra.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
"""Tests for infra module.
2-
3-
pip module."""
1+
"""Tests for infra module."""
42
import pytest
5-
from builder.infra import check_available_binary
3+
from pathlib import Path
4+
from builder.infra import check_available_binary, remove_local_wheels
65
from unittest.mock import patch
76

87

@@ -147,3 +146,65 @@ def test_check_available_binary_for_missing_constraint() -> None:
147146
)
148147
== "grpcio"
149148
)
149+
150+
151+
@pytest.fixture
152+
def tmppath(tmpdir):
153+
return Path(tmpdir)
154+
155+
156+
def test_remove_local_wheel(tmppath: Path) -> None:
157+
"""Test removing an existing wheel."""
158+
159+
p = tmppath / "google_cloud_pubsub-2.9.0-py2.py3-none-any.whl"
160+
p.touch()
161+
p = tmppath / "grpcio-1.31.0-cp39-none-any.whl"
162+
p.touch()
163+
assert {p.name for p in tmppath.glob("*.whl")} == {
164+
"grpcio-1.31.0-cp39-none-any.whl",
165+
"google_cloud_pubsub-2.9.0-py2.py3-none-any.whl",
166+
}
167+
168+
remove_local_wheels(
169+
TEST_INDEX_URL,
170+
skip_exists=["grpcio"],
171+
packages=[
172+
"google_cloud_pubsub==2.9.0",
173+
"grpcio==1.31.0", # Exists in index
174+
],
175+
wheels_dir=tmppath,
176+
)
177+
178+
# grpc is removed
179+
assert {p.name for p in tmppath.glob("*.whl")} == {
180+
"google_cloud_pubsub-2.9.0-py2.py3-none-any.whl",
181+
}
182+
183+
184+
def test_remove_local_wheel_preserves_newer(tmppath: Path) -> None:
185+
"""Test that the wheel is preserved when newer than in the index."""
186+
187+
p = tmppath / "google_cloud_pubsub-2.9.0-py2.py3-none-any.whl"
188+
p.touch()
189+
p = tmppath / "grpcio-1.43.0-cp39-none-any.whl"
190+
p.touch()
191+
assert {p.name for p in tmppath.glob("*.whl")} == {
192+
"grpcio-1.43.0-cp39-none-any.whl",
193+
"google_cloud_pubsub-2.9.0-py2.py3-none-any.whl",
194+
}
195+
196+
remove_local_wheels(
197+
TEST_INDEX_URL,
198+
skip_exists=["grpcio"],
199+
packages=[
200+
"google_cloud_pubsub==2.9.0",
201+
"grpcio==1.43.0", # Newer than index
202+
],
203+
wheels_dir=tmppath,
204+
)
205+
206+
# grpc is removed
207+
assert {p.name for p in tmppath.glob("*.whl")} == {
208+
"grpcio-1.43.0-cp39-none-any.whl",
209+
"google_cloud_pubsub-2.9.0-py2.py3-none-any.whl",
210+
}

0 commit comments

Comments
 (0)