"""Shared helpers: source copying + PROVENANCE.log (mandatory traceability). RULE (user directive 2026-07-23): script-processed RTL lives ONLY in the fixed directory /vivado/rtl_ip// ; the Vivado project may reference RTL ONLY from there, NEVER from the MATLAB hdlsrc directly. PROVENANCE.log in the fixed directory records the absolute path of the original MATLAB RTL. Generated Xilinx IPs live in /ip/. """ import difflib, hashlib, os, re, shutil, time SRC_ORIG = 'src_orig' # verbatim snapshot of the MATLAB RTL this run is based on TRANSFORM_DIFF = 'TRANSFORM.diff' def src_files(d): return [fn for fn in sorted(os.listdir(d)) if fn.endswith('.v') and not fn.endswith('_tb.v')] def sha1_file(p): return hashlib.sha1(open(p, 'rb').read()).hexdigest() def copy_sources(hdlsrc, outdir, force=False): os.makedirs(outdir, exist_ok=True) copied = [] for fn in src_files(hdlsrc): dst = os.path.join(outdir, fn) if force or not os.path.exists(dst): shutil.copy(os.path.join(hdlsrc, fn), dst) copied.append(fn) snapshot_sources(hdlsrc, outdir) write_provenance(outdir, hdlsrc) return copied def snapshot_sources(hdlsrc, outdir): """Task12 rule 14: keep the ORIGINAL MATLAB RTL this run is based on, so the next run can diff against it and reuse unchanged work (incremental mode).""" d = os.path.join(outdir, SRC_ORIG) os.makedirs(d, exist_ok=True) for fn in src_files(hdlsrc): dst = os.path.join(d, fn) if not os.path.exists(dst) or sha1_file(dst) != sha1_file(os.path.join(hdlsrc, fn)): shutil.copy(os.path.join(hdlsrc, fn), dst) return d _HUNK = re.compile(r'^@@ -\d+(,\d+)? \+\d+(,\d+)? @@') def write_transform_diff(outdir): """src_orig/*.v -> rewritten *.v, as removed/added lines only (n=0) with hunk line numbers normalised away: two runs whose scripts performed the SAME rewrite produce byte-identical files, even if the MATLAB source changed elsewhere (that is exactly the class-B case of Task12 §5.2).""" orig = os.path.join(outdir, SRC_ORIG) out = [] for fn in src_files(orig): mod = os.path.join(outdir, fn) if not os.path.exists(mod): out.append(f'=== {fn}: MISSING in outdir') continue a = open(os.path.join(orig, fn), errors='replace').read().splitlines() b = open(mod, errors='replace').read().splitlines() d = [_HUNK.sub('@@', ln) for ln in difflib.unified_diff(a, b, fromfile=f'orig/{fn}', tofile=f'ip/{fn}', n=0, lineterm='')] if d: out.extend(d) p = os.path.join(outdir, TRANSFORM_DIFF) open(p, 'w').write('\n'.join(out) + '\n') return p def scripts_sha1(script_dir=None): """Fingerprint of the skill itself -- changed scripts forbid inheriting a previous cycle-true PASS (Task12 §5.4/5.5).""" d = script_dir or os.path.dirname(os.path.abspath(__file__)) h = hashlib.sha1() for fn in sorted(os.listdir(d)): if fn.endswith(('.py', '.tcl')): h.update(fn.encode()) h.update(open(os.path.join(d, fn), 'rb').read()) return h.hexdigest()[:12] def write_provenance(outdir, hdlsrc): hdlsrc = os.path.abspath(hdlsrc) lines = ['# PROVENANCE.log -- generated by rtl-ip-replace, do not edit', '# RULE: the Vivado project must use RTL from THIS directory only;', '# direct use of the MATLAB-generated hdlsrc below is FORBIDDEN.', 'source_hdlsrc: ' + hdlsrc, 'updated_at: ' + time.strftime('%Y-%m-%d %H:%M:%S'), 'ip_subdir: ip/', 'files:'] for fn in src_files(hdlsrc): p = os.path.join(hdlsrc, fn) h = sha1_file(p)[:12] mt = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.path.getmtime(p))) lines.append(f' {fn} src_sha1={h} src_mtime={mt}') lines.append('src_snapshot: ' + SRC_ORIG + '/ # original MATLAB RTL kept for the next incremental run') lines.append('scripts_sha1: ' + scripts_sha1()) open(os.path.join(outdir, 'PROVENANCE.log'), 'w').write('\n'.join(lines) + '\n') def append_provenance(outdir, lines): """Append the incremental-run block (Task12 §5.6). Called last, after the transform scripts have rewritten PROVENANCE.log.""" with open(os.path.join(outdir, 'PROVENANCE.log'), 'a') as f: f.write('\n'.join(lines) + '\n')