-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathBuild
executable file
·327 lines (256 loc) · 12 KB
/
Build
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
#!/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='')