"""Build supplemental bundle evaluator only; never launches product or converter.""" from pathlib import Path import argparse,datetime,hashlib,json,os,shutil,subprocess HERE=Path(__file__).resolve().parent WORKSPACE=HERE.parents[2] REPO=WORKSPACE/'.local/traceability-view-20260923' PIN='17f1f89fef4a6bf520f4b09a2116548b967d030e' ENV=WORKSPACE/'.local/toolchain/env.ps1' RUNTIME=WORKSPACE/'.local/toolchain/llvm-mingw-20260908-ucrt-x86_64/bin/libunwind.dll' TARGET=WORKSPACE/'target' def rec(p): p=Path(p).resolve();h=hashlib.sha256() with p.open('rb') as f: for b in iter(lambda:f.read(1024*1024),b''):h.update(b) return {'path':str(p),'bytes':p.stat().st_size,'sha256':h.hexdigest()} def save(p,v): with p.open('x',encoding='utf-8') as f:json.dump(v,f,indent=2);f.write('\n') def git(*a):return subprocess.check_output(['git','-c','core.longpaths=true','-C',str(REPO),*a]) def clean(): if git('rev-parse','HEAD').decode().strip()!=PIN or git('status','--porcelain'):raise RuntimeError('Exact clean source required') def inventory():return [rec(REPO/x.decode()) for x in sorted(git('ls-files','--cached','--others','--exclude-standard','-z').split(b'\0')) if x and (REPO/x.decode()).is_file()] def configs(): result=[];cargo=Path(os.environ['CARGO_HOME']) for d in dict.fromkeys([REPO,*REPO.parents,cargo]): for name in ('config','config.toml'): p=d/name if d==cargo else d/'.cargo'/name;result.append({'path':str(p),'present':p.exists(),'record':rec(p) if p.is_file() else None}) return result def main(): ap=argparse.ArgumentParser();ap.add_argument('--attempt',required=True);a=ap.parse_args() if not __import__('re').fullmatch(r'build-[0-9]{2}',a.attempt):raise RuntimeError('Fresh build-NN required') clean() if Path(os.environ.get('CARGO_TARGET_DIR','')).resolve()!=TARGET.resolve() or Path(os.environ.get('CARGO_HOME','')).resolve()!=(WORKSPACE/'.local/toolchain/cargo').resolve() or os.environ.get('RUSTUP_TOOLCHAIN')!='stable-x86_64-pc-windows-gnullvm':raise RuntimeError('Source admitted env.ps1 and set root target') if rec(RUNTIME)['sha256']!='aa282fa47bb2c4e210cea5e470cdbdca58568f0be0bc73e404d86ab9cd95ef6a':raise RuntimeError('Runtime mismatch') out=HERE/a.attempt;out.mkdir(exist_ok=False);(out/'package').mkdir() before=inventory();save(out/'inputs-before.json',before) cfg=configs();bound=[rec(ENV),rec(Path(__file__)),rec(HERE/'bundle_converter.rs'),rec(HERE/'capture_bundle.py'),rec(HERE/'README.md'),rec(RUNTIME)] save(out/'environment.json',{'environment':{k:os.environ.get(k) for k in ['PATH','CARGO_HOME','RUSTUP_HOME','RUSTUP_TOOLCHAIN','CARGO_TARGET_DIR','CARGO_BUILD_JOBS','CARGO_INCREMENTAL','RUSTFLAGS','CARGO_ENCODED_RUSTFLAGS','CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER','RC']},'configs':cfg,'bound':bound}) steps=[];failure=None;outputs=[];libraries=[];after=None def run(name,argv): with (out/(name+'.stdout.log')).open('xb') as stdout,(out/(name+'.stderr.log')).open('xb') as stderr: start=datetime.datetime.now(datetime.timezone.utc).isoformat();p=subprocess.run(argv,cwd=REPO,stdout=stdout,stderr=stderr) r={'argv':argv,'cwd':str(REPO),'started_utc':start,'exit_code':p.returncode,'stdout':rec(out/(name+'.stdout.log')),'stderr':rec(out/(name+'.stderr.log'))};save(out/(name+'.result.json'),r);steps.append(r) if p.returncode:raise RuntimeError(name+' failed') def preserve(p): r=rec(p);dest=out/'package'/p.name with p.open('rb') as src,dest.open('xb') as dst:shutil.copyfileobj(src,dst) c=rec(dest) if rec(p)!=r or any(c[k]!=r[k] for k in ('bytes','sha256')):raise RuntimeError('Copy changed') outputs.append(c);return dest try: run('cargo-lib',['cargo','build','-p','oa-core','--lib','--locked','--offline','--message-format=json']) messages=[json.loads(x) for x in (out/'cargo-lib.stdout.log').read_text().splitlines() if x.strip()] candidates=[m for m in messages if m.get('reason')=='compiler-artifact' and m.get('target',{}).get('name')=='oa_core' and 'lib' in m['target']['kind'] and Path(m['manifest_path']).resolve()==(REPO/'crates/oa-core/Cargo.toml').resolve()] if len(candidates)!=1 or not any(m.get('reason')=='build-finished' and m.get('success') is True for m in messages):raise RuntimeError('Unambiguous successful Cargo artifact required') artifact=candidates[0];rlibs=[Path(x) for x in artifact['filenames'] if x.endswith('.rlib')] if len(rlibs)!=1:raise RuntimeError('Exactly one core rlib required') rlib=rlibs[0].resolve();debug=(TARGET/'debug').resolve();deps=debug/'deps' if not ((rlib.parent==debug and rlib.name=='liboa_core.rlib') or (rlib.parent==deps and __import__('re').fullmatch(r'liboa_core-[0-9a-f]+\.rlib',rlib.name))):raise RuntimeError('Cargo core artifact outside admitted target/debug locations') if not deps.is_dir():raise RuntimeError('Admitted dependency directory missing') save(out/'cargo-artifact.json',{'message':artifact,'rlib':rec(rlib),'dependency_directory':str(deps)}) preserved=preserve(rlib) run('rustc-version',['rustc','--version','--verbose']);run('rustc-sysroot',['rustc','--print','sysroot']) sysroot=Path((out/'rustc-sysroot.stdout.log').read_text().strip()) paths=set(p for d in [deps,sysroot/'lib/rustlib/x86_64-pc-windows-gnullvm/lib'] for p in d.rglob('*') if p.is_file() and p.suffix.lower() in ('.rlib','.rmeta','.lib','.a','.dll')) paths.update(p for p in (sysroot/'bin').glob('*.dll') if p.is_file());paths.add(sysroot/'bin/rustc.exe');linker=shutil.which('clang.exe') if not linker:raise RuntimeError('Linker missing') paths.add(Path(linker));libraries=[rec(p) for p in sorted(paths)];save(out/'libraries-before.json',libraries) run('converter-build',['rustc','--edition=2024',str(HERE/'bundle_converter.rs'),'--crate-name','traceability_bundle_converter','--extern','oa_core='+str(preserved),'-L','dependency='+str(deps),'-C','linker='+linker,'-o',str(out/'package/bundle_converter.exe')]) outputs.append(rec(out/'package/bundle_converter.exe'));preserve(RUNTIME) afterlibs=[rec(x['path']) for x in libraries];save(out/'libraries-after.json',afterlibs) if afterlibs!=libraries:raise RuntimeError('Compiler/library inputs changed') except Exception as e:failure=str(e) finally: try: after=inventory();save(out/'inputs-after.json',after);clean() if after!=before or configs()!=cfg or any(rec(x['path'])!=x for x in bound):raise RuntimeError('Bound inputs changed') for r in outputs: if rec(r['path'])!=r:raise RuntimeError('Preserved output changed') except Exception as e:failure='; '.join(x for x in [failure,str(e)] if x) save(out/'summary.json',{'source_commit':PIN,'passed':failure is None,'failure':failure,'inputs_unchanged':after==before,'steps':steps,'outputs':outputs,'library_input_count':len(libraries),'scope':'New supplemental evidence converter build, no GUI or converter execution, not a replacement product gate'}) if failure:raise SystemExit(failure) print('Supplemental build passed; review receipt before converter admission') if __name__=='__main__':main()