Plezy consumed mpv through four unrelated supply chains: an MPVKit fork via SwiftPM for the Apple platforms, a libmpv-android fork's AAR for Android, an in-CI from-source build for Linux, and an unpinned sourceforge mpv-dev 7z for Windows. All four now consume the same per-commit, content-addressed binaries from https://github.com/edde746/mpv-build, pinned to one commit and built from one set of pinned sources (mpv v0.41.0 on Apple/Android/Windows, ffmpeg n8.0.1, our libass fork). - Apple: the SwiftPM package moves from edde746/MPVKit to edde746/mpv-build across the ios/macos/tvos projects; scripts/set_mpvkit_revision.sh becomes set_native_revision.sh, writes every pin site plus the new repo-root mpv-build.lock.json, and tvos/scripts/wire_mpv.rb derives the package repo from the locks. - Android: the mpv Kotlin API and JNI glue move in-app under android/libmpv (repackaged com.edde746.plezy.libmpv, exports renamed, shrinker rules covered), and the module downloads per-ABI native tarballs (lib/*.so incl. libc++_shared.so + include/) driven entirely by mpv-build.lock.json, with a PLEZY_LOCAL_MPV_DIR escape hatch for locally built artifacts. The fork AAR and its Maven coordinates are gone. - Linux: CI downloads the prebuilt self-relocating libmpv prefix (lock-driven, sha256-verified) instead of compiling mpv/ffmpeg/dav1d/ libplacebo/shaderc from source; linux/packaging/build-libmpv.sh and its test are deleted and native-inputs.json shrinks to the simdutf entry the CMake builds still fetch. - Windows: both arches FetchContent the mpv-build dev zips with URL_HASH enforcement, replacing the checksum-less sourceforge download and its ARM64 7-Zip special case; guard scripts updated. The lock plus the Apple pin sites all point at mpv-build commit d7c3d559, whose manifest was verified asset-by-asset against the published release digests (17/17 match). Verified locally: wire and pin-script suites green, runtime-input checks green, all four Android ABI tarballs downloaded/verified/extracted through the real Gradle tasks, the Windows FetchContent block exercised end to end through cmake, the Linux asset hash and layout checked against the workflow contract, and xcodebuild resolved the flipped SwiftPM graph with SwiftPM validating every binary checksum.
228 lines
9.9 KiB
Python
Executable File
228 lines
9.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Offline verification for reviewed Linux native and vendored binding inputs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
HEX_256 = re.compile(r"^[0-9a-f]{64}$")
|
|
HEX_COMMIT = re.compile(r"^[0-9a-f]{40}$")
|
|
NATIVE_NAMES = {"simdutf"}
|
|
BINDING_ARTIFACTS = {
|
|
"pigeons/messages.dart",
|
|
"android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/WakelockPlusMessages.g.kt",
|
|
"ios/wakelock_plus/Sources/wakelock_plus/include/wakelock_plus/messages.g.h",
|
|
"ios/wakelock_plus/Sources/wakelock_plus/messages.g.m",
|
|
}
|
|
|
|
|
|
def _load_json(path: Path, errors: list[str]) -> dict[str, Any]:
|
|
try:
|
|
value = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as error:
|
|
errors.append(f"{path}: cannot load JSON: {error}")
|
|
return {}
|
|
if not isinstance(value, dict):
|
|
errors.append(f"{path}: top-level value must be an object")
|
|
return {}
|
|
return value
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as source:
|
|
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _require_text(value: Any, label: str, errors: list[str]) -> str:
|
|
if not isinstance(value, str) or not value.strip():
|
|
errors.append(f"{label}: must be non-empty text")
|
|
return ""
|
|
return value
|
|
|
|
|
|
def _locked_package(lock_text: str, name: str) -> tuple[str, str] | None:
|
|
pattern = re.compile(
|
|
rf"^ {re.escape(name)}:\n(?P<body>(?: .*\n| .*\n)+?)(?=^ [a-zA-Z0-9_]+:|\Z)",
|
|
re.MULTILINE,
|
|
)
|
|
match = pattern.search(lock_text)
|
|
if match is None:
|
|
return None
|
|
body = match.group("body")
|
|
version = re.search(r'^ version: "([^\"]+)"$', body, re.MULTILINE)
|
|
checksum = re.search(r'^ sha256: "?([0-9a-f]{64})"?$', body, re.MULTILINE)
|
|
if version is None or checksum is None:
|
|
return None
|
|
return version.group(1), checksum.group(1)
|
|
|
|
|
|
def _validate_native(root: Path, errors: list[str]) -> None:
|
|
manifest_path = root / "linux/packaging/native-inputs.json"
|
|
manifest = _load_json(manifest_path, errors)
|
|
if manifest.get("formatVersion") != 1:
|
|
errors.append(f"{manifest_path}: formatVersion must be 1")
|
|
|
|
|
|
inputs = manifest.get("inputs")
|
|
if not isinstance(inputs, dict) or set(inputs) != NATIVE_NAMES:
|
|
errors.append(f"{manifest_path}: inputs must be exactly {sorted(NATIVE_NAMES)}")
|
|
return
|
|
|
|
for name, value in inputs.items():
|
|
label = f"{manifest_path}: inputs.{name}"
|
|
if not isinstance(value, dict):
|
|
errors.append(f"{label}: must be an object")
|
|
continue
|
|
version = _require_text(value.get("version"), f"{label}.version", errors)
|
|
url = _require_text(value.get("url"), f"{label}.url", errors)
|
|
_require_text(value.get("provenance"), f"{label}.provenance", errors)
|
|
if url and not url.startswith("https://"):
|
|
errors.append(f"{label}.url: production source must use HTTPS")
|
|
if version and url and version not in url:
|
|
errors.append(f"{label}.url: must identify declared version {version}")
|
|
if value.get("kind") != "archive":
|
|
errors.append(f"{label}.kind: must be archive")
|
|
checksum = value.get("sha256")
|
|
if not isinstance(checksum, str) or HEX_256.fullmatch(checksum) is None:
|
|
errors.append(f"{label}.sha256: must be a lowercase full SHA-256")
|
|
|
|
cmake_path = root / "linux/CMakeLists.txt"
|
|
try:
|
|
cmake = cmake_path.read_text(encoding="utf-8")
|
|
except OSError as error:
|
|
errors.append(f"{cmake_path}: cannot read: {error}")
|
|
cmake = ""
|
|
simdutf = inputs.get("simdutf")
|
|
if isinstance(simdutf, dict):
|
|
simdutf_url = simdutf.get("url")
|
|
simdutf_sha256 = simdutf.get("sha256")
|
|
if isinstance(simdutf_url, str) and simdutf_url and f"URL {simdutf_url}" not in cmake:
|
|
errors.append(f"{cmake_path}: simdutf URL differs from native-inputs.json")
|
|
if (
|
|
isinstance(simdutf_sha256, str)
|
|
and HEX_256.fullmatch(simdutf_sha256) is not None
|
|
and f"URL_HASH SHA256={simdutf_sha256}" not in cmake
|
|
):
|
|
errors.append(f"{cmake_path}: simdutf SHA-256 differs from native-inputs.json")
|
|
|
|
|
|
def _validate_wakelock(root: Path, errors: list[str]) -> None:
|
|
package = root / "packages/wakelock_plus"
|
|
provenance_path = package / "provenance.json"
|
|
provenance = _load_json(provenance_path, errors)
|
|
if provenance.get("formatVersion") != 1:
|
|
errors.append(f"{provenance_path}: formatVersion must be 1")
|
|
|
|
upstream = provenance.get("upstream")
|
|
if not isinstance(upstream, dict) or HEX_COMMIT.fullmatch(str(upstream.get("commit", ""))) is None:
|
|
errors.append(f"{provenance_path}: upstream.commit must be a full Git commit")
|
|
|
|
|
|
artifacts = provenance.get("artifacts")
|
|
if not isinstance(artifacts, dict) or set(artifacts) != BINDING_ARTIFACTS:
|
|
errors.append(f"{provenance_path}: artifacts must be exactly the schema and three host outputs")
|
|
else:
|
|
for relative, expected in artifacts.items():
|
|
path = package / relative
|
|
if not isinstance(expected, str) or HEX_256.fullmatch(expected) is None:
|
|
errors.append(f"{provenance_path}: invalid artifact SHA-256 for {relative}")
|
|
elif not path.is_file():
|
|
errors.append(f"{path}: required binding artifact is missing")
|
|
else:
|
|
actual = _sha256(path)
|
|
if actual != expected:
|
|
errors.append(f"{path}: SHA-256 drift (expected {expected}, got {actual})")
|
|
|
|
try:
|
|
pubspec = (package / "pubspec.yaml").read_text(encoding="utf-8")
|
|
lock = (package / "pubspec.lock").read_text(encoding="utf-8")
|
|
schema = (package / "pigeons/messages.dart").read_text(encoding="utf-8")
|
|
root_lock = (root / "pubspec.lock").read_text(encoding="utf-8")
|
|
except OSError as error:
|
|
errors.append(f"{package}: cannot read package provenance input: {error}")
|
|
return
|
|
|
|
generator = provenance.get("generator") if isinstance(provenance.get("generator"), dict) else {}
|
|
client = provenance.get("externalDartClient") if isinstance(provenance.get("externalDartClient"), dict) else {}
|
|
expected_pigeon = (str(generator.get("version", "")), str(generator.get("archiveSha256", "")))
|
|
expected_client = (str(client.get("version", "")), str(client.get("archiveSha256", "")))
|
|
if not re.search(rf"^ pigeon: {re.escape(expected_pigeon[0])}$", pubspec, re.MULTILINE):
|
|
errors.append(f"{package / 'pubspec.yaml'}: Pigeon must be pinned exactly to {expected_pigeon[0]}")
|
|
if not re.search(
|
|
rf"^ wakelock_plus_platform_interface: {re.escape(expected_client[0])}$", pubspec, re.MULTILINE
|
|
):
|
|
errors.append(
|
|
f"{package / 'pubspec.yaml'}: wakelock_plus_platform_interface must be pinned exactly to {expected_client[0]}"
|
|
)
|
|
if _locked_package(lock, "pigeon") != expected_pigeon:
|
|
errors.append(f"{package / 'pubspec.lock'}: Pigeon version/checksum differs from provenance.json")
|
|
if _locked_package(lock, "wakelock_plus_platform_interface") != expected_client:
|
|
errors.append(
|
|
f"{package / 'pubspec.lock'}: platform-interface version/checksum differs from provenance.json"
|
|
)
|
|
if _locked_package(root_lock, "wakelock_plus_platform_interface") != expected_client:
|
|
errors.append(
|
|
f"{root / 'pubspec.lock'}: runtime platform-interface version/checksum differs from provenance.json"
|
|
)
|
|
|
|
if "dartPackageName: 'wakelock_plus_platform_interface'" not in schema:
|
|
errors.append(f"{package / 'pigeons/messages.dart'}: external Dart package name is not explicit")
|
|
if re.search(r"\bdart(?:Test)?Out\s*:", schema):
|
|
errors.append(f"{package / 'pigeons/messages.dart'}: host-only schema must not generate Dart outputs")
|
|
for relative in BINDING_ARTIFACTS - {"pigeons/messages.dart"}:
|
|
if relative not in schema:
|
|
errors.append(f"{package / 'pigeons/messages.dart'}: missing owned output {relative}")
|
|
|
|
binding_sources = (
|
|
("Kotlin", package / "android/src/main/kotlin/dev/fluttercommunity/plus/wakelock/WakelockPlusMessages.g.kt"),
|
|
("Objective-C", package / "ios/wakelock_plus/Sources/wakelock_plus/messages.g.m"),
|
|
)
|
|
for generated_name, generated_path in binding_sources:
|
|
try:
|
|
generated = generated_path.read_text(encoding="utf-8")
|
|
except OSError as error:
|
|
errors.append(f"{generated_path}: cannot read generated binding: {error}")
|
|
continue
|
|
if "26.2.3" not in generated:
|
|
errors.append(f"{generated_name} binding was not generated by Pigeon 26.2.3")
|
|
for method in ("WakelockPlusApi.toggle", "WakelockPlusApi.isEnabled"):
|
|
if method not in generated:
|
|
errors.append(f"{generated_name} binding is missing channel suffix {method}")
|
|
for tag in ("129", "130"):
|
|
if tag not in generated:
|
|
errors.append(f"{generated_name} binding is missing codec tag {tag}")
|
|
|
|
|
|
def validate(root: Path) -> list[str]:
|
|
errors: list[str] = []
|
|
_validate_native(root, errors)
|
|
_validate_wakelock(root, errors)
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2])
|
|
arguments = parser.parse_args()
|
|
errors = validate(arguments.root.resolve())
|
|
if errors:
|
|
print("Runtime input provenance verification failed:", file=sys.stderr)
|
|
for error in errors:
|
|
print(f"- {error}", file=sys.stderr)
|
|
return 1
|
|
print("Runtime input provenance verified offline")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|