-
Notifications
You must be signed in to change notification settings - Fork 0
Amiga port design
This page is the index and architecture reference for the AmigaOS 3.x
MicroPython port. The per-phase implementation log has been split into
individual pages — each phase in the status table below links to its own
page. 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.
Each phase links to its detail page. The small foundational phases are grouped onto two era pages (Phases 0–11 — Foundation, Phases 13–27 — Platform integration); the substantial phases each have their own page.
- 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.
Register-based NLR: py/nlr.h auto-detects __m68k__ → MICROPY_NLR_M68K,
implemented in ports/amiga/nlr68k.S + nlr68k_jump.c (Phase 12a). This
replaced the original setjmp fallback, which was incompatible with the native
emitter's try/except/with handling — see
Phase 12.
Phase 14 manages a dynamic, growable heap of AllocVec'd chunks. See
Phase 14. 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}/
Speculative work that isn't yet a phase. Each entry is a sized cost/benefit sketch so the call can be made later in context.
ClassAct is the BOOPSI-based GUI toolkit that shipped on
Aminet in the mid-90s and was later acquired and folded into
the OS as ReAction (AmigaOS 3.5 / 3.9 / 4.x). Same class
hierarchy, same tag-driven object model, just rebranded and
bundled in SYS:Classes/. On a stock OS 3.5+ install — and on
every modern AmigaOS 4.x and MorphOS system — it is the
default toolkit for native windowed UIs (the post-OS-3.0
counterpart to raw intuition.library gadget lists and the
MUI alternative on systems without MUI installed).
The port currently exposes only modal Intuition requesters (Phase 30) and the ASL file requester (Phase 31). A real windowed UI surface — list browsers, button panels, layout groups, draggable windows that survive a Workbench depth-arrange — would need either ClassAct/ReAction or MUI. ClassAct is the more attractive of the two for "ships with the OS" reasons.
What it would expose (sketch):
from amiga import reaction as ra # or amiga.classact — same lib
win = ra.Window(
title="Hello",
inner=ra.Layout(orientation="vertical", children=[
ra.Button(label="OK", id=1),
ra.Button(label="Quit", id=2),
]),
)
win.open()
for msg in win.events(): # IDCMP pump
if msg.id == 2:
break
win.close()Underlying ABI: NewObject(class, NULL, tag, value, ..., TAG_END)
returning a BOOPSI Object *, then SetAttrs / GetAttr /
DoMethod against it. Window objects expose an IDCMP signal
mask that integrates with the existing Wait() loop the REPL
already uses for Ctrl+C and timer.device.
Cost estimate:
- ~30 BOOPSI classes worth wrapping for a useful subset
(
window.class,layout.gadget,button.gadget,string.gadget,checkbox.gadget,chooser.gadget,listbrowser.gadget,label.image,bitmap.image,getfile.gadget/getfont.gadget/getscreenmode.gadget, plus a handful of menu / scroller / palette classes). - Each class is a
NewObject(class_lib->cl, ...)open plus a tag-table translator (Python kwargs →struct TagItem[]). The Phase 17tag.pyinfrastructure already does the hard half of this — adding a class is roughly "open the library, add its tag IDs to the catalogue, write a thin Python wrapper". Call it ~200 lines per class for a polished surface, ~50 if we go barebones tag-passthrough. - Event loop:
DoMethod(win, WM_HANDLEINPUT, &result)returns a 32-bit code whose meaning is class-defined. Workable but needs a Python-side dispatcher table. - Layout engine:
layout.gadgetdoes the sizing for us, so we don't need to reimplement Intuition's brutal hand-positioning story. Big win. - Binary cost: the classes themselves are external
.classfiles inSYS:Classes/Gadgets/etc., so the port-side binary is just glue — probably 8–15 KB text for a useful subset.
Open questions:
-
OS version floor. ClassAct shipped standalone for OS 3.0+;
ReAction is OS 3.5+. The port currently targets V37 (OS 2.04)
as the floor for most features. A ClassAct binding would
either bump the floor for the GUI surface specifically (lazy
open,
OSError(MP_ENOSYS)on older Kickstarts) or require shipping the ClassAct.classfiles alongside the port. The former is almost certainly the right call. -
AmigaOS 4 / MorphOS divergence. OS4 ships ReAction with
subtle tag-name differences from OS3 ClassAct
(
WINDOW_PositionvsWA_Left/WA_Topetc.). Need to confirm tag IDs are identical or paper over per-OS via the tag catalogue. -
Event loop integration. Right now the REPL's
Wait()loop only knows about the console signal mask, Ctrl+C, and timer.device. A ClassAct window adds its own signal bit (win.sigmask) and would benefit from being multiplexed into the sameWait()so the REPL doesn't busy-poll. Plumbing this cleanly is what the deferred scheduler-lite story is partly about (see Phase 39 step 6webrepl); the two share a need. - MUI overlap. A subset of the Amiga community runs MUI as their preferred toolkit, with ClassAct/ReAction as fallback. If we ship a ClassAct binding, MUI users get a working GUI surface either way (ClassAct/ReAction installs cleanly on MUI systems). The reverse isn't true. So ClassAct first is the right ordering even for MUI shops.
-
Required vs. headline classes. A
Hello worldwindow needswindow.class+layout.gadget+button.gadgetat minimum; that's the "is this even tractable" milestone. Everything else is incremental.
Verdict: likely a high-value phase if a real caller shows
up (Amiga-side tooling that wants a tidier UI than ASL +
EasyRequest), but it's a multi-step effort — probably split as
"Phase 4x — amiga.reaction BOOPSI plumbing + 3-class
proof-of-life" then "Phase 4y — polished class coverage". Worth
a follow-up investigation pass to confirm tag-ID parity between
OS3 ClassAct and OS4 ReAction (item 2 above) before
committing — that's the single biggest risk to the binding
working unchanged across the supported OS range.
The port already mechanically harvests constants from the NDK
for the built-in libraries via three host-side tools:
amiga-fdgen.py (LVO + register tables from .fd files),
amiga-taggen.py (resolves TAG_USER + N tag macros), and
amiga-structgen.py (struct layouts via sizeof() /
offsetof() against a whitelist). The output is baked into
the frozen modules at build time — _amiga_libs.py,
_amiga_tags.py, _amiga_structs.py.
That covers NDK-shipped libraries (intuition, dos, asl, ...)
but leaves a real gap: anyone calling a third-party library
(thirdparty.library, MUI, AHI, cybergraphics.library,
identify.library, the various Aminet .library releases,
the user's own .library) has to either:
- Hand-translate the C header to Python literals — brittle, error-prone, no machine-checkable link back to the source.
- Drop magic numbers inline in their script — the same problem one fork further down.
- Ship a hand-written shim alongside the library — works, but every distributor reinvents the wheel.
Proposed tool: amiga-headergen (working title) — a
host-side script that takes a path to a SDK:Include/ (or
equivalent) tree plus a list of .h files to ingest, and emits
a single .py file the caller drops into PROGDIR: (or
anywhere on sys.path). The emitted module exposes:
# AmiSSL example — pretend this isn't already wired into the port.
from openssl_consts import (
SSL_VERIFY_PEER,
SSL_OP_NO_SSLv3,
OpenSSLBase_LVO_SSL_new, # LVO offset, ready for amiga.lib_call
SSL_CTX_struct, # offsetof / sizeof dict, structgen shape
OPENSSL_TAG_BASE, # tag-base value if the library uses TAG_USER+N
)How the tool would work:
- Run the bebbo cross-
cppover a generated probe.cto resolve#definechains to literal values, the same trickamiga-taggen.pyalready uses forTAG_USER + N. - For
#define FOO 0x1234constants: emitFOO = 0x1234directly. Skip#definemacros with arguments (those are function-shaped and not portable to Python without a wrapper — surface them as# FOO: function-like macro, skippedso the user knows). - For
structdefinitions found in the header: reuse theamiga-structgen.pyoffsetof()/sizeof()probe to produce(name, type_code, offset)tuples consumable byamiga.uctypes_lite/ Phase 17 struct-ctypes. Inline fixed-byte char arrays harvest their length viasizeof(((X*)0)->field)just like structgen does today. - For
.fdfiles paired with the header: pull LVO + register table via the same engine asamiga-fdgen.pyand emitLIBNAME_LVO_Funcplus a(reg_map, signature)tuple per function. Caller passes those straight toamiga.lib_call. - For tag bases (
#define LIB_TagBase (TAG_USER + 0x80000)), resolve and emit as plain ints. - Emit a header block at the top of the
.pylisting the source headers, their last-modified mtimes, and the tool version — so a stale generated file is detectable at a glance. - Optional
--manifestmode that writes a frozen-module manifest entry instead of aPROGDIR:drop-in, for users baking the constants into a custom MicroPython build.
Why this is tractable:
- All three engines already exist (
-fdgen,-taggen,-structgen) and use the same bebbo-cross-gcc plumbing. The new tool is a thin orchestrator that picks which engine(s) to apply per input header and emits a single combined.py. - The trickiest bit — turning C preprocessor output into
Python literals — is already solved by
amiga-taggen.py. - Output is a plain Python file; no MicroPython-specific
runtime support needed. It "just works" with the existing
amiga.library/amiga.lib_callsurface.
Open questions:
-
Anonymous unions / bitfields. C structs with anonymous
unions or bitfields can't be cleanly represented as
(name, offset)tuples. Need a strategy: drop them with a comment, or invent a Python-side BIT_FIELD marker. The NDK structgen whitelist sidesteps this by hand-picking safe structs; a third-party tool can't. -
#includeresolution. Real-world headers#includeeach other across SDK subdirs. The tool needs an-I sdk/Include/flag and a way to not drag the whole transitive include set into the output — otherwise amylib.hthat includes<exec/types.h>ends up emitting the entire NDK as constants. Probably "only emit#defines from headers explicitly listed on the command line, but resolve their values using everything reachable via-I". -
Naming collisions across libraries. If a user generates
modules for two libraries that both define
FOO, the second wins. Tool should warn loudly. Optional--namespace mylibflag wraps everything in a class / dict so collisions are scoped. -
Distribution shape. Should the tool ship in
tools/alongside the existing trio, in a separatetools/contrib/directory, or as a standalone Python package the userpip installs? Probablytools/for parity with the existing tools — they all assume the bebbo cross-gcc is onPATHanyway, so the docker wrapper (tools/amiga-build.sh) gives a clean way to run it. -
Cross-host portability. Does the tool need to work on
AmigaOS itself (so a user with sas/c or vbcc on-box can
regenerate their own module from a stock C header), or is
"run on Linux/macOS via the Docker wrapper" sufficient?
Almost certainly the latter for v1 — the on-Amiga story
would mean compiling the probe
.cwith sas/c or vbcc, which is a much bigger compatibility ask.
Cost estimate: ~300 lines of Python on top of the existing
three engines, plus a docs page and a couple of worked
examples (identify.library, cybergraphics.library).
Probably 2–3 days of focused work plus a follow-up week of
shaking out real third-party headers.
Verdict: medium-effort, high-leverage. The existing trio already proved the engine works; this is a packaging / ergonomics pass that converts "you can call any Amiga library from MicroPython if you're willing to hand-roll the constants" into "you can call any Amiga library from MicroPython, full stop". Pairs naturally with the ClassAct / ReAction GUI binding sketch above — if and when ClassAct lands, the same tool would let users generate constants for whatever third-party BOOPSI classes they want to use alongside the bundled ones.
| Issue | Status | Fix |
|---|---|---|
try/except in @micropython.native crashes |
Fixed (Phase 12a) | Register-based NLR in nlr68k.S + nlr68k_jump.c saving D2–D7/A2–A6 in nlr_buf_t
|
| Fixed (Phase 12b) | Big-endian byte order: byte-swap the prelude_ptr_index / generator start_offset words on N_68K in py/emitnative.c
|
|
@micropython.viper holds only 1 local in a register (D7) |
MAX_REGS_FOR_LOCAL_VARS = 1 |
Performance only — results are correct (extra locals spill to the stack). Optional future work: spill across D2–D7. Partly intrinsic to the 68k register layout. |
@micropython.viper ptr16/ptr32 accesses are big-endian |
m68k is big-endian | Not a bug — native machine memory order. The little-endian upstream .exp files differ; see the testing wiki's platform-differences table. |
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. |
For the full enumerated list of currently-failing tests (35 entries
grouped by cause — Phase-12 NLR/viper gaps, AmiSSL subset, vamos
socket/select/clock gaps, port-config one-offs), see the
Amiga port test status
scoreboard's Remaining real-hardware failures table. Captured
2026-06-13 against commit 98a3a6e4c.
-
@micropython.nativebinary ops with a small-int literal mostly failed.asm_68k_moveqinpy/asm68k.hplaced the destination data register at bits 10-8 instead of 11-9, soMOVEQ #1, D2(wanted opcode0x7401) emitted0x7201=MOVEQ #1, D1— clobbering the wrong register and leaving the intended one stale. Visible asTypeError: unsupported types for __X__: 'int', ''for any</>/+/-/*against a literal in roughly -64..63, or as a silent wrong-answer for==/!=(the runtime's small-int fast path turns the corrupted-rhs comparison into a default-false). Fixed in commitfb1aeab92; bug had been present since the original 68k native emitter commit (42beaaeca, Phase 12). Regression test intests/ports/amiga/test_native_emitter_smoke.py.
A non-feature housekeeping pass that brought the port's surface in
line with the canonical project conventions in
CODECONVENTIONS.md.
Nothing user-visible changed; the diff was structural / cosmetic.
-
tools/amiga-structgen.py(new). Completes the trio withamiga-fdgen.py(parses.fdfiles into LVO + register tables) andamiga-taggen.py(compiles tag macros to resolve theirTAG_USER + NIDs). The new tool emitsoffsetof()/sizeof()expressions against an NDK-struct whitelist, compiles them with the bebbo cross-gcc, and parses the resolved.longliterals back to produceports/amiga/modules/_amiga_structs.py. For inline fixed-byte strings (e.g.fib_FileName[108]) the array size is harvested viasizeof(((X*)0)->field)and the emitted type code is suffixed accordingly (s108), so the previously hand-typed magic numbers are gone. The output exactly matches the prior hand-curated table; the file is now mechanically tied to whatever NDK the build is targeting. -
MIT license header retrofit. Sister ports carry the standard
MicroPython
/* This file is part of the MicroPython project ... */block at the head of effectively every.c/.h; before this pass, 18 of 19ports/amiga/*.cplus 4.hplus 3variants/*/mpconfigvariant.hfiles lacked it. All 25 retrofitted withCopyright (c) 2026 Simon Dick;modjson.calready carried the upstream Damien P. George block (inherited from theextmod/modjson.cit replaces) and was left alone. -
mphalport.hguard. Switched from#pragma onceto the canonicalMICROPY_INCLUDED_AMIGA_MPHALPORT_Hform used by every other port'smphalport.h. -
Uncrustify clean.
main.chad two#if/#endiflines inside anelse ifchain that the formatter wanted indented one level deeper; everything else round-trips clean againsttools/uncrustify.cfg. -
Commit policy realigned. Going forward, port commits use
git commit -s(Signed-off-by, matching upstream's DCO requirement). Earlier commits on this branch deliberately don't carry a sign-off — the branch will be squashed before any upstream merge attempt, so the historical lack of sign-off doesn't matter.
Verification: full tests/ports/amiga/ smoke suite passes (28/28),
and the upstream basics / float / import / micropython /
extmod directories produce an identical failure set on the
post-conformance HEAD as on its parent (diff of the two
failure lists is empty).
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.