Add reversible old-identity migration and freeze corrected release identities

这个提交包含在:
dayu 于 2026-10-22 18:37:21 +08:00 提交
1 父节点 47f6e55ecd
当前提交 767bba8ac1
6 个文件已更改
+283 -26

没有与搜索条件相匹配的文件

@@ -22,14 +22,14 @@
containerPortal = 6945FC833088B7220013E108 /* Project object */; containerPortal = 6945FC833088B7220013E108 /* Project object */;
proxyType = 1; proxyType = 1;
remoteGlobalIDString = 6945FC953088B7220013E108; remoteGlobalIDString = 6945FC953088B7220013E108;
remoteInfo = "XPC Service"; remoteInfo = "FCP-BeautyGirl XPC Service";
}; };
6945FCA43088B7220013E108 /* PBXContainerItemProxy */ = { 6945FCA43088B7220013E108 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 6945FC833088B7220013E108 /* Project object */; containerPortal = 6945FC833088B7220013E108 /* Project object */;
proxyType = 1; proxyType = 1;
remoteGlobalIDString = 6945FC953088B7220013E108; remoteGlobalIDString = 6945FC953088B7220013E108;
remoteInfo = "XPC Service"; remoteInfo = "FCP-BeautyGirl XPC Service";
}; };
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
@@ -174,8 +174,8 @@
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
6945FCA63088B7220013E108 /* FCP-BeautyGirl */, 6945FCA63088B7220013E108 /* FCP-BeautyGirl */,
); );
name = "Wrapper Application"; name = "FCP-BeautyGirl";
productName = "Wrapper Application"; productName = "FCP-BeautyGirl";
productReference = 6945FC8B3088B7220013E108 /* FCP-BeautyGirl.app */; productReference = 6945FC8B3088B7220013E108 /* FCP-BeautyGirl.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
@@ -194,8 +194,8 @@
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
6945FCA63088B7220013E108 /* FCP-BeautyGirl */, 6945FCA63088B7220013E108 /* FCP-BeautyGirl */,
); );
name = "XPC Service"; name = "FCP-BeautyGirl XPC Service";
productName = "XPC Service"; productName = "FCP-BeautyGirl XPC Service";
productReference = 6945FC963088B7220013E108 /* FCP-BeautyGirl XPC Service.pluginkit */; productReference = 6945FC963088B7220013E108 /* FCP-BeautyGirl XPC Service.pluginkit */;
productType = "com.apple.product-type.bundle"; productType = "com.apple.product-type.bundle";
}; };
@@ -323,7 +323,7 @@
); );
MACH_O_TYPE = mh_execute; MACH_O_TYPE = mh_execute;
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.fcpbeautygirl.fxplug; PRODUCT_BUNDLE_IDENTIFIER = com.dayu.fcpbeautygirl.fxplug;
PRODUCT_NAME = "FCP-BeautyGirl $(TARGET_NAME)"; PRODUCT_NAME = "FCP-BeautyGirl XPC Service";
SWIFT_OBJC_BRIDGING_HEADER = "FCP-BeautyGirl/Plugin/XPC Service-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "FCP-BeautyGirl/Plugin/XPC Service-Bridging-Header.h";
WRAPPER_EXTENSION = pluginkit; WRAPPER_EXTENSION = pluginkit;
}; };
@@ -346,7 +346,7 @@
); );
MACH_O_TYPE = mh_execute; MACH_O_TYPE = mh_execute;
PRODUCT_BUNDLE_IDENTIFIER = com.dayu.fcpbeautygirl.fxplug; PRODUCT_BUNDLE_IDENTIFIER = com.dayu.fcpbeautygirl.fxplug;
PRODUCT_NAME = "FCP-BeautyGirl $(TARGET_NAME)"; PRODUCT_NAME = "FCP-BeautyGirl XPC Service";
SWIFT_OBJC_BRIDGING_HEADER = "FCP-BeautyGirl/Plugin/XPC Service-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "FCP-BeautyGirl/Plugin/XPC Service-Bridging-Header.h";
WRAPPER_EXTENSION = pluginkit; WRAPPER_EXTENSION = pluginkit;
}; };
+1 -1
查看文件
@@ -9,7 +9,7 @@
"sourceRoot": "FCP-BeautyGirl", "sourceRoot": "FCP-BeautyGirl",
"sourceDirectory": "FCP-BeautyGirl/FCP-BeautyGirl", "sourceDirectory": "FCP-BeautyGirl/FCP-BeautyGirl",
"xcodeProject": "FCP-BeautyGirl/FCP-BeautyGirl.xcodeproj", "xcodeProject": "FCP-BeautyGirl/FCP-BeautyGirl.xcodeproj",
"scheme": "Wrapper Application", "scheme": "FCP-BeautyGirl",
"installationPath": "/Library/Plug-Ins/FxPlug/FCP-BeautyGirl.app", "installationPath": "/Library/Plug-Ins/FxPlug/FCP-BeautyGirl.app",
"filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241", "filterUUID": "FB14B0E6-59B2-4836-916F-CD0C559E9241",
"teamIdentifier": "B3W25FX5H5", "teamIdentifier": "B3W25FX5H5",
+19 -17
查看文件
@@ -3,26 +3,24 @@
The final prompt keeps the lock held while native-host validation is performed. The final prompt keeps the lock held while native-host validation is performed.
""" """
import argparse, datetime, json, os, pathlib, plistlib, re, shutil, subprocess, sys, time import argparse, datetime, json, os, pathlib, plistlib, re, shutil, subprocess, sys, time
from product_identity import PRODUCT, LegacyMigration, delivery_locks, validate_app
root=pathlib.Path(__file__).resolve().parent.parent root=pathlib.Path(__file__).resolve().parent.parent
parser=argparse.ArgumentParser(description=__doc__) parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--wait-for-hosts',action='store_true',help='After notarization, retain the delivery lock and wait for Motion/FCP to exit naturally before installing; never control or quit either host.') parser.add_argument('--wait-for-hosts',action='store_true',help='After notarization, retain the delivery lock and wait for Motion/FCP to exit naturally before installing; never control or quit either host.')
parser.add_argument('--user-tests',action='store_true',help='After non-UI installed-app verification, record native effect QA as pending user testing and clean known generated copies without the interactive validation prompt.') parser.add_argument('--user-tests',action='store_true',help='After non-UI installed-app verification, record native effect QA as pending user testing and clean known generated copies without the interactive validation prompt.')
parser.add_argument('--resume-prepared',action='store_true',help='Resume an already Accepted and stapled staged app after checking its recorded signature, version and unchanged product source.') parser.add_argument('--resume-prepared',action='store_true',help='Resume an already Accepted and stapled staged app after checking its recorded signature, version and unchanged product source.')
args=parser.parse_args() args=parser.parse_args()
bundle_id='com.dayu.fcpbeautygirl' bundle_id=PRODUCT['bundleIdentifier']
lock=pathlib.Path('/Users/dayu/.codex/app-delivery-locks')/(bundle_id+'.lock') locks=delivery_locks();locks.__enter__()
lock.parent.mkdir(parents=True,exist_ok=True) staging=pathlib.Path('/tmp/fcp-beautygirl-release')
try:lock.mkdir()
except FileExistsError:raise SystemExit('Delivery lock occupied; inspect owner.json and the owner process')
(lock/'owner.json').write_text(json.dumps({'pid':os.getpid(),'cwd':str(root),'bundle_id':bundle_id}))
staging=pathlib.Path('/tmp/paintfcp-build-20261021')
logs=root/'build'/'delivery';logs.mkdir(parents=True,exist_ok=True) logs=root/'build'/'delivery';logs.mkdir(parents=True,exist_ok=True)
app=staging/'Build/Products/Release/FCP-BeautyGirl.app' app=staging/'Build/Products/Release'/(PRODUCT['name']+'.app')
installed=pathlib.Path('/Library/Plug-Ins/FxPlug/FCP-BeautyGirl.app') installed=pathlib.Path(PRODUCT['installationPath'])
archive=logs/'notarization.zip' archive=logs/'notarization.zip'
identity='Developer ID Application: zhong luo (B3W25FX5H5)' identity='Developer ID Application: zhong luo (B3W25FX5H5)'
keychain='/Users/dayu/Library/Keychains/login.keychain-db' keychain='/Users/dayu/Library/Keychains/login.keychain-db'
previous=logs/'previous-FCP-BeautyGirl.app' previous=logs/'previous-FCP-BeautyGirl.app'
migration=LegacyMigration(logs)
receipt_path=logs/'delivery-receipt.json' receipt_path=logs/'delivery-receipt.json'
receipt={'status':'preparing','bundleIdentifier':bundle_id,'installationPath':str(installed), receipt={'status':'preparing','bundleIdentifier':bundle_id,'installationPath':str(installed),
'options':{'waitForHosts':args.wait_for_hosts,'userTests':args.user_tests}, 'options':{'waitForHosts':args.wait_for_hosts,'userTests':args.user_tests},
@@ -80,7 +78,7 @@ def cleanup_known_products():
try: try:
delivery_source={'commit':run(['git','rev-parse','HEAD']).strip(), delivery_source={'commit':run(['git','rev-parse','HEAD']).strip(),
'trackedWorktreeChanges':run(['git','status','--porcelain','--untracked-files=no']).splitlines()} 'trackedWorktreeChanges':run(['git','status','--porcelain','--untracked-files=no']).splitlines()}
plugin=app/'Contents/PlugIns/FCP-BeautyGirl XPC Service.pluginkit' plugin=app/'Contents/PlugIns'/PRODUCT['pluginName']
if args.resume_prepared: if args.resume_prepared:
old_receipt=json.loads(receipt_path.read_text()) old_receipt=json.loads(receipt_path.read_text())
result=old_receipt.get('notarization',{}) result=old_receipt.get('notarization',{})
@@ -94,7 +92,7 @@ try:
if not expected or not actual or expected.group(1)!=actual.group(1):raise RuntimeError('Prepared signature hash changed') if not expected or not actual or expected.group(1)!=actual.group(1):raise RuntimeError('Prepared signature hash changed')
if info.get('CFBundleShortVersionString')!=old_receipt.get('version') or info.get('CFBundleVersion')!=old_receipt.get('build'):raise RuntimeError('Prepared version changed') if info.get('CFBundleShortVersionString')!=old_receipt.get('version') or info.get('CFBundleVersion')!=old_receipt.get('build'):raise RuntimeError('Prepared version changed')
source_commit=old_receipt.get('source',{}).get('commit') source_commit=old_receipt.get('source',{}).get('commit')
if not source_commit or run(['git','diff',source_commit,'--','FCP-BeautyGirl']).strip():raise RuntimeError('Product source changed since prepared build') if not source_commit or run(['git','diff',source_commit,'--',PRODUCT['sourceRoot'],'product.json']).strip():raise RuntimeError('Product source changed since prepared build')
receipt.update(old_receipt);receipt.pop('failure',None) receipt.update(old_receipt);receipt.pop('failure',None)
receipt['resumedBy']=delivery_source receipt['resumedBy']=delivery_source
receipt['options']={'waitForHosts':args.wait_for_hosts,'userTests':args.user_tests,'resumePrepared':True} receipt['options']={'waitForHosts':args.wait_for_hosts,'userTests':args.user_tests,'resumePrepared':True}
@@ -107,14 +105,14 @@ try:
identities=run(['security','find-identity','-v','-p','codesigning',keychain]) identities=run(['security','find-identity','-v','-p','codesigning',keychain])
if identity not in identities:raise RuntimeError('Valid Developer ID Application identity is unavailable') if identity not in identities:raise RuntimeError('Valid Developer ID Application identity is unavailable')
print('Building universal Release…',flush=True) print('Building universal Release…',flush=True)
run(['xcodebuild','-project','FCP-BeautyGirl/FCP-BeautyGirl.xcodeproj','-scheme','Wrapper Application','-configuration','Release','-derivedDataPath',str(staging),'-destination','generic/platform=macOS','build'],logs/'build.log') run(['xcodebuild','-project',PRODUCT['xcodeProject'],'-scheme',PRODUCT['scheme'],'-configuration','Release','-derivedDataPath',str(staging),'-destination','generic/platform=macOS','build'],logs/'build.log')
# Xcode can register a fresh build. Remove this known generated path before # Xcode can register a fresh build. Remove this known generated path before
# lengthy signing/notarization while a user may still be testing the old app. # lengthy signing/notarization while a user may still be testing the old app.
unregister_generated_plugin(plugin) unregister_generated_plugin(plugin)
for framework in sorted((plugin/'Contents/Frameworks').glob('*.framework')): for framework in sorted((plugin/'Contents/Frameworks').glob('*.framework')):
run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(framework)]) run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(framework)])
run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(plugin)]) run(['codesign','--force','--timestamp','--options','runtime','--sign',identity,str(plugin)])
run(['codesign','--force','--timestamp','--options','runtime','--entitlements',str(root/'FCP-BeautyGirl/FCP-BeautyGirl/Wrapper Application/SandboxEntitlements.entitlements'),'--sign',identity,str(app)]) run(['codesign','--force','--timestamp','--options','runtime','--entitlements',str(root/PRODUCT['sourceDirectory']/'Wrapper Application/SandboxEntitlements.entitlements'),'--sign',identity,str(app)])
run(['codesign','--verify','--deep','--strict',str(app)]) run(['codesign','--verify','--deep','--strict',str(app)])
info=require_product(app) info=require_product(app)
receipt['version']=info.get('CFBundleShortVersionString');receipt['build']=info.get('CFBundleVersion') receipt['version']=info.get('CFBundleShortVersionString');receipt['build']=info.get('CFBundleVersion')
@@ -148,6 +146,8 @@ try:
if installed.exists(): if installed.exists():
shutil.move(str(installed),str(previous)) shutil.move(str(installed),str(previous))
try: try:
validate_app(app,bundle_id)
migration.relocate(unregister_generated_plugin)
shutil.copytree(app,installed,symlinks=True) shutil.copytree(app,installed,symlinks=True)
run(['codesign','--verify','--deep','--strict',str(installed)]) run(['codesign','--verify','--deep','--strict',str(installed)])
run(['xcrun','stapler','validate',str(installed)]) run(['xcrun','stapler','validate',str(installed)])
@@ -156,21 +156,23 @@ try:
signature=subprocess.run(['codesign','-dvvv',str(installed)],capture_output=True,text=True,check=True).stderr signature=subprocess.run(['codesign','-dvvv',str(installed)],capture_output=True,text=True,check=True).stderr
if info.get('CFBundleIdentifier')!=bundle_id or 'TeamIdentifier=B3W25FX5H5' not in signature:raise RuntimeError('Installed app identity mismatch') if info.get('CFBundleIdentifier')!=bundle_id or 'TeamIdentifier=B3W25FX5H5' not in signature:raise RuntimeError('Installed app identity mismatch')
(logs/'installed-signature.txt').write_text(signature) (logs/'installed-signature.txt').write_text(signature)
run(['pluginkit','-a',str(installed/'Contents/PlugIns/FCP-BeautyGirl XPC Service.pluginkit')]) run(['pluginkit','-a',str(installed/'Contents/PlugIns'/PRODUCT['pluginName'])])
# Xcode may register its build product. Only the installed plugin should # Xcode may register its build product. Only the installed plugin should
# be discoverable while native host validation is in progress. # be discoverable while native host validation is in progress.
unregister_generated_plugin(plugin) unregister_generated_plugin(plugin)
installed_plugin=installed/'Contents/PlugIns/FCP-BeautyGirl XPC Service.pluginkit' installed_plugin=installed/'Contents/PlugIns'/PRODUCT['pluginName']
plugin_info=plistlib.loads((installed_plugin/'Contents/Info.plist').read_bytes()) plugin_info=plistlib.loads((installed_plugin/'Contents/Info.plist').read_bytes())
discovered=run(['pluginkit','-m','-A','-D','-v','-i',plugin_info['CFBundleIdentifier']]) discovered=run(['pluginkit','-m','-A','-D','-v','-i',plugin_info['CFBundleIdentifier']])
if str(installed_plugin) not in discovered:raise RuntimeError('PluginKit did not report the formal installed plug-in path') if str(installed_plugin) not in discovered:raise RuntimeError('PluginKit did not report the formal installed plug-in path')
(logs/'installed-discovery.txt').write_text(discovered) (logs/'installed-discovery.txt').write_text(discovered)
save_receipt('installed',installedSignature=signature,installedSignatureVerified=True, save_receipt('installed',installedSignature=signature,installedSignatureVerified=True,
installedStapleValidated=True,gatekeeperAccepted=True, installedStapleValidated=True,gatekeeperAccepted=True,
identityMigration={'legacyInstallationsRemoved':[str(source) for legacy,source,backup in migration.moved], 'filterUUIDPreserved':PRODUCT['filterUUID']},
pluginDiscovery={'bundleIdentifier':plugin_info['CFBundleIdentifier'],'formalPath':str(installed_plugin),'output':discovered}) pluginDiscovery={'bundleIdentifier':plugin_info['CFBundleIdentifier'],'formalPath':str(installed_plugin),'output':discovered})
except Exception: except Exception:
if installed.exists():shutil.rmtree(installed) if installed.exists():shutil.rmtree(installed)
if previous.exists():shutil.move(str(previous),str(installed)) if previous.exists():shutil.move(str(previous),str(installed))
migration.rollback(lambda path:run(['pluginkit','-a',str(path)]))
raise raise
print(json.dumps({'status':'installed','path':str(installed),'notarization':result}),flush=True) print(json.dumps({'status':'installed','path':str(installed),'notarization':result}),flush=True)
if args.user_tests: if args.user_tests:
@@ -179,11 +181,11 @@ try:
input('HOST_VALIDATION_PENDING: press Return only after host verification and duplicate inventory.\n') input('HOST_VALIDATION_PENDING: press Return only after host verification and duplicate inventory.\n')
receipt['nativeEffectQA']={'status':'operator_confirmed','performedByScript':False} receipt['nativeEffectQA']={'status':'operator_confirmed','performedByScript':False}
# Only this task’s known generated product copies are removed. # Only this task’s known generated product copies are removed.
removed=cleanup_known_products() removed=migration.cleanup()+cleanup_known_products()
save_receipt('installed_user_qa_pending' if args.user_tests else 'complete',removedGeneratedPaths=removed) save_receipt('installed_user_qa_pending' if args.user_tests else 'complete',removedGeneratedPaths=removed)
print('Delivery staging and notarization archive cleaned.',flush=True) print('Delivery staging and notarization archive cleaned.',flush=True)
except BaseException as error: except BaseException as error:
save_receipt('failed',failure={'type':type(error).__name__,'message':str(error)}) save_receipt('failed',failure={'type':type(error).__name__,'message':str(error)})
raise raise
finally: finally:
(lock/'owner.json').unlink();lock.rmdir() locks.__exit__(None,None,None)
+83
查看文件
@@ -0,0 +1,83 @@
"""Add this product's reversible identity migration to PKG Forge's postinstall."""
from product_identity import PRODUCT
def inject_migration(post, product=PRODUCT):
legacy, = product['legacyProducts']
block = r'''
# FCP-BeautyGirl identity migration. Payload and template work are already complete.
BG_NEW_APP="${TARGET_VOLUME%/}@@NEW_PATH@@"
BG_OLD_APP="${TARGET_VOLUME%/}@@OLD_PATH@@"
BG_BACKUP_DIR=""
BG_OLD_MOVED=0
bg_validate_identity() {
local app="$1" expected="$2" plugin="$3" actual signature uuid
[[ -d "$app" && ! -L "$app" ]] || return 1
actual="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$app/Contents/Info.plist")" || return 1
[[ "$actual" == "$expected" ]] || return 1
/usr/bin/codesign --verify --all-architectures --deep --strict "$app" || return 1
signature="$(/usr/bin/codesign -dvvv "$app" 2>&1)" || return 1
[[ "$signature" == *'TeamIdentifier=@@TEAM@@'* ]] || return 1
uuid="$(/usr/libexec/PlistBuddy -c 'Print :ProPlugPlugInList:0:uuid' "$app/Contents/PlugIns/$plugin/Contents/Info.plist")" || return 1
[[ "$uuid" == '@@UUID@@' ]]
}
bg_unregister_identity() {
# Both bootstrap session and Unix identity belong to the desktop user.
/bin/launchctl asuser "$CONSOLE_UID" /usr/bin/sudo -u "$CONSOLE_USER" /usr/bin/pluginkit -r "$1" >/dev/null 2>&1 || true
}
bg_rollback_identity() {
local result=$?
trap - EXIT
if (( result != 0 && BG_OLD_MOVED )); then
bg_validate_identity "$BG_NEW_APP" '@@NEW_ID@@' '@@NEW_PLUGIN@@' || {
/bin/echo "新 APP 身份已变化,旧版保留在 $BG_BACKUP_DIR" >&2
exit "$result"
}
bg_unregister_identity "$BG_NEW_APP/Contents/PlugIns/@@NEW_PLUGIN@@"
/bin/rm -rf "$BG_NEW_APP"
/bin/mv "$BG_BACKUP_DIR/@@OLD_NAME@@.app" "$BG_OLD_APP"
/bin/rmdir "$BG_BACKUP_DIR"
fx_verify_app "$BG_OLD_APP" 0 'Contents/PlugIns/@@OLD_PLUGIN@@' '@@OLD_PLUGIN_ID@@' '1.1' || true
fi
exit "$result"
}
bg_finish_identity() {
if (( BG_OLD_MOVED )); then
bg_validate_identity "$BG_BACKUP_DIR/@@OLD_NAME@@.app" '@@OLD_ID@@' '@@OLD_PLUGIN@@'
/bin/rm -rf "$BG_BACKUP_DIR/@@OLD_NAME@@.app"
/bin/rmdir "$BG_BACKUP_DIR"
fi
trap - EXIT
}
bg_validate_identity "$BG_NEW_APP" '@@NEW_ID@@' '@@NEW_PLUGIN@@' || {
/bin/echo '新版 APP 签名或身份验证失败,未迁移旧版。' >&2
exit 1
}
if [[ -e "$BG_OLD_APP" || -L "$BG_OLD_APP" ]]; then
bg_validate_identity "$BG_OLD_APP" '@@OLD_ID@@' '@@OLD_PLUGIN@@' || {
/bin/echo '旧路径的 APP 身份不属于本产品,未移动或删除。' >&2
exit 1
}
BG_RECOVERY_ROOT="${TARGET_VOLUME%/}/Library/Application Support/@@NAME@@/Migration"
/bin/mkdir -p "$BG_RECOVERY_ROOT"
BG_BACKUP_DIR="$(/usr/bin/mktemp -d "$BG_RECOVERY_ROOT/.upgrade-XXXXXXXX")"
bg_unregister_identity "$BG_OLD_APP/Contents/PlugIns/@@OLD_PLUGIN@@"
/bin/mv "$BG_OLD_APP" "$BG_BACKUP_DIR/@@OLD_NAME@@.app"
BG_OLD_MOVED=1
fi
trap bg_rollback_identity EXIT
'''
values = {'NEW_PATH': product['installationPath'], 'OLD_PATH': legacy['installationPath'],
'NEW_ID': product['bundleIdentifier'], 'OLD_ID': legacy['bundleIdentifier'],
'NEW_PLUGIN': product['pluginName'], 'OLD_PLUGIN': legacy['pluginName'],
'OLD_PLUGIN_ID': legacy['pluginBundleIdentifier'], 'OLD_NAME': legacy['name'],
'NAME': product['name'], 'TEAM': product['teamIdentifier'], 'UUID': product['filterUUID']}
for key, value in values.items():
if any(c in value for c in ['"', "'", '\n', '$', '`']):
raise ValueError('Unsafe installer identity value: ' + key)
block = block.replace('@@' + key + '@@', value)
marker = '\ntrap - EXIT\nfx_verify_app '
if post.count(marker) != 1 or not post.rstrip().endswith('exit 0'):
raise ValueError('Unexpected PKG Forge postinstall structure')
post = post.replace(marker, '\ntrap - EXIT\n' + block + '\nfx_verify_app ', 1)
end = post.rfind('exit 0')
return post[:end] + 'bg_finish_identity\n' + post[end:]
+81
查看文件
@@ -0,0 +1,81 @@
"""Shared product identity and reversible, identity-checked legacy APP migration."""
import contextlib, json, os, pathlib, plistlib, shutil, subprocess
ROOT = pathlib.Path(__file__).resolve().parent.parent
PRODUCT = json.loads((ROOT / 'product.json').read_text())
def validate_app(path, bundle_id, team=PRODUCT['teamIdentifier']):
path = pathlib.Path(path)
info = plistlib.loads((path / 'Contents/Info.plist').read_bytes())
if info.get('CFBundleIdentifier') != bundle_id:
raise RuntimeError('APP identity mismatch: ' + str(path))
subprocess.run(['codesign', '--verify', '--all-architectures', '--deep', '--strict', str(path)], check=True, capture_output=True)
signature = subprocess.run(['codesign', '-dvvv', str(path)], check=True, text=True, capture_output=True).stderr
if ('TeamIdentifier=' + team) not in signature:
raise RuntimeError('APP signing team mismatch: ' + str(path))
plugins = list((path / 'Contents/PlugIns').glob('*.pluginkit'))
if not any(PRODUCT['filterUUID'] in [p.get('uuid') for p in plistlib.loads((plugin / 'Contents/Info.plist').read_bytes()).get('ProPlugPlugInList', [])] for plugin in plugins):
raise RuntimeError('APP does not contain this product filter UUID: ' + str(path))
return info
@contextlib.contextmanager
def delivery_locks(product=PRODUCT):
"""Hold both old and new identity locks throughout an identity migration."""
base = pathlib.Path('/Users/dayu/.codex/app-delivery-locks')
base.mkdir(parents=True, exist_ok=True)
held = []
try:
for bundle_id in sorted({product['bundleIdentifier']} | {p['bundleIdentifier'] for p in product.get('legacyProducts', [])}):
lock = base / (bundle_id + '.lock')
lock.mkdir()
held.append(lock)
(lock / 'owner.json').write_text(json.dumps({'pid': os.getpid(), 'cwd': str(ROOT), 'bundle_id': bundle_id, 'product': product['name']}))
yield
finally:
for lock in reversed(held):
(lock / 'owner.json').unlink(missing_ok=True)
lock.rmdir()
class LegacyMigration:
def __init__(self, backup_root, *, product=PRODUCT, volume=pathlib.Path('/'), verifier=validate_app):
self.product, self.backup_root, self.verifier = product, pathlib.Path(backup_root), verifier
self.volume, self.moved = pathlib.Path(volume), []
def candidates(self):
result = []
for legacy in self.product.get('legacyProducts', []):
source = self.volume / pathlib.Path(legacy['installationPath']).relative_to('/')
if source.exists():
self.verifier(source, legacy['bundleIdentifier'])
backup = self.backup_root / ('legacy-' + legacy['name'] + '.app')
if backup.exists():
raise RuntimeError('Legacy recovery copy already exists: ' + str(backup))
result.append((legacy, source, backup))
return result
def relocate(self, unregister):
# Validate every candidate before moving any of them.
for legacy, source, backup in self.candidates():
self.verifier(source, legacy['bundleIdentifier'])
unregister(source / 'Contents/PlugIns' / legacy['pluginName'])
backup.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(source), str(backup))
self.moved.append((legacy, source, backup))
def rollback(self, register):
for legacy, source, backup in reversed(self.moved):
if source.exists():
raise RuntimeError('Refusing to overwrite a new APP during rollback: ' + str(source))
self.verifier(backup, legacy['bundleIdentifier'])
shutil.move(str(backup), str(source))
register(source / 'Contents/PlugIns' / legacy['pluginName'])
self.moved.clear()
def cleanup(self):
removed = []
for legacy, source, backup in self.moved:
self.verifier(backup, legacy['bundleIdentifier'])
shutil.rmtree(backup)
removed.append(str(backup))
self.moved.clear()
return removed
+91
查看文件
@@ -0,0 +1,91 @@
import importlib.util, json, pathlib, plistlib, subprocess, tempfile, unittest
from unittest.mock import patch
ROOT = pathlib.Path(__file__).resolve().parent.parent
spec = importlib.util.spec_from_file_location('product_identity', ROOT / 'scripts/product_identity.py')
identity = importlib.util.module_from_spec(spec)
spec.loader.exec_module(identity)
class IdentityMigrationTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(prefix='beautygirl-identity-test.')
self.volume = pathlib.Path(self.tmp.name)
self.legacy = identity.PRODUCT['legacyProducts'][0]
self.old = self.volume / pathlib.Path(self.legacy['installationPath']).relative_to('/')
(self.old / 'Contents/PlugIns' / self.legacy['pluginName'] / 'Contents').mkdir(parents=True)
(self.old / 'Contents/Info.plist').write_bytes(plistlib.dumps({'CFBundleIdentifier': self.legacy['bundleIdentifier']}))
(self.old / 'Contents/PlugIns' / self.legacy['pluginName'] / 'Contents/Info.plist').write_bytes(plistlib.dumps({'ProPlugPlugInList': [{'uuid': identity.PRODUCT['filterUUID']}]}))
self.signature = patch.object(identity.subprocess, 'run', return_value=subprocess.CompletedProcess([], 0, '', 'TeamIdentifier=B3W25FX5H5\n'))
self.signature.start()
self.migration = identity.LegacyMigration(self.volume / 'backups', volume=self.volume)
def tearDown(self):
self.signature.stop()
self.tmp.cleanup()
def test_success_removes_only_validated_old_product(self):
original = (self.old / 'Contents/Info.plist').read_bytes()
unrelated = self.old.parent / 'Unrelated.app'
unrelated.mkdir()
calls = []
self.migration.relocate(calls.append)
self.assertFalse(self.old.exists())
self.assertEqual(len(calls), 1)
self.assertEqual((self.migration.moved[0][2] / 'Contents/Info.plist').read_bytes(), original)
self.assertEqual(len(self.migration.cleanup()), 1)
self.assertTrue(unrelated.exists())
def test_failed_new_install_restores_old_bytes_and_registration(self):
original = (self.old / 'Contents/Info.plist').read_bytes()
self.migration.relocate(lambda p: None)
calls = []
self.migration.rollback(calls.append)
self.assertEqual((self.old / 'Contents/Info.plist').read_bytes(), original)
self.assertEqual(calls, [self.old / 'Contents/PlugIns' / self.legacy['pluginName']])
def test_wrong_bundle_id_is_never_moved(self):
(self.old / 'Contents/Info.plist').write_bytes(plistlib.dumps({'CFBundleIdentifier': 'com.unrelated.product'}))
with self.assertRaisesRegex(RuntimeError, 'identity mismatch'):
self.migration.relocate(lambda p: self.fail('Must not deregister another product'))
self.assertTrue(self.old.exists())
def test_wrong_filter_uuid_is_never_moved(self):
(self.old / 'Contents/PlugIns' / self.legacy['pluginName'] / 'Contents/Info.plist').write_bytes(plistlib.dumps({'ProPlugPlugInList': []}))
with self.assertRaisesRegex(RuntimeError, 'filter UUID'):
self.migration.relocate(lambda p: self.fail('Must not deregister another filter'))
self.assertTrue(self.old.exists())
def test_invalid_signature_keeps_original(self):
with patch.object(identity.subprocess, 'run', side_effect=subprocess.CalledProcessError(1, 'codesign')):
with self.assertRaises(subprocess.CalledProcessError):
self.migration.relocate(lambda p: self.fail('Unverified APP must remain'))
self.assertTrue(self.old.exists())
def test_existing_backup_cannot_be_overwritten(self):
backup = self.volume / 'backups' / ('legacy-' + self.legacy['name'] + '.app')
backup.mkdir(parents=True)
with self.assertRaisesRegex(RuntimeError, 'already exists'):
self.migration.relocate(lambda p: self.fail('Recovery copy must be preserved'))
self.assertTrue(self.old.exists())
def test_failed_deregistration_keeps_old_installation(self):
def unavailable(path):
raise RuntimeError('registry unavailable')
with self.assertRaisesRegex(RuntimeError, 'registry unavailable'):
self.migration.relocate(unavailable)
self.assertTrue(self.old.exists())
def test_project_and_legacy_template_share_stable_filter_identity(self):
p = identity.PRODUCT
pbx = (ROOT / p['xcodeProject'] / 'project.pbxproj').read_text()
self.assertEqual(pbx.count('PRODUCT_BUNDLE_IDENTIFIER = ' + p['bundleIdentifier'] + ';'), 2)
self.assertEqual(pbx.count('PRODUCT_BUNDLE_IDENTIFIER = ' + p['pluginBundleIdentifier'] + ';'), 2)
self.assertNotIn('beatygril', pbx)
plugin = plistlib.loads((ROOT / p['sourceDirectory'] / 'Plugin/Info.plist').read_bytes())
self.assertEqual(plugin['ProPlugPlugInList'][0]['className'], p['pluginClass'])
self.assertEqual(plugin['ProPlugPlugInList'][0]['uuid'], p['filterUUID'])
self.assertEqual(plugin['CFBundleShortVersionString'], p['version'])
self.assertIn('pluginUUID="' + p['filterUUID'] + '"', (ROOT / 'Templates/大鱼人像美化/大鱼人像美化.moef').read_text())
if __name__ == '__main__':
unittest.main()