studio/install: repair upstream llama.cpp prebuilt mangled symlinks (#5465)

The macos-arm64 prebuilt tarball for llama.cpp b9165 and b9169 ships
symlinks whose linkname is missing both the directory separator AND
the leading character of the target basename:

  llama-b9165/libggml-rpc.0.dylib -> llama-b9165ibggml-rpc.0.11.1.dylib

extract_tar_safely correctly classified those as unresolved and made
install.sh fall back to source-build, which Mac CI then fails as a
hard error (Studio must use the prebuilt llama-bNNNN-bin-macos-arm64
on Apple Silicon).

Add _try_repair_missing_slash inside safe_link_target: when a
linkname starts with the member's top-level dir but no following
slash, search the archive for an entry under that dir whose name
ends with the mangled suffix. Accept only when the suffix uniquely
identifies a real archive entry, so legitimate archives are
untouched.

Verified against /tmp/llama-b9165.tar.gz: all 18 link entries
repair to real files in the archive.
This commit is contained in:
Daniel Han 2026-05-15 14:44:52 -07:00 committed by GitHub
commit 4f59c8e539
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -3433,10 +3433,52 @@ def extract_archive(archive_path: Path, destination: Path) -> None:
) from exc
return target
def _try_repair_missing_slash(
member_name: str, link_name: str, archive_names: set[str]
) -> str | None:
"""Some upstream llama.cpp Mac releases (e.g. b9165, b9169) ship
symlinks whose linkname is missing the directory separator AND
the leading character of the file basename between the
top-level dir and the rest of the path:
llama-b9165/libggml-rpc.0.dylib -> llama-b9165ibggml-rpc.0.11.1.dylib
That cannot be resolved as written. Detect the pattern
(linkname starts with the top-level dir name but no following
slash) and search archive entries under that dir for a real
file whose basename ends with the mangled suffix. Only accept
when the suffix uniquely identifies a real archive entry."""
if "/" not in member_name or "/" in link_name:
return None
top, _, _ = member_name.partition("/")
if not link_name.startswith(top) or len(link_name) <= len(top):
return None
bad_suffix = link_name[len(top) :]
if not bad_suffix or bad_suffix.startswith("/"):
return None
prefix = f"{top}/"
candidates = [
name
for name in archive_names
if name.startswith(prefix)
and "/" not in name[len(prefix) :]
and name[len(prefix) :].endswith(bad_suffix)
]
if len(candidates) != 1:
return None
return candidates[0]
def safe_link_target(
base: Path, member_name: str, link_name: str, target: Path
base: Path,
member_name: str,
link_name: str,
target: Path,
archive_names: set[str],
) -> tuple[str, Path]:
normalized = link_name.replace("\\", "/")
repaired = _try_repair_missing_slash(member_name, normalized, archive_names)
if repaired is not None:
normalized = repaired
link_path = Path(normalized)
if link_path.is_absolute():
raise PrebuiltFallback(
@ -3473,8 +3515,10 @@ def extract_archive(archive_path: Path, destination: Path) -> None:
def extract_tar_safely(source: Path, base: Path) -> None:
pending_links: list[tuple[tarfile.TarInfo, Path]] = []
archive_names: set[str] = set()
with tarfile.open(source, "r:gz") as archive:
for member in archive.getmembers():
archive_names.add(member.name)
target = safe_extract_path(base, member.name)
if member.isdir():
target.mkdir(parents = True, exist_ok = True)
@ -3501,7 +3545,7 @@ def extract_archive(archive_path: Path, destination: Path) -> None:
progressed = False
for member, target in unresolved:
normalized_link, resolved_target = safe_link_target(
base, member.name, member.linkname, target
base, member.name, member.linkname, target, archive_names
)
if not resolved_target.exists() and not resolved_target.is_symlink():
next_round.append((member, target))