What it is
A port of AndroWish — Tcl/Tk on Android, via an SDL2 + AGG X11-emulation backend — rebuilt on Tcl/Tk 9.1b0 (the 2026-06-30 beta), targeting the 9.1 release.
Upstream AndroWish ships Tcl/Tk 8.6.10 on both its trunk and wtf-8-experiment Fossil branches, and there is no Tk-9-on-SDL upstream. The method: take stock Tk 9.1 generic/ and re-insert the SDL-specific regions that Christian Werner guards behind #ifdef PLATFORM_SDL — about 177 markers across 37 files — then port the self-contained sdl/ backend (~49k lines) and xlib/ (~1.8k lines) to Tk 9.1 internals (Tcl_Size, changed structs, and the Tcl 9 object-based widget-option APIs). It is not a three-way merge: the sdl2tk generic/ is forked from an older 8.6.x, so diffing against 8.6.10 is misleading.
Status
0.1-alpha. A signed arm64 APK boots on Android to an interactive Tcl console (File/Edit menu, % prompt) plus a main window with a Demos menu (12 entries) whose demos launch. All 84 native libraries build against Tcl/Tk 9.1b0 with zero errors, including the full "batteries included" extension set.
Runtime is verified end to end: the Tcl 9.1b0 interpreter runs on Android arm64 (tcl_patchLevel = 9.1b0, bignum arithmetic via libtommath), and the signed APK installs, launches, and renders the Tk root window. Outstanding: a default icon, and the empty placeholder . window.
A signed build is attached to the project's Releases.
Building (NDK/toolchain)
The upstream sources are supplied by the builder; this repo carries documentation and build configuration only.
Requirements:
- The AndroWish source tree (Fossil), plus the Tcl 9.1 and Tk 9.1 sources
- Android NDK r27d
- JDK 21 (Android Studio's JBR works)
- arm64-v8a only,
android-21baseline
The core Tcl module (libtcl.so) is regenerated from the Tcl 9.1 tarball into an ndk-build module. Seven changes distinguish it from the 8.6 module, including: regenerating the source list from the Tcl 9.1 unix/Makefile.in OBJS; adding utf8proc with -DUTF8PROC_STATIC (a new 9.x encoding dependency); pointing TCL_LIBRARY at the staged tcl9.1 assets; and dropping TCL_UTF_MAX=6. The standalone stock-configure route fails on macOS (a Darwin define leak makes tclUnixTime #error); the ndk-build path is the supported one.
export ANDROID_NDK_HOME=.../ndk/27.3.13750724
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
export PATH="$ANDROID_NDK_HOME:$PATH"
"$ANDROID_NDK_HOME/ndk-build" NDK_PROJECT_PATH=. \
APP_BUILD_SCRIPT=./jni/Android.mk \
NDK_APPLICATION_MK=./jni/Application64.mk APP_ABI=arm64-v8a -j8
./gradlew assembleRelease
Two gotchas:
- Stale
.ofiles. Changing a compiler flag in a*-config.mkdoes not invalidate ndk-build's object files. After editing one,rm -rf obj/local/arm64-v8a/objs/<module>or the flag will not apply. - Stale
.sorepackaging. Gradle can skip repackaging when a native.sochanges outside its task tracking, packaging a stale library. Run thendk-buildstep first, then./gradlew --no-daemon assembleRelease --rerun-tasks, and verify the.soinside the APK matches the built one.
Packaging (APK)
The alpha APK is Tcl/Tk 9.1b0, arm64-v8a only, minSdkVersion 21, targetSdkVersion 36, versionName 0.1-alpha, with 84 native libraries and the full batteries-included extension set. Its applicationId is tk.tcl.wish.tcl91, so it installs alongside stock AndroWish (tk.tcl.wish) rather than replacing it. The release build is 32,145,360 bytes (sha256 8651613169…f959).
Two Android packaging facts:
- Fonts must be staged. Stock Tk 9.1 omits AndroWish's bundled DejaVu/Symbola faces, so they must be copied into
assets/sdl2tk9.1/fonts/(13 files) or font setup fails at first draw. - The zipfs 128 MB self-mount cap. An AndroWish app mounts its own APK via Tcl zipfs at startup, in C, before Tcl runs. Tcl zipfs (
ZipFS.zipmax) defaults to a 128 MB limit, so an APK larger than that failszipfs mountwith "invalid file size" and dies at Tcl init. It cannot be raised from Tcl because the self-mount is native-first; the fix bumps both initializers to 1.5 GB (below the ~2 GB 32-bit-offset wall).
Under Tcl 9, zipfs mounts live under //zipfs:/ (TIP 430), so the boot directory and TCL_LIBRARY/TK_LIBRARY are rewritten to //zipfs:/assets/... for the 9.x build.
Implementation notes
Four runtime bugs stood between "it links" and "it runs". All are written up in docs/PORT.md.
Tcl_GetIntFromObjfailed for every non-negative integer on arm64, surfacing asbad level "#0"fromupvar #0insidetkInit— no console and no menu.TCL_WIDE_INT_IS_LONGwas undefined on this LP64 build, soTcl_GetLongFromObjtook its 32-bit-longbranch where(Tcl_WideInt)(ULONG_MAX)overflows to-1, making the range checkw <= -1reject everything. Arithmetic still worked, because bytecode literals are parsed at compile time; only runtime string→integer conversion broke. Fix: define the macro.Stack corruption (
SIGABRT,-fstack-protector) inwm title. The 8.6-era X11-emulation layer passedint*where Tcl 9 expectsTcl_Size*(8-byte) out-params, smashing the stack. These are silent-Wincompatible-pointer-typeswarnings; everyTcl_GetStringFromObj,Tcl_ListObjGetElements,Tcl_GetByteArrayFromObjandTcl_SplitListin the SDL layer had to be checked for anintout-param.Pointer-truncation
SIGSEGV.-Wno-implicit-function-declarationhad masked missing prototypes for SDL-backend calls into pointer-returning Tk internals, so their results were int-truncated. Removing the flag and adding the missing prototypes fixed it. Implicit-declaration warnings on a 64-bit build hide pointer truncation.Fonts. Several failures stacked up: an uninitialized font hash before first window-decoration draw; the bundled TTFs not being staged (see above); and a wildcard XLFD that could not match a concrete family, panicking with "cannot get any font".
A separate issue: everything rendered tiny on high-DPI. Tk was correct — it computed 44.5 px for a 10 pt font at 320 dpi. The bug was in the emulated XListFonts, which answered generic family requests (Helvetica, courier) with a hardcoded XLFD carrying pixel size 12. In X11 a non-zero pixel size means a fixed bitmap font, so Tk loaded it as-is and discarded the requested size. Synthesized font-list entries must use pixel size 0 (= scalable).
A 100%-reproducible launch crash on API 34+: addGpsStatusListener/addNmeaListener throw UnsupportedOperationException on every launch, killing the console process where wish runs, so the app never appears. The fix guards both registrations (and their teardown) behind an SDK-version check with try/catch. The modern GnssStatus/OnNmeaMessageListener replacements are not yet wired to Tcl, so borg gps/NMEA is inactive on API 34+.
On-device verification uses a repeatable check: a smoke script asserts all four bugs and the console wiring on-device and passes 15/15 at both 320 dpi / API 34 and 420 dpi / API 31. Not yet tested on physical hardware.
Repository
- Project page: https://johnbuckman.github.io/tcltk/androwish-tcl91.html
- Collection: https://johnbuckman.github.io/tcltk/
- Source: https://github.com/johnbuckman/androwish-tcl91
The repo contains documentation and ndk-build configuration only; it does not redistribute the Tcl, Tk, SDL2 or AndroWish sources. Docs: docs/PORT.md (roadmap and every runtime fix), docs/AGENT-BOOTSTRAP.md (build/test commands and environment traps), docs/SDL2TK-GRAFT.md (how the backend was grafted onto stock Tk 9.1), and docs/BATTERIES-TRIAGE.md (the per-extension Tcl 9 migration matrix). Licensed under the same terms as Tcl/Tk. AndroWish is by Christian Werner; Tcl/Tk by the Tcl Core Team.