platform/android.sx: `sx_android_bootstrap(app)` now also reads the
ANativeActivity's `assetManager` (offset 64) and `internalDataPath`
(offset 32) into module globals so consumers can route file I/O
through the APK's bundled `assets/` tree.
target.zig (`createApk`): also zips the project's `./assets/`
directory into the APK alongside `lib/<arch>/`. Resolves relative
to the user's CWD at invoke time — matches the convention chess
uses (assets/ next to main.sx).
gles3.sx: scissor is currently a no-op on Android. The renderer's
ScrollView clip_push path feeds bounds that land outside the
framebuffer (clipping everything off-screen). With scissor disabled
the chess board + pieces render correctly. TODO recorded in the
file to fix the bounds path properly.
C imports (stb_image, stb_truetype) compiled via the embedded LLVM
clang library now resolve bionic headers on Android. main.zig auto-
fills target_config.sysroot with the NDK root (mirroring the iOS path
that auto-fills the iOS SDK path); c_import.zig derives the bionic
sysroot inside it and passes `--sysroot <ndk>/toolchains/llvm/prebuilt/<host>/sysroot`
to the embedded clang.
Android link branch in target.zig stops auto-appending entries from the
collected `#library` / `#framework` lists. Most `#library` directives
in the stdlib (`objc.sx`'s `objc :: #library "objc";`, frameworks set
by uikit.sx, etc.) describe Apple-specific intent that's nonsensical on
Android. Users opt into Android-side libs via `opts.add_link_flag(...)`
in build.sx — same shape as how the iOS branch already lists frameworks
in chess's configure_build.
Verified end-to-end: chess game compiles for Android, packages into a
debug-signed APK, installs and launches on Pixel 7 Pro. It crashes in
UIRenderer.init because raw GL calls run before EGL is up — same
architectural gap iOS bridges via the Metal GPU protocol. Next step
is a GLES3 GPU impl or a lazy-init in UIRenderer. 86/86 regression
tests + iOS-sim chess build clean.
Android (toolchain):
--target android / --target android-arm64 → aarch64-linux-android21.
target.zig discovers $ANDROID_NDK_HOME (or scans
~/Library/Android/sdk/ndk/* for the newest), invokes the NDK clang
with -shared -fPIC and links libsxhello.so against -llog -landroid
-lEGL -lGLESv3 -lm -ldl. native_app_glue.c from the NDK is compiled
and linked alongside the sx .o so apps can use the conventional
android_main(struct android_app*) shape; -u ANativeActivity_onCreate
keeps glue's symbol live.
Android (APK):
--apk <out> wraps the .so into a debug-signed installable APK.
target.zig discovers the SDK at $ANDROID_HOME (or
~/Library/Android/sdk), picks the newest build-tools + platforms,
generates a NativeActivity AndroidManifest.xml from --bundle-id,
packages via aapt2 link, appends the lib/ tree, zipalign, then
apksigner against ~/.android/debug.keystore (auto-generated via
keytool on first use). One command end-to-end:
sx build --target android --apk out.apk \\
--bundle-id co.swipelab.foo main.sx
Verified on Pixel 7 Pro: install + launch reaches android_main.
Compiler (entry-point linkage):
Top-level fn defs default to LLVM internal linkage and are lazily
lowered (only `main` was eagerly lowered before). Added
isExportedEntryName() — a small allowlist for names the OS loader
calls: `main`, `android_main`, `ANativeActivity_onCreate`,
`JNI_OnLoad`. These get eagerly lowered AND keep external linkage,
so they actually land in .dynsym.
LSP (imports):
DocumentStore now takes the install-discovered stdlib_paths and
forwards them into resolveImportPath, mirroring the compiler. Before
this, every `#import "modules/..."` resolved through the stdlib path
failed silently inside the LSP and identifiers from those modules
showed as `undefined variable`. Repro on label.sx: 1 false positive
before, 0 after.
Four root causes for "chess UI shows white screen" — all fixed:
1. Hybrid legacy-app + scene-API path on iOS 26. Without
UIApplicationSceneManifest in the Info.plist, iOS 26 booted us in
[rb-legacy] mode and -[UIApplication connectedScenes] returned an
empty set. didFinishLaunching's window-setup code bailed at "no scene"
and the UIWindow never appeared on screen. Fix: emit the manifest in
buildInfoPlist (src/target.zig) AND split the window/view/layer setup
from didFinishLaunching into a new SxSceneDelegate's
scene:willConnectToSession:options: IMP. didFinishLaunching now just
subscribes the keyboard observer and returns YES.
2. UISceneDelegate formal protocol conformance. iOS 26 checks
-[cls conformsToProtocol:@protocol(UISceneDelegate)] before
instantiating the scene delegate; without it the runtime logs
"SxSceneDelegate does not conform to the UISceneDelegate protocol"
and silently uses a default delegate that does nothing. Fix:
look up UISceneDelegate + UIWindowSceneDelegate via objc_getProtocol
and class_addProtocol BEFORE objc_registerClassPair. The protocol
metadata is present at link time (unlike UIApplicationDelegate per
the long-standing legacy note in CHECKPOINT).
3. Protocol method return types via type aliases lowered as void.
The GPU protocol declares `create_shader(...) -> ShaderHandle` where
`ShaderHandle :: u32`. The protocol-decl lowering at lower.zig:7547
passed the return AST node through type_bridge.resolveAstType which
doesn't know about the type_alias_map. resolveTypeName fell through
to its "assume named struct" branch and registered ShaderHandle as
an empty struct ({ }). LLVM IR for the protocol call_indirect then
read `call {} %fn_ptr(...)` — return value discarded; the
subsequent abi.coerce load from a zero-init'd alloca yielded 0.
Symptom: UIRenderer.mtl_shader = 0, set_shader sees state == null,
the render-encoder fires draw with no pipeline state bound, GPU
rejects the command buffer with MTLCommandBufferErrorInternal.
Fix: at the protocol-decl method-type resolution sites in
lower.zig, check type_alias_map BEFORE falling through to
type_bridge.resolveAstType for both params and return type. A
chess-side companion fix in /Users/agra/projects/game/main.sx
(separate commit) memsets the MetalGPU struct after alloc so the
List(*void) fields' len/cap/items aren't garbage.
After all four (this commit + memset companion in chess repo):
- 71/71 regression tests pass.
- Chess game now boots, scene-connects, ticks CADisplayLink, renders
dark-gray clear + UI text + panel dividers every frame on iOS sim.
- Metal-clear example still renders.
Chess board + pieces visual contrast and faint-text-color are remaining
visual-polish items, not compiler/platform-setup issues.
- `-F <dir>` CLI flag adds Apple framework search paths (parallel to `-L`).
- `TargetConfig.framework_paths` flows into the iOS link line (`-F<dir>`).
- iOS link adds `-Wl,-rpath,@executable_path/Frameworks` so embedded
frameworks resolve at runtime.
- `createBundle` now takes the framework list; for each one it locates
`<name>.framework` in the `-F` paths and `cp -R`s it into
`<bundle>.app/Frameworks/`.
- `c_import.compileCToObjects` forwards `-target`/`-isysroot` to clang so
`#c_import` works under cross-compile (was using host clang implicitly).
- iOS SDK is auto-discovered once at startup and shared by both the C
compile and the link paths.
- `SX_DEBUG_LINK=1` prints the resolved link argv.
- `library/modules/sdl3.sx`: drop `#library "SDL3"` — linking is now
per-target (build.sx handles `-lSDL3` on macOS, `-framework SDL3` on iOS).