Files
plezy/scripts/set_mpvkit_revision.sh
edde746 adef523894 chore(build): add a SwiftPM refresh script for stale MPVKit pins
After set_mpvkit_revision.sh moves the MPVKit pin, macOS builds die in the
SwiftPM integration migration with "could not find the commit <sha>": Flutter
runs the macOS Xcode steps with -skipPackageUpdates, and its one fetch-capable
step only ever runs for iOS, so the local mirror never learns the new revision.

scripts/refresh_apple_spm.sh runs a single -resolvePackageDependencies per
platform to update the mirror without touching the tracked locks, with --reset
for an internally inconsistent SourcePackages directory. The header of
set_mpvkit_revision.sh now points at it.
2026-08-24 06:12:06 +02:00

216 lines
7.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Move Plezy's MPVKit dependency to an exact upstream commit.
#
# Why this exists: the MPVKit fork publishes content-addressed binaries on every
# push to main -- each xcframework asset is named after a hash of the inputs that
# produced it, so the artifacts for any commit stay downloadable forever and are
# never overwritten. A commit, not a semver tag, is therefore the unit Plezy
# pins: picking up an mpv or FFmpeg patch no longer requires cutting a release
# upstream, and the pin names the exact bytes we ship.
#
# Nine tracked files carry that pin and must move together:
# <p>/Runner.xcodeproj/project.pbxproj the requirement
# <p>/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/... the SwiftPM lock
# <p>/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved Xcode's duplicate lock
# for p in ios, macos, tvos. The duplicate locks are not redundant to us:
# scripts/checks/check_apple_spm_locks.py fails the build when a pair drifts.
#
# Nothing else needs editing: tvos/scripts/wire_mpv.rb reads the sha back out of
# the tvOS lock, so re-wiring the tvOS project cannot revert a bump made here,
# and tvos/scripts/test_wire_mpv.rb asserts all nine sites name one commit.
#
# Usage:
# scripts/set_mpvkit_revision.sh <full-40-char-commit-sha>
#
# Rerunning with the same sha is a no-op and says so. Edits are targeted text
# replacements on purpose: `plutil -convert` round-trips would reformat an entire
# pbxproj, and re-serializing Package.resolved would churn every unrelated pin.
# Each file must match exactly once; anything else aborts before a byte is
# written. The locks name the new revision, but no local SwiftPM mirror fetches
# it on its own: a macOS Flutter build resolves with `-skipPackageUpdates` and
# aborts with "could not find the commit <sha>" until the mirror catches up. Run
# scripts/refresh_apple_spm.sh afterwards (or open the project in Xcode once).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT"
if [ "$#" -ne 1 ]; then
echo "usage: scripts/set_mpvkit_revision.sh <full-40-char-commit-sha>" >&2
exit 2
fi
REVISION="$1"
if [[ ! "$REVISION" =~ ^[0-9a-f]{40}$ ]]; then
echo "error: '$REVISION' is not a full 40-character lowercase commit sha" >&2
echo "hint: git -C ../MPVKit rev-parse main" >&2
exit 2
fi
PROJECTS=()
LOCKS=()
MISSING=()
for platform in ios macos tvos; do
PROJECTS+=("$platform/Runner.xcodeproj/project.pbxproj")
LOCKS+=("$platform/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved")
LOCKS+=("$platform/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved")
done
for target in "${PROJECTS[@]}" "${LOCKS[@]}"; do
if [ ! -f "$target" ]; then
MISSING+=("$target")
fi
done
if [ "${#MISSING[@]}" -gt 0 ]; then
echo "error: missing MPVKit pin site(s); refusing to run:" >&2
printf ' %s\n' "${MISSING[@]}" >&2
exit 1
fi
MPVKIT_REVISION="$REVISION" python3 - "${#PROJECTS[@]}" "${PROJECTS[@]}" "${LOCKS[@]}" <<'PY'
"""Rewrite the MPVKit requirement and SwiftPM pins in place.
Every file is parsed and rewritten in memory first; nothing is written unless
all nine edits are unambiguous, so a malformed file can never leave the pin
half-moved.
"""
from __future__ import annotations
import json
import os
import re
import sys
revision = os.environ["MPVKIT_REVISION"]
project_count = int(sys.argv[1])
projects = sys.argv[2 : 2 + project_count]
locks = sys.argv[2 + project_count :]
# The `*/ = {` suffix is what makes this anchor unique: the same comment appears
# in packageReferences lists and product dependencies, but only the object
# definition opens a brace.
REQUIREMENT = re.compile(
r'/\* XCRemoteSwiftPackageReference "MPVKit" \*/ = \{\n'
r"(?:[^\n]*\n)*?"
r"(?P<indent>[\t ]*)requirement = \{\n"
r"(?P<body>(?:[^\n]*\n)*?)"
r"(?P=indent)\};\n"
)
PIN_STATE = re.compile(
r'"identity"[ \t]*:[ \t]*"mpvkit",\n'
r"(?:[^\n]*\n)*?"
r'(?P<indent>[ \t]*)"state"[ \t]*:[ \t]*\{\n'
r"(?P<body>(?:[^\n]*\n)*?)"
r"(?P=indent)\}"
)
errors: list[str] = []
writes: list[tuple[str, str]] = []
unchanged: list[str] = []
def replace_once(pattern: re.Pattern[str], text: str, path: str, render) -> str | None:
matches = list(pattern.finditer(text))
if len(matches) != 1:
errors.append(f"{path}: expected exactly 1 MPVKit pin, found {len(matches)}")
return None
match = matches[0]
return text[: match.start()] + render(match) + text[match.end() :]
def check_lock_schema(path: str, text: str) -> bool:
try:
payload = json.loads(text)
except json.JSONDecodeError as error:
errors.append(f"{path}: not valid JSON ({error})")
return False
version = payload.get("version") if isinstance(payload, dict) else None
if version not in (2, 3):
errors.append(f"{path}: unsupported SwiftPM lock schema version {version!r}")
return False
pins = payload.get("pins")
if not isinstance(pins, list):
errors.append(f"{path}: missing pins array")
return False
pin = next(
(
item
for item in pins
if isinstance(item, dict) and item.get("identity") == "mpvkit"
),
None,
)
if pin is None:
errors.append(f"{path}: no mpvkit pin")
return False
if pin.get("kind") != "remoteSourceControl":
errors.append(f"{path}: mpvkit pin is {pin.get('kind')!r}, expected remoteSourceControl")
return False
return True
for path in projects:
with open(path, encoding="utf-8") as handle:
original = handle.read()
def render(match: re.Match[str]) -> str:
indent = match.group("indent")
head = match.group(0)[: match.start("body") - match.start()]
return (
f"{head}"
f"{indent}\tkind = revision;\n"
f"{indent}\trevision = {revision};\n"
f"{indent}}};\n"
)
updated = replace_once(REQUIREMENT, original, path, render)
if updated is None:
continue
if updated == original:
unchanged.append(path)
else:
writes.append((path, updated))
for path in locks:
with open(path, encoding="utf-8") as handle:
original = handle.read()
if not check_lock_schema(path, original):
continue
def render(match: re.Match[str]) -> str:
indent = match.group("indent")
return (
match.group(0)[: match.start("body") - match.start()]
+ f'{indent} "revision" : "{revision}"\n'
+ f"{indent}}}"
)
updated = replace_once(PIN_STATE, original, path, render)
if updated is None:
continue
if updated == original:
unchanged.append(path)
else:
writes.append((path, updated))
if errors:
for error in errors:
print(f"error: {error}", file=sys.stderr)
sys.exit(1)
for path, content in writes:
with open(path, "w", encoding="utf-8") as handle:
handle.write(content)
print(f" updated {path}")
for path in unchanged:
print(f" unchanged {path}")
short = revision[:12]
if writes:
print(f"\nMPVKit pinned to {short} ({len(writes)} file(s) rewritten, {len(unchanged)} already correct).")
else:
print(f"\nMPVKit was already pinned to {short}; nothing to do.")
PY