mirror of
https://github.com/elliotnunn/BuildCubeE.git
synced 2025-03-02 02:29:48 +00:00
328 lines
12 KiB
Python
Executable File
328 lines
12 KiB
Python
Executable File
#!/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, Folder, File
|
|
from macresources import parse_file, make_file
|
|
|
|
########################################################################
|
|
|
|
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 7 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')
|
|
group = args.add_mutually_exclusive_group()
|
|
group.add_argument('--resedit', action='store_true', help='placeholder')
|
|
group.add_argument('--tomeviewer', action='store_true', help='placeholder')
|
|
group.add_argument('-c', dest='mpwcmd', 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('Loading source tree into memory', flush=True)
|
|
|
|
vol = Volume()
|
|
vol.name = 'Disk'
|
|
vol['Src'] = Folder()
|
|
vol['Src'].read_folder(treedest, date=0x90000000, mpw_dates=True)
|
|
|
|
########################################################################
|
|
|
|
log('Inserting System and apps', flush=True)
|
|
|
|
def folder(name):
|
|
return path.join(path.dirname(path.abspath(__file__)), name)
|
|
|
|
def pstring(string):
|
|
string = string.encode('mac_roman')
|
|
return bytes([len(string)]) + string
|
|
|
|
# We need a System Folder
|
|
vol['System Folder'] = Folder()
|
|
vol['System Folder'].read_folder(folder('System-7.0.1'), date=0x80000000)
|
|
|
|
if 1: # has MPW
|
|
vol['MPW'] = Folder()
|
|
vol['MPW'].read_folder(folder('MPW-3.2.3'), date=0x80000000)
|
|
|
|
# Clear Worksheet window and reset its position and size
|
|
vol['MPW']['Worksheet'].data = vol['MPW']['Worksheet'].rsrc = b''
|
|
|
|
if 1: # has apps in Apple Menu
|
|
vol['System Folder']['Apple Menu Items'] = Folder()
|
|
vol['System Folder']['Apple Menu Items'].read_folder(folder('Apps'), date=0x80000000)
|
|
|
|
# Tell MPW what to do when it starts (can use {Src} in their -c command)
|
|
bootscript = 'Set Src Disk:Src:; Export Src; Directory {Src}\r'
|
|
if args.mpwcmd: bootscript += 'Set Exit 0; %s; Quit\r' % args.mpwcmd
|
|
vol['MPW']['UserStartup'].data = bootscript.encode('mac_roman')
|
|
|
|
# Evil tuning: more RAM for MPW because some tools do not use temp mem
|
|
mpw = list(parse_file(vol['MPW']['MPW Shell'].rsrc))
|
|
for resource in mpw:
|
|
if resource.type == b'SIZE':
|
|
resource.data = resource.data[:2] + (0x300000).to_bytes(4, byteorder='big') * 2 + resource.data[10:]
|
|
vol['MPW']['MPW Shell'].rsrc = make_file(mpw)
|
|
|
|
# Patch the Process Manager to shut down when the last visible app quits,
|
|
# which it normally does when the startup app isn't a 'FNDR'.
|
|
# At that point, replace ShutDwnUserChoice() with ShutDwnPower().
|
|
# Also, follow a special path to the startup app, instead of Finder name global.
|
|
if args.mpwcmd is not None:
|
|
startapp = 'Disk:MPW:MPW Shell'
|
|
elif args.resedit:
|
|
startapp = 'Disk:System Folder:Apple Menu Items:ResEdit'
|
|
elif args.tomeviewer:
|
|
startapp = 'Disk:System Folder:Apple Menu Items:TomeViewer'
|
|
else:
|
|
startapp = None
|
|
|
|
sys = list(parse_file(vol['System Folder']['System'].rsrc))
|
|
for resource in sys:
|
|
if resource.type != b'scod': continue # "System CODE" resources = Process Mgr
|
|
|
|
resource.data = resource.data.replace(b'FNDR', b'LUSR')
|
|
resource.data = resource.data.replace(b'\x3F\x3C\x00\x05\xA8\x95', b'\x3F\x3C\x00\x01\xA8\x95')
|
|
|
|
if startapp: # Change arg 3 of FSMakeFSSpec(BootVol, 0, FinderName, &mySpec) to custom path
|
|
data = bytearray(resource.data)
|
|
pea = b'\x48\x78\x02\xE0' # Find 'PEA $2E0' (accesses the Str15 FinderName)...
|
|
str_offset = None
|
|
while pea in data:
|
|
if not str_offset:
|
|
while len(data) % 2: data.append(0)
|
|
str_offset = len(data)
|
|
data.extend(pstring(startapp))
|
|
|
|
offset = data.index(pea)
|
|
data[offset:offset+2] = b'\x48\x7A' # ...replace with PC-relative 'PEA startapp'
|
|
data[offset+2:offset+4] = (str_offset - (offset+2)).to_bytes(2, byteorder='big')
|
|
resource.data = bytes(data)
|
|
vol['System Folder']['System'].rsrc = make_file(sys)
|
|
|
|
# Patch the Finder to give it a Quit menu item
|
|
finder = list(parse_file(vol['System Folder']['Finder'].rsrc))
|
|
for resource in finder:
|
|
if resource.type == b'fmnu' and b'Put Away' in resource.data:
|
|
data = bytearray(resource.data)
|
|
data[3] += 2
|
|
data.extend(b'xxx0\0\0\0\0\x01\x2D')
|
|
data.extend(b'quit\x81\x00\x51\x00\x04Quit\0')
|
|
resource.data = bytes(data)
|
|
vol['System Folder']['Finder'].rsrc = make_file(finder)
|
|
|
|
########################################################################
|
|
|
|
log('Writing out disk image')
|
|
with open(imgdest, 'wb') as f:
|
|
f.write(vol.write(256*1024*1024, align=4096, desktopdb=False))
|
|
|
|
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())
|
|
vol['Src'].write_folder(args.src)
|
|
|
|
if args.mpwcmd is not None:
|
|
wsheet = vol['MPW']['Worksheet'].data.decode('mac_roman').replace('\r', '\n')
|
|
print(wsheet, end='')
|