scripts/ had ~80 flat files. Entry points (ci_*.sh, codegen.sh, run_tests.sh, format_native.sh, setup_hooks.sh, upload-symbols.*) and the shared pubspec_version.py stay at the root; checkers, generators, maestro tooling and release tooling move into subdirectories with their tests. Updated every reference: workflow steps, guard-test glob, Docker COPY paths and .dockerignore whitelist, website audit path, dart test imports, and regenerated the five outputs whose headers embed generator paths.
163 lines
6.4 KiB
Python
Executable File
163 lines
6.4 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
|
|
from urllib.request import url2pathname
|
|
|
|
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(url2pathname(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[2])
|
|
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())
|