84 lines
6.9 KiB
Python
84 lines
6.9 KiB
Python
"""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()
|