"""Hash-checked deployment and removal of this devkit's owned files.""" import json from pathlib import Path import shutil import time from . import support ROOT=support.STATE_ROOT def owned_scripts(receipt): return [(Path(receipt['mod_path']),receipt['mod_sha256']), (Path(receipt['evaluation']['path']),receipt['evaluation']['sha256'])] + [ (Path(item['path']),item['sha256']) for item in receipt.get('assets',[])] def verify(): receipt=json.loads((ROOT/'deployment.json').read_text()) native=json.loads((ROOT/'native/deployment.json').read_text()) files=owned_scripts(receipt)+[(Path(f['path']),f['sha256']) for f in native['files']] if 'executable' in native:files.append((Path(native['executable']['path']),native['executable']['sha256'])) for path,digest in files: if not path.is_file() or support.manage.digest(path)!=digest: raise RuntimeError('Installed dependency differs from verified receipt: '+str(path)) from . import background background_enabled=background.verify() from . import compatibility checked=compatibility.verify_receipt(native) result={'ok':True,'build':receipt['build'],'verified_files':len(files),'background_enabled':background_enabled} if checked: result['compatibility']=checked return result def install_native(): support.manage.ensure_game_closed();verify() path=ROOT/'native/deployment.json';receipt=json.loads(path.read_text()) from . import compatibility compatibility.closed_boundary() checked=compatibility.verify_receipt(receipt) if checked: _,profile=compatibility.inspect_game() for name in ('PathInput.asi','WitcherNativeBridge.asi'): candidate=support.ROOT/'native/build'/name if not candidate.is_file() or support.manage.digest(candidate)!=profile['native_files'][name]: raise ValueError('compatibility: candidate adapter is not approved for this executable') # verify already established the exact installed profile. Do not recopy # or re-receipt potentially changing build outputs for a verified no-op. return {'ok':True,'native_installed':False,'already_current':True,'profile':profile['id']} folder=ROOT/'artifacts'/('native-install-'+str(time.time_ns()));folder.mkdir() support.atomic_json(folder/'previous-receipt.json',receipt) changed=[] try: for entry in receipt['files']: target=Path(entry['path']) if target.name not in ('PathInput.asi','WitcherNativeBridge.asi'):continue source=support.ROOT/'native/build'/target.name if not source.is_file():raise RuntimeError('Build first: '+str(source)) candidate=source.read_bytes() shutil.copy2(target,folder/target.name);changed.append(target) temp=target.with_suffix(target.suffix+'.devkit-next');temp.write_bytes(candidate);temp.replace(target) entry['sha256']=support.manage.digest(target) except Exception: for target in changed:shutil.copy2(folder/target.name,target) raise # Only reconcile-game-update can adopt a changed executable after checking # the full reviewed dependency chain; ordinary adapter updates retain its pin. support.atomic_json(path,receipt) return {'ok':True,'native_installed':True,'rollback_artifacts':str(folder)} def install_ui(): support.manage.ensure_game_closed();verify() path=ROOT/'deployment.json';receipt=json.loads(path.read_text()) entries=receipt.get('assets',[]) if {Path(e['path']).name for e in entries}!={'blob0.bundle','metadata.store'}: raise RuntimeError('Expected the two owned UI bundle/index files') folder=ROOT/'artifacts'/('ui-install-'+str(time.time_ns()));folder.mkdir() support.atomic_json(folder/'previous-receipt.json',receipt) changed=[] try: for entry in entries: target=Path(entry['path']);source=support.ROOT/'research/ui-build/packed'/target.name if not source.is_file():raise RuntimeError('Build UI assets first') shutil.copy2(target,folder/target.name);changed.append(target) temporary=target.with_suffix(target.suffix+'.devkit-next') shutil.copy2(source,temporary);temporary.replace(target) entry['sha256']=support.manage.digest(target) receipt['ui_revision']='PathDevkitUIv3' support.atomic_json(path,receipt) except Exception: for target in changed:shutil.copy2(folder/target.name,target) raise return {'ok':True,'ui_installed':True,'rollback_artifacts':str(folder)} def uninstall(): support.manage.ensure_game_closed();verify() receipt=json.loads((ROOT/'deployment.json').read_text()) native=json.loads((ROOT/'native/deployment.json').read_text()) scripts=owned_scripts(receipt) modroot=support.manage.GAME/'mods/modPathDevkit' if set(p for p in modroot.rglob('*') if p.is_file()) != set(p for p,_ in scripts): raise RuntimeError('Untracked files in devkit mod; refusing partial removal') folder=ROOT/'artifacts'/('uninstall-'+str(time.time_ns()));folder.mkdir() paths=[p for p,_ in scripts]+[Path(f['path']) for f in native['files']] for i,p in enumerate(paths):shutil.copy2(p,folder/(str(i)+'-'+p.name)) from . import background background_result=None if background.verify(): shutil.copy2(background.TARGET,folder/'PathBackground.asi') background_result=background.disable() for p in paths:p.unlink() for p in sorted((p for p in modroot.rglob('*') if p.is_dir()),key=lambda p:len(p.parts),reverse=True):p.rmdir() modroot.rmdir() shutil.move(ROOT/'deployment.json',folder/'deployment.json') shutil.move(ROOT/'native/deployment.json',folder/'native-deployment.json') return {'ok':True,'removed':'modPathDevkit and three native loader/adapter files','artifacts':str(folder),'background':background_result}