Add notarized 1.2.0 release installer, isolated update/rollback verification and publication materials

这个提交包含在:
dayu 于 2026-10-22 01:43:55 +08:00 提交
1 父节点 5ac0e5833c
当前提交 3c7261c3a8
6 个文件已更改
+241

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

+66
查看文件
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Publish this reviewed release through the connected Dayu article publisher.
Secrets are read from macOS Keychain into memory; no auth material is persisted.
"""
from pathlib import Path
import base64,hashlib,html,json,mimetypes,ssl,subprocess,urllib.error,urllib.parse,urllib.request,uuid
ROOT=Path(__file__).resolve().parent.parent
STATE=ROOT/'build/publish-operation.json';STATE.parent.mkdir(exist_ok=True)
state=json.loads(STATE.read_text()) if STATE.exists() else {'operation':str(uuid.uuid4())}
def save():STATE.write_text(json.dumps(state,ensure_ascii=False,indent=2)+'\n')
save()
secret=subprocess.run(['security','find-generic-password','-s','dayu-article-publisher','-a','dayu','-w'],text=True,capture_output=True,check=True).stdout.strip()
auth='Basic '+base64.b64encode(('dayu:'+secret).encode()).decode()
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):return None
opener=urllib.request.build_opener(NoRedirect,urllib.request.HTTPSHandler(context=ssl.create_default_context(cafile='/etc/ssl/cert.pem')))
def request(path,method='GET',body=None,headers=None):
assert path.startswith('/wp-json/') and '\n' not in path
h={'Authorization':auth,'Accept':'application/json'};h.update(headers or {})
if isinstance(body,dict):body=json.dumps(body,ensure_ascii=False).encode();h['Content-Type']='application/json'
req=urllib.request.Request('https://dayu.media'+path,data=body,headers=h,method=method)
with opener.open(req,timeout=180) as f:return json.load(f)
def upload(name,path,mime):
if name in state:return state[name]
filename=path.name
matches=request('/wp/v2/media?search='+urllib.parse.quote(path.stem)+'&per_page=30&context=edit')
for item in matches:
if urllib.parse.unquote(item.get('source_url','')).endswith('/'+filename):
# Only reuse a byte-identical previous upload.
with urllib.request.urlopen(item['source_url'],context=ssl.create_default_context(cafile='/etc/ssl/cert.pem'),timeout=90) as r:data=r.read()
if hashlib.sha256(data).digest()==hashlib.sha256(path.read_bytes()).digest():
state[name]={'id':item['id'],'url':item['source_url']};save();return state[name]
result=request('/wp/v2/media','POST',path.read_bytes(),{'Content-Type':mime,'Content-Disposition':"attachment; filename*=UTF-8''"+urllib.parse.quote(filename)})
state[name]={'id':result['id'],'url':result['source_url']};save();return state[name]
status=request('/dayu-publisher/v1/status');assert status['can_publish'] and status['can_upload'] and 'seo' in status['fields']
source=upload('source',ROOT/'build/Dayu-Portrait-1.2.0-source.zip','application/zip')
card=upload('card',ROOT/'beatygril/beatygril/Wrapper Application/Resources/LauncherInfo.png','image/png')
content=(ROOT/'releases/1.2.0/article.html').read_text().replace('{{source_url}}',html.escape(source['url'],quote=True)).replace('{{card_url}}',html.escape(card['url'],quote=True))
seo={
'title':{'enabled':True,'value':'大鱼人像美化 1.2.0|FCP / Motion 免费开源自动美颜插件'},
'description':{'enabled':True,'value':'大鱼人像美化是适用于 Final Cut Pro 和 Motion 的免费开源 FxPlug4 插件,支持磨皮、美白、自动人脸跟随及瘦脸调整。提供 Apple 签名公证 PKG 安装包和源码,安装自动注册,无需激活码,并附具体安装、使用及红色警告排查说明。'},
'keywords':{'enabled':True,'value':'大鱼人像美化,FCP美颜插件,Motion磨皮,自动瘦脸,自动跟随,FxPlug4,免费开源'}
}
payload={'title':'大鱼人像美化 1.2.0:FCP / Motion 免费开源自动美颜插件','slug':'dayu-portrait-beauty','status':'publish','content':content,'excerpt':'磨皮、美白、瘦脸与自动人脸跟随,原生卡片面板实时预览。免费开源,自动注册,无需激活码;提供签名公证安装包和源码。','categories':[3],'tags':[], 'resource':{'cao_status':True,'cao_price':0,'cao_vip_rate':1,'cao_close_novip_pay':False,'cao_expire_day':0,'cao_pwd':'','cao_info':[{'title':'版本','desc':'1.2.0(14)'},{'title':'宿主','desc':'Final Cut Pro / Motion'},{'title':'系统','desc':'macOS 13 或更高版本'},{'title':'架构','desc':'Apple Silicon / Intel'},{'title':'授权','desc':'免费开源,无需激活码'}]},'seo':seo}
(ROOT/'releases/1.2.0/article-payload.json').write_text(json.dumps(payload,ensure_ascii=False,indent=2)+'\n')
if 'post_id' not in state:
existing=request('/wp/v2/posts?slug=dayu-portrait-beauty&context=edit');assert not existing,'Matching post exists; inspect and continue its ID instead of creating a duplicate'
boundary='DayuRelease'+uuid.uuid4().hex;parts=[]
def part(name,data,filename=None,mime=None):
head='--'+boundary+'\r\nContent-Disposition: form-data; name="'+name+'"'
if filename:head+='; filename="'+filename+'"'
head+='\r\n'
if mime:head+='Content-Type: '+mime+'\r\n'
parts.append(head.encode()+b'\r\n'+data+b'\r\n')
part('payload',json.dumps(payload,ensure_ascii=False).encode())
part('package',(ROOT/'releases/1.2.0/大鱼人像美化-1.2.0.pkg').read_bytes(),'Dayu-Portrait-1.2.0.pkg','application/octet-stream')
part('cover',(ROOT/'Assets/Artwork/portrait-cover.png').read_bytes(),'Dayu-Portrait-Cover.png','image/png')
body=b''.join(parts)+('--'+boundary+'--\r\n').encode()
result=request('/dayu-publisher/v1/posts','POST',body,{'Content-Type':'multipart/form-data; boundary='+boundary,'Idempotency-Key':state['operation']})
state['response']=result;save()
# Response schema may vary; resolve the exact saved slug through standard REST.
rows=request('/wp/v2/posts?slug=dayu-portrait-beauty&context=edit');assert len(rows)==1
state['post_id']=rows[0]['id'];state['url']=rows[0]['link'];save()
saved=request('/dayu-publisher/v1/posts/'+str(state['post_id']))
(ROOT/'docs/verification/v1.2.0/published-article.json').write_text(json.dumps(saved,ensure_ascii=False,indent=2)+'\n')
print(json.dumps({'post_id':state['post_id'],'url':state['url'],'source':source,'card':card,'snapshot_saved':True},ensure_ascii=False))
+67
查看文件
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Verify an expanded release payload and run template-only update/rollback in isolation."""
from pathlib import Path
import hashlib,json,os,plistlib,shutil,subprocess,sys
root=Path(__file__).resolve().parent.parent
expanded=root/'build/pkg-expanded/Payload.pkg';payload=expanded/'Payload'
formal=Path('/Library/Plug-Ins/FxPlug/beatygril.app');packed=payload/'Library/Plug-Ins/FxPlug/beatygril.app'
sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
def files(p):
return {str(x.relative_to(p)):('symlink:'+os.readlink(x) if x.is_symlink() else sha(x)) for x in p.rglob('*') if x.is_file() or x.is_symlink()}
assert files(formal)==files(packed),'APP differs from signed installed product'
subprocess.run(['codesign','--verify','--deep','--strict',str(packed)],check=True)
subprocess.run(['xcrun','stapler','validate',str(packed)],check=True,capture_output=True)
source=root/'Templates/大鱼人像美化';template=next(payload.rglob('大鱼人像美化.moef')).parent
runtime=['大鱼人像美化.moef','Drop Zone.tiff','large.png','small.png']
assert sorted(x.name for x in template.iterdir())==sorted(runtime)
assert all(sha(source/n)==sha(template/n) for n in runtime)
post=(expanded/'Scripts/postinstall').read_text();pre=(expanded/'Scripts/preinstall').read_text()
for p in [expanded/'Scripts/postinstall',expanded/'Scripts/preinstall']:
subprocess.run(['/bin/zsh','-n',str(p)],check=True)
locks=[l for l in post.splitlines() if l.startswith('/usr/bin/chflags uchg "${FINAL_TARGET}')]
assert len(locks)==2 and any('/large.png' in l for l in locks) and any('/small.png' in l for l in locks)
assert 'Template Backups' in post and 'restore_original_locks' in post
# Keep the exact generated functions and template body. Only identity lookup,
# chown and external FxPlug registration are replaced to confine execution.
base=root/'build/pkg-isolation';base.mkdir(exist_ok=False)
volume=base/'volume';home=base/'user';home.mkdir()
start=post.index('CURRENT_ACTIVE=0')
isolated='#!/bin/zsh\nset -eu\nTARGET_VOLUME="'+str(volume)+'"\nCONSOLE_USER="'+os.getlogin()+'"\nCONSOLE_UID="'+str(os.getuid())+'"\nCONSOLE_GROUP="staff"\nUSER_HOME="'+str(home)+'"\n'+post[start:]
isolated=isolated[:isolated.index('fx_verify_app "${TARGET_VOLUME%/}/Library/Plug-Ins/FxPlug/beatygril.app"')]+'exit 0\n'
isolated=isolated.replace('/usr/sbin/chown -R','/usr/bin/true')
new_copy=next(l for l in isolated.splitlines() if l.startswith('/usr/bin/ditto "${TARGET_VOLUME%/}'))
def refill():
target=volume/template.parent.relative_to(payload)
if target.exists():
subprocess.run(['chflags','-R','nouchg',str(target)],check=True);shutil.rmtree(target)
target.parent.mkdir(parents=True,exist_ok=True);shutil.copytree(template.parent,target,symlinks=True)
def run_script(name,text,success):
p=base/name;p.write_text(text)
proc=subprocess.run(['/bin/zsh',str(p)],text=True,capture_output=True)
(base/(name+'.output')).write_text(proc.stdout+proc.stderr)
assert (proc.returncode==0)==success,(name,proc.returncode,proc.stderr)
return proc.returncode
final=home/'Movies/Motion Templates.localized/Effects.localized/大鱼磨皮瘦脸/大鱼人像美化'
refill();run_script('fresh-install.zsh',isolated,True)
assert all(sha(final/n)==sha(source/n) for n in runtime)
assert all((final/n).stat().st_flags & 2 for n in ['large.png','small.png'])
# Locked old images are overwritten, then protected again.
subprocess.run(['chflags','nouchg',str(final/'large.png')],check=True)
(final/'large.png').write_bytes(b'old locked thumbnail');subprocess.run(['chflags','uchg',str(final/'large.png')],check=True)
refill();run_script('update-locked.zsh',isolated,True)
assert all(sha(final/n)==sha(source/n) for n in runtime)
assert all((final/n).stat().st_flags & 2 for n in ['large.png','small.png'])
# Simulate a failed payload copy after corrupting one new file; the actual EXIT
# trap must restore the previous bytes and previous immutable flags.
original=files(final);refill()
failing=isolated.replace(new_copy,'/bin/echo partial > "${FINAL_TARGET}/大鱼人像美化/large.png"\n/usr/bin/false',1)
run_script('failed-copy-rollback.zsh',failing,False)
assert files(final)==original
assert all((final/n).stat().st_flags & 2 for n in ['large.png','small.png'])
backups=list((home/'Library/Application Support/PKGForge/Template Backups').rglob('.locked-paths'))
assert backups and all('/Movies/' not in str(x) for x in backups)
package=root/'releases/1.2.0/大鱼人像美化-1.2.0.pkg'
report={'package':str(package.relative_to(root)),'bytes':package.stat().st_size,'sha256':sha(package),'version':'1.2.0','build':'14','app_files_compared':len(files(formal)),'app_byte_and_symlink_equality':True,'template_files':{n:sha(template/n) for n in runtime},'locked_relative_paths':['大鱼人像美化/large.png','大鱼人像美化/small.png'],'script_sha256':{'preinstall':sha(expanded/'Scripts/preinstall'),'postinstall':sha(expanded/'Scripts/postinstall')},'isolated_script_tests':['fresh install','replace old locked thumbnail','partial copy failure restores old bytes and locks'],'isolation_changes':['Console identity/home redirected to task fixture','chown replaced with true','FxPlug verification skipped; formal app signature and discovery verified separately'],'real_installer_execution':False,'app_and_template_installed':'APP installed through signed delivery workflow; user thumbnail directory updated separately, not by running this PKG','fonts':'No editable third-party font dependency; promotional lettering baked into PNG. No optional font ZIP needed.'}
(root/'docs/verification/v1.2.0/pkg-verification.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n');print(json.dumps(report,ensure_ascii=False,indent=2))
# This fixture belongs solely to this verifier; remove its locks before cleanup.
subprocess.run(['chflags','-R','nouchg',str(base)],check=True);shutil.rmtree(base)