162 lines
6.3 KiB
Python
Executable File
162 lines
6.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Validate duplicate Apple SwiftPM locks against resolved Flutter plugins."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from urllib.parse import unquote, urlparse
|
|
|
|
LOCK_PAIRS = {
|
|
"iOS": (
|
|
Path("ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"),
|
|
Path("ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved"),
|
|
),
|
|
"macOS": (
|
|
Path("macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"),
|
|
Path("macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved"),
|
|
),
|
|
}
|
|
SENTRY_IDENTITY = "sentry-cocoa"
|
|
EXACT_REQUIREMENT = re.compile(
|
|
r"\.package\s*\(\s*url\s*:\s*[\"'][^\"']*sentry-cocoa(?:\.git)?[\"']\s*,\s*exact\s*:\s*[\"']([^\"']+)[\"']",
|
|
re.DOTALL,
|
|
)
|
|
|
|
|
|
def _load_pin_map(path: Path, errors: list[str]) -> dict[str, dict]:
|
|
try:
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as error:
|
|
errors.append(f"{path}: cannot read version-2 SwiftPM lock: {error}")
|
|
return {}
|
|
if not isinstance(payload, dict) or payload.get("version") != 2 or not isinstance(payload.get("pins"), list):
|
|
errors.append(f"{path}: expected a version-2 SwiftPM lock with a pins array")
|
|
return {}
|
|
|
|
pins: dict[str, dict] = {}
|
|
for pin in payload["pins"]:
|
|
identity = pin.get("identity") if isinstance(pin, dict) else None
|
|
if not isinstance(identity, str):
|
|
errors.append(f"{path}: pin without a string identity")
|
|
continue
|
|
canonical = {
|
|
"kind": pin.get("kind"),
|
|
"location": pin.get("location"),
|
|
"state": pin.get("state"),
|
|
}
|
|
if identity in pins:
|
|
errors.append(f"{path}: duplicate pin identity {identity}")
|
|
pins[identity] = canonical
|
|
return pins
|
|
|
|
|
|
def _resolved_sentry_root(root: Path, errors: list[str]) -> Path | None:
|
|
config_path = root / ".dart_tool/package_config.json"
|
|
try:
|
|
config = json.loads(config_path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as error:
|
|
errors.append(f"{config_path}: unavailable ({error}); run flutter pub get first")
|
|
return None
|
|
packages = config.get("packages") if isinstance(config, dict) else None
|
|
if not isinstance(packages, list):
|
|
errors.append(f"{config_path}: missing packages list; run flutter pub get first")
|
|
return None
|
|
package = next(
|
|
(item for item in packages if isinstance(item, dict) and item.get("name") == "sentry_flutter"),
|
|
None,
|
|
)
|
|
if package is None or not isinstance(package.get("rootUri"), str):
|
|
errors.append(f"{config_path}: sentry_flutter is not resolved; run flutter pub get first")
|
|
return None
|
|
|
|
uri = package["rootUri"]
|
|
parsed = urlparse(uri)
|
|
if parsed.scheme == "file":
|
|
return Path(unquote(parsed.path))
|
|
if parsed.scheme:
|
|
errors.append(f"{config_path}: unsupported sentry_flutter root URI {uri!r}")
|
|
return None
|
|
return (config_path.parent / unquote(uri)).resolve()
|
|
|
|
|
|
def _manifest_requirement(path: Path, errors: list[str]) -> str | None:
|
|
try:
|
|
source = path.read_text(encoding="utf-8")
|
|
except OSError as error:
|
|
errors.append(f"{path}: unavailable ({error}); run flutter pub get first")
|
|
return None
|
|
versions = EXACT_REQUIREMENT.findall(source)
|
|
if len(versions) != 1:
|
|
errors.append(f"{path}: expected exactly one exact sentry-cocoa requirement, found {len(versions)}")
|
|
return None
|
|
return versions[0]
|
|
|
|
|
|
def validate(root: Path) -> list[str]:
|
|
root = root.resolve()
|
|
errors: list[str] = []
|
|
lock_maps: dict[Path, dict[str, dict]] = {}
|
|
for platform, relative_paths in LOCK_PAIRS.items():
|
|
first_path, second_path = (root / path for path in relative_paths)
|
|
first = _load_pin_map(first_path, errors)
|
|
second = _load_pin_map(second_path, errors)
|
|
lock_maps[first_path] = first
|
|
lock_maps[second_path] = second
|
|
if first != second:
|
|
identities = sorted(set(first) | set(second))
|
|
differing = [identity for identity in identities if first.get(identity) != second.get(identity)]
|
|
errors.append(
|
|
f"{platform} SwiftPM locks differ between {first_path} and {second_path}: "
|
|
+ ", ".join(differing)
|
|
)
|
|
|
|
sentry_root = _resolved_sentry_root(root, errors)
|
|
required_versions: list[str] = []
|
|
if sentry_root is not None:
|
|
for platform in ("ios", "macos"):
|
|
version = _manifest_requirement(sentry_root / platform / "sentry_flutter/Package.swift", errors)
|
|
if version is not None:
|
|
required_versions.append(version)
|
|
if len(set(required_versions)) > 1:
|
|
errors.append("iOS and macOS sentry_flutter manifests require different sentry-cocoa versions")
|
|
|
|
required_version = required_versions[0] if required_versions and len(set(required_versions)) == 1 else None
|
|
sentry_pins: list[tuple[Path, dict]] = []
|
|
for path, pins in lock_maps.items():
|
|
pin = pins.get(SENTRY_IDENTITY)
|
|
if pin is None:
|
|
errors.append(f"{path}: missing {SENTRY_IDENTITY} pin")
|
|
continue
|
|
sentry_pins.append((path, pin))
|
|
state = pin.get("state")
|
|
version = state.get("version") if isinstance(state, dict) else None
|
|
if required_version is not None and version != required_version:
|
|
errors.append(f"{path}: {SENTRY_IDENTITY} is {version!r}, manifest requires exactly {required_version}")
|
|
|
|
if sentry_pins:
|
|
canonical_path, canonical_pin = sentry_pins[0]
|
|
for path, pin in sentry_pins[1:]:
|
|
if pin != canonical_pin:
|
|
errors.append(f"{path}: {SENTRY_IDENTITY} state differs from {canonical_path}")
|
|
return errors
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
|
|
args = parser.parse_args(argv)
|
|
errors = validate(args.root)
|
|
if errors:
|
|
for error in errors:
|
|
print(f"error: {error}")
|
|
return 1
|
|
print("Apple SwiftPM locks match each other and the resolved Sentry manifests.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|