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.
This commit is contained in:
2026-01-22 21:15:07 +09:00
parent d4d805443e
commit 7a954b3f32
5 changed files with 386 additions and 60 deletions

View File

@@ -7,6 +7,41 @@ lower() { echo "$1" | tr '[:upper:]' '[:lower:]'; }
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
ensure_keychain_search_list() {
# codesign builds the cert chain using the user keychain search list.
# If the list is missing the System keychain, you can get:
# "unable to build chain to self-signed root" + errSecInternalComponent
local login_kc="$HOME/Library/Keychains/login.keychain-db"
local system_kc="/Library/Keychains/System.keychain"
local roots_kc="/System/Library/Keychains/SystemRootCertificates.keychain"
local current
current="$(security list-keychains -d user 2>/dev/null | tr -d '"' | tr -d ' ' || true)"
if echo "$current" | grep -Fq "$system_kc"; then
return 0
fi
echo "==> [$(ts)] Note: adding System keychains to the user keychain search list (fixes common codesign chain errors)"
echo " (This changes the user keychain search list; run 'security list-keychains -d user' to view.)"
security list-keychains -d user -s "$login_kc" "$system_kc" "$roots_kc" >/dev/null 2>&1 || true
}
preflight_identity() {
local what="$1"
local policy="$2"
local identity="$3"
# NOTE: security expects "-p <policy>" as *two* args; do not pass "-p codesigning" as one string.
if ! security find-identity -p "$policy" -v 2>/dev/null | grep -Fq "$identity"; then
echo "Error: ${what} identity not found/usable in Keychain: $identity" >&2
echo "Run: ./build_tools/macos/check_signing_identities.sh" >&2
exit 2
fi
}
usage() {
cat <<'EOF'
Usage:
@@ -147,40 +182,10 @@ echo "==> [$(ts)] Embedding provisioning profile"
cp -f "$provisionprofile" "${app_path}/Contents/embedded.provisionprofile"
ensure_keychain_search_list
preflight_identity "codesign" "-p codesigning" "$codesign_identity"
preflight_identity "installer" "-p basic" "$installer_identity"
preflight_identity "codesign" "codesigning" "$codesign_identity"
preflight_identity "installer" "basic" "$installer_identity"
ensure_keychain_search_list() {
# codesign builds the cert chain using the user keychain search list.
# If the list is missing the System keychain, you can get:
# "unable to build chain to self-signed root" + errSecInternalComponent
local login_kc="$HOME/Library/Keychains/login.keychain-db"
local system_kc="/Library/Keychains/System.keychain"
local roots_kc="/System/Library/Keychains/SystemRootCertificates.keychain"
local current
current="$(security list-keychains -d user 2>/dev/null | tr -d '"' | tr -d ' ' || true)"
if echo "$current" | grep -Fq "$system_kc"; then
return 0
fi
echo "==> [$(ts)] Note: adding System keychains to the user keychain search list (fixes common codesign chain errors)"
echo " (This changes the user keychain search list; run 'security list-keychains -d user' to view.)"
security list-keychains -d user -s "$login_kc" "$system_kc" "$roots_kc" >/dev/null 2>&1 || true
}
preflight_identity() {
local what="$1"
local predicate="$2"
local identity="$3"
if ! security find-identity "$predicate" -v 2>/dev/null | grep -Fq "$identity"; then
echo "Error: ${what} identity not found/usable in Keychain: $identity" >&2
echo "Run: ./build_tools/macos/check_signing_identities.sh" >&2
exit 2
fi
}
echo "==> [$(ts)] Codesigning app (Mac App Store)"
codesign_args=( --force --timestamp --options runtime --sign "$codesign_identity" --entitlements "$entitlements" )

View File

@@ -156,4 +156,52 @@ fi
echo "Tip: to pick the right MAS profile for a bundle id, run:"
echo " ./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id com.dryark.strawberry"
\n\necho\n
echo "==> [$(ts)] Recommended SHA-1 values to use (avoids ambiguity when names are duplicated)"
cat <<'EOF'
When you have multiple identities with the same display name, prefer using the SHA-1 hash in scripts:
--codesign-identity "<SHA1>"
--installer-identity "<SHA1>"
This prevents codesign/productbuild from picking an unexpected identity.
EOF
echo
extract_identities() {
local policy="$1" # codesigning | basic
# Output: SHA1|LABEL
security find-identity -p "$policy" -v 2>/dev/null \
| sed -n 's/^[[:space:]]*[0-9][0-9]*[)] \([0-9A-F]\{40\}\) "\(.*\)"$/\1|\2/p'
}
print_sha_list() {
local title="$1"
local policy="$2"
local label_match="$3"
echo "$title"
local matches
matches="$(extract_identities "$policy" | grep -F "$label_match" || true)"
if [[ -z "$matches" ]]; then
echo " (none found)"
return 0
fi
local first=1
while IFS='|' read -r sha label; do
[[ -z "$sha" || -z "$label" ]] && continue
if [[ $first -eq 1 ]]; then
echo " recommended: $sha ($label)"
first=0
else
echo " alternative: $sha ($label)"
fi
done <<<"$matches"
}
print_sha_list "Mac App Store (app signing) [use with --codesign-identity]:" "codesigning" "Apple Distribution:"
print_sha_list "Mac App Store (pkg signing) [use with --installer-identity]:" "basic" "3rd Party Mac Developer Installer:"
print_sha_list "Developer ID (app signing) [outside App Store]:" "codesigning" "Developer ID Application:"
print_sha_list "Developer ID (pkg signing) [outside App Store]:" "basic" "Developer ID Installer:"

View File

@@ -0,0 +1,210 @@
#!/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())

View File

@@ -10,7 +10,7 @@ Usage:
What it does:
- Scans common macOS provisioning profile locations (new Xcode + legacy)
- Decodes each *.provisionprofile (CMS) into plist
- Uses Apple's `security cms -D` to decode each *.provisionprofile into a plist
- Prints a readable table and recommends a best match for the given bundle id
Notes:
@@ -44,6 +44,12 @@ if ! command -v security >/dev/null 2>&1; then
exit 1
fi
plistbuddy_print() {
local keypath="$1"
local plist="$2"
/usr/libexec/PlistBuddy -c "Print :${keypath}" "$plist" 2>/dev/null || true
}
plutil_extract() {
local keypath="$1"
local plist="$2"
@@ -53,37 +59,29 @@ plutil_extract() {
find_profiles_in_dir() {
local dir="$1"
if [[ -d "$dir" ]]; then
find "$dir" -maxdepth 1 -type f -name "*.provisionprofile" 2>/dev/null || true
find "$dir" -maxdepth 1 -type f \( -name "*.provisionprofile" -o -name "*.mobileprovision" \) 2>/dev/null || true
fi
}
declare -a candidates
candidates=()
# Newer Xcode location (as reported by user)
while IFS= read -r f; do candidates+=("$f"); done < <(find_profiles_in_dir "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles")
# Legacy location
while IFS= read -r f; do candidates+=("$f"); done < <(find_profiles_in_dir "$HOME/Library/MobileDevice/Provisioning Profiles")
if [[ ${#candidates[@]} -eq 0 ]]; then
echo "==> [$(ts)] No .provisionprofile files found in common locations."
echo "Checked:"
echo " - $HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
echo " - $HOME/Library/MobileDevice/Provisioning Profiles"
echo "==> [$(ts)] No provisioning profiles found in common locations."
exit 1
fi
echo "==> [$(ts)] Scanning ${#candidates[@]} provisioning profile(s) for bundle id: ${bundle_id}"
echo
printf "%-4s %-36s %-10s %-25s %-45s %s\n" "No." "UUID" "TeamID" "Expires" "AppID" "Path"
printf "%s\n" "---- ------------------------------------ ---------- ------------------------- --------------------------------------------- ----"
best_score=-1
best_path=""
best_reason=""
printf "%-4s %-36s %-10s %-25s %-40s %s\n" "No." "UUID" "TeamID" "Expires" "AppID" "Path"
printf "%s\n" "---- ------------------------------------ ---------- ------------------------- ---------------------------------------- ----"
idx=0
for f in "${candidates[@]}"; do
idx=$((idx + 1))
@@ -97,44 +95,42 @@ for f in "${candidates[@]}"; do
uuid="$(plutil_extract UUID "$tmp")"
name="$(plutil_extract Name "$tmp")"
teamid="$(plutil_extract 'TeamIdentifier.0' "$tmp")"
if [[ -z "$teamid" ]]; then
teamid="$(plutil_extract 'ApplicationIdentifierPrefix.0' "$tmp")"
fi
exp="$(plutil_extract ExpirationDate "$tmp")"
# Profiles vary in which key they use for the app identifier.
appid="$(plutil_extract 'Entitlements.application-identifier' "$tmp")"
# App identifier lives under Entitlements; use PlistBuddy because some key names contain dots.
appid="$(plistbuddy_print 'Entitlements:application-identifier' "$tmp")"
if [[ -z "$appid" ]]; then
appid="$(plutil_extract 'Entitlements.com.apple.application-identifier' "$tmp")"
appid="$(plistbuddy_print 'Entitlements:com.apple.application-identifier' "$tmp")"
fi
rm -f "$tmp" >/dev/null 2>&1 || true
# Fallbacks for display.
[[ -z "$uuid" ]] && uuid="(unknown)"
[[ -z "$teamid" ]] && teamid="(unknown)"
[[ -z "$exp" ]] && exp="(unknown)"
[[ -z "$appid" ]] && appid="(unknown)"
printf "%-4s %-36s %-10s %-25s %-40s %s\n" "$idx" "$uuid" "$teamid" "$exp" "$appid" "$f"
printf "%-4s %-36s %-10s %-25s %-45s %s\n" "$idx" "$uuid" "$teamid" "$exp" "$appid" "$f"
# Score match quality.
score=0
reason=""
# Prefer exact team+bundle match.
if [[ "$appid" != "(unknown)" && "$teamid" != "(unknown)" ]]; then
if [[ "$appid" == "${teamid}.${bundle_id}" ]]; then
score=100
reason="exact match (${appid})"
elif [[ "$appid" == "${teamid}."* && "$appid" == *"*"* ]]; then
# Wildcard profile like TEAMID.*
score=60
reason="wildcard match (${appid})"
elif [[ "$appid" == *".${bundle_id}" ]]; then
score=50
reason="endswith match (${appid})"
elif [[ "$appid" == "${teamid}."* && "$appid" == *"*"* ]]; then
score=40
reason="wildcard match (${appid})"
fi
fi
# Prefer profiles with a plausible name for MAS (heuristic).
if [[ "$score" -gt 0 && -n "$name" ]]; then
case "$name" in
*Mac\ App\ Store*|*App\ Store*|*appstore*|*AppStore*)
@@ -161,7 +157,4 @@ fi
echo "==> [$(ts)] Recommended profile:"
echo " $best_path"
echo " reason: $best_reason"
echo
echo "Use it like:"
echo " ./build_tools/macos/build_mas_pkg.sh --run ... --provisionprofile \"$best_path\""

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail
ts() { date +"%H:%M:%S"; }
usage() {
cat <<'EOF'
Usage:
./build_tools/macos/print_mas_build_cmd.sh [--bundle-id com.dryark.strawberry] [--profile <path>]
What it does:
- Tries to auto-pick a provisioning profile for the bundle id
- Prints an exact build command you can copy/paste for build_mas_pkg.sh
Notes:
- This helper intentionally does NOT try to auto-pick signing identities by parsing Apple tool output.
Use SHA-1 identities from:
./build_tools/macos/check_signing_identities.sh
EOF
}
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "Error: This script is for macOS only." >&2
exit 1
fi
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
bundle_id="com.dryark.strawberry"
profile_path=""
codesign_identity=""
installer_identity=""
while [[ $# -gt 0 ]]; do
case "$1" in
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
--profile) profile_path="${2:-}"; shift 2 ;;
--codesign-identity) codesign_identity="${2:-}"; shift 2 ;;
--installer-identity) installer_identity="${2:-}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
esac
done
if [[ -z "$profile_path" ]]; then
# Attempt to auto-select profile using the finder script.
finder="${repo_root}/build_tools/macos/find_mas_provisioning_profile.sh"
if [[ -x "$finder" ]]; then
out="$("$finder" --bundle-id "$bundle_id" 2>/dev/null || true)"
# Parse the line after "Recommended profile:"
profile_path="$(printf '%s\n' "$out" | awk 'found{print $1; exit} /^Recommended profile:/{found=1} found && $0 ~ /^ \\// {print $1; exit}' | sed 's/^[[:space:]]*//')"
fi
fi
echo "==> [$(ts)] Recommended build command:"
echo
echo "./build_tools/macos/build_mas_pkg.sh --run --release --clean \\"
echo " --codesign-identity \"${codesign_identity:-<SHA1 from check_signing_identities.sh>}\" \\"
echo " --installer-identity \"${installer_identity:-<SHA1 from check_signing_identities.sh>}\" \\"
if [[ -n "$profile_path" ]]; then
echo " --provisionprofile \"${profile_path}\""
else
echo " --provisionprofile \"</path/to/profile.provisionprofile>\""
echo
echo "Note: could not auto-pick a provisioning profile for bundle id '${bundle_id}'."
echo "Run:"
echo " ./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id ${bundle_id}"
fi