Skip to content

Commit f294e20

Browse files
hoodmaneryanking13pre-commit-ci[bot]
authored
Make needs_rebuild aware of wheel tags (#134)
This switches to using the wheel itself instead of the ".packaged" marker to decide if a wheel needs to be rebuilt. Static and dynamic libraries still use the ".packaged" marker. This solves the problem that whenever I switch branches between a Python update branch and any other branch, absolutely everything has to be rebuilt. Instead, this saves the wheels from each branch next to each other. This should also resolve the annoying `Found multiple matching wheels` errors when switching abis or Python versions. --------- Co-authored-by: Gyeongjae Choi <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 147c155 commit f294e20

File tree

7 files changed

+184
-89
lines changed

7 files changed

+184
-89
lines changed

pyodide_build/build_env.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import re
77
import subprocess
88
import sys
9-
from collections.abc import Iterator
9+
from collections.abc import Iterator, Sequence
1010
from contextlib import nullcontext, redirect_stdout
1111
from io import StringIO
1212
from pathlib import Path
@@ -219,7 +219,7 @@ def wheel_platform() -> str:
219219
return f"pyodide_{abi_version}_wasm32"
220220

221221

222-
def pyodide_tags() -> Iterator[Tag]:
222+
def pyodide_tags_() -> Iterator[Tag]:
223223
"""
224224
Returns the sequence of tag triples for the Pyodide interpreter.
225225
@@ -235,6 +235,11 @@ def pyodide_tags() -> Iterator[Tag]:
235235
yield Tag(interpreter=f"cp{PYMAJOR}{PYMINOR}", abi="none", platform="any")
236236

237237

238+
@functools.cache
239+
def pyodide_tags() -> Sequence[Tag]:
240+
return list(pyodide_tags_())
241+
242+
238243
def replace_so_abi_tags(wheel_dir: Path) -> None:
239244
"""Replace native abi tag with emscripten abi tag in .so file names"""
240245
import sysconfig

pyodide_build/common.py

+40-12
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import warnings
1515
import zipfile
1616
from collections import deque
17-
from collections.abc import Generator, Iterable, Iterator, Mapping
17+
from collections.abc import Generator, Iterable, Iterator, Mapping, Sequence
1818
from contextlib import contextmanager
1919
from pathlib import Path
2020
from tempfile import NamedTemporaryFile, TemporaryDirectory
@@ -35,12 +35,16 @@ def xbuildenv_dirname() -> str:
3535
return f".pyodide-xbuildenv-{__version__}"
3636

3737

38-
def find_matching_wheels(
39-
wheel_paths: Iterable[Path], supported_tags: Iterator[Tag]
38+
def _find_matching_wheels(
39+
wheel_paths: Iterable[Path],
40+
supported_tags: Sequence[Tag],
41+
version: str | None = None,
4042
) -> Iterator[Path]:
4143
"""
4244
Returns the sequence wheels whose tags match the Pyodide interpreter.
4345
46+
We don't bother ordering them carefully because we are only hoping to find one.
47+
4448
Parameters
4549
----------
4650
wheel_paths
@@ -52,17 +56,41 @@ def find_matching_wheels(
5256
-------
5357
The subset of wheel_paths that have tags that match the Pyodide interpreter.
5458
"""
55-
wheel_paths = list(wheel_paths)
56-
wheel_tags_list: list[frozenset[Tag]] = []
57-
58-
for wheel in wheel_paths:
59-
_, _, _, tags = parse_wheel_filename(wheel.name)
60-
wheel_tags_list.append(tags)
61-
62-
for supported_tag in supported_tags:
63-
for wheel_path, wheel_tags in zip(wheel_paths, wheel_tags_list, strict=True):
59+
for wheel_path in wheel_paths:
60+
_, wheel_version, _, wheel_tags = parse_wheel_filename(wheel_path.name)
61+
if version and version != str(wheel_version):
62+
continue
63+
for supported_tag in supported_tags:
6464
if supported_tag in wheel_tags:
6565
yield wheel_path
66+
continue
67+
68+
69+
def find_matching_wheel(
70+
wheel_paths: Iterable[Path], supported_tags: Sequence[Tag], version: str = None
71+
) -> Path | None:
72+
"""
73+
Find a matching wheel or raise an error if none is present.
74+
75+
Parameters
76+
----------
77+
wheel_paths
78+
A list of paths to wheels
79+
supported_tags
80+
A list of tags that the environment supports
81+
82+
Returns
83+
-------
84+
The subset of wheel_paths that have tags that match the Pyodide interpreter.
85+
"""
86+
result = list(_find_matching_wheels(wheel_paths, supported_tags, version))
87+
if not result:
88+
return None
89+
if len(result) > 1:
90+
raise RuntimeError(
91+
"Found multiple matching wheels:\n" + "\n".join(w.name for w in result)
92+
)
93+
return result[0]
6694

6795

6896
def parse_top_level_import_name(whlfile: Path) -> list[str] | None:

pyodide_build/recipe/builder.py

+36-18
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
_get_sha256_checksum,
3434
chdir,
3535
exit_with_stdio,
36-
find_matching_wheels,
36+
find_matching_wheel,
3737
make_zip_archive,
3838
modify_wheel,
3939
retag_wheel,
@@ -119,6 +119,7 @@ def __init__(
119119
self.source_metadata = self.recipe.source
120120
self.build_metadata = self.recipe.build
121121
self.package_type = self.build_metadata.package_type
122+
self.is_wheel = self.package_type in ["package", "cpython_module"]
122123

123124
self.build_dir = (
124125
Path(build_dir).resolve() if build_dir else self.pkg_root / "build"
@@ -176,7 +177,8 @@ def build(self) -> None:
176177
try:
177178
self._build()
178179

179-
(self.build_dir / ".packaged").touch()
180+
if not self.is_wheel:
181+
(self.build_dir / ".packaged").touch()
180182
except (Exception, KeyboardInterrupt):
181183
success = False
182184
raise
@@ -199,7 +201,11 @@ def _build_package(self, bash_runner: BashRunnerWithSharedEnvironment) -> None:
199201

200202
def _build(self) -> None:
201203
if not self.force_rebuild and not needs_rebuild(
202-
self.pkg_root, self.build_dir, self.source_metadata
204+
self.pkg_root,
205+
self.build_dir,
206+
self.source_metadata,
207+
self.is_wheel,
208+
self.version,
203209
):
204210
return
205211

@@ -523,8 +529,7 @@ def _build_package(self, bash_runner: BashRunnerWithSharedEnvironment) -> None:
523529
self._compile(bash_runner)
524530

525531
self._package_wheel(bash_runner)
526-
shutil.rmtree(self.dist_dir, ignore_errors=True)
527-
shutil.copytree(self.src_dist_dir, self.dist_dir)
532+
shutil.copytree(self.src_dist_dir, self.dist_dir, dirs_exist_ok=True)
528533

529534
def _package_wheel(
530535
self,
@@ -541,12 +546,13 @@ def _package_wheel(
541546
The runner we will use to execute our bash commands. Preserves
542547
environment variables from one invocation to the next.
543548
"""
544-
wheel, *rest = find_matching_wheels(
545-
self.src_dist_dir.glob("*.whl"), pyodide_tags()
549+
wheel = find_matching_wheel(
550+
self.src_dist_dir.glob("*.whl"), pyodide_tags(), version=self.version
546551
)
547-
if rest:
548-
raise Exception(
549-
f"Unexpected number of wheels {len(rest) + 1} when building {self.name}"
552+
if not wheel:
553+
raise RuntimeError(
554+
f"Found no wheel while building {self.name}. Candidates:\n"
555+
+ "\n".join(f.name for f in self.src_dist_dir.glob("*.whl"))
550556
)
551557

552558
if "emscripten" in wheel.name:
@@ -781,7 +787,11 @@ def unvendor_tests(
781787

782788
# TODO: move this to common.py or somewhere else
783789
def needs_rebuild(
784-
pkg_root: Path, buildpath: Path, source_metadata: _SourceSpec
790+
pkg_root: Path,
791+
buildpath: Path,
792+
source_metadata: _SourceSpec,
793+
is_wheel: bool = True,
794+
version: str | None = None,
785795
) -> bool:
786796
"""
787797
Determines if a package needs a rebuild because its meta.yaml, patches, or
@@ -798,14 +808,22 @@ def needs_rebuild(
798808
src_metadata
799809
The source section from meta.yaml.
800810
"""
801-
packaged_token = buildpath / ".packaged"
802-
if not packaged_token.is_file():
803-
logger.debug(
804-
"%s needs rebuild because %s does not exist", pkg_root, packaged_token
811+
dist_dir = pkg_root / "dist"
812+
if is_wheel:
813+
previous_wheel = find_matching_wheel(
814+
dist_dir.glob("*.whl"), pyodide_tags(), version=version
805815
)
806-
return True
807-
808-
package_time = packaged_token.stat().st_mtime
816+
if not previous_wheel:
817+
return True
818+
package_time = previous_wheel.stat().st_mtime
819+
else:
820+
packaged_token = buildpath / ".packaged"
821+
if not packaged_token.is_file():
822+
logger.debug(
823+
"%s needs rebuild because %s does not exist", pkg_root, packaged_token
824+
)
825+
return True
826+
package_time = packaged_token.stat().st_mtime
809827

810828
def source_files() -> Iterator[Path]:
811829
yield pkg_root / "meta.yaml"

pyodide_build/recipe/graph_builder.py

+15-11
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
download_and_unpack_archive,
3434
exit_with_stdio,
3535
extract_wheel_metadata_file,
36-
find_matching_wheels,
36+
find_matching_wheel,
3737
find_missing_executables,
3838
repack_zip_archive,
3939
)
@@ -78,6 +78,7 @@ def __init__(self, pkgdir: Path, config: MetaConfig):
7878
self.version = self.meta.package.version
7979
self.disabled = self.meta.package.disabled
8080
self.package_type = self.meta.build.package_type
81+
self.is_wheel = self.package_type in ["package", "cpython_module"]
8182

8283
assert self.name == pkgdir.name, f"{self.name} != {pkgdir.name}"
8384

@@ -119,7 +120,14 @@ def build_path(self, build_dir: Path) -> Path:
119120
return build_dir / self.name / "build"
120121

121122
def needs_rebuild(self, build_dir: Path) -> bool:
122-
return needs_rebuild(self.pkgdir, self.build_path(build_dir), self.meta.source)
123+
res = needs_rebuild(
124+
self.pkgdir,
125+
self.build_path(build_dir),
126+
self.meta.source,
127+
self.is_wheel,
128+
self.version,
129+
)
130+
return res
123131

124132
def build(self, build_args: BuildArgs, build_dir: Path) -> None:
125133
p = subprocess.run(
@@ -179,16 +187,12 @@ def __init__(self, pkgdir: Path, config: MetaConfig) -> None:
179187

180188
def dist_artifact_path(self) -> Path | None:
181189
dist_dir = self.pkgdir / "dist"
182-
candidates = list(
183-
find_matching_wheels(dist_dir.glob("*.whl"), build_env.pyodide_tags())
190+
wheel = find_matching_wheel(
191+
dist_dir.glob("*.whl"), build_env.pyodide_tags(), version=self.version
184192
)
185-
186-
if len(candidates) != 1:
187-
raise RuntimeError(
188-
f"Unexpected number of wheels/archives {len(candidates)} when building {self.name}"
189-
)
190-
191-
return candidates[0]
193+
if not wheel:
194+
raise RuntimeError(f"Found no wheel while building {self.name}")
195+
return wheel
192196

193197
def tests_path(self) -> Path | None:
194198
tests = list((self.pkgdir / "dist").glob("*-tests.tar"))

pyodide_build/tests/recipe/test_builder.py

+35-9
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,23 @@ def _check_patches_extra(self) -> Self:
248248
return self
249249

250250

251-
def test_needs_rebuild(tmpdir):
251+
@pytest.mark.parametrize("is_wheel", [False, True])
252+
def test_needs_rebuild(tmpdir, is_wheel):
252253
pkg_root = Path(tmpdir)
253254
buildpath = pkg_root / "build"
254255
meta_yaml = pkg_root / "meta.yaml"
255-
packaged = buildpath / ".packaged"
256+
version = "12"
257+
if is_wheel:
258+
dist_dir = pkg_root / "dist"
259+
dist_dir.mkdir()
260+
# Build of current version with wrong abi
261+
(dist_dir / "regex-12-cp311-cp311-pyodide_2024_0_wasm32.whl").touch()
262+
# Build of old version with current abi
263+
(dist_dir / "regex-11-cp312-cp312-pyodide_2024_0_wasm32.whl").touch()
264+
# the version we're trying to build
265+
packaged = dist_dir / "regex-12-cp312-cp312-pyodide_2024_0_wasm32.whl"
266+
else:
267+
packaged = buildpath / ".packaged"
256268

257269
patch_file = pkg_root / "patch"
258270
extra_file = pkg_root / "extra"
@@ -277,39 +289,53 @@ def test_needs_rebuild(tmpdir):
277289
src_path_file.touch()
278290

279291
# No .packaged file, rebuild
280-
assert _builder.needs_rebuild(pkg_root, buildpath, source_metadata) is True
292+
assert _builder.needs_rebuild(
293+
pkg_root, buildpath, source_metadata, is_wheel, version
294+
)
281295

282296
# .packaged file exists, no rebuild
283297
packaged.touch()
284-
assert _builder.needs_rebuild(pkg_root, buildpath, source_metadata) is False
298+
assert not _builder.needs_rebuild(
299+
pkg_root, buildpath, source_metadata, is_wheel, version
300+
)
285301

286302
# newer meta.yaml file, rebuild
287303
packaged.touch()
288304
time.sleep(0.01)
289305
meta_yaml.touch()
290-
assert _builder.needs_rebuild(pkg_root, buildpath, source_metadata) is True
306+
assert _builder.needs_rebuild(
307+
pkg_root, buildpath, source_metadata, is_wheel, version
308+
)
291309

292310
# newer patch file, rebuild
293311
packaged.touch()
294312
time.sleep(0.01)
295313
patch_file.touch()
296-
assert _builder.needs_rebuild(pkg_root, buildpath, source_metadata) is True
314+
assert _builder.needs_rebuild(
315+
pkg_root, buildpath, source_metadata, is_wheel, version
316+
)
297317

298318
# newer extra file, rebuild
299319
packaged.touch()
300320
time.sleep(0.01)
301321
extra_file.touch()
302-
assert _builder.needs_rebuild(pkg_root, buildpath, source_metadata) is True
322+
assert _builder.needs_rebuild(
323+
pkg_root, buildpath, source_metadata, is_wheel, version
324+
)
303325

304326
# newer source path, rebuild
305327
packaged.touch()
306328
time.sleep(0.01)
307329
src_path_file.touch()
308-
assert _builder.needs_rebuild(pkg_root, buildpath, source_metadata) is True
330+
assert _builder.needs_rebuild(
331+
pkg_root, buildpath, source_metadata, is_wheel, version
332+
)
309333

310334
# newer .packaged file, no rebuild
311335
packaged.touch()
312-
assert _builder.needs_rebuild(pkg_root, buildpath, source_metadata) is False
336+
assert not _builder.needs_rebuild(
337+
pkg_root, buildpath, source_metadata, is_wheel, version
338+
)
313339

314340

315341
def test_copy_sharedlib(tmp_path):

pyodide_build/tests/test_build_env.py

+18-14
Original file line numberDiff line numberDiff line change
@@ -219,17 +219,21 @@ def test_wheel_paths(dummy_xbuildenv):
219219
strings.append(f"wrapt-1.13.3-{interp}-{abi}-{arch}.whl")
220220

221221
paths = [Path(x) for x in strings]
222-
assert [
223-
x.stem.split("-", 2)[-1]
224-
for x in common.find_matching_wheels(paths, build_env.pyodide_tags())
225-
] == [
226-
f"{current_version}-{current_version}-{PLATFORM}",
227-
f"{current_version}-abi3-{PLATFORM}",
228-
f"{current_version}-none-{PLATFORM}",
229-
f"{old_version}-abi3-{PLATFORM}",
230-
f"py3-none-{PLATFORM}",
231-
f"py2.py3-none-{PLATFORM}",
232-
"py3-none-any",
233-
"py2.py3-none-any",
234-
f"{current_version}-none-any",
235-
]
222+
assert sorted(
223+
[
224+
x.stem.split("-", 2)[-1]
225+
for x in common._find_matching_wheels(paths, build_env.pyodide_tags())
226+
]
227+
) == sorted(
228+
[
229+
f"{current_version}-{current_version}-{PLATFORM}",
230+
f"{current_version}-abi3-{PLATFORM}",
231+
f"{current_version}-none-{PLATFORM}",
232+
f"{old_version}-abi3-{PLATFORM}",
233+
f"py3-none-{PLATFORM}",
234+
f"py2.py3-none-{PLATFORM}",
235+
"py3-none-any",
236+
"py2.py3-none-any",
237+
f"{current_version}-none-any",
238+
]
239+
)

0 commit comments

Comments
 (0)