Files
strawberry/build_tools/macos/find_mas_provisioning_profile.py
David Helkowski 7a954b3f32 Enhance macOS build scripts for provisioning profile handling and identity management
This commit improves the `find_mas_provisioning_profile.sh` script by expanding the search for provisioning profiles to include both `.provisionprofile` and `.mobileprovision` files. It also introduces a new function to print SHA-1 values for identities, helping to avoid ambiguity when multiple identities share the same display name. Additionally, the `check_signing_identities.sh` script is updated to provide clearer recommendations for using SHA-1 hashes with codesigning and installer identities, enhancing the overall usability and clarity for developers working with macOS builds.
2026-01-22 21:15:07 +09:00

211 lines
6.6 KiB
Python

#!/usr/bin/env python3
import argparse
import datetime as dt
import os
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import plistlib
@dataclass
class ProfileInfo:
path: Path
uuid: str
name: str
team_id: str
expiration: Optional[dt.datetime]
app_id: str
platforms: List[str]
def run(cmd: List[str]) -> Tuple[int, bytes, bytes]:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
return p.returncode, out, err
def decode_profile_plist_bytes(profile_path: Path) -> Optional[bytes]:
# Provisioning profiles are typically CMS/PKCS#7 SignedData blobs whose payload is a plist.
# However, some tools store them as plain XML plists already. Also, LibreSSL/OpenSSL behavior
# differs: LibreSSL usually requires an explicit '-verify' to emit the embedded content.
data = profile_path.read_bytes()
# Fast path: already a plist (XML).
if b"<plist" in data:
return data
# Decode CMS/PKCS7 to extract embedded plist payload.
# Try a small matrix of commands/inform formats for compatibility.
candidates: List[List[str]] = []
for inform in ("DER", "PEM"):
candidates.append(["/usr/bin/openssl", "cms", "-verify", "-noverify", "-inform", inform, "-in", str(profile_path)])
candidates.append(["/usr/bin/openssl", "smime", "-verify", "-noverify", "-inform", inform, "-in", str(profile_path)])
for cmd in candidates:
rc, out, _err = run(cmd)
if rc == 0 and b"<plist" in out:
return out
return None
def parse_plist(plist_bytes: bytes) -> Dict[str, Any]:
return plistlib.loads(plist_bytes)
def iso(dt_obj: Optional[dt.datetime]) -> str:
if not dt_obj:
return "(unknown)"
# Force UTC-ish display if tz-aware, otherwise as-is.
try:
return dt_obj.isoformat().replace("+00:00", "Z")
except Exception:
return str(dt_obj)
def safe_str(v: Any) -> str:
if v is None:
return ""
if isinstance(v, bytes):
try:
return v.decode("utf-8", errors="replace")
except Exception:
return repr(v)
return str(v)
def profile_info_from_plist(path: Path, p: Dict[str, Any]) -> ProfileInfo:
uuid = safe_str(p.get("UUID", "")) or "(unknown)"
name = safe_str(p.get("Name", "")) or "(unknown)"
team_ids = p.get("TeamIdentifier") or []
team_id = safe_str(team_ids[0]) if isinstance(team_ids, list) and team_ids else ""
if not team_id:
prefixes = p.get("ApplicationIdentifierPrefix") or []
team_id = safe_str(prefixes[0]) if isinstance(prefixes, list) and prefixes else "(unknown)"
exp = p.get("ExpirationDate")
expiration = exp if isinstance(exp, dt.datetime) else None
ent = p.get("Entitlements") or {}
app_id = safe_str(ent.get("application-identifier") or ent.get("com.apple.application-identifier") or "") or "(unknown)"
platforms = p.get("Platform") or []
if isinstance(platforms, str):
platforms = [platforms]
platforms = [safe_str(x) for x in platforms if x is not None]
return ProfileInfo(path=path, uuid=uuid, name=name, team_id=team_id or "(unknown)", expiration=expiration, app_id=app_id, platforms=platforms)
def score(profile: ProfileInfo, bundle_id: str, now: dt.datetime) -> Tuple[int, str]:
# Prefer non-expired.
if profile.expiration and profile.expiration < now:
return (-1, "expired")
score = 0
reason = []
# Prefer exact app id match TEAMID.bundle_id
if profile.team_id != "(unknown)" and profile.app_id != "(unknown)":
exact = f"{profile.team_id}.{bundle_id}"
if profile.app_id == exact:
score += 100
reason.append(f"exact {profile.app_id}")
elif profile.app_id.endswith(f".{bundle_id}"):
score += 60
reason.append(f"endswith {profile.app_id}")
elif "*" in profile.app_id and profile.app_id.startswith(f"{profile.team_id}."):
score += 40
reason.append(f"wildcard {profile.app_id}")
# Heuristic: name suggests MAS.
n = profile.name.lower()
if "mac app store" in n or "app store" in n or "appstore" in n:
score += 5
reason.append("name looks like MAS")
# Prefer macOS platform if present.
plats = [p.lower() for p in profile.platforms]
if any("macos" in p for p in plats):
score += 2
reason.append("platform macos")
return (score, ", ".join(reason) if reason else "")
def find_profiles() -> List[Path]:
dirs = [
Path.home() / "Library" / "Developer" / "Xcode" / "UserData" / "Provisioning Profiles",
Path.home() / "Library" / "MobileDevice" / "Provisioning Profiles",
]
out: List[Path] = []
for d in dirs:
if not d.is_dir():
continue
for p in d.iterdir():
if p.is_file() and (p.name.endswith(".provisionprofile") or p.name.endswith(".mobileprovision")):
out.append(p)
return sorted(out)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--bundle-id", required=True)
args = ap.parse_args()
bundle_id = args.bundle_id
if not Path("/usr/bin/openssl").exists():
print("Error: /usr/bin/openssl not found.", file=sys.stderr)
return 1
candidates = find_profiles()
if not candidates:
print("No provisioning profiles found in common locations.", file=sys.stderr)
return 1
print(f"Scanning {len(candidates)} provisioning profile(s) for bundle id: {bundle_id}")
print()
print(f"{'No.':<4} {'UUID':<36} {'TeamID':<10} {'Expires':<25} {'AppID':<45} Path")
print(f"{'-'*4} {'-'*36} {'-'*10} {'-'*25} {'-'*45} ----")
infos: List[ProfileInfo] = []
for i, p in enumerate(candidates, start=1):
plist_bytes = decode_profile_plist_bytes(p)
if not plist_bytes:
continue
try:
pl = parse_plist(plist_bytes)
info = profile_info_from_plist(p, pl)
infos.append(info)
print(f"{i:<4} {info.uuid:<36} {info.team_id:<10} {iso(info.expiration):<25} {info.app_id:<45} {info.path}")
except Exception:
continue
if not infos:
print("\nCould not decode any provisioning profiles with openssl cms.", file=sys.stderr)
return 2
now = dt.datetime.now(dt.timezone.utc)
best: Optional[Tuple[int, str, ProfileInfo]] = None
for info in infos:
sc, why = score(info, bundle_id, now)
if best is None or sc > best[0]:
best = (sc, why, info)
print()
if best is None or best[0] <= 0:
print(f"Could not confidently auto-select a profile for {bundle_id}.", file=sys.stderr)
print("Pick the profile whose AppID is TEAMID.<bundle-id> and is a macOS Mac App Store profile.", file=sys.stderr)
return 2
_, why, info = best
print("Recommended profile:")
print(f" {info.path}")
print(f" reason: {why}")
return 0
if __name__ == "__main__":
raise SystemExit(main())