-
Notifications
You must be signed in to change notification settings - Fork 0
Amiga port design
This page is the implementation log and design reference for the
AmigaOS 3.x MicroPython port. For an upstream-style introduction to
the port, see ports/amiga/README.md
in the source tree. The full testing runbook lives on
Amiga port testing.
Port of MicroPython to AmigaOS 3.x on Motorola 68k (68020+). CLI-driven REPL
with file-system access, based on the ports/minimal template.
| # | Phase | Status |
|---|---|---|
| 0 | Toolchain — bebbo GCC | ✅ |
| 1 | Skeleton — builds and runs in Amiberry | ✅ |
| 2 | File system access and import
|
✅ |
| 3 | Standard MicroPython library modules | ✅ |
| 4 | Amiga-specific amiga C module |
✅ |
| 5 | 68k native code emitter | ✅ |
| 6 | Package imports | ✅ |
| 7 | Ctrl+C interrupt handling | ✅ |
| 8 | Native AmigaOS API migration | ✅ |
| 9 | Networking via bsdsocket.library
|
✅ |
| 10 | Command-line argument parsing | ✅ |
| 11 | CI build workflow | ✅ |
| 12 | 68k native emitter rework (fix ASM_CALL_IND crash) | planned |
| 13 | Interactive line editing at the REPL | ✅ |
| 14 | Dynamic heap growth | ✅ |
| 15 | withdrawn | — |
| 16 | Pythonic file I/O via VFS | ✅ |
| 17 | Native AmigaOS library access (amiga.library) |
✅ (step 7 deferred) |
| 18 | ARexx integration (inbound + outbound) | ✅ |
| 19 | Workbench launch support | ✅ |
| 20 | Env-var integration (os.getenv/putenv/unsetenv) |
✅ |
| 21 | Volume / assign introspection | ✅ |
| 22 | AmigaDOS pattern matching | ✅ |
| 23 |
timer.device-backed timing |
✅ |
| 24 | Persistent REPL history | ✅ |
| 25 | Extra break signals | ✅ |
| 26 |
PROGDIR: on sys.path
|
✅ |
| 27 | Additional build variants | ✅ |
| 28 | TLS/SSL via AmiSSL v5 | ✅ |
| 29 |
urequests frozen HTTP/HTTPS client |
✅ |
| 30 | Intuition requester dialogs (amiga.intuition) |
✅ |
| 31 | ASL file requester (amiga.asl) |
✅ |
| 32 | ARexx polish (rexx_exists / rexx_list / persistent RexxClient) |
✅ |
| 33 |
platform.amiga_info() + frozen platform.py
|
✅ |
| 34 | Frozen os.py extensions + AmigaOS-aware os.path
|
✅ |
| 35 |
amiga.icon — .info file read / write / manipulation |
✅ |
| 36 |
amiga.catalog — locale.library catalog lookup |
✅ |
| 37 |
amiga.datatypes — datatypes.library file recognition |
planned (low priority) |
| 38 |
ports/amiga/README.md for upstreaming |
✅ |
- Full Workbench GUI / Intuition windows — Phases 30/31 add modal requesters only; arbitrary window/widget surfaces stay out of scope
- 68000 alignment-safe build — target 68020+ first
Two other Python-on-Amiga efforts exist and surface in design conversations:
-
OoZe1911/micropython-amiga-port — a parallel MicroPython
AmigaOS 3.x port. Different deltas (depth of
amiga.*C surface, the Phase 5 native emitter, three-variant CPU/FP matrix, dynamic heap, the library proxy +.fdtrampoline) but a converging top-level shape (amiga.intuition,amiga.asl,urequests,platform, ARexx surface). -
OS4 Python — a port of CPython 2.x to AmigaOS 4 /
PowerPC. Six Amiga-specific module names documented on
wiki.amigaos.net:
amiga,arexx,asl,catalog,icon,installer.
Policy: surface-compatible where it's cheap, not byte-compatible.
- Match their module names (
amiga.intuition,amiga.asl,amiga.icon,amiga.catalog, etc.) so users moving scripts between ports find familiar import paths. - Match their function signatures where the underlying AmigaOS API fits cleanly. Phases 30–36 mirror OoZe / OS4 shapes deliberately.
- Where their choice doesn't fit OS3 / Python 3 / MicroPython
(Posix-style
chmodtranslation in Phase 34; OS4-interface- based library calls; CPython-2 stdlib expectations), pick the shape that fits our target and document the divergence in the per-phase out-of-scope list.
Don't promise true compatibility:
- OS4 Python is CPython 2.x with the full CPython stdlib and C-extension ecosystem. We're MicroPython on OS3 / 68k. Any non-trivial OS4 Python script will hit Python-2 idioms or stdlib modules we can't ship; mirroring AmigaOS-specific module names doesn't change that.
- OoZe1911 has feature deltas we've deliberately chosen not to
match (
smtplib+emailmodule, OS-side path conventions baked deeper than ours). When their choice is reasonable, we copy. When ours is materially better (the library proxy, native emitter, timer-backed timing), we keep ours.
The win from surface-compat is low friction — someone with an
import amiga.icon; icon.read(...) script from OS4 has a chance
of running it without edits. The win from not over-committing
is avoiding broken promises about strict portability.
- 68k big-endian, 32-bit. Minimum 68020 (unaligned access).
- AmigaOS register-based calling convention for OS calls; standard C ABI otherwise.
-
MP_ENDIANNESS_LITTLE = 0(auto-detected from GCC's__BYTE_ORDER__).
bebbo's GCC (m68k-amigaos-gcc), GCC 6.5.0b. Produces native AmigaOS HUNK
executables.
Recommended: container-based build. tools/amiga-build.sh runs the
build inside stefanreinauer/amiga-gcc:latest — the same image CI uses,
so local and CI binaries are bit-identical. Output lands in the standard
ports/amiga/build-<variant>/ paths so tools/amiga-vamos-run.sh and
friends find the binaries unchanged. Files are written as the host user
(via --user), not root.
tools/amiga-build.sh # all four variants
tools/amiga-build.sh standard # one
tools/amiga-build.sh standard 68040 # several
tools/amiga-build.sh clean # clean all build dirsNative install (alternative). Install from https://franke.ms/git/bebbo/amiga-gcc:
git clone https://franke.ms/git/bebbo/amiga-gcc
cd amiga-gcc
make PREFIX=/opt/amiga all # installs gcc, binutils, clib2, NDK
export PATH=/opt/amiga/bin:$PATHmake min does not install C library headers and cannot compile MicroPython.
Always use all or at minimum clib2 ndk. Don't interleave native and
container builds in the same mpy-cross/build/ without running
tools/amiga-build.sh clean first — the host and Linux mpy-cross binaries
share that path and aren't compatible.
Uses MICROPY_NLR_SETJMP (1) — MicroPython's setjmp fallback. No 68k assembly
NLR needed.
Phase 14 manages a dynamic, growable heap of AllocVec'd chunks. See Phase 14
below. GC stack scan runs from current SP up to gc_stack_top (captured in
main() at startup) — tc_SPUpper would be principled but vamos leaves it
zero on the initial process and the resulting ~4 GB scan walks off into
unmapped memory.
ports/amiga/
├── Makefile # Build rules and toolchain config
├── mpconfigport.h # Feature flags and type definitions
├── mphalport.h/.c # HAL stubs + console I/O
├── main.c # Entry point, gc_collect(), heap pre-scan
├── vfs_amiga.c # VfsAmiga + FileIO/TextIOWrapper
├── sysstdio.c # mp_sys_stdin/stdout/stderr stream objects
├── floatconv.c # bebbo soft-float library bug fixes
├── modjson.c # Port-local json.loads bypass
├── modamiga.c # The `amiga` C module
├── modos.c # os.getenv/putenv/unsetenv (Phase 20)
├── modsocket.c # socket module (Phase 9)
├── amiga_timer.c # timer.device + EClock timing (Phase 23/25)
├── amiga_history.c # Persistent REPL history (Phase 24)
├── amiga_lib_call.S # Library trampoline (Phase 17)
├── qstrdefsport.h # Port-specific interned strings
├── manifest.py # Frozen modules (amiga.py + _amiga_fd etc.)
├── modules/ # Frozen Python modules
└── variants/{standard,68020fpu,68040}/
bebbo GCC 6.5.0b at /opt/amiga. m68k-amigaos-gcc --version confirms.
Builds an AmigaOS loadseg()ble HUNK executable; confirmed running in Amiberry.
Notable mpconfigport.h settings worth remembering:
-
MICROPY_NLR_SETJMP (1)— setjmp-based exceptions -
MICROPY_LONGINT_IMPL_LONGLONG—'q'/'Q'struct codes and >30-bit literals need this; adds ~5 KB. -
MICROPY_ENABLE_PYSTACK (1)— not optional. bebbo gcc 6.5'sallocareturns 2-byte-aligned addresses on 68k, which breakmp_obj_is_obj's(o & 3) == 0check on iterator slots; routing throughmp_pystack_alloc(4 KBaligned(8)buffer) gives deterministic alignment.
mp_builtin_open_obj must be supplied by the port (py/modio.c references
but doesn't define it).
open(), read, write, seek, tell, readline, with, and import
from any mounted volume all work. Originally newlib stdio in amigaio.c /
amigafile.c; Phase 8 migrated to direct dos.library; Phase 16 then
rewrote behind a VFS layer (vfs_amiga.c) — those files are gone.
MICROPY_CONFIG_ROM_LEVEL_EXTRA_FEATURES. Working: math, struct, json,
re, hashlib, float. sys.platform == "amiga".
-msoft-float is in CFLAGS — no hardware FPU on 68020. FPU variants (Phase 27)
drop this.
json.loads workaround. Upstream extmod/modjson.c fails on 68k with
OSError: stream operation not supported — it builds a stack-allocated
mp_obj_stringio_t and routes through the stream protocol, but
mp_obj_is_obj() requires 4-byte alignment which the 68k SysV ABI doesn't
guarantee for every stack frame. ports/amiga/modjson.c is a port-local
replacement that bypasses the stream protocol for loads; Makefile filters
out extmod/modjson.c.
ports/amiga/modamiga.c exposes exec/dos primitives. Must be in SRC_QSTR
(not just SRC_C) so MP_REGISTER_MODULE and qstrs are picked up.
import amiga
amiga.os_version() # (version, revision)
amiga.find_task(name=None) # int task pointer (current if None)
amiga.alloc_vec(size, flags=amiga.MEMF_ANY) # int address; MemoryError on fail
amiga.free_vec(addr)
amiga.execute(cmd) # int rc (0/5/10/20, -1=start fail)
amiga.exists(path) # bool, suppresses volume requesters
# MEMF_ANY/PUBLIC/CHIP/FAST/CLEAR constantsamiga.execute() uses SystemTagList() (not Execute()) so the real CLI
return code surfaces. amiga.exists() brackets Lock with
pr_WindowPtr = (APTR)-1 so an unmounted volume doesn't pop an
"Insert volume" requester.
Later phases add many more bindings to this module — see their sections.
@micropython.native / --emit native via GENERIC_ASM_API.
| File | Role |
|---|---|
py/asm68k.h/.c |
68k instruction encoder + helpers |
py/emit68k.c |
#define N_68K 1 + #include "py/emitnative.c"
|
Register allocation: D0 = RET/ARG_1, D1/D2/D3 = ARG_2/3/4 (D2/D3 callee-saved,
reloaded in prologue), D4–D6 = temps, D7 = REG_LOCAL_1, A2/A3 = LOCAL_2/3 (used
for REG_GENERATOR_STATE / REG_QSTR_TABLE), A4 = REG_FUN_TABLE, A5 = frame ptr.
MAX_REGS_FOR_LOCAL_VARS = 1 — only D7 is a data register safe for arithmetic.
Calling convention: AmigaOS/cdecl, args pushed right-to-left.
LINK A5,#-N; MOVEM.L D2-D7/A2-A4,-(SP) for entry, branches always .W form,
CMP.L; Scc; ANDI.L #1.
Known limitations (also tracked as Phase 12):
-
try/exceptin native mode is broken —NLR_BUF_IDX_LOCAL_1falls inside thejmp_bufand gets overwritten bysetjmp. - Viper integer arithmetic on address registers is prevented by
MAX_REGS_FOR_LOCAL_VARS = 1. - Calling another function from a
@micropython.nativebody crashes — see Phase 12.
mp_import_stat() uses dos.library Lock/Examine; fib_DirEntryType > 0
is a directory. import mypackage works.
Two paths:
- During computation:
MICROPY_VM_HOOK_LOOPpollsCheckSignal(SIGBREAKF_CTRL_C)every 1024 bytecodes (amiga_check_ctrl_cinmphalport.c). - During input:
mp_hal_stdin_rx_chr()checksmp_interrupt_char.
shared/runtime/interrupt_char.c provides mp_interrupt_char and
mp_hal_set_interrupt_char().
All newlib stdio replaced with dos.library:
| Component | Now uses |
|---|---|
| Console input | FGetC(Input()) |
| Console output | Write(Output(), buf, len) |
| File I/O |
BPTR + Open/Read/Write/Close/Seek/Flush
|
| Heap | AllocVec(MEMF_ANY|MEMF_PUBLIC|MEMF_CLEAR) |
| GC stack bounds | FindTask(NULL)->tc_SPUpper |
mp_hal_delay_ms |
Delay() (later replaced by Phase 23 timer.device) |
ports/amiga/modsocket.c via bsdsocket.library. SocketBase opened in
main() (silently absent if library missing — socket creation raises
OSError). Errno() (per-library) instead of global errno;
IoctlSocket(FIONBIO); SO_RCVTIMEO/SO_SNDTIMEO; Inet_NtoA/inet_addr/
gethostbyname; getaddrinfo/freeaddrinfo/gethostname. Stream protocol
implemented so readline(), with, etc. work. CloseSocket() (not close)
on shutdown. __NO_NETINCLUDE_TIMEVAL guard avoids devices/timer.h /
sys/time.h conflict.
main.c parses argc/argv before the REPL:
micropython # interactive REPL
micropython script.py [args] # sys.argv = ["script.py", ...]
micropython -c "code" [args] # sys.argv = ["-c", ...]
micropython -m module [args] # sys.argv = ["module", ...]
micropython -h / --help / --version
MICROPY_PY_SYS_ARGV (1), mp_sys_path = [""]. Script directory is prepended
to sys.path[0] using AmigaOS path parsing. -c via pyexec_vstr; -m via
mp_builtin___import__. Raw console mode (SetMode(stdin, 1)) is REPL-only.
genhdr/mpversion.h included for MICROPY_BANNER_NAME_AND_VERSION.
The bebbo argv parser is broken under vamos with multi-arg invocations;
amiga_parse_args parses pr_Arguments itself.
.github/workflows/ports_amiga.yml, manually triggered via
workflow_dispatch with a ref input (branch / tag / commit, default
amiga-port). One job runs inside stefanreinauer/amiga-gcc:latest,
builds mpy-cross once, then builds all four variants sequentially with
make -j$(nproc) inside each. Each variant's binary is uploaded as a
separate artifact named
micropython-amiga-<variant>-<ref>-<sha>.
tools/amiga-build.sh mirrors the workflow exactly for local container
builds — same image, same commands, same output paths — so local and CI
binaries are bit-identical (see Toolchain).
Required submodule. make -C ports/amiga submodules runs before the
build; the frozen-content rule unconditionally checks for
lib/micropython-lib/README.md whenever FROZEN_MANIFEST is set, even
though manifest.py only freezes ports/amiga/modules.
Timing. Image pull dominates at ~87 s; the rest (checkout, submodule, mpy-cross, four variant builds, four uploads) is ~45 s — total wall time ~130 s, billed compute matches.
Possible follow-ups (deferred):
-
Image tarball cache.
docker save | gzip+actions/cachewould drop repeat-run wall time from ~130 s to ~40 s. Not worth the complexity for a manual-trigger workflow. -
Release flow. When the input
refis a tag, also publish a draft GitHub release with the four binaries attached. - AmiSSL SDK. Phase 28 will need the SDK layered into the build environment — see Phase 28 "SDK provisioning" subsection for options.
Phase 5 landed a working emitter, but only for the single-statement
return <const> shape. The moment a @micropython.native body calls another
function — anything that goes through mp_fun_table — the CPU jumps off into
the 68k vector-table area (typically PC=0x404, SR=0x0700) and faults. About
47 of the tests/micropython/native_*.py and viper_*.py tests fail in this
pattern:
-
D0 = self_inis correct on entry -
REG_FUN_TABLE(A4) ends up pointing at random memory by the timeASM_CALL_INDruns - The indirect
MOVEA.L (idx*4, A4), A0; JSR (A0)then lands in the vector table
Probable causes (need investigation):
-
Prologue ordering.
asm_68k_entrydoesLINK A5,#-N; MOVEM.L D2-D7/A2-A4,-(SP); load D0..D3 from 8(A5)..20(A5). A4 is in the saved set, so its value at function entry is whatever the parent left there.emitnative.cloads A4 from the const table viaREG_PARENT_ARG_1(D0) throughasm_68k_ensure_areg— verify A4 holds the right value when the firstASM_CALL_INDruns. -
Register clobbering through CALL_IND. Bebbo cdecl says callers preserve
nothing; we save D2–D7/A2–A4 across the whole function, but every
ASM_CALL_INDmay clobber A4 inside the callee. If the C function doesn't preserve A4, the next CALL_IND sees garbage. Need per-call save/restore or a different register. -
Stack frame mismatch. Verify
sp += MP_OBJ_ITER_BUF_NSLOTS - 1and friends match whatLINK A5, #-Nactually allocates.
Start with the simplest repro (native_const.py line 14: nested native
function returning 123, called from a wrapper) and work up.
try/except in native mode is the other gap — NLR_BUF_IDX_LOCAL_1 falls
inside the jmp_buf that setjmp overwrites. Needs a 68k assembly NLR
(nlr68k.S) that saves/restores D2–D7/A2–A5 in the nlr_buf_t.
shared/readline/readline.c drives the REPL. Cursor keys, history, kill/yank,
Home/End all work.
CSI translation. AmigaOS's console.device emits cursor reports as
single-byte CSI (0x9B) followed by parameters; shared/readline/ expects
the two-byte ESC [ form. mp_hal_stdin_rx_chr() keeps a one-byte pending
buffer: when FGetC returns 0x9B, return ESC and hand [ to the next
call. Hosts that already emit ESC [ (vamos's xterm pass-through) see no
change.
Running the REPL under vamos: tools/amiga-vamos-repl.sh puts the host
TTY into -icanon -echo -isig for the duration of the run (vamos's
SetMode(stdin,1) doesn't translate to tcsetattr on the host TTY).
AMIGA_VARIANT=68040 selects the build.
Heap is a chain of AllocVec chunks managed via MICROPY_GC_SPLIT_HEAP_AUTO.
Initial size from (priority order) -X heap=<N>[K|M], MICROPYHEAP env-var
(dos.library GetVar), compile-time MICROPY_HEAP_SIZE. Cap via
-X maxheap=<N> / MICROPYHEAPMAX.
main.c owns amiga_heap_chunks[16] for tracking (AllocVec is not
reclaimed by AmigaOS on task exit, so all chunks must be FreeVec'd
explicitly at shutdown). gc_get_max_new_split() reports
AvailMem(MEMF_ANY|MEMF_PUBLIC|MEMF_LARGEST) minus a small headroom, clamped
to -X maxheap. The GC's own sweep auto-releases empty grown chunks.
>>> amiga.heap_info() # → (total_bytes, free_bytes, num_arenas)
(256000, 248000, 1)Originally exec.library memory pools. Phase 14's dynamic GC heap covers the
common case; explicit native-buffer lifetimes can use alloc_vec. Number kept
to avoid renumbering later phases.
MICROPY_VFS=1 + MICROPY_READER_VFS=1 with a port-local VfsAmiga in
vfs_amiga.c. Stateless wrapper around Lock / Examine / CurrentDir /
Open / Read / Write / CreateDir / DeleteFile / Rename. AmigaDOS
keeps cwd in pr_CurrentDir. main() mounts a single VfsAmiga at /;
AmigaOS-style paths (volume:dir/file or relative) route directly.
amigafile.c and amigaio.c are deleted — mp_lexer_new_from_file comes
from extmod/vfs_reader.c; mp_import_stat is the inline that delegates to
mp_vfs_import_stat; mp_builtin_open_obj is aliased to mp_vfs_open_obj.
vfs_amiga.c reuses the pr_WindowPtr = -1 requester-suppression pattern
around every Lock. os.chdir keeps the first inherited cwd lock (it's the
shell's, not ours) and UnLocks subsequent ones. ilistdir uses a finaliser
so an abandoned for f in os.listdir(...) doesn't leak the directory lock.
open(..., "r+b") correctly maps to MODE_OLDFILE (fail-if-missing), not
MODE_READWRITE (create-on-missing).
Generic library-call mechanism. Any function in any library — system or third-party — callable from Python with no port-side C per library.
- Every library function lives at a negative offset (LVO) from the base.
- Args go in a fixed set of D/A registers — never on the stack.
- Return value always in D0. Library base in A6.
- All values 32-bit; no struct-in-register, no varargs.
NDK ships a .fd (Function Definition) file per library at
/opt/amiga/m68k-amigaos/ndk-include/fd/*.fd — enough to mechanically look
up call signatures.
Layer 1 — low-level trampoline. ports/amiga/amiga_lib_call.S is a
single hand-written 68k routine. C entry:
uint32_t amiga_lib_call_asm(
uint32_t base, int32_t offset,
uint32_t d0..d7, uint32_t a0..a5);Saves callee-saved set (d2-d7/a2-a6), loads A6 with the base, loads the
14 register slots from the stack. The jump to base + offset uses an
RTS-trick rather than computed JSR: with all D/A regs committed to
user values, no scratch register is free for the call target. Pushing
target + local return label and issuing rts transfers to the library;
the library's own rts pops the local label and returns to our cleanup.
C extension is registered as _amiga; three entry points:
amiga.lib_open(name, version=0) # OpenLibrary; OSError(ENOENT) on fail
amiga.lib_close(base) # CloseLibrary; tolerates 0 base
amiga.lib_call(base, offset, **regs, ret="d0")Register kwargs: d0–d7, a0–a5 (A6 is the base; A7 is SP).
ret="d0" signed, "d0u" unsigned, "void" returns None.
Layer 2 — .fd-driven proxy. Frozen amiga.py re-exports everything
from _amiga, adds a Library class on top of the FD table baked in from
_amiga_fd.py:
with amiga.library("intuition.library", 37) as intuition:
intuition.DisplayBeep(0)
win = intuition.OpenWindow(nw_ptr)
intuition.CloseWindow(win)Library.__getattr__ looks up the signature, builds a closure that maps
positional args into the right register kwargs of _amiga.lib_call, and
setattrs the closure onto the instance so subsequent reads skip
__getattr__. Context-manager support; explicit close(); GC-time
__del__ cleanup. Errors: unknown library → OSError(ENOENT); missing
function → AttributeError; wrong arg count → TypeError; call after
close → ValueError.
tools/amiga-fdgen.py parses every .fd under one or more NDK trees and
emits a Python module whose LIBRARIES dict maps each openable name to
{function_name: (lvo, regs_csv, since)}.
-
Chronological ordering. Hyperion's 3.1.4 → 3.2 → 3.2.x are
calendar-newer than 3.5/3.9 (development restarted years after 3.9).
Hand-maintained
AMIGA_OS_RELEASE_ORDERlist avoids3.2 < 3.9numeric sort wrecking thesincestamps. - Drift detection. LVOs are append-only; a function with a different offset/register list in a later NDK is a hard warning. Earlier entry wins.
-
Public-only.
##privatesections still consume LVO slots (so offsets are correct) but functions are dropped unless--include-private. -
CIA and
mathieeedoub*exceptions don't fit the convention; the tool warns and skips them.
Against current bebbo NDK: 76 .fd files → 75 openable names → 1146 public function signatures, ~80 KB Python source.
When Library(name) doesn't find name in the frozen table, it falls back
to a pure-Python .fd parser in amiga.py and walks PROGDIR:fd/ then
LIBS:fd/, trying both bebbo <base>_lib.fd and bare <base>.fd.
Alternatively, Library(name, version, signatures=...) or
amiga.library_from_signatures(name, version, sigs) bypasses both lookups.
Tag lists are pervasive in OS3.x. Three pieces in modamiga.c:
- Memory primitives:
peek_b/w/l/bytes(addr[, n]),poke_b/w/l/bytes(addr, v). -
TagListclass wrapping anAllocVec'dTagItem[].(tag, value)slots are 8 bytes; ints go straight intoti_Data;bytes/strget their ownAllocVec'd NUL-terminated buffer with the address stored inti_Data. All allocations released byclose()/__exit__/ GC__del__. Defines__int__returning the head address. -
amiga.taglist(...)factory. Kwargs resolved against_amiga_tags.TAGS; positional args support(tag, value)pairs, iterable, or alternatingtag, value, tag, value. Unknown tag names raiseKeyError. -
Libraryproxy runsint(v)on any non-int arg soTagListpasses through cleanly:
with amiga.taglist(WA_Width=640, WA_Height=480, WA_Title="hi") as tags:
with amiga.library("intuition.library", 37) as intuition:
intuition.OpenWindowTagList(0, tags)tools/amiga-taggen.py drives bebbo's m68k-gcc to harvest tag IDs from
NDK headers (preprocess with -E -dM, compile as const ULONG _x_<NAME> = (ULONG)(<NAME>); with -S -O2, parse .long from the assembly). Keeps
only TAG_USER-namespace values (bit 31 set) plus universal TAG_* markers.
1072 tag IDs in _amiga_tags.py, ~48 KB frozen.
amiga.parse_taglist(addr, max_items=64) walks a TagItem array back into a
{tag_id: value} dict, following TAG_MORE chains, dropping TAG_IGNORE,
honouring TAG_SKIP. taglist(..., more=other) writes a TAG_MORE
terminator pointing at other.addr and pins it alive on the outer.
amiga.Struct(addr, layout, name=None) wraps a C struct at a fixed address.
layout is {field_name: (offset, type_code)} with codes B/b/H/h/L/l/P/sN/S.
Defines __int__ so an instance passes straight to a Library call.
Starter layouts shipped in _amiga_structs.py:
| factory | struct |
|---|---|
amiga.Node(addr) |
struct Node |
amiga.Task(addr) |
struct Task |
amiga.Library_struct(addr) |
struct Library (trailing _struct to avoid clash with the proxy class) |
amiga.DateStamp(addr) |
struct DateStamp |
amiga.FileInfoBlock(addr) |
struct FileInfoBlock |
amiga.IntuiMessage(addr) |
struct IntuiMessage |
Callback thunks for hook-driven library calls (CreateNewProc, blit hooks,
ARexx command dispatchers). Needs executable memory and per-callable
thunks. Defer until a concrete user need lands; most OS3.x scripting
doesn't need it.
ports/amiga/manifest.py is freeze("$(PORT_DIR)/modules"). Makefile sets
FROZEN_MANIFEST = manifest.py and MPY_TOOL_FLAGS = -mlongint-impl=longlong
to match MICROPY_LONGINT_IMPL_LONGLONG (else mpy-tool.py emits MPZ
literals which fail the static_assert in frozen_content.c).
ARexx is the Amiga IPC mechanism: every well-behaved app exposes an ARexx port. Both directions implemented.
Five C primitives in modamiga.c, RexxMessage facade in amiga.py:
name = amiga.rexx_open() # opens MICROPYTHON.1 (or .N)
while True:
msg = amiga.rexx_recv(timeout_ms=1000)
if msg is None: continue
try: msg.reply(str(eval(msg.command.decode("ascii"))), rc=0)
except Exception as e: msg.reply(str(e), rc=10)
amiga.rexx_close()
amiga.rexx_serve(lambda cmd: eval(cmd)) # ready-made dispatcher-
rexx_open(stem="MICROPYTHON")finds the lowest free.Nsuffix underForbid()/Permit(),CreateMsgPort()+AddPort(). -
rexx_close()drains the queue (replying rc=20 to anything still queued so a hungrxdoesn't stay blocked),RemPort,DeleteMsgPort, closesrexxsyslib.libraryif opened. Called frommain.con exit as a safety net. -
rexx_recv(timeout_ms=None)polls thenWait()s. The Phase 25 asynctimer.deviceport supplies the timeout signal bit;SIGBREAKF_CTRL_Cis always ORed in and raisesKeyboardInterruptif it fires. -
RexxMessage.reply(result=None, rc=0, secondary=0)setsrm_Result1=rc. Ifrc == 0and result non-None, lazy-opensrexxsyslib.library,CreateArgstringover the bytes, stores inrm_Result2; otherwisesecondarygoes intorm_Result2.ReplyMsg.__del__sends an automatic rc=20 reply if the script forgets — a forgotten reply would block the sender forever.
result = amiga.rexx("PPAINT.1", "ScreenToFront") # bytes
rc, result = amiga.rexx("HOST.1", "DoIt", check=False) # (rc, result|None)Textbook send-and-wait: FindPort → lazy OpenLibrary("rexxsyslib.library")
(cached, two-way scripts pay one open) → CreateMsgPort for private reply
port → CreateRexxMsg → CreateArgstring for the command → rm_Action = RXCOMM | RXFF_RESULT → PutMsg → Wait(reply | CTRL_C) → unpack
rm_Result1/rm_Result2, copy result bytes via mp_obj_new_bytes +
LengthArgstring, DeleteArgstring both → DeleteMsgPort.
Ctrl+C is deferred. Once PutMsg is in flight we can't tear down the
reply port — the host would PutMsg into freed memory. The wait latches
the CTRL_C bit but keeps spinning until the reply is in hand; then
KeyboardInterrupt is raised before returning.
Lazy-opened. So import amiga still works, the inbound port works for
rc-only replies, but:
-
RexxMessage.reply(result="...", rc=0)→ replies rc=10 to sender, raisesOSError("rexxsyslib.library unavailable"). -
amiga.rexx(host, command)→ raisesOSError(...)before any send.
call open(...), call writeln(...), call close(...) each clobber the
result special variable. Any rx script wanting to inspect the host
reply must snapshot it immediately after address:
options results
address MICROPYTHON.1 'some command'
saved_rc = rc
saved_result = resultWB-launched processes get a WBStartup message instead of argc/argv, and
their config comes from .info tooltypes.
Detecting. Bebbo's crt0.o already does the heavy lifting — when
pr_CLI == 0 it WaitPorts, GetMsgs the WBStartup, stashes the pointer
in the global _WBenchMsg, and on exit Forbid()s and ReplyMsgs back to
Workbench. We just extern struct WBStartup *_WBenchMsg; and test for null.
Console. WB-launched processes have pr_CIS/pr_COS both NULL, so
main.c opens CON:0/30/640/200/MicroPython/AUTO/CLOSE/WAIT and points
stdin/stdout/console-task at it. /AUTO defers window appearance until
write; /WAIT keeps it open after main() returns.
Tooltypes. icon.library GetDiskObject(sm_ArgList[0].wa_Name); handle
cached in IconBase + amiga_wb_diskobject. Two consumed at startup:
-
SCRIPT=<path>— script to run instead of REPL -
HEAP=<N>/MAXHEAP=<N>— same parser as-X heap=(env vars still win, since they're more explicitly user-set)
if amiga.launched_from_workbench():
extra = amiga.tooltype("EXTRA_PATH", "")
for path in amiga.wb_selected_files(): # shift-clicked icons
process(path)wb_selected_files() renders each WBArg with NameFromLock + AddPart.
Cannot be exercised under vamos (no Workbench, no icon.library); full
validation requires Amiberry/FS-UAE or real hardware.
os.getenv / os.putenv / os.unsetenv via dos.library GetVar/SetVar
with flags=0 (local CLI vars, falling through to global ENV:). Matches
Unix os.putenv semantics — visible to child processes spawned via
amiga.execute(), not to unrelated shells. For system-wide / persistent,
write to ENV:/ENVARC: directly with open().
mpconfigport.h: MICROPY_PY_OS_GETENV_PUTENV_UNSETENV (1) +
MICROPY_PY_OS_INCLUDEFILE "ports/amiga/modos.c". modos.c is #included
by extmod/modos.c; no Makefile changes.
Vamos workarounds (both correct on real AmigaOS):
-
GetVarreturns 0 (not -1) for a missing variable; we treatlen <= 0as missing (misreports a genuine empty-string variable on real AmigaOS, correct on vamos). -
DeleteVarreadsflagsfromD4, but the NDK fd specifiesD2. We use the V36-documented "SetVarwithNULLbuffer deletes" form instead.
amiga.volumes() # ['Python:', 'Ram Disk:', 'Workbench:']
amiga.assigns() # {'C:': 'Workbench:C', 'LIBS:': 'Workbench:Libs', ...}
amiga.disk_info(path) # (free_bytes, total_bytes, block_size)volumes()/assigns() walk LockDosList(LDF_VOLUMES/LDF_ASSIGNS | LDF_READ)
via NextDosEntry. Assign targets from NameFromLock(dol_Lock) for
DLT_DIRECTORY, or dol_misc.dol_assign.dol_AssignName for late/non-binding
assigns. Multi-directory assigns report first dir only.
disk_info(path) locks (with pr_WindowPtr=-1), calls Info(), computes
byte counts as uint64_t for >4 GB volumes. IoErr() mapped to MP_E*
constants (e.g. ERROR_DEVICE_NOT_MOUNTED → MP_ENODEV).
for path in amiga.match("S:#?"): # eager list
for path in amiga.imatch("Work:#?.py"): # lazy iteratorOne AnchorPath with trailing 512-byte buffer; ap_Strlen preset.
MatchFirst parses the pattern (no separate ParsePattern). Empty list for
ERROR_NO_MORE_ENTRIES / ERROR_OBJECT_NOT_FOUND; other DOS errors raise
OSError.
imatch uses mp_type_polymorph_iter_with_finaliser; the iterator owns the
AnchorPath, its finaliser calls MatchEnd + FreeVec so an abandoned
loop (for p in amiga.imatch(...): break) doesn't leak. MatchFirst runs
eagerly inside imatch() so the first result is ready on the first
next(). Both suppress auto-requesters.
Replaced clock()-based path (busy-wait, ms-ish resolution) with:
-
mp_hal_delay_us(n)→timer.device TR_ADDREQUESTviaDoIO()for ≥200 µs, tightReadEClock()busy-loop below. -
mp_hal_delay_ms(n)→timer.devicefor arbitrary-millisecond accuracy (the previousDelay()had 20 ms granularity). -
mp_hal_ticks_us/ms()→ReadEClock()(hardware counter, monotonic, cheap). EClock frequency cached at init.
Setup in amiga_timer.c: CreateMsgPort + CreateIORequest +
OpenDevice("timer.device", UNIT_MICROHZ, ...) once at startup, before
mp_init(). TimerBase (referenced by bebbo proto/timer.h inlines) is
set from the request's io_Device. IORequest stored port-local-static;
not thread-safe but the port is single-threaded.
amiga_history.c loads / saves S:MicroPython.history (override via
MICROPYHISTORY env-var). One entry per line, oldest first; CRLF tolerated
on read. MICROPY_READLINE_HISTORY_SIZE bumped to 32 (was 8). Both paths
suppress auto-requesters; failure (no S: volume, read-only, corrupt) is
silent — history just doesn't persist that session.
main.c calls amiga_history_load() after mp_init() and
amiga_history_save() before mp_deinit(). amiga.readline_history() and
amiga.readline_push_history(line) exposed for scripting / testing.
Not in ENVARC: — that's for preferences; a frequently-written log there
would make every reboot's copy ENVARC: ENV: all slower. S: is the
conventional AmigaOS dotfile spot.
amiga.signal(other_task_addr, amiga.SIGBREAKF_CTRL_E)
mask = amiga.wait_signal(amiga.SIGBREAKF_CTRL_D | amiga.SIGBREAKF_CTRL_E,
timeout_ms=5000)-
signal(task_addr, sigmask)→exec.library Signal(). NULL task raisesValueError. -
wait_signal(mask, timeout_ms=None)→Wait().SIGBREAKF_CTRL_Cis always ORed into the internal mask (so the user can break out) but always stripped from the return (so Ctrl+C never spuriously satisfies a user signal); if CTRL_C fires,KeyboardInterrupt.
timeout_ms uses a second, async timer.device IORequest in
amiga_timer.c (kept separate from Phase 23's synchronous request so a
pending Wait can't collide with an in-flight mp_hal_delay_us). SendIO
arms the timer; the MsgPort's signal bit ORs into the Wait mask; the
request is aborted/drained on return. If the second port fails at startup,
wait_signal falls back to an untimed Wait — documented best-effort.
PROGDIR: is the auto-assign AmigaDOS creates per process for the
executable's directory. main.c appends it to sys.path after
mp_init() (prepending would lose it on every script run, since
positional-script handling replaces sys.path[0] with the script's
directory). After a script launch, sys.path is
['<script_dir>', '.frozen', 'PROGDIR:'] — script wins, PROGDIR: as final
fallback.
make VARIANT=<name>; build dir is build-<variant>.
| Variant | CPU | Heap | Notes |
|---|---|---|---|
standard (default) |
-m68020 -msoft-float |
256 KB | Any 68020+, no FPU. Default for stock A1200 / unaccelerated 68030 |
68020fpu |
-m68020 -m68881 |
512 KB | 68020/30 + 68881/2 (A2630, A3000). No libgcc soft-float wraps |
68040 |
-m68040 |
1 MB | 68040 built-in FPU (A3640, A4000/040) |
Text-segment sizes: standard 356 KB, 68020fpu 330 KB, 68040 339 KB.
A500 isn't a target — its 68000 lacks unaligned access. Why 68040
is larger than 68020fpu: Motorola dropped transcendentals (FSIN/COS/TAN/
ATAN/etc.) from the 68040; gcc emits libm calls instead of inlines, pulling
in ~9 KB. Alternative -m68040 -m68881 would shrink it back and rely on
AmigaOS's FPSP (68040.library) to trap-and-emulate, but a binary that
uses FSIN on a 68040 without FPSP loaded (custom Workbench, stripped
startup, certain demos) gurus. The 9 KB is the price of self-containment.
sys.implementation._machine reports "Amiga with 68EC020" /
"68020/68881" / "68040". floatconv.c pow/tgamma wraps apply on
all variants (math-library bugs, not FPU codegen).
FPU variants require bebbo to have FPU multilibs (make all provides them);
if m68k-amigaos-gcc -m68881 -print-multi-lib shows only a soft-float entry,
the link fails with cannot find -lgcc.
Landed in six commits, end-to-end verified against www.python.org
on 2026-05-31 with CERT_REQUIRED chain validation
(HTTP/1.1 200 OK, 53 912 B HTML). The implementation log lives in
docs/phase28-ssl-plan.md — measured heap
costs, the AmigaOS capath trailing-slash gotcha, and the modern-CDN
TLS 1.3 limitation tracked as a follow-up. The section below is
the design and rationale — what and why; the companion doc
covers how it shipped.
Upstream MicroPython ships axTLS (small, dated, no SNI) and mbedTLS (~250 KB add). Neither fits the AmigaOS ethos when the platform already has a perfectly serviceable TLS implementation — AmiSSL — as a shared library. TLS code stays on the user's machine; we ship only the thin wrapper.
AmiSSL v5 is OpenSSL 3.x: TLS 1.3, ChaCha20-Poly1305, simpler
TLS_client_method(). v4 (OpenSSL 1.1.x) is EOL upstream and not worth a
deprecated-API codepath.
Unlike bsdsocket.library, AmiSSL has a master-indirection step. v5 has a
unified OpenAmiSSLTags() replacing the legacy v3/v4 triple:
AmiSSLMasterBase = OpenLibrary("amisslmaster.library",
AMISSLMASTER_MIN_VERSION);
OpenAmiSSLTags(AMISSL_CURRENT_VERSION, // positional
AmiSSL_UsesOpenSSLStructs, FALSE, // opaque-only
AmiSSL_GetAmiSSLBase, &AmiSSLBase,
AmiSSL_GetAmiSSLExtBase, &AmiSSLExtBase, // OS3 split
AmiSSL_SocketBase, (ULONG)SocketBase,
AmiSSL_ErrNoPtr, (ULONG)&errno,
TAG_DONE);-
AMISSL_CURRENT_VERSIONis positional, not a tag. Compiling against the v5 SDK produces a binary that requires AmiSSL v5 at runtime — that's the point of the master indirection. -
AmiSSL_UsesOpenSSLStructs = FALSE— MicroPython treatsSSL_CTX */SSL */X509 *as opaque pointers. -
AmiSSLExtBaseis OS3-only: v5's API surface is large enough they split it across two bases on m68k. -
SocketBaseis supplied in the same call —amiga_ssl_open()must run afteramiga_socket_open().
Link against -lamisslstubs (function stubs needed for callbacks like
SSL_CTX_set_verify(..., X509_free)); don't use -lamisslauto (its
constructor-magic open fires before SocketBase is valid).
Headers: straight #include <openssl/...>, same as desktop.
OS3 callbacks under AmiSSL take args on the stack (not registers) and need
STDARGS SAVEDS annotations:
STDARGS SAVEDS static int amiga_ssl_verify_cb(int preverify_ok,
X509_STORE_CTX *ctx) { ... }bebbo gcc has __attribute__((stkparm)) and __saveds. Worth a
AMISSL_CB macro in amiga_ssl.h.
ports/amiga/modssl.c follows the upstream ssl API:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.load_verify_locations(capath="AmiSSL:certs/")
ws = ctx.wrap_socket(s, server_hostname="www.example.com")SSLContext wraps SSL_CTX *; SSLSocket wraps SSL * + fd and
implements the stream protocol so it slots into readline()/with. Per
socket: SSL_new → SSL_set_fd → SSL_set_tlsext_host_name (SNI) →
SSL_connect → SSL_read/write → SSL_shutdown + SSL_free. Transient
SSL_ERROR_WANT_READ/WRITE → MP_EAGAIN; terminal errors raise OSError
with ERR_error_string text.
Two paths:
-
SSL_CTX_set_default_verify_paths()picks up theAmiSSL:certs/c_rehash dir (the hashed CAs the AmiSSL installer drops there). -
ctx.load_verify_locations(cafile=...)/cadata=...for custom roots.
Don't ship a CA bundle in the binary — ~200 KB, gets stale fast.
#define MICROPY_PY_AMIGA_SSL (MICROPY_PY_AMIGA_SOCKET)All three shipped variants link AmiSSL. If amisslmaster.library isn't
installed at runtime, import ssl raises ImportError.
OpenSSL 3.x is memory-hungry: SSL_CTX ~16 KB, each SSL ~48 KB, CA store
~200 KB. Two HTTPS connections push the 256 KB standard heap to the edge
— but AmiSSL allocates from system memory (MEMF_ANY), not MicroPython's
GC heap, so -X heap= doesn't help; the lever is total system Fast RAM.
- Server-side TLS — straightforward, but the Amiga isn't a webserver host.
- OpenSSL config-file integration — AmiSSL ships sane defaults.
- Asyncio-friendly handshake —
MICROPY_PY_ASYNCIO (0); revisit when asyncio lights up. - Touching upstream
extmod/modssl_*.c— AmiSSL-only, port-side module.
ports/amiga/modssl.c — module def, SSLContext, SSLSocket
ports/amiga/amiga_ssl.c — AmiSSL master open / init / cleanup
ports/amiga/amiga_ssl.h — shared prototypes + base pointers
SDK ref:
jens-maus/amissl:dist/README-SDK.
Examples/httpget.c and Examples/https.c are minimal working clients
worth mirroring.
stefanreinauer/amiga-gcc:latest ships only vbcc-side AmiSSL stubs
(proto/amisslmaster.h, proto/amissl.h, inline/amissl_protos.h
under …/vbcc/…). The GCC tree has no <openssl/*> headers, no
<libraries/amissl[master].h>, and no libamisslstubs.a/
libamisslauto.a. Phase 28 needs the AmiSSL v5 SDK layered in. Three
options when we get there:
-
Extend the container image. Custom Dockerfile
FROM stefanreinauer/amiga-gcc:latestthat fetches the SDK from https://github.com/jens-maus/amissl/releases, unpacks headers into/opt/amiga/m68k-amigaos/include/and libs into/opt/amiga/m68k-amigaos/lib/. Push to GHCR; CI workflow andtools/amiga-build.shboth point at it. Cleanest, one-time setup. - Workflow step. Download + unpack the SDK on every CI run. Simpler setup, but adds 10–30 s per run and a network dependency on GitHub releases.
-
Vendored in-tree. Drop the headers + stubs under
ports/amiga/amissl-sdk/. No external deps; downside is shipping a few MB of third-party code in the repo.
Pick when Phase 28 actually starts; option 1 is the current preference.
- AmiSSL 4 fallback. Land v5-only initially.
-
Cert pinning helpers (
set_servername_callback, raw-public-key APIs) — punt to a follow-up. - Async-friendly handshake — tied to asyncio gating.
-
urequests/mip— pure-Python, freeze into variant manifests oncessllands. (Phase 29 picks upurequests.)
Shipped in five steps over commits 3455810e4 (Step 1),
de03cc93b (Steps 2–4), and the doc-flip in this section.
End-to-end verified under Amiberry against
www.example.com (HTTP + gzip), httpbin.org (HTTP +
chunked + POST echo for both json= and data=dict), and
www.python.org (HTTPS, 48 887 B of HTML through the Phase 28
TLS stack). See docs/phase29-urequests-plan.md
for the step-by-step log.
Convenience client built on top of Phase 9 (socket) and Phase 28
(ssl). Same shape as upstream micropython-lib's requests:
import urequests
r = urequests.get("https://www.python.org/")
print(r.status_code)
print(r.text[:200])
r.close()urequests.get / post / put / delete / patch / head-
Responsewith.status_code,.reason,.headers,.text,.content,.json(),.close() - HTTP/1.0 wire format (
Connection: close), HTTP/1.1 read tolerance -
https://via the existingssl.SSLContextsetup (CERT_REQUIRED+set_default_verify_paths()against the AmiSSL trust store) - Optional gzip decompression via the existing
deflatemodule - POSTing
data=(urlencoded),json=(auto-serialised),headers=overrides
- Persistent / keep-alive connections — needs HTTP/1.1 keep-alive state-machine, deferred
- Sessions / cookies — deferred
- Streaming uploads /
multipart/form-data— deferred - HTTP/2, async — deferred (no asyncio yet)
Inherits the Phase 28 AmiSSL ↔ modern-CDN issue
(phase28-ssl-plan): urequests.get against
TLS-1.3-eager hosts (Cloudflare, GitHub) will succeed the handshake
then fail the write. Direct origins that still negotiate TLS 1.2 by
default work.
ports/amiga/modules/urequests.py — frozen pure-Python module
docs/phase29-urequests-plan.md — step plan
tests/ports/amiga/test_urequests_smoke.py — on-target smoke (Amiberry, AmiSSL installed)
All three shipped variants include it.
amiga.intuition C sub-module exposing intuition.library's
EasyRequest family. Three entry points:
from amiga import intuition
idx = intuition.easy_request("Title", "Body line one.\nBody line two.",
["Yes", "No", "Cancel"])
ok = intuition.auto_request("Replace existing file?", yes="Yes", no="No")
intuition.message("Done.", button="OK")-
easy_request(title, body, buttons)→ int (0-based button index; rightmost button is always the "cancel" / 0 convention) -
auto_request(body, yes="Yes", no="No")→ bool, two-button wrapper -
message(body, button="OK")→ None, single-button notice - printf-escape safety (raw user strings are passed as
%sargs, not as the format string) - Latin-1 codec for the title/body so non-ASCII text renders cleanly under AmigaOS Topaz/CP-1252 displays
- Arbitrary window/widget surfaces — see "Non-goals"
- Custom gadgets, file/font/screen pickers (Phase 31 handles file)
- Non-blocking / async requesters — modal only
-
intuition.libraryopened lazily on first call (cached base) - Uses Phase 17's library-calling infrastructure as fallback if preferred over explicit C, but a direct C module is probably simpler given how concentrated the API surface is
ports/amiga/modintuition.c — C module (~180 LOC)
ports/amiga/modules/amiga.py — adds `import _intuition as intuition`
tests/ports/amiga/test_intuition_smoke.py — vamos arg-shape smoke test
docs/phase30-intuition-plan.md — step plan
Variants: all four. ~1.5 KB text per variant, no network/SSL deps.
Three Python entry points, all backed by a single EasyRequestArgs
call through intuition.library v36+:
| Function | Returns |
|---|---|
intuition.easy_request(title, body, buttons) |
int — 0-based leftmost button index |
intuition.auto_request(body, yes="Yes", no="No") |
bool — True iff Yes clicked |
intuition.message(body, button="OK") |
None |
Translation: AmigaOS conventionally numbers buttons rightmost-is-0
(EasyRequest returns 0 for the right gadget, 1 for the next, …,
N-1 for the leftmost). The module flips that to Python's natural
0-based-leftmost so buttons[easy_request(...)] does the right thing.
es_TextFormat is hard-coded to "%s" and the body is passed as the
single varargs arg, so a % in the body is rendered literally rather
than interpreted as a printf directive.
Vamos has a no-op EasyRequest stub — useful for confirming the call
threads through cleanly but not for end-to-end visual checks. Those
need Amiberry or real hardware. Verified working under Amiberry
2026-05-31; intuition.library opens its own screen if no public
screen is currently up, so the requester appears regardless of
whether Workbench is loaded.
amiga.asl C sub-module wrapping asl.library's
AslRequest(ASL_FileRequest, ...). Native Amiga file chooser
dialog usable from both CLI and Workbench launches.
from amiga import asl
path = asl.file_request(title="Pick a script",
initial_drawer="Work:scripts/",
pattern="#?.py")
if path is None:
print("cancelled")
# Save dialog
out = asl.file_request(title="Save as", save=True,
initial_file="output.txt")
# Multi-select returns list
paths = asl.file_request(multi=True)
# Drawer-only
drawer = asl.file_request(drawers_only=True)-
file_request(...)keyword args:title,initial_drawer,initial_file,pattern,save=False,multi=False,drawers_only=False - Returns
strfor single-pick,list[str]formulti=True,Nonefor user-cancelled - Paths joined via
dos.library AddPart()so volume separators come out right on AmigaOS - Latin-1 codec for filenames
- Font / screen / draw mode / palette requesters — file is the only one with clear practical use from a scripted REPL
- Custom hooks / per-entry filter callbacks —
patternis enough
-
asl.libraryopened lazily on first call (cached base) - Same C-or-Phase-17 trade-off as Phase 30
ports/amiga/modasl.c — C module (~280 LOC)
ports/amiga/modules/amiga.py — adds `import _asl as asl`
tests/ports/amiga/test_asl_smoke.py — vamos arg-shape smoke test
docs/phase31-asl-plan.md — step plan
Variants: all three shipped variants. ~1.2 KB text cost per variant.
One entry point, behaviour driven entirely by kwargs:
| Kwargs | Returns |
|---|---|
(title, initial_drawer, initial_file, pattern) |
str — full path of the picked file |
save=True |
str — full path (editable filename gadget) |
drawers_only=True |
str — drawer path (no file component) |
multi=True |
list[str] — one full path per shift-clicked file |
| user clicked Cancel | None |
multi=True + save=True
|
ValueError |
asl.library is famously stack-hungry: a default ~4 KB shell stack
trips a CHK exception (0x80000006) on the post-pick code path
even though the dialog renders fine. file_request runs the bare
AllocAslRequest + AslRequest calls on a 32 KB scratch stack
via StackSwap so callers don't have to remember Stack 32768
at the shell prompt; the path-build and string allocation happen
back on the original stack so the GC's stack-scan range stays
correct.
Path buffer is 1024 bytes (vs the 512 used by older modamiga.c
surfaces) to comfortably accommodate long-name filesystems
(SFS / PFS3 / FFS2).
Three additions to the existing ARexx surface (amiga.rexx_send,
amiga.rexx_open/recv/reply from Phase 18):
import amiga
# Phase 32 additions:
if amiga.rexx_exists("IBROWSE"):
rc, html = amiga.rexx_send("IBROWSE", "QUERY ITEM=URL")
for port in amiga.rexx_list():
print(port)
# Persistent client -- caches the reply MsgPort across sends so a
# tight loop doesn't pay CreateMsgPort/DeleteMsgPort per call.
with amiga.RexxClient("DOPUS.1") as ib:
ib.send("LISTER NEW")
rc, result = ib.send("LISTER QUERY 1 PATH", check=False)-
amiga.rexx_exists(name)→bool. Thin wrapper aroundFindPort(name)withForbid()/Permit()braces. -
amiga.rexx_list()→list[str]. WalksSysBase->PortList, returning every public port'sln_Name. -
amiga.RexxClient(host)class:- Opens a reply
MsgPortin__init__; reuses it across calls. -
.send(command, check=True)returns the same shape asamiga.rexx(host, command). -
.close()/__exit__/__del__tear down the reply port. - Tracked in an at-exit cleanup chain so a forgotten close doesn't leak the MsgPort on process exit.
- Opens a reply
- Migrating the existing
amiga.rexx_*surface into anamiga.rexx.*sub-module — breaking change for callers and the inbound server side (Phase 18) is already deeply embedded in the module flat namespace - Reshaping the one-shot
amiga.rexx_send/amiga.rexxhelpers to use the client class internally — the per-call MsgPort cost is invisible for a single send; the polish is for tight loops
-
rexxsyslib.library(already opened lazily by Phase 18amiga.rexx_send)
ports/amiga/modamiga.c — five new C entries
(rexx_exists, rexx_list,
rexx_client_open/close/send)
ports/amiga/modules/amiga.py — RexxClient Python facade
tests/ports/amiga/test_rexx_polish.py — vamos arg-shape smoke
docs/phase32-arexx-polish-plan.md — step plan
Variants: all three. ~1.4 KB text per variant.
Five new entry points alongside the Phase 18 ARexx surface:
| Entry | Returns |
|---|---|
amiga.rexx_exists(name) |
bool (Forbid-fenced FindPort) |
amiga.rexx_list() |
list[str] (Forbid-fenced walk of SysBase->PortList) |
amiga.RexxClient(host) |
persistent client; .send(cmd, check=True), context manager, __del__ cleanup |
amiga.rexx_client_open / _close / _send
|
low-level primitives that back the class |
The class's reply MsgPort is registered in a 16-slot static
chain at construction. amiga_rexx_shutdown (already wired into
main.c's cleanup path) walks the chain on process exit and
DeleteMsgPorts anything still live, so a script that forgot
close() doesn't leak the port. The send body was factored out
of amiga_rexx_send_fn into amiga_rexx_send_via_port so the
one-shot and persistent paths share the wire format / Ctrl+C
latch / argstring handling.
Frozen CPython-shaped platform.py module surfacing AmigaOS
identity. Modeled on OoZe1911's port:
>>> import platform
>>> platform.system()
'AmigaOS'
>>> platform.machine()
'68020'
>>> platform.amiga_info()
'CPU: 68020 | FPU: 68881 | Chipset: AGA | Kickstart: 45.57 | Chip: 1856KB | Fast: 14336KB'- Six new C accessors on the
amigamodule:-
amiga.cpu()→str(e.g."68020","68040") -
amiga.fpu()→str(e.g."none","68881","68040") -
amiga.chipset()→str("OCS"/"ECS"/"AGA") -
amiga.kickstart()→str(e.g."45.57") -
amiga.chipmem()→int(bytes available) -
amiga.fastmem()→int(bytes available)
-
- Frozen
platform.pywrapping the above with the CPython API shape (system(),machine(),processor(),version(),release(),python_implementation(),python_version(),platform(),node()) plus the convenienceamiga_info().
- Full CPython
platformparity (uname(),linux_distribution(), etc.) — beyond what's meaningful on AmigaOS - Caching the strings; they're cheap to recompute and the input state can change (memory free shifts continuously)
ports/amiga/modamiga.c — six accessor functions
ports/amiga/modules/platform.py — frozen facade
tests/ports/amiga/test_platform_smoke.py — vamos smoke
docs/phase33-platform-plan.md — step plan
Variants: all three. ~1.6 KB text per variant (six C accessors + the frozen module + the lazy graphics.library hook).
amiga.* accessor |
platform.* wrapper |
Returns |
|---|---|---|
amiga.cpu() |
platform.machine / processor
|
str — highest AttnFlags CPU bit ("68000" .. "68060") |
amiga.fpu() |
platform.fpu |
str — "none" / "68881" / "68882" / "68040"
|
amiga.chipset() |
platform.chipset |
str — "OCS" / "ECS" / "AGA" (lazy graphics.library) |
amiga.kickstart() |
platform.release (and version prefix) |
str — "VV.RR"
|
amiga.chipmem() |
platform.chipmem |
int — bytes currently free in Chip RAM |
amiga.fastmem() |
platform.fastmem |
int — bytes currently free in Fast RAM |
Plus the CPython-standard platform.system() → "AmigaOS",
node() → "Amiga", python_implementation() /
python_version(), and the convenience
platform.platform() ("AmigaOS-<kick>-<cpu>-MicroPython_<pyver>")
and platform.amiga_info()
("CPU: 68020 | FPU: 68881 | Chipset: AGA | Kickstart: 45.57 |
Chip: 1856KB | Fast: 14336KB").
Values reflect runtime AmigaOS state, not compile-time targeting:
a standard (68020 / soft-float) binary running on a 68040
reports "68040" / "68040" for cpu() / fpu().
Closes the os / os.path gap that showed up when comparing our
port to OoZe1911's. The C-side os module remains extensible
(via MP_REGISTER_EXTENSIBLE_MODULE), and a frozen os.py /
_ospath.py merges on top so the standard surface (chdir /
getcwd / listdir / mkdir / remove / rename / rmdir /
stat / statvfs / getenv / putenv / unsetenv) coexists
with the new additions.
import os
os.makedirs("Work:scripts/sub/dir", exist_ok=True)
for root, dirs, files in os.walk("Work:"):
print(root, len(files))
os.chmod("DH0:foo", 0) # 0 = no denials -- grant R, W, E, D
mask = os.getprotect("DH0:foo") # raw fib_Protection ULONG
print(mask & os.FIBF_DELETE) # nonzero ⇒ delete is denied
os.path.join("Work:", "scripts", "foo.py") # 'Work:scripts/foo.py'
os.path.abspath("foo.py") # uses cwd, volume aware
os.path.isabs("Sys:Prefs") # True (':' anywhere)
os.path.normpath("Work:scripts/../bin") # 'Work:bin'os.* additions:
| Call | Returns | Notes |
|---|---|---|
os.chmod(path, mask) |
None |
SetProtection wrapper. Suppresses AmigaDOS auto-requesters. OSError (translated AmigaDOS errno) on failure. |
os.getprotect(path) |
int |
Lock + Examine + read fib_Protection. Returns the raw 32-bit mask; AND with FIBF_* to test bits. |
os.makedirs(name, exist_ok=False) |
None |
Recursive mkdir, volume-aware. The volume prefix itself isn't created. EEXIST family swallowed when exist_ok=True. |
os.walk(top, topdown=True) |
generator | Yields (dirpath, dirnames, filenames) triples. Same shape as CPython; no onerror / followlinks (AmigaOS has no symlinks). |
os.FIBF_READ / WRITE / EXECUTE / DELETE |
int | RWED bits. Set bit ⇒ denied (AmigaDOS inverted convention). |
os.FIBF_ARCHIVE / PURE / SCRIPT / HOLD |
int | APSH bits. Set bit ⇒ asserted. |
os.path |
module | The frozen _ospath module (volume-aware path helpers). |
os.path (== _ospath) surface:
| Call | Returns | Notes |
|---|---|---|
path.join(*parts) |
str |
No separator after : (volume terminator); / between other components. A part containing : resets the join. |
path.split(p) |
(dir, base) |
Splits at the last / or :. : stays on the dirname side. |
path.splitext(p) |
(root, ext) |
Last . after the last separator. Leading . (dot-files) returns ("", p) semantics on basename. |
path.basename(p) / path.dirname(p)
|
str |
split(p)[1] / split(p)[0]. |
path.isabs(p) |
bool |
True if p contains any : (AmigaDOS treats Work:, :foo, Sys:Prefs all as absolute). |
path.abspath(p) |
str |
p if isabs(p); else join(getcwd(), p) then normpath. |
path.normpath(p) |
str |
Collapses . / ..; clamps at the volume boundary (Work:.. stays Work:). |
path.exists(p) / path.isfile(p) / path.isdir(p)
|
bool |
Wrap os.stat. False on any OSError. |
The four RWED bits in fib_Protection are inverted relative to
Unix:
| Bit | Set means | Clear means |
|---|---|---|
FIBF_READ |
read denied | read allowed |
FIBF_WRITE |
write denied | write allowed |
FIBF_EXECUTE |
execute denied | execute allowed |
FIBF_DELETE |
delete denied | delete allowed |
So os.chmod(path, 0) grants R, W, E, and D (nothing denied), and
os.chmod(path, os.FIBF_DELETE) denies only delete.
The APSH bits follow the conventional "set means yes":
| Bit | Set means |
|---|---|
FIBF_ARCHIVE |
file has been backed up since last change |
FIBF_PURE |
binary is reentrant (can be made resident) |
FIBF_SCRIPT |
file is a Shell script |
FIBF_HOLD |
keep a pure module resident on first use |
This matches the AmigaShell Protect command's encoding.
- Posix mode → AmigaDOS bit translation for
chmod.rwxandrweddon't map cleanly; callers wanting cross-platform code should branch onsys.platform. -
os.path.expanduser(no~concept),expandvars(would have to walkdos.library GetVar— separate phase if needed). -
os.walkfollow-symlinks /onerrorcallback. AmigaOS has no symlinks and the on-error case is rare enough to wrap in user code. - Full CPython
os.pathparity (commonpath,relpath,samefile,getmtime, ...) — easy to add later if a real call site needs them.
-
dos.librarySetProtection/Lock/Examine/UnLock(V36+).
ports/amiga/modosamiga.c — chmod + getprotect + FIBF_*
ports/amiga/modules/os.py — frozen extension (makedirs,
walk, _osamiga re-exports,
import _ospath as path)
ports/amiga/modules/_ospath.py — AmigaOS-aware path helpers
tests/ports/amiga/test_os_smoke.py — vamos smoke + RAM: round trip
docs/phase34-os-path-plan.md — step plan
Variants: all three. ~5 KB text per variant (1 KB C + 4 KB frozen Python bytecode).
amiga.icon is a C sub-module wrapping icon.library for full
.info file manipulation. Goes beyond the read-only
amiga.tooltype() we already had (which only looks up one
tooltype on the launched tool icon).
from amiga import icon
# Read a .info file (path without the .info suffix, AmigaOS convention)
dobj = icon.read("Work:Tools/Editor")
print(dobj.type) # 'tool' / 'project' / 'drawer' / ...
print(dobj.default_tool) # 'C:Ed' or similar for project icons
print(dobj.stack_size) # 8192
# Tooltypes are exposed as a dict-shaped accessor.
print(dobj.tooltypes["WINDOW"])
dobj.tooltypes["FONT"] = "topaz.font/8"
dobj.tooltypes["FLAG"] = None # bare flag tooltype (no '=')
del dobj.tooltypes["OLD_KEY"]
# Position on the parent drawer.
dobj.current_x, dobj.current_y = 16, 24
# Write back.
icon.write("Work:Tools/Editor", dobj)
# Create a fresh project icon from scratch.
new = icon.new(icon.WBPROJECT, default_tool="C:Ed",
tooltypes={"WINDOW": "CON:0/0/640/256/Title"})
icon.write("Work:Notes", new)
new.close()| Call | Returns | Notes |
|---|---|---|
icon.read(path) |
DiskObject |
GetDiskObject wrapper. OSError(ENOENT) if the .info doesn't exist. |
icon.write(path, dobj) |
None |
PutDiskObject. OSError(EIO) on failure. Doesn't refresh the parent Workbench window (out of scope — that's workbench.library's UpdateWorkbench). |
icon.new(type, **kwargs) |
DiskObject |
GetDefDiskObject(type) for the system default image. Kwargs default_tool / stack_size / current_x / current_y / tooltypes applied in one pass. OSError(EINVAL) if the type code isn't recognised. |
icon.WBDISK / WBDRAWER / WBTOOL / WBPROJECT / WBGARBAGE / WBDEVICE / WBKICK / WBAPPICON |
int | Raw do_Type values from <workbench/workbench.h>. |
icon.DiskObject |
type | Re-exported so isinstance(d, icon.DiskObject) works. |
DiskObject attributes:
| Attr | Type | R/W | Notes |
|---|---|---|---|
.type |
str |
R |
"disk" / "drawer" / "tool" / "project" / "garbage" / "device" / "kick" / "appicon". Falls back to the raw int for any future code. |
.default_tool |
str or None
|
R/W |
None clears (icon.library treats NULL/empty as "none"). |
.stack_size |
int |
R/W |
do_StackSize. |
.current_x, .current_y
|
int |
R/W | Icon position on the parent drawer. |
.tooltypes |
DiskObjectTooltypes |
R/W via methods | Dict-shaped mapping (see below). |
.close() / __del__
|
— | — | Releases the underlying allocation. Idempotent. |
DiskObjectTooltypes mapping methods:
| Op | Notes |
|---|---|
tt[k] |
Returns the value as bytes (empty b"" for flag-style entries). KeyError if absent. |
tt[k] = v |
v is str / bytes (writes "KEY=VALUE") or None (writes flag-style "KEY"). TypeError for anything else. |
del tt[k] |
KeyError if absent. |
k in tt / len(tt) / iter(tt)
|
Standard dict semantics; iteration yields keys as str. |
tt.keys() / .values() / .items() / .get(k, default)
|
List-returning helpers (no view objects). |
Ownership: a freshly-read DiskObject has icon.library-owned
do_DefaultTool / do_ToolTypes; the first mutation deep-copies
them into Python-owned AllocVec buffers so subsequent edits are
free. .close() swaps the original pointers back in before
FreeDiskObject so the library's teardown only sees memory it
allocated.
- Editing the icon's image data (
do_Gadget->GadgetRender/SelectRender) — that's a substantial surface (planar image munging, palette handling); separate phase if ever needed. - App icons (
AddAppIcon) — that'sworkbench.library, noticon.library. - NewIcon / GlowIcon / OS3.5+ ColorIcon extended IFF chunks —
the basic
do_Gadgetplanar icon is enough for round-tripping. - Workbench-window refresh after
icon.write— caller can drive that via ARexx (WORKBENCH UPDATE) if they need it.
-
icon.library(lazy open, same pattern as Phases 30 / 31 / 33). Already partially used internally foramiga.tooltype/amiga.wb_selected_filesvia the cached DiskObject in main.c — Phase 35 exposes the broader surface.
ports/amiga/modicon.c — C module + DiskObject type
ports/amiga/modules/amiga.py — `import _icon as icon`
tests/ports/amiga/test_icon_smoke.py — vamos arg-shape + RAM: round trip
docs/phase35-icon-plan.md — step plan
Variants: all three. ~5 KB text per variant (DiskObject + tooltype mapping protocol + write + new).
amiga.catalog wraps locale.library's catalog lookup so
localized apps can read their translated strings, plus surfaces
the system's preferred language.
from amiga import catalog
print(catalog.language()) # 'english' / 'german' / 'français'
# Stock-Workbench tip: pass built_in_language= so locale.library
# actually loads a translation file rather than short-circuiting
# on "you already have English built in".
with catalog.open("MyApp.catalog", version=1,
language="english",
built_in_language="german") as cat:
print(cat.lookup(1, "Default English string"))
print(cat.lookup(2, "Cancel"))| Call | Returns | Notes |
|---|---|---|
catalog.open(name, version=0, language=None, built_in_language=None) |
Catalog |
OpenCatalogA(NULL, name, [OC_Version, OC_BuiltInLanguage?, OC_Language?, TAG_DONE]). OSError(ENOENT) if locale.library returns NULL (catalog not found, or language matches built_in_language so nothing to load). OSError(EIO) if locale.library itself can't open. |
catalog.language() |
str |
First preferred language from Locale->loc_PrefLanguages[0]. Returns "english" if no preference is set or locale.library is unavailable. |
catalog.Catalog |
type | Re-exported so isinstance(c, catalog.Catalog) works. |
Catalog methods:
| Op | Notes |
|---|---|
cat.lookup(id, default) |
GetCatalogStr(cat, id, default). Returns the catalog string or the default if id is absent. Defaults to AmigaOS contract: never raises on a missing entry. A closed catalog also returns the default (the NULL is forwarded straight into GetCatalogStr). |
cat.close() / __del__
|
CloseCatalog. Idempotent. |
with catalog.open(...) as cat: |
__enter__ / __exit__ close the catalog on block exit. |
built_in_language= kwarg note: AmigaOS OpenCatalog short-circuits
when the requested language matches the catalog's built-in
language — there's nothing to load, so it returns NULL. To force a
translation lookup even when asking for the "same" language as the
binary's built-in strings (e.g. when probing English-only catalogs
on an English Workbench), pass a different code via
built_in_language. The plan-doc walk-through has an example.
- Writing catalogs (
flexcat-style compilation). That's a build- time tool, not a runtime API. - Conversion of locale-specific date / number / currency formats
— separate phase if needed; would wrap
FormatStringetc. - Multi-catalog merging / fallback chains —
OpenCatalogalready picks the right language automatically. - Exposing
Localedirectly.language()covers the one field callers actually want; the rest ofstruct Localeis a read-only system snapshot that would invite lifetime confusion.
-
locale.libraryv38+ (OS 2.1). Lazy open; no explicit close.
ports/amiga/modcatalog.c — C module + Catalog type
ports/amiga/modules/amiga.py — `import _catalog as catalog`
tests/ports/amiga/test_catalog_smoke.py — vamos arg-shape smoke
docs/phase36-catalog-plan.md — step plan
Variants: all three. ~1 KB text per variant.
Wrap datatypes.library (V39+) for universal file recognition.
datatypes.library is AmigaOS's plug-in registry of file-format
handlers (.datatype modules in SYS:Classes/DataTypes/); any
running system knows about every datatype that's been installed
on it.
from amiga import datatypes
# "What is this file?" — works for everything the system has a
# datatype for: PNG/JPEG/GIF/IFF ILBM, 8SVX/AIFF/WAV, ASCII/IFF FTXT,
# anim, etc.
info = datatypes.recognize("Work:Photos/holiday.jpg")
# {'group': 'picture', 'type': 'jpeg', 'name': 'JPEG (JFIF)'}
# Slightly more: full DTA_* attribute dump.
attrs = datatypes.info("Work:Photos/holiday.jpg")
# {'BaseName': 'JPEG', 'GroupID': 'pict', 'Width': 1280, 'Height': 720, ...}-
amiga.datatypes.recognize(path)→ dict withgroup,type,name. UsesObtainDataTypeA(DTST_FILE, lock, NULL)thenGetDTAttrs(DTA_BaseName, DTA_GroupID, DTA_Name), releases viaReleaseDataType. -
amiga.datatypes.info(path)→ dict. Same asrecognizebut also queries common attrs (DTA_NominalHoriz/_NominalVertfor pictures,DTA_Duration/_SampleLengthfor sound, etc.) so callers can do quick metadata reads withoutNewDTObject. -
amiga.datatypes.groups()→ list of installed group IDs (one of"sound"/"picture"/"text"/"anim"/"system"/"document"). Iterates thedtl_*chain inDataTypesList.
-
NewDTObjectA/DoDTMethodA(DTM_DRAW)rendering — requires aRastPortand a targetBitMap, plus colour-map handling for CLUT formats. That's a much larger surface (graphics.library BitMap wrappers + intuition Screen/Window plumbing) and the payoff for a MicroPython workload is narrow. - Save-as-other-format (
DTM_WRITE) — same render-pipeline problem in reverse. - Audio playback (
DTM_TRIGGERon sound datatypes) — needs AHI on modern hosts. - BOOPSI object exposure —
NewDTObjectreturns anObject *that callers normally drive viaSetDTAttrs/GetDTAttrs/DoDTMethod. Surfacing that as a Python type is a whole separate phase (one BOOPSI superclass per Python class).
-
datatypes.libraryv39+. Lazy open. The library is V39+ (OS 3.0) baseline — earlier Kickstarts simply skip the import (we already require V37 for things likelib_call, so V39 is a small bump but worth a cleanOSError(MP_ENOSYS)if missing).
ports/amiga/moddatatypes.c — C module
ports/amiga/modules/amiga.py — adds `import _datatypes as datatypes`
tests/ports/amiga/test_datatypes_smoke.py — vamos arg-shape smoke
docs/phase37-datatypes-plan.md — step plan (TBD)
Variants: all three. ~2 KB text per variant.
recognize / info is genuinely useful but rarely on the critical
path for a MicroPython script — file-type sniffing is more often
done by reading the first few bytes (b"\x89PNG", b"\xff\xd8").
The big win is universal sniffing across formats the system has
been taught about (third-party PNG / JPEG / WebP .datatypes), and
that lives or dies by which datatypes are installed on the target
host. Land Phase 35 / 36 first; pick up Phase 37 if a real caller
shows up.
A ports/amiga/README.md that matches the style of the other
port READMEs (rp2, stm32, esp32, unix, ...) and is suitable for
merging into upstream. The existing docs/amiga.md is a phase log
plus design notes -- valuable for in-port iteration, not the right
shape for a casual visitor opening ports/amiga/.
Per the upstream-README convention:
-
Heading —
MicroPython port to AmigaOS 3.xwith an====underline. - Intro paragraph — what the port is (AmigaOS 3.x / 3.1+, 68020+, bebbo gcc 6.5 toolchain) and how to think about it (it produces a regular AmigaDOS executable; runs from Shell or Workbench).
-
Supported features — bullet list covering REPL line
editing, frozen modules, 68k native code emitter, dynamic heap,
Pythonic VFS file I/O, AmigaOS APIs (
amiga.library, ARexx, Intuition / ASL / icon / catalog wrappers, env-var integration, volume / assign / pattern primitives, persistent REPL history), networking (bsdsocket.library), TLS (AmiSSL v5),urequests. -
Build instructions — bebbo install pointer, mpy-cross
prerequisite, the three variants (
standard,68020fpu,68040), CI parity viatools/amiga-build.sh. - Running / accessing — drop the binary on a real Amiga or Amiberry image, invoke from Shell. Brief Workbench-launch note.
-
Testing — pointer to vamos / Amiberry harness with
one-paragraph summaries; full runbook stays in
docs/amiga-testing.md. -
Further reading — links to
docs/amiga.md(design and phase status) anddocs/amiga-testing.md.
Target length is ~150–200 lines (matches stm32's 176, esp8266's 251 -- not as short as rp2's 109, because the AmigaOS surface is broader).
- Phase-by-phase history (lives in
docs/amiga.md). - Detailed test runbook / known-good failures
(lives in
docs/amiga-testing.md). - Architecture deep-dives (the trampoline, FD parser, native emitter internals).
- Build matrix per board — there's only one board (any AmigaOS
3.x machine), so no
boards/subdirectory.
ports/amiga/README.md — the README itself
docs/phase38-readme-plan.md — step plan (TBD)
No variant-size impact (documentation only).
Shipped ports/amiga/README.md (233 lines, within the 150-200
target ±15% once tables and the icon / tooltype reference grew).
Sections, in order:
- Heading + intro paragraph (AmigaOS 3.x / V37+, 68020+, bebbo gcc, produces a regular AmigaDOS executable).
-
Supported features — bullet list grouped by area (REPL,
native emitter, frozen modules, dynamic heap, VFS file I/O
with the standard
ossurface,os.path,amiga.*API wrappers — library proxy, intuition, asl, icon, catalog — ARexx in/out, volume/assign/match, env-vars,timer.devicetime,socket, AmiSSL,urequests,platform). -
Build variants — table covering
standard,68020fpu,68040, all three TLS-enabled. -
Building —
make -C mpy-crossprereq, bebbo install pointer,make,VARIANT=selection,tools/amiga-build.shDocker mirror, AmiSSL SDK note. -
Deploying —
C:install,protect rwed, Workbench icons (micropython.info,python_script.info), tooltype reference (SCRIPT=/HEAP=/MAXHEAP=/CON=), emulation note. -
Running — Shell invocation, script-as-arg,
PROGDIR:onsys.path, WorkbenchSCRIPT=tooltype. - Testing — vamos + Amiberry one-paragraph each, pointer to the testing wiki page for the full runbook.
-
Limitations — 68k NLR (
try/exceptin@micropython.native), viper single-register-local cap, AmiSSL runtime requirement, 68000 not supported. - Further reading — links to the design and testing wiki pages.
Verified against the live phase status table: no planned-phase
features leaked into the "Supported features" list. Phase 12
(68k NLR rework), Phase 37 (amiga.datatypes), and Phase 39
(extmod opt-ins like time.time(), hashlib.md5/sha1, btree,
deflate compress, marshal, websocket/webrepl) all stay
off the README — listed under "Limitations" where relevant or
simply absent.
Verified against the upstream port-README shape: each port has
its own section layout reflecting its platform (unix is
shortest at "Building" only; esp32 longest with hardware-setup
sections). The Amiga port's Building / Deploying / Running / Testing / Limitations / Further reading shape stays in keeping
with the convention; "Deploying" carries the Amiga-specific
icon / tooltype / emulation content that doesn't fit elsewhere.
Verified link resolution: all wiki page links and the bebbo gcc external link return HTTP 200.
Deviations from the original step plan (call-outs for the upstream PR moment):
- The original plan instructed "no
sidick/micropythonreferences" in the README. As of the docs migration (the design log and testing runbook moved off-tree to the fork wiki), the "Testing" and "Further reading" sections carry fork-specific URLs (github.com/sidick/micropython/wiki/...). These will need rewriting at the upstream PR moment to whatever the upstream wiki location becomes (or to a generic "the project wiki" reference if a permanent home isn't established). - The original plan listed
docs/amiga.mdanddocs/amiga-testing.mdas the "Further reading" targets. Those files no longer exist in-tree; the wiki pages are the canonical home now.
Audit of extmod/ against the current Amiga port config turned up a
handful of cross-port modules and feature flags that aren't currently
enabled but would plausibly pay off on AmigaOS. This phase is a menu;
which items actually land is to be decided when the phase is picked
up. Each candidate below carries a rough cost/benefit so the choice
can be made in context.
The current baseline (via MICROPY_CONFIG_ROM_LEVEL_EXTRA_FEATURES
plus the port-local opt-ins) already includes binascii, hashlib
(SHA-256 only), heapq, random, re, deflate decompress,
uctypes, cmath, framebuf, errno, select, plus port-local
json, socket, ssl, and VfsAmiga. The candidates here are the
gaps left by that baseline that look genuinely useful on Amiga (i.e.
not GPIO / radio / micro-flash territory).
-
Wall-clock
timesurface —time.time(),time.time_ns(),time.gmtime(),time.localtime(),time.mktime(). Currently thetimemodule only exposesticks_*andsleep*(defaults forMICROPY_PY_TIME_GMTIME_LOCALTIME_MKTIMEandMICROPY_PY_TIME_TIME_TIME_NSare both 0 and the port hasn't overridden them). Backing:dos.library/DateStamp()(day + minute- tick since 1978-01-01) for the wallclock, plus optionally
battclock.resourcefor sub-minute granularity on machines that have a battery-backed clock. Implementation lives in a port-localMICROPY_PY_TIME_INCLUDEFILE. Probably the highest-impact item on the list — a lot of stdlib-flavoured Python assumestime.time()exists. ~150 lines of glue. The Amiga epoch offset is2922 * 86400seconds (8 years from 1970-01-01 to 1978-01-01); locale handling stays minimal (UTC;locale.libraryintegration is out of scope).
- tick since 1978-01-01) for the wallclock, plus optionally
-
hashlib.md5+hashlib.sha1— currently gated on upstreamMICROPY_PY_SSL(which this port doesn't set; it usesMICROPY_PY_AMIGA_SSLinstead), so both digests are off despite the C sources being compiled in for free withhashlib. One-line opt-in via#define MICROPY_PY_HASHLIB_MD5 (1)andMICROPY_PY_HASHLIB_SHA1 (1). Cheap and routinely needed for file checksums, HTTP digest auth, and content-addressed lookups. -
btree(extmod/modbtree.c+ the bundledlib/berkeley-db-1.xxsubmodule, already a git submodule of the repo). Pure C, no networking, no GPIO — just a persistent K/V store over the file API. Gives Amiga scripts a real on-disk dict without writing flat-file plumbing. ~30 KB binary cost. Theberkeley-dbsubmodule must be initialised (it isn't pulled by default on a fresh clone). Pairs naturally withVfsAmiga. -
MICROPY_PY_DEFLATE_COMPRESS— extends the existingdeflatemodule from decompress-only to a full read/write surface (gzip / zlib output). Single-flag opt-in. Useful for log compression, network payload packing, and.tar.gz/.zipgeneration paired withbinascii.crc32(already on). -
marshal— bytecode (de)serialisation. Disabled belowROM_LEVEL_EVERYTHING. Cheap to enable; only useful if a use case shows up for serialising code objects (tooling,.mpyintrospection beyond whatmpy-toolalready covers). Optional / low priority. -
websocket+webrepl(extmod/modwebsocket.c,extmod/modwebrepl.c). Layers on the existing port-local socket module to give a REPL over the network — a meaningful quality-of-life win on AmigaOS where the local console is the classic Shell window. Caveats: webrepl wants aduptermhook and theMICROPY_ENABLE_SCHEDULER, which this port has deferred along withasyncio(seeMICROPY_PY_ASYNCIO (0)andMICROPY_ENABLE_SCHEDULER (0)inmpconfigport.h). So this item is the most work of the lot — likely needs a small cooperative poll-from-VM-hook shim rather than a real scheduler. Defer until after theasynciostory is resolved.
-
asyncio— already deferred port-wide pending threads / scheduler. -
bluetooth,lwip,cyw43,wiznet5k,ninaw10,esp_hosted,nimble,btstack,network_ppp_lwip— hardware-specific (radio / Ethernet MAC drivers) with no Amiga fit. Networking on this port isbsdsocket.library-only and that's already wired. -
machine_*(adc,pwm,i2c,spi,uart,timer,wdt,usb_device,i2s,can,pulse,bitstream,signal,pinbase) — no Amiga peripheral equivalent. -
onewire— GPIO-based. -
vfs_fat,vfs_lfs,vfs_posix,vfs_rom,vfs_blockdev—VfsAmigaalready covers the filesystem layer viados.library. Mounting FAT/LFS images would be a separate, large project. -
modopenamp— multi-core (M4/M7) RPMsg. N/A. -
modplatform— platform-identification strings. Already covered byplatform.amiga_info()(Phase 33).
Which subset to actually implement is decided when the phase is picked up. Likely landing order if all are taken:
- Wall-clock
time(biggest API-surface gain). -
hashlibMD5 + SHA-1 (one-line, lots of use cases). -
MICROPY_PY_DEFLATE_COMPRESS(one-line). -
btree(medium effort, niche-but-real use case). -
marshal(optional). -
websocket+webrepl(waits on a scheduler-lite story; arguably its own phase rather than part of this one).
Each item lands as a discrete commit so a partial Phase 39 can stop cleanly between any two.
ports/amiga/mpconfigport.h — feature-flag flips
ports/amiga/modtime.c (new) — for item 1 (time wallclock)
ports/amiga/Makefile — SRC_C / SRC_QSTR additions
tests/ports/amiga/test_phase39_*.py — per-item smoke tests
docs/phase39-extmod-opt-ins-plan.md — step plan (TBD)
Variants: all three. Cumulative size impact depends on which items
land — rough upper bound if everything except webrepl is taken is
~40 KB text (btree dominates).
Shipped:
-
time.time()andtime.time_ns(), backed bytimer.deviceGetSysTime()(already opened at startup byamiga_timer.cfor ticks_/delay_; no additional resource open). -
time.gmtime()/time.localtime()/time.mktime()viaextmod/modtime.c's timeutils path (MICROPY_EPOCH_IS_1970selected to match CPython; the 1978-1970 offset of2922 * 86400= 252,460,800 seconds is applied inports/amiga/modtime.c).
battclock.resource was considered and rejected: the AmigaOS
system clock is synchronised from battclock at boot and tracks any
user Date edits thereafter, so GetSysTime() is the authoritative
runtime source. Reading battclock directly during runtime would
race the boot synchronisation and skip the user's edits.
There is no timezone database on AmigaOS, so time.gmtime(t) and
time.localtime(t) return the same broken-down tuple for the same
t. Whatever the user has set their AmigaDOS clock to (local time
or UTC) is reflected directly; the port doesn't try to add a
timezone-offset layer.
vamos doesn't emulate GetSysTime (the call returns d0=0 with
the destination buffer untouched), so the smoke test
test_time_wallclock_smoke.py gates the "is this a real recent
wall-clock?" assertions behind a runtime detection and only
verifies static surface coverage (gmtime(0), gmtime(amiga_epoch),
mktime round-trip, time.time() / time_ns() return-type) on
that host. Full wall-clock validation lands on Amiberry / real
hardware.
Variants: all three (no FPU sensitivity). Build sizes:
| Variant | text |
|---|---|
standard |
586,660 bytes |
68020fpu |
557,244 bytes |
68040 |
567,828 bytes |
(Size delta from Phase 38: ~6-8 KB across variants, dominated by
shared/timeutils/timeutils.c which now compiles into the port for
the first time, plus the conditional gmtime/mktime/time/time_ns
helpers in extmod's modtime.c.)
Shipped:
-
hashlib.md5andhashlib.sha1are now in the module alongsidehashlib.sha256. All three follow the standard upstream API: one-shothashlib.foo(b"..."), streamingupdate(...)/digest(), and a final"hash is final"ValueErrorafter the firstdigest().
The upstream extmod/modhashlib.c only implements MD5 / SHA-1 via
MICROPY_SSL_MBEDTLS or MICROPY_SSL_AXTLS. We don't ship mbedtls
and AmiSSL is an OpenSSL-shaped runtime library (wrong API for both
backends), so the port routes through the axtls code path:
-
mpconfigport.hflipsMICROPY_SSL_AXTLS=1,MICROPY_PY_HASHLIB_MD5=1,MICROPY_PY_HASHLIB_SHA1=1. -
MICROPY_PY_SSLstays0, which keepsmodtls_axtls.candmodcryptolib.cdormant — only the algorithm sources are linked, not the rest of axTLS (notls1, nox509, nobigint...). -
ports/amiga/Makefileaddslib/axtls/crypto/md5.candlib/axtls/crypto/sha1.cdirectly toSRC_C, along with-Iflags forlib/axtls/ssl,lib/axtls/crypto, andextmod/axtls-include. A per-fileCFLAGSoverride silences axtls's older-Wall-noisy style (mirrorsextmod.mk's rule). - A minimal
ports/amiga/axtls-include/axtls_os_port.hshadows the upstream shim. The upstream copy pullsarpa/inet.h(for tls1'sSOCKET_READmacro), which on AmigaOS drags inexec/types.hvianetinet/in.h → sys/socket.h → devices/timer.h. That collides withlib/crypto-algorithms/sha256.h'sBYTE/WORDtypedefs (AmigaOS defines them as signedint8_t/int16_t; crypto- algorithms defines them asunsigned char/unsigned int). The port-local shim provides just#include <stdint.h>— enough for md5.c / sha1.c, and they don't use any of the upstream shim's socket / SHA-256-alias macros anyway.
tests/ports/amiga/test_hashlib_smoke.py pins each algorithm
against canonical vectors:
- MD5: RFC 1321 (
"","abc","message digest"). - SHA-1: FIPS 180-1 (
"","abc", the 56-byte"abcdbcdecdefdefg..."vector). - SHA-256: FIPS 180-2 (
"","abc").
Also covers streaming (split update(...) chains must equal the
one-shot digest) and the final-state contract (update / digest
after digest() must raise ValueError).
Side note from the test-vector verification: an early version of the test had a transcribed-wrong SHA-256(b"abc") expected value, which made the Amiga output look 4-bit-shifted vs the (typo'd) expected. SHA-256 was always producing the correct FIPS digest — confirmed by running the same input through host CPython. No SHA-256 bug exists.
Variants: all three. Build sizes:
| Variant | text | Δ vs Step 1 |
|---|---|---|
standard |
514,324 bytes | ~+3 KB |
68020fpu |
488,204 bytes | ~+3 KB |
68040 |
497,952 bytes | +2,764 B |
(Δ is the cost of md5.c + sha1.c plus the additional dispatch
in modhashlib.c. The 68040 baseline was re-measured against
HEAD~1 for the delta column; the other two are within a few
hundred bytes of the same figure.)
Shipped:
-
deflate.DeflateIO(stream, format)now writes as well as reads for all three formats (deflate.RAW,deflate.ZLIB,deflate.GZIP), so scripts can both decompress and compress. - The decompress half has been on since the port joined
EXTRA_FEATURES; Step 3 just flipsMICROPY_PY_DEFLATE_COMPRESSto1inmpconfigport.h. Single-line change.
No Makefile work was needed: extmod/moddeflate.c #includes
lib/uzlib/lz77.c (which in turn #includes defl_static.c)
under #if MICROPY_PY_DEFLATE_COMPRESS, so the compressor source
is already pulled into the existing moddeflate.o translation
unit.
Test coverage in tests/ports/amiga/test_deflate_compress_smoke.py:
- Compress→decompress round-trip for RAW, ZLIB, and GZIP against a repetitive payload, asserting both bytes-out < bytes-in and exact match after decode.
- Streaming write split across multiple
write(...)calls (including a zero-length write) yields the same compressed bytes as a single concatenated write. -
write(...)on a closedDeflateIOraisesOSError. - Documents the uzlib quirk that compressing an empty input
produces an empty byte stream rather than a header-only frame.
(CPython's
zlibemits a 2-byte zlib header + 4-byte Adler32 trailer; uzlib only emits anything if data has actually been written. The test pins the current behaviour rather than treating it as a bug — anyone needing CPython-compatible framing can prepend the header by hand.)
Variants: all three. Build sizes:
| Variant | text | Δ vs Step 2 |
|---|---|---|
standard |
515,976 bytes | +1,652 B |
68020fpu |
489,876 bytes | +1,672 B |
68040 |
499,648 bytes | +1,696 B |
(Δ is the incremental cost of lz77.c + defl_static.c once they
compile into moddeflate.o.)
Shipped:
-
import btreeopens a B-tree-on-stream key/value store viabtree.open(stream, ...). Keys and values are arbitrary bytes (anything implementing the buffer protocol works for both); the store layers on top of whatever stream the caller provides, so it works equally well against anio.BytesIO, aVfsAmigafile handle (open(path, "w+b")), or any other MicroPython stream. - Supports the standard dict-ish surface:
db[key]/db[key] = val/del db[key]/key in db,db.get(k[, default]),db.put(k, v), plusdb.keys()/db.values()/db.items()(withstart,end, andbtree.INCL/btree.DESCflags), anddb.flush()/db.close().
Wiring in this port:
-
ports/amiga/MakefilesetsMICROPY_PY_BTREE = 1as a make variable before includingextmod.mk. That make-side flag is what triggersextmod.mkto pull inlib/berkeley-db-1.xx/btree/*.c+mpool/mpool.cand add the matching-Ipath. The corresponding C macro is set automatically byextmod.mkviaCFLAGS_EXTMOD += -DMICROPY_PY_BTREE=1. -
ports/amiga/mpconfigport.hflipsMICROPY_STREAMS_POSIX_API = 1so themp_stream_posix_{read,write,lseek,fsync}shims (declared inpy/stream.h, defined inpy/stream.c) are visible toextmod/modbtree.c. Those shims bridge berkeley-db'sread(fd, buf, n)/write(fd, buf, n)/lseek(fd, ...)calls onto a Python stream object'sMP_STREAM_OP_*methods. - No port-local Berkeley DB config file is needed -- the upstream
extmod/berkeley-db/berkeley_db_config_port.hbuilds cleanly against AmigaOS clib2, which providessys/file.h,sys/param.h,sys/stat.h,fcntl.h, etc. with the expected signatures.
(Going through the stream protocol rather than direct file I/O is
also what lets the test exercise the whole module against an
in-memory io.BytesIO without touching the filesystem.)
Test coverage in tests/ports/amiga/test_btree_smoke.py:
-
open()/put/__getitem__/__contains__/__delitem__, including theKeyErrorpaths for missing keys. - Buffer-protocol arguments (memoryview slices used for both key and value).
-
keys(),items(),items(start, end),items(None, None, btree.DESC), plainfor k in db-- all asserted against the sorted key list. (Note: btree shares a single iteration cursor across all calls on the same DB object, so you can't runkeys()andvalues()concurrently viazip(). The test cross-checks values viadb[k]lookup instead.) -
flush()+close(), plus the "use after close" guard which raises eitherOSErrororValueError("database closed")depending on the call site (currentlyValueError, but the test accepts both for forward compatibility).
Variants: all three. Build sizes:
| Variant | text | Δ vs Step 3 |
|---|---|---|
standard |
535,292 bytes | +19,316 B |
68020fpu |
509,192 bytes | +19,316 B |
68040 |
519,152 bytes | +19,504 B |
(Δ is dominated by the 13 berkeley-db source files
[bt_*.c + mpool.c] plus modbtree.c itself, against
the MICROPY_STREAMS_POSIX_API shim weight.)
- Step 5:
marshal(optional). - Step 6:
websocket+webrepl(defer until scheduler-lite story resolved — likely promoted to its own phase).
| Issue | Status | Fix |
|---|---|---|
try/except in @micropython.native crashes |
Known bug | Needs a 68k assembly NLR (nlr68k.S) saving D2–D7/A2–A5 in nlr_buf_t
|
@micropython.viper limited to 1 register local (D7) |
MAX_REGS_FOR_LOCAL_VARS = 1 |
68k-specific viper register allocator, or accept stack-based locals |
amiga.assigns() resolved-target buffer is 256 bytes (modamiga.c line 265), tight for deeply-nested assigns on long-name filesystems |
Surfaced during Phase 31 buffer audit | Bump to 1024 if a real-world case hits it; assign targets are usually short. |
REPL history file path is 256 bytes (amiga_history.c AMIGA_HISTORY_PATH_MAX), tight for unusual install locations |
Surfaced during Phase 31 buffer audit | Bump to 1024; history file lives in ENVARC: by convention so unlikely to hit. |
Three paths, each for a different stage:
-
vamos (host, headless) — day-to-day iteration; widest automated
coverage via
tests/run-tests.pydriven throughtools/amiga-vamos-run.sh. Current snapshot under thestandardvariant: ~722 pass / ~256 self-skip / ~75 fail out of 1053 files, of which only 4 are real platform differences (m68k struct alignment, long-double precision); the rest are Phase-12 native/viper gaps, Unix-port-specific cmdline tests, or vamos emulation gaps. -
Amiberry (full emulation) — anything needing Kickstart: Workbench
launch,
icon.library, 68881 FPU (the68020fpuvariant), persistent REPL history, real AmiSSL. On-device runnertools/amiga-runtests.pywalks a test dir and diffs against.expfiles pre-generated on the host viatools/amiga-gen-exp.py. -
CI —
.github/workflows/ports_amiga.yml(Phase 11). Cross-compile gate; produces release-style artefacts. No test execution.
Full runbook — install paths, variant selection, exclude lists, suite snapshot, known-good failures, soft-float library bugs, on-device runner setup — lives on the Amiga port testing wiki page.