Skip to content

Commit 09e6701

Browse files
committed
Audit vendored extensions by package, not by abi3 tag
abi3 answers only half the question. protobuf-py-ext's wheel is cp310-abi3-manylinux_2_17_x86_64: stable across CPython versions, and still unable to load on Windows, macOS or ARM. Letting the tag alone grant a pass would have waved through a future dependency whose extension is required, breaking every platform except the Linux builder, with no check firing. What makes an extension safe to vendor into a single cross-platform artifact is the package treating it as optional. Both of these do, so list them and fail on anything else: protobuf-py catches the failed import and falls back to pure Python, so its extension is kept for the platform it does load on, and nothing imports playhouse at all, so its 2 MB are dropped. Refuse a line in uv's export that is neither a comment, a --hash continuation, nor an option, instead of skipping it. Options add no distribution and stay skipped; an editable would, and silently leaves it outside every later check. Both found by Codex reviewing the branch.
1 parent dc1c161 commit 09e6701

1 file changed

Lines changed: 28 additions & 19 deletions

File tree

scripts/build.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,16 @@ def canonical_name(name: str) -> str:
7171
def requirement_names(requirements: str) -> "set[str]":
7272
names = set()
7373
for line in requirements.splitlines():
74-
# Continuation lines are indented; uv also emits option lines such as --index-url, which a
75-
# leading dash would otherwise turn into a requirement named "-index-url".
74+
if not line.strip() or line.startswith("#") or line[0].isspace():
75+
continue # blank, a comment, or an indented --hash continuation
76+
if line.startswith("--"):
77+
continue # --index-url and friends configure the install; they add no distribution
7678
match = re.match(r"[A-Za-z0-9][A-Za-z0-9._-]*", line)
77-
if match:
78-
names.add(canonical_name(match.group()))
79+
if not match:
80+
# Skipping silently is what makes this worth raising over: an editable (-e ../pkg)
81+
# installs something that every check below would then be blind to.
82+
raise SystemExit(f"cannot read {line!r} as a requirement; check what uv export emitted")
83+
names.add(canonical_name(match.group()))
7984
return names
8085

8186

@@ -134,27 +139,31 @@ def requirement_names(requirements: str) -> "set[str]":
134139
text=True,
135140
)
136141

137-
# One artifact ships to every interpreter and platform Anki runs on, so an extension compiled for
138-
# the one that installed it can only load by accident - and that is the layer's Python, 3.9 or 3.10,
139-
# not the build machine's. An abi3 extension is the exception: it declares an ABI stable across
140-
# CPython versions, which is how protobuf-py-ext ships. playhouse's are optional, so they are
141-
# dropped - peewee works without them and nothing imports playhouse. Anything else has to be looked
142-
# at before it reaches users rather than deleted quietly, since something may need it to import.
143-
DROPPABLE_EXTENSION_PACKAGES = ("playhouse",)
144-
145-
unloadable = []
142+
# A compiled extension is built for one interpreter and one platform, and this one artifact ships to
143+
# every combination Anki runs on. abi3 answers only the first half: protobuf-py-ext's wheel is
144+
# cp310-abi3-manylinux_2_17_x86_64, so it still cannot load on Windows, macOS or ARM. What makes an
145+
# extension safe to vendor is therefore the package treating it as optional, not its tag - and both
146+
# of these do. protobuf-py catches the failed import and falls back to pure Python, confirmed on
147+
# macOS arm64, so its extension is kept for the platform it does load on. Nothing imports playhouse
148+
# at all, so its 2 MB are dropped. Anything else fails the build rather than being kept or deleted
149+
# on a guess: kept, it breaks every platform but the builder; deleted, it breaks whatever needed it.
150+
KEPT_EXTENSION_PACKAGES = ("protobuf_ext",)
151+
DROPPED_EXTENSION_PACKAGES = ("playhouse",)
152+
153+
unaudited = []
146154
for pattern in ("*.so", "*.pyd"):
147155
for extension in ANKIHUB_LIB_TARGET.rglob(pattern):
148-
if ".abi3." in extension.name:
156+
package = extension.relative_to(ANKIHUB_LIB_TARGET).parts[0]
157+
if package in KEPT_EXTENSION_PACKAGES:
149158
continue
150-
if extension.relative_to(ANKIHUB_LIB_TARGET).parts[0] in DROPPABLE_EXTENSION_PACKAGES:
159+
if package in DROPPED_EXTENSION_PACKAGES:
151160
extension.unlink()
152161
else:
153-
unloadable.append(str(extension.relative_to(ANKIHUB_LIB_TARGET)))
154-
if unloadable:
162+
unaudited.append(str(extension.relative_to(ANKIHUB_LIB_TARGET)))
163+
if unaudited:
155164
raise SystemExit(
156-
f"{sorted(unloadable)} are compiled for one interpreter and platform, so they cannot load "
157-
"on the ones this artifact ships to; vendor an abi3 wheel or drop the extension"
165+
f"{sorted(unaudited)} are compiled extensions this artifact would ship to platforms they "
166+
"cannot load on; confirm the package falls back without them, then list it above"
158167
)
159168

160169
shutil.rmtree(ANKIHUB_LIB_TARGET / "bin", ignore_errors=True)

0 commit comments

Comments
 (0)