2019-06-29 14:03:22 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
# In this folder
|
|
|
|
from splice import splice
|
2019-06-29 14:56:27 +00:00
|
|
|
from folder import folder
|
2019-06-29 14:03:22 +00:00
|
|
|
|
|
|
|
# python3 -m pip install machfs macresources
|
|
|
|
import machfs
|
|
|
|
import macresources
|
|
|
|
|
|
|
|
# First party
|
|
|
|
from os import path
|
|
|
|
import argparse
|
|
|
|
import glob
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import shlex
|
|
|
|
import shutil
|
|
|
|
import subprocess
|
|
|
|
import sys
|
2019-07-17 03:21:34 +00:00
|
|
|
import tempfile
|
2019-06-29 14:03:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description='''
|
|
|
|
Build the sources in the working directory.
|
|
|
|
''')
|
|
|
|
|
2019-06-29 14:56:27 +00:00
|
|
|
parser.add_argument('worktree', metavar='WORKTREE', action='store', type=lambda x: folder('worktree', x), help='Worktree (assumed in ../worktree/)')
|
2019-06-29 14:03:22 +00:00
|
|
|
parser.add_argument('passthru', metavar='ARG', nargs='+', help='Build script args (RISC/ROM/LC930/dbLite or FNDR)')
|
|
|
|
config = parser.parse_args()
|
|
|
|
|
|
|
|
config.worktree = os.getcwd()
|
2019-07-17 03:21:34 +00:00
|
|
|
config.tmpdir = tempfile.mkdtemp()
|
|
|
|
handle, config.hfsimg = tempfile.mkstemp(suffix='.dmg')
|
2019-06-29 14:03:22 +00:00
|
|
|
config.vmac = open(path.join(path.dirname(__file__), 'vmac_path.conf')).read().rstrip('\n')
|
|
|
|
|
|
|
|
|
|
|
|
if config.passthru[:1] == ['FNDR']:
|
|
|
|
config.startapp = None
|
|
|
|
config.passthru = None
|
|
|
|
|
|
|
|
else:
|
|
|
|
config.startapp = 'Disk:MPW:MPW Shell'
|
|
|
|
|
|
|
|
# Massage the args to make sense to :Make:Build
|
|
|
|
if '-src' not in config.passthru:
|
|
|
|
loc = 0
|
|
|
|
if config.passthru and config.passthru[0].isalnum():
|
|
|
|
loc = 1
|
|
|
|
config.passthru[loc:loc] = ['-src', '{Src}']
|
|
|
|
|
|
|
|
config.passthru[0:0] = ['{Src}Make:Build']
|
|
|
|
|
|
|
|
config.passthru = ' '.join(config.passthru)
|
|
|
|
|
|
|
|
########################################################################
|
|
|
|
|
|
|
|
try:
|
|
|
|
shutil.rmtree(config.tmpdir)
|
|
|
|
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(config.worktree, config.tmpdir, ignore=myignore, copy_function=shutil.copy2)
|
|
|
|
|
|
|
|
all_makefiles = list(glob.glob(path.join(glob.escape(config.worktree), '**', '*.[Mm][Aa][Kk][Ee]'), recursive=True))
|
|
|
|
|
|
|
|
########################################################################
|
|
|
|
|
|
|
|
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 = re.match(rb'^(\w+)\s*=\s*(.+)', line)
|
|
|
|
if m:
|
|
|
|
try:
|
|
|
|
left = m.group(1).decode('ascii')
|
|
|
|
right = re.sub(rb'{(\w+)}', grabber, m.group(2)).decode('ascii')
|
|
|
|
vardict[left] = right
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
return vardict
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
for key, macpath in defines.items():
|
|
|
|
macpath = macpath.replace('"', '')
|
|
|
|
if key.endswith('Dir') and macpath.startswith('BuildResults:'):
|
|
|
|
nativepath = path.join(config.tmpdir, *macpath.split(':')[:-1])
|
|
|
|
os.makedirs(nativepath, exist_ok=True)
|
|
|
|
|
|
|
|
########################################################################
|
|
|
|
|
|
|
|
splice(config.tmpdir)
|
|
|
|
|
|
|
|
########################################################################
|
|
|
|
|
|
|
|
vol = machfs.Volume()
|
|
|
|
vol.name = 'Disk'
|
|
|
|
vol['Src'] = machfs.Folder()
|
|
|
|
vol['Src'].read_folder(config.tmpdir, date=0x90000000, mpw_dates=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'] = machfs.Folder()
|
|
|
|
vol['System Folder'].read_folder(folder('System-7.0.1'), date=0x80000000)
|
|
|
|
|
|
|
|
if 1: # has MPW
|
|
|
|
vol['MPW'] = machfs.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''
|
|
|
|
|
|
|
|
# Tell MPW what to do when it starts (can use {Src} in their -c command)
|
|
|
|
if config.passthru:
|
2019-12-20 22:03:52 +00:00
|
|
|
bootscript = '''
|
|
|
|
Set Src Disk:Src:
|
|
|
|
Export Src
|
2019-12-21 23:52:26 +00:00
|
|
|
Directory {Src}
|
2019-12-20 22:03:52 +00:00
|
|
|
Set Exit 0
|
|
|
|
|
2019-12-21 23:52:26 +00:00
|
|
|
''' + config.passthru + '''
|
2019-12-20 22:03:52 +00:00
|
|
|
|
|
|
|
# DumpObj SeriousFile.c.o to SeriousFile.c.d
|
|
|
|
Files -r -o -f -t 'OBJ ' -c 'MPS ' Disk:Src:BuildResults | StreamEdit ∂
|
|
|
|
-e "1,$ change 'If ∂`Newer ' . ' ' . '.dump∂`; DumpObj ' . '>' . '.dump; End'; replace -c 2 /[a-z]+.dump/ 'd'" ∂
|
|
|
|
>Disk:DumpObjScript
|
|
|
|
Disk:DumpObjScript
|
|
|
|
|
|
|
|
Quit
|
|
|
|
'''
|
|
|
|
vol['MPW']['UserStartup'].data = bootscript.replace('\n', '\r').encode('mac_roman')
|
2019-06-29 14:03:22 +00:00
|
|
|
|
|
|
|
# Evil tuning: more RAM for MPW because some tools do not use temp mem
|
|
|
|
mpw = list(macresources.parse_file(vol['MPW']['MPW Shell'].rsrc))
|
|
|
|
for resource in mpw:
|
|
|
|
if resource.type == b'SIZE':
|
2019-07-11 04:58:13 +00:00
|
|
|
resource.data = resource.data[:2] + (0x600000).to_bytes(4, byteorder='big') * 2 + resource.data[10:]
|
2019-06-29 14:03:22 +00:00
|
|
|
vol['MPW']['MPW Shell'].rsrc = macresources.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.
|
2019-12-20 21:59:27 +00:00
|
|
|
system = list(macresources.parse_file(vol['System Folder']['System'].rsrc))
|
|
|
|
for resource in system:
|
2019-06-29 14:03:22 +00:00
|
|
|
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 config.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(config.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)
|
2019-12-20 21:59:27 +00:00
|
|
|
vol['System Folder']['System'].rsrc = macresources.make_file(system)
|
2019-06-29 14:03:22 +00:00
|
|
|
|
|
|
|
# Patch the Finder to give it a Quit menu item
|
|
|
|
finder = list(macresources.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 = macresources.make_file(finder)
|
|
|
|
|
|
|
|
########################################################################
|
|
|
|
|
|
|
|
with open(config.hfsimg, 'wb') as f:
|
|
|
|
f.write(vol.write(256*1024*1024, align=4096, desktopdb=False))
|
|
|
|
|
2019-12-20 21:59:27 +00:00
|
|
|
ran = subprocess.run(config.vmac + ' ' + shlex.quote(config.hfsimg), shell=True, capture_output=True)
|
2019-06-29 14:03:22 +00:00
|
|
|
|
2019-12-20 21:59:27 +00:00
|
|
|
# Some emulators print nonsense
|
|
|
|
if ran.returncode != 0:
|
|
|
|
sys.stdout.buffer.write(ran.stdout)
|
|
|
|
sys.stderr.buffer.write(ran.stderr)
|
|
|
|
sys.exit(ran.returncode)
|
2019-06-29 14:03:22 +00:00
|
|
|
|
|
|
|
########################################################################
|
|
|
|
|
|
|
|
vol = machfs.Volume()
|
|
|
|
with open(config.hfsimg, 'rb') as f:
|
|
|
|
vol.read(f.read())
|
|
|
|
vol['Src'].write_folder(config.worktree)
|
|
|
|
|
|
|
|
shutil.rmtree(config.tmpdir)
|
|
|
|
os.remove(config.hfsimg)
|
|
|
|
|
|
|
|
if config.passthru is not None:
|
|
|
|
wsheet = vol['MPW']['Worksheet'].data.decode('mac_roman').replace('\r', '\n')
|
2019-12-20 22:02:06 +00:00
|
|
|
|
|
|
|
# Convert all paths to native for my IDE of choice
|
|
|
|
def pathconverter(m):
|
|
|
|
components = m.group(1).split(':')
|
|
|
|
return path.join(*components)
|
|
|
|
wsheet = re.sub(r'\bDisk:Src:([^"\'\s]+)', pathconverter, wsheet)
|
|
|
|
|
2019-06-29 14:03:22 +00:00
|
|
|
print(wsheet, end='')
|
2019-12-20 22:00:11 +00:00
|
|
|
|
|
|
|
# Slightly hacky way to extract an exit status
|
|
|
|
if re.search(r'^### MPW Shell - Execution of .* terminated', wsheet, re.MULTILINE):
|
|
|
|
sys.exit(1)
|