gsasm 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gsasm/__init__.py +0 -0
- gsasm/__main__.py +307 -0
- gsasm/asm.py +2683 -0
- gsasm/expr.py +241 -0
- gsasm/expressload.py +1240 -0
- gsasm/link.py +243 -0
- gsasm/linkiigs.py +782 -0
- gsasm/m65816.py +425 -0
- gsasm/makebin.py +197 -0
- gsasm/omf.py +1108 -0
- gsasm/rez/__init__.py +69 -0
- gsasm/rez/convert.py +47 -0
- gsasm/rez/emit.py +310 -0
- gsasm/rez/gen.py +1271 -0
- gsasm/rez/lexer.py +670 -0
- gsasm/rez/parser.py +963 -0
- gsasm-0.2.0.dist-info/METADATA +287 -0
- gsasm-0.2.0.dist-info/RECORD +21 -0
- gsasm-0.2.0.dist-info/WHEEL +4 -0
- gsasm-0.2.0.dist-info/entry_points.txt +4 -0
- gsasm-0.2.0.dist-info/licenses/LICENSE +21 -0
gsasm/__init__.py
ADDED
|
File without changes
|
gsasm/__main__.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""CLI entry points for gsasm (assembler) and gslink (linker)."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def asm_main():
|
|
9
|
+
"""gsasm — assemble an MPW IIgs source file to an OMF object file."""
|
|
10
|
+
p = argparse.ArgumentParser(
|
|
11
|
+
prog="gsasm",
|
|
12
|
+
description="MPW IIgs-compatible assembler (65816, OMF v2).",
|
|
13
|
+
)
|
|
14
|
+
p.add_argument("source", help="source file (.asm or .aii)")
|
|
15
|
+
p.add_argument(
|
|
16
|
+
"-I", dest="incdirs", metavar="DIR", action="append", default=[],
|
|
17
|
+
help="include search directory (may be repeated)",
|
|
18
|
+
)
|
|
19
|
+
p.add_argument(
|
|
20
|
+
"-d", dest="defines", metavar="KEY=VAL", action="append", default=[],
|
|
21
|
+
help="pre-define a symbol, e.g. -d Big=1 (may be repeated)",
|
|
22
|
+
)
|
|
23
|
+
p.add_argument(
|
|
24
|
+
"-o", dest="output", metavar="FILE",
|
|
25
|
+
help="output file (default: <source>.obj)",
|
|
26
|
+
)
|
|
27
|
+
args = p.parse_args()
|
|
28
|
+
|
|
29
|
+
defines = {}
|
|
30
|
+
for kv in args.defines:
|
|
31
|
+
if "=" in kv:
|
|
32
|
+
k, v = kv.split("=", 1)
|
|
33
|
+
try:
|
|
34
|
+
v = int(v, 0)
|
|
35
|
+
except ValueError:
|
|
36
|
+
pass
|
|
37
|
+
defines[k] = v
|
|
38
|
+
else:
|
|
39
|
+
defines[kv] = 1
|
|
40
|
+
|
|
41
|
+
src = args.source
|
|
42
|
+
incdirs = args.incdirs or [os.path.dirname(os.path.abspath(src))]
|
|
43
|
+
outfile = args.output or (src + ".obj")
|
|
44
|
+
|
|
45
|
+
from gsasm import asm, omf
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
a = asm.assemble(src, incdirs, defines=defines)
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
print(f"gsasm: error: {exc}", file=sys.stderr)
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
for e in a.errors:
|
|
54
|
+
print(e, file=sys.stderr)
|
|
55
|
+
if a.errors:
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
obj = omf.emit(a)
|
|
59
|
+
with open(outfile, "wb") as fh:
|
|
60
|
+
fh.write(obj)
|
|
61
|
+
|
|
62
|
+
# print segment summary
|
|
63
|
+
for seg in a.segs:
|
|
64
|
+
nm = seg.name or "(unnamed)"
|
|
65
|
+
n = seg.length()
|
|
66
|
+
print(f" {nm:24s} {n:#8x} ({n} bytes)")
|
|
67
|
+
print(f"→ {outfile} ({len(obj)} bytes)")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def link_main():
|
|
71
|
+
"""gslink — link an OMF object file to a load file."""
|
|
72
|
+
p = argparse.ArgumentParser(
|
|
73
|
+
prog="gslink",
|
|
74
|
+
description="OMF v2 linker: evaluates relocation records and produces a load file.",
|
|
75
|
+
)
|
|
76
|
+
p.add_argument("obj", help="OMF object file (.obj)")
|
|
77
|
+
p.add_argument(
|
|
78
|
+
"-o", dest="output", metavar="FILE",
|
|
79
|
+
help="output file (default: <obj without .obj>.out, or <obj>.out)",
|
|
80
|
+
)
|
|
81
|
+
args = p.parse_args()
|
|
82
|
+
|
|
83
|
+
objfile = args.obj
|
|
84
|
+
if args.output:
|
|
85
|
+
outfile = args.output
|
|
86
|
+
elif objfile.endswith(".obj"):
|
|
87
|
+
outfile = objfile[:-4] + ".out"
|
|
88
|
+
else:
|
|
89
|
+
outfile = objfile + ".out"
|
|
90
|
+
|
|
91
|
+
from gsasm import link, omf
|
|
92
|
+
|
|
93
|
+
with open(objfile, "rb") as fh:
|
|
94
|
+
obj_bytes = fh.read()
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
load_bytes = link.link(obj_bytes)
|
|
98
|
+
except Exception as exc:
|
|
99
|
+
print(f"gslink: error: {exc}", file=sys.stderr)
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
# print segment summary from the output
|
|
103
|
+
off = 0
|
|
104
|
+
while off < len(load_bytes):
|
|
105
|
+
h = omf.parse_header(load_bytes[off:])
|
|
106
|
+
if h["BYTECNT"] == 0:
|
|
107
|
+
break
|
|
108
|
+
nm = h["SEGNAME"].decode("mac_roman", "replace").strip()
|
|
109
|
+
print(f" {nm:24s} {h['LENGTH']:#8x} ({h['LENGTH']} bytes)")
|
|
110
|
+
off += h["BYTECNT"]
|
|
111
|
+
|
|
112
|
+
with open(outfile, "wb") as fh:
|
|
113
|
+
fh.write(load_bytes)
|
|
114
|
+
print(f"→ {outfile} ({len(load_bytes)} bytes)")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _resolve_read_file(name, search_dirs):
|
|
118
|
+
"""Case-insensitive filename search across `search_dirs`, in order
|
|
119
|
+
(mirrors gsasm.rez.lexer's `#include` search-path convention: the
|
|
120
|
+
including file's own directory, then each configured search directory,
|
|
121
|
+
matched case-insensitively). Returns the resolved path, or None."""
|
|
122
|
+
lname = name.lower()
|
|
123
|
+
for d in search_dirs:
|
|
124
|
+
if not d or not os.path.isdir(d):
|
|
125
|
+
continue
|
|
126
|
+
for entry in os.listdir(d):
|
|
127
|
+
if entry.lower() == lname:
|
|
128
|
+
return os.path.join(d, entry)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _parse_meta_override(kv, defaults):
|
|
133
|
+
"""Parse one `--meta KEY=VAL` argument against `emit.DEFAULT_META`'s
|
|
134
|
+
field types (bool / int / bytes / str). Raises SystemExit with a
|
|
135
|
+
`gsrez:`-prefixed message on an unknown key or malformed value."""
|
|
136
|
+
if "=" not in kv:
|
|
137
|
+
raise SystemExit(f"gsrez: --meta expects KEY=VAL, got {kv!r}")
|
|
138
|
+
key, val = kv.split("=", 1)
|
|
139
|
+
if key not in defaults:
|
|
140
|
+
raise SystemExit(f"gsrez: unknown --meta key {key!r} "
|
|
141
|
+
f"(known: {', '.join(sorted(defaults))})")
|
|
142
|
+
default = defaults[key]
|
|
143
|
+
if isinstance(default, bool):
|
|
144
|
+
return key, val.lower() not in ("0", "false", "no", "")
|
|
145
|
+
if isinstance(default, int):
|
|
146
|
+
try:
|
|
147
|
+
return key, int(val, 0)
|
|
148
|
+
except ValueError:
|
|
149
|
+
raise SystemExit(f"gsrez: --meta {key}: invalid integer value "
|
|
150
|
+
f"{val!r}")
|
|
151
|
+
if isinstance(default, bytes):
|
|
152
|
+
# An explicit `0x` prefix means hex bytes; anything else is taken
|
|
153
|
+
# literally (encoded latin-1). Guessing "looks like hex" from
|
|
154
|
+
# content alone would be ambiguous -- e.g. a 4-character creator
|
|
155
|
+
# like "ABCD" is simultaneously valid hex (2 bytes) and a valid
|
|
156
|
+
# literal (4 bytes) -- so an explicit marker is required instead.
|
|
157
|
+
if val[:2].lower() == "0x":
|
|
158
|
+
try:
|
|
159
|
+
return key, bytes.fromhex(val[2:])
|
|
160
|
+
except ValueError:
|
|
161
|
+
raise SystemExit(f"gsrez: --meta {key}: invalid hex value "
|
|
162
|
+
f"{val!r}")
|
|
163
|
+
return key, val.encode("latin-1")
|
|
164
|
+
return key, val
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def rez_main():
|
|
168
|
+
"""gsrez — compile an Apple IIgs Rez `.r` source into a raw resource-fork
|
|
169
|
+
image (docs/design/rez.md, milestone M7; replaces MPW `RezIIgs`).
|
|
170
|
+
|
|
171
|
+
Mirrors the Sys.Resources makefile's `reziigs -rd sys.resources.r -o
|
|
172
|
+
SYS.RESOURCES -t "F9 "` invocation as:
|
|
173
|
+
|
|
174
|
+
gsrez sys.resources.r -o SYS.RESOURCES -t F9
|
|
175
|
+
|
|
176
|
+
Pipeline: `gsasm.rez.parser.parse()` (always predefining `RezIIGS=1`,
|
|
177
|
+
exactly as the real RezIIgs tool predefines it — see
|
|
178
|
+
`gsasm/rez/lexer.py`'s `_Preprocessor.__init__` docstring: this gates
|
|
179
|
+
the null-longint array terminator some type templates need) ->
|
|
180
|
+
`gsasm.rez.gen.generate()` -> resolve every `read` statement's file
|
|
181
|
+
(searched case-insensitively across each `--read-dir`, in order, then
|
|
182
|
+
the source file's own directory) through `gsasm.rez.convert.
|
|
183
|
+
convert_load()` -> `gsasm.rez.gen.to_emit_tuples()` ->
|
|
184
|
+
`gsasm.rez.emit.emit_fork()` -> write the raw fork bytes to `-o`.
|
|
185
|
+
|
|
186
|
+
Output is the RAW RESOURCE-FORK IMAGE ONLY: packaging it together with
|
|
187
|
+
a (typically empty) data fork into one dual-fork disk file is out of
|
|
188
|
+
scope here (see e.g. a2til's `Volume.write_file(..., resource=...)` for
|
|
189
|
+
that step).
|
|
190
|
+
|
|
191
|
+
Fork metadata (docs/design/rez.md "Golden fork format", `gsasm/rez/
|
|
192
|
+
emit.py`'s `DEFAULT_META`): this CLI keeps HONEST, non-golden defaults
|
|
193
|
+
(creator `'pdos'`; a zero memo timestamp; no file type set) rather than
|
|
194
|
+
guessing at a specific captured file's undocumented bytes -- reproducing
|
|
195
|
+
one archival fork byte-exact (its name, creation timestamp, ...) is a
|
|
196
|
+
HARNESS's job (`work/rezbuildcheck.py`), done through `-t`/`-c`/`--meta`
|
|
197
|
+
or by calling `gsasm.rez.emit.emit_fork()` directly as a library.
|
|
198
|
+
"""
|
|
199
|
+
p = argparse.ArgumentParser(
|
|
200
|
+
prog="gsrez",
|
|
201
|
+
description="Rez resource compiler (Apple IIgs resource-fork image).",
|
|
202
|
+
)
|
|
203
|
+
p.add_argument("source", help="Rez source file (.r/.rez/.rii)")
|
|
204
|
+
p.add_argument(
|
|
205
|
+
"-I", dest="incdirs", metavar="DIR", action="append", default=[],
|
|
206
|
+
help="#include search directory (may be repeated)",
|
|
207
|
+
)
|
|
208
|
+
p.add_argument(
|
|
209
|
+
"-o", dest="output", metavar="FILE",
|
|
210
|
+
help="output file (default: <source>.rsrc)",
|
|
211
|
+
)
|
|
212
|
+
p.add_argument(
|
|
213
|
+
"-t", "--filetype", dest="filetype", metavar="TT",
|
|
214
|
+
help="2-hex-digit ProDOS file type, e.g. -t F9 (default: unset)",
|
|
215
|
+
)
|
|
216
|
+
p.add_argument(
|
|
217
|
+
"-c", "--creator", dest="creator", default="pdos", metavar="CCCC",
|
|
218
|
+
help='4-character creator (default: "pdos", matching every '
|
|
219
|
+
'observed golden fork)',
|
|
220
|
+
)
|
|
221
|
+
p.add_argument(
|
|
222
|
+
"--read-dir", dest="read_dirs", metavar="DIR", action="append",
|
|
223
|
+
default=[],
|
|
224
|
+
help="directory to search for `read` statement files (may be "
|
|
225
|
+
"repeated; searched before the source file's own directory)",
|
|
226
|
+
)
|
|
227
|
+
p.add_argument(
|
|
228
|
+
"--meta", dest="meta_overrides", metavar="KEY=VAL", action="append",
|
|
229
|
+
default=[],
|
|
230
|
+
help="override a gsasm.rez.emit fork-metadata field (may be "
|
|
231
|
+
"repeated), e.g. --meta creation_mac_ts=2819554517",
|
|
232
|
+
)
|
|
233
|
+
args = p.parse_args()
|
|
234
|
+
|
|
235
|
+
from gsasm.rez import lexer, parser as rez_parser, gen, emit, convert
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
stmts = rez_parser.parse(args.source, include_dirs=args.incdirs,
|
|
239
|
+
predefined={"RezIIGS": 1})
|
|
240
|
+
except (lexer.LexError, rez_parser.ParseError) as exc:
|
|
241
|
+
print(f"gsrez: error: {exc}", file=sys.stderr)
|
|
242
|
+
sys.exit(1)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
entries = gen.generate(stmts)
|
|
246
|
+
except gen.GenError as exc:
|
|
247
|
+
print(f"gsrez: error: {exc}", file=sys.stderr)
|
|
248
|
+
sys.exit(1)
|
|
249
|
+
|
|
250
|
+
src_dir = os.path.dirname(os.path.abspath(args.source))
|
|
251
|
+
search_dirs = args.read_dirs + [src_dir]
|
|
252
|
+
|
|
253
|
+
read_stmts = [s for s in stmts if isinstance(s, rez_parser.ReadStmt)]
|
|
254
|
+
read_entries = [e for e in entries if e.kind == "read"]
|
|
255
|
+
# generate() appends exactly one 'read' GenEntry per ReadStmt, in the
|
|
256
|
+
# same source order (see gen.py's "Public API" docstring) -- zipping
|
|
257
|
+
# them pairs each statement with its resolved (rtype, rid) without
|
|
258
|
+
# reimplementing gen.py's own (private) id-expression evaluator here.
|
|
259
|
+
assert len(read_stmts) == len(read_entries)
|
|
260
|
+
|
|
261
|
+
read_data = {}
|
|
262
|
+
for stmt, entry in zip(read_stmts, read_entries):
|
|
263
|
+
filename = stmt.filename.decode("latin-1")
|
|
264
|
+
path = _resolve_read_file(filename, search_dirs)
|
|
265
|
+
if path is None:
|
|
266
|
+
print(f"gsrez: error: {stmt.file}:{stmt.line}: read file not "
|
|
267
|
+
f"found (case-insensitively) in search path: "
|
|
268
|
+
f"{filename!r}", file=sys.stderr)
|
|
269
|
+
sys.exit(1)
|
|
270
|
+
with open(path, "rb") as fh:
|
|
271
|
+
raw = fh.read()
|
|
272
|
+
read_data[(entry.rtype, entry.rid)] = convert.convert_load(raw)
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
tuples = gen.to_emit_tuples(entries, read_data)
|
|
276
|
+
except gen.GenError as exc:
|
|
277
|
+
print(f"gsrez: error: {exc}", file=sys.stderr)
|
|
278
|
+
sys.exit(1)
|
|
279
|
+
|
|
280
|
+
meta = {"creator": args.creator}
|
|
281
|
+
if args.filetype:
|
|
282
|
+
try:
|
|
283
|
+
filetype_val = int(args.filetype, 16)
|
|
284
|
+
except ValueError:
|
|
285
|
+
raise SystemExit(f"gsrez: -t/--filetype: invalid hex value "
|
|
286
|
+
f"{args.filetype!r}")
|
|
287
|
+
meta["filetype"] = emit.format_filetype(filetype_val)
|
|
288
|
+
for kv in args.meta_overrides:
|
|
289
|
+
key, val = _parse_meta_override(kv, emit.DEFAULT_META)
|
|
290
|
+
meta[key] = val
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
fork = emit.emit_fork(tuples, meta)
|
|
294
|
+
except ValueError as exc:
|
|
295
|
+
print(f"gsrez: error: {exc}", file=sys.stderr)
|
|
296
|
+
sys.exit(1)
|
|
297
|
+
|
|
298
|
+
outfile = args.output or (args.source + ".rsrc")
|
|
299
|
+
with open(outfile, "wb") as fh:
|
|
300
|
+
fh.write(fork)
|
|
301
|
+
print(f" {len(tuples)} resource(s)")
|
|
302
|
+
print(f"→ {outfile} ({len(fork)} bytes)")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
if __name__ == "__main__":
|
|
306
|
+
# allow `python -m gsasm` to run the assembler
|
|
307
|
+
asm_main()
|