#!/usr/bin/env python3 # Script to insert AmphibianDNA binaries into the build tree import argparse import os import os.path as path import time import shutil import re from datetime import datetime from subprocess import run, PIPE, DEVNULL from machfs import Volume ######################################################################## RE1 = re.compile(rb'^(\w+)\s*=\s*(.+)') RE2 = re.compile(rb'{(\w+)}') def extract_makefile_defines(makefile, seed={}): makefile = makefile.replace(b'\r', b'\n') # tolerate all sorts of crud vardict = dict(seed) grabber = lambda m: vardict.get(m.group(1).decode('ascii'), '').encode('ascii') for line in text.split(b'\n'): m = RE1.match(line) if m: try: left = m.group(1).decode('ascii') right = RE2.sub(grabber, m.group(2)).decode('ascii') vardict[left] = right except UnicodeDecodeError: pass return vardict ######################################################################## REB = re.compile(rb'Build-date: (\d\d\d\d-\d\d-\d\d)') def get_build_date(from_dir): try: msg = run(['git', 'rev-list', '--format=%B', '--max-count=1', 'HEAD'], cwd=from_dir, stdout=PIPE, stderr=DEVNULL, check=True) msg = msg.stdout except: return for l in msg.split(b'\n'): m = REB.match(l) if m: return m.group(1).decode('ascii') def ticks_from_str(s): delta = datetime.strptime(datstr, '%Y-%m-%d') - datetime(1904, 1, 1) delta = int(delta.total_seconds()) return delta ######################################################################## # Argparse args = argparse.ArgumentParser(description=''' Copy an MPW source tree into a System 6 disk image that builds on boot. Difficult parts of the build tree can be elegantly replaced by dropping pre-built objects or binaries into the AmphibianDNA folder. Every file in AmphibianDNA will be spliced into the build system by rewriting the makefile rule that seems to target it, or by direct copying if no rule is found. All necessary BuildResults folders are also created. ''') args.add_argument('src', metavar='SOURCES', action='store', help='Source tree') args.add_argument('-e', dest='emu', metavar='VMAC', action='store', default=None, help='path to emulator') args.add_argument('-c', dest='cmd', metavar='CMD', action='store', default=None, help='MPW Shell command line') args.add_argument('-v', dest='verbose', action='store_true', help='verbose') args = args.parse_args() treedest = path.join(args.src, 'BuildImage') imgdest = path.join(args.src, 'BuildImage.dmg') ######################################################################## def log(*a, **kwa): if args.verbose: print(*a, **kwa) ######################################################################## log('Copying source tree', flush=True) try: shutil.rmtree(treedest) except FileNotFoundError: pass myignore = shutil.ignore_patterns('BuildImage*', '.*', '*.dmg', '*.dsk', '*.sh', '*.py') # copy2 preserves mod times, which we need to eventually allow MPW Make to work right shutil.copytree(args.src, treedest, ignore=myignore, copy_function=shutil.copy2) all_makefiles = [] for root, dirs, files in os.walk(treedest): for f in files: if f.lower().endswith('.make'): all_makefiles.append(path.join(root, f)) ######################################################################## log('Creating build folders', flush=True) # I have better code for this! main_makefiles = [] for mkfile in all_makefiles: with open(mkfile, 'rb') as f: text = f.read() defines = extract_makefile_defines(text) if 'BuildDir' not in defines: continue main_makefiles.append(mkfile) for key, macpath in defines.items(): macpath = macpath.replace('"', '') if key.endswith('Dir') and macpath.startswith('BuildResults:'): nativepath = path.join(treedest, *macpath.split(':')[:-1]) os.makedirs(nativepath, exist_ok=True) ######################################################################## log('Splicing amphibian DNA', flush=True) # Ugly, but keeps src tree clean OVERDIR = 'AmphibianDNA' try: overs = [n for n in os.listdir(path.join(treedest, OVERDIR)) if path.splitext(n)[1].lower() not in ('.idump', '.rdump')] except FileNotFoundError: overs = [] if overs: remaining = list(overs) overs_re = '|'.join(re.escape(x) for x in overs) overs_re = r'^[^#\s]*\b(Thing.lib)"?\s*ƒ\s*'.replace('Thing.lib', overs_re) overs_re = re.compile(overs_re, re.IGNORECASE) for f in all_makefiles: mfile = open(f).read().split('\n') havechanged = False newmfile = [] idx = -1 while idx + 1 < len(mfile): idx += 1 m = overs_re.match(mfile[idx]) if m: thefile = m.group(1) havechanged = True newmfile.append('# Rule replaced at build time by ' + path.basename(__file__)) remaining = [x for x in remaining if x.upper() != thefile.upper()] srcfile = '{Sources}%s:%s' % (OVERDIR, thefile) newmfile.append(m.group(0) + srcfile) newmfile.append('\tDuplicate -y {Deps} {Targ}') lastidx = idx # how many "old" lines should be commented out? if mfile[idx].endswith('∂'): while lastidx + 1 < len(mfile) and mfile[lastidx + 1].endswith('∂'): lastidx += 1 # capture continuations of first line while lastidx + 1 < len(mfile) and mfile[lastidx + 1].startswith('\t'): lastidx += 1 # capture build lines starting with tab while idx <= lastidx: newmfile.append('#\t' + mfile[idx]) idx += 1 else: newmfile.append(mfile[idx]) if havechanged: open(f, 'w').write('\n'.join(newmfile)) if remaining: # try to find where these override files with *no build rule* should go found_locations = {k: [] for k in remaining} overs_re = '|'.join(re.escape(x) for x in remaining) overs_re = r'^[^#]*"({\w+}(?:\w+:)*)(Thing.lib)"'.replace('Thing.lib', overs_re) overs_re = re.compile(overs_re, re.IGNORECASE) for f in all_makefiles: mfile = open(f).read().split('\n') for line in mfile: m = overs_re.match(line) if m: orig_name = next(x for x in remaining if x.upper() == m.group(2).upper()) found_loc = m.group(1)+m.group(2) if found_loc.upper() not in (x.upper() for x in found_locations[orig_name]): found_locations[orig_name].append(found_loc) for f in main_makefiles: with open(f, 'a') as fd: fd.write('\n# Rules created at build time by %s\n' % path.basename(__file__)) for orig_name, found_locs in found_locations.items(): if len(found_locs) == 1: remaining = [x for x in remaining if x != orig_name] fd.write(found_locs[0]) fd.write(' ƒ {Sources}%s:%s\n' % (OVERDIR, orig_name)) fd.write('\tDuplicate -y {Deps} {Targ}\n') diag = 'Successfully spliced: %d/%d' % (len(overs)-len(remaining), len(overs)) if remaining: diag += '; Failed: ' + ' '.join(remaining) log(diag) ######################################################################## log('Copying System Folder', flush=True) sysfold = path.join(path.dirname(path.abspath(__file__)), 'BootableMPW') sysfold2 = path.join(treedest, 'BootableMPW') shutil.copytree(sysfold, sysfold2) with open(path.join(treedest, 'BootableMPW', 'UserStartup'), 'a') as f: print('SetDirectory Src:', file=f) print('Set Exit 0', file=f) datstr = get_build_date(args.src) if datstr: log('Setting build date: %s' % datstr, flush=True) ticks = ticks_from_str(datstr) print('SetTicks %d # %s' % (ticks, datstr), file=f) if args.cmd: print(args.cmd, file=f) print('Quit', file=f) ######################################################################## log('Generating disk image', flush=True) vol = Volume() vol.name = 'Src' vol.read_folder(treedest, date=0x90000000, mpw_dates=True) image = vol.write(256*1024*1024, startapp=('BootableMPW', 'MPW Shell'), align=4096) with open(imgdest, 'wb') as f: f.write(image) ######################################################################## if not args.emu: exit() log('Starting emulator...', flush=True) run([args.emu, imgdest], check=True) ######################################################################## log('Slurping output', flush=True) vol = Volume() with open(imgdest, 'rb') as f: vol.read(f.read()) bootdir = vol.pop('BootableMPW') vol.write_folder(treedest) wsheet = bootdir['Worksheet'].data.decode('mac_roman').replace('\r', '\n') print(wsheet, end='')