40 lines
4.7 KiB
Python
40 lines
4.7 KiB
Python
from __future__ import annotations
|
|
import argparse,hashlib,json,os,shutil,subprocess,tarfile,tempfile
|
|
from pathlib import Path
|
|
|
|
SOURCE_FILES=["VERSION","versions/V2R1C7.json","MANIFEST.sha256","artifacts/v2r1c7/SOURCE-MANIFEST.json","active-runtime/app/deployment_state.py","active-runtime/app/v2r1c6_privacy_tools.py","active-runtime/app/v2r1c7_health_service.py","deploy/aios-v2/systemd/aios-v2r1c7-production.env.example","deploy/aios-v2/systemd/aios-v2r1c7-production.service","deploy/aios-v2/v2r1c7-runtime-config.json","deploy/aios-v2/v2r1c7-rollback.json","release-manifests/V2R1C7.json","scripts/generate_v2r1c7_manifest.py","scripts/probe_v2r1c7_health.py","scripts/install_v2r1c7_staging.sh","scripts/rollback_v2r1c7_release.sh","scripts/verify_v2r1c7_release.py","services/aios-release-controller/aios_release_controller/__init__.py","services/aios-release-controller/aios_release_controller/manifest.py","services/aios-release-controller/aios_release_controller/cli.py"]
|
|
|
|
def canon(v:object)->bytes: return (json.dumps(v,sort_keys=True,separators=(",",":"))+"\n").encode()
|
|
def digest(p:Path)->str: return hashlib.sha256(p.read_bytes()).hexdigest()
|
|
def git(root:Path,*args:str)->str: return subprocess.check_output(["git",*args],cwd=root,text=True).strip()
|
|
|
|
def build(root:Path,target_oid:str,base_oid:str,output:Path,epoch:int)->dict[str,object]:
|
|
if git(root,"rev-parse","HEAD")!=target_oid: raise SystemExit("source_head_mismatch")
|
|
if git(root,"status","--porcelain"): raise SystemExit("clean_worktree_required")
|
|
source=json.loads((root/"release-manifests/V2R1C7.json").read_text(encoding="utf-8"))
|
|
if source.get("version")!="V2R1C7" or source.get("base_oid")!=base_oid: raise SystemExit("release_source_binding_invalid")
|
|
with tempfile.TemporaryDirectory(prefix="v2r1c7-build-") as td:
|
|
payload=Path(td)/"payload"; payload.mkdir()
|
|
for rel in SOURCE_FILES:
|
|
src=root/rel
|
|
if not src.is_file() or src.is_symlink(): raise SystemExit("required_source_missing:"+rel)
|
|
dst=payload/rel; dst.parent.mkdir(parents=True,exist_ok=True); shutil.copyfile(src,dst); dst.chmod(0o755 if rel.endswith(".sh") else 0o644)
|
|
meta={"schema":"aios.release-artifact-build.v3","version":"V2R1C7","repository":"aios/aios","source_head_oid":target_oid,"base_oid":base_oid,"source_date_epoch":epoch,"clean_worktree":True,"production_deployed":False,"tag_created":False,"baseline_changed":False,"secrets_included":False,"executable_payload":True}
|
|
(payload/"BUILD-METADATA.json").write_bytes(canon(meta))
|
|
rollback=json.loads((payload/"deploy/aios-v2/v2r1c7-rollback.json").read_text(encoding="utf-8")); (payload/"ROLLBACK-METADATA.json").write_bytes(canon(rollback))
|
|
names=sorted("payload/"+str(p.relative_to(payload)).replace(os.sep,"/") for p in payload.rglob("*") if p.is_file())
|
|
(payload/"ARTIFACT-MANIFEST.json").write_bytes(canon({"schema":"aios.release-artifact-manifest.v3","version":"V2R1C7","source_head_oid":target_oid,"base_oid":base_oid,"files":names+["payload/ARTIFACT-MANIFEST.json"],"deployable":True,"production_deployed":False,"tag_created":False,"baseline_changed":False}))
|
|
tar=Path(td)/"release.tar"
|
|
with tarfile.open(tar,"w") as archive:
|
|
for p in sorted(payload.rglob("*")):
|
|
info=archive.gettarinfo(str(p),arcname="payload/"+str(p.relative_to(payload)).replace(os.sep,"/")); info.uid=0; info.gid=0; info.uname="root"; info.gname="root"; info.mtime=epoch; info.mode=0o755 if p.is_dir() or p.suffix==".sh" else 0o644
|
|
if p.is_file():
|
|
with p.open("rb") as stream: archive.addfile(info,stream)
|
|
else: archive.addfile(info)
|
|
output.parent.mkdir(parents=True,exist_ok=True); subprocess.run(["zstd","-T1","-19","--no-progress","-q","-f",str(tar),"-o",str(output)],check=True)
|
|
receipt={"schema":"aios.release-artifact-receipt.v3","version":"V2R1C7","source_head_oid":target_oid,"base_oid":base_oid,"artifact":output.name,"artifact_sha256":digest(output),"artifact_size":output.stat().st_size,"deployable":True,"executable_payload":True,"production_deployed":False,"tag_created":False,"baseline_changed":False}
|
|
output.with_name(output.stem+".manifest.json").write_bytes(canon(receipt)); (output.parent/"BUILD-RECEIPT.json").write_bytes(canon(receipt)); return receipt
|
|
|
|
if __name__=="__main__":
|
|
p=argparse.ArgumentParser(); p.add_argument("--repo-root",type=Path,required=True); p.add_argument("--target-oid",required=True); p.add_argument("--base-oid",required=True); p.add_argument("--output",type=Path,required=True); p.add_argument("--source-date-epoch",type=int,default=0); a=p.parse_args(); print(json.dumps(build(a.repo_root.resolve(),a.target_oid,a.base_oid,a.output,a.source_date_epoch),sort_keys=True))
|