From 4c6c29b299c169f676261dc4b3620f8941ec4288 Mon Sep 17 00:00:00 2001 From: agra Date: Sat, 23 May 2026 01:35:05 +0300 Subject: [PATCH] =?UTF-8?q?specs:=20=C2=A710.5=20Bundling=20and=20Post-Lin?= =?UTF-8?q?k=20Callbacks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the post-link callback model that the bundling-in-sx campaign landed (Weeks 6 + 7): - Explicit opt-in via `BuildOptions.set_post_link_callback(fn)` or `set_post_link_module(name)` from a user `#run` block. No stdlib default; no implicit prelude. CLI `--bundle` / `--apk` auto-fallback to `post_link_module = "platform.bundle"` so existing CLI invocations keep working without an in-source registration. - `BuildOptions` surface: setters (link_flag / framework / output_path / wasm_shell / asset_dir / post_link_callback / post_link_module / bundle_path / bundle_id / codesign_identity / provisioning_profile / manifest_path / keystore_path) + accessors (binary_path / target_triple / is_macos / is_ios / is_ios_device / is_ios_simulator / is_android / framework / framework_path / jni_main / asset_dir families). Returned strings are "" when unset; counts are 0. - `fs.sx` / `process.sx` stdlib modules. Both work in two execution contexts: at runtime via the dynamic linker, and at #run / post-link via `src/ir/host_ffi.zig`'s dlsym(RTLD_DEFAULT) trampolines. - Per-target Apple `.app` flow: stage + Info.plist (macOS minimal vs iOS-shaped UIDeviceFamily/LSRequiresIPhoneOS/UIApplicationSceneManifest/ DTPlatformName) + provisioning embed (iOS device) + Frameworks/ embed (iOS) + entitlements extraction (`security cms` + 3× `plutil`) + codesign with --entitlements when present. - Android `.apk` flow: SDK discovery → highest build-tools / platforms via `ls -1 | sort -V | tail -1` → stage lib/arm64-v8a/ → manifest synth (NativeActivity vs `#jni_main` Activity) → javac + d8 per `#jni_main` decl → aapt2 link → zip lib/dex/assets → zipalign → keytool debug keystore (first use) → apksigner sign. --- specs.md | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/specs.md b/specs.md index f776570..a82fd5c 100644 --- a/specs.md +++ b/specs.md @@ -1850,6 +1850,163 @@ Full triples are also accepted and passed through as-is. --- +## 10.5 Bundling and Post-Link Callbacks + +Platform-specific bundling (Apple `.app`, Android `.apk`) lives in +[library/modules/platform/bundle.sx](library/modules/platform/bundle.sx). +The compiler shrinks to: parse → IR → codegen → link → invoke a sx +function. Bundling, codesigning, manifest generation, Java compilation +(via `javac` + `d8`), etc. are all sx code running in the IR +interpreter post-link. + +### Discovery + +Users opt in **explicitly** from their own `#run` block: + +```sx +#import "modules/compiler.sx"; +#import "modules/platform/bundle.sx"; + +#run { + opts := build_options(); + opts.set_bundle_path("MyApp.app"); + opts.set_bundle_id("com.example.app"); + opts.set_post_link_callback(bundle_main); +} +``` + +Programs that don't register a callback simply don't bundle — the +linked binary is produced and nothing further runs. There is no +stdlib default and no implicit prelude. + +Two registration forms: + +| Setter | Behavior | +|--------|----------| +| `BuildOptions.set_post_link_callback(cb: () -> bool)` | First-class function value. Preferred. | +| `BuildOptions.set_post_link_module(name: [:0]u8)` | Name-based fallback; compiler resolves `.bundle_main` post-link. | + +CLI `--bundle ` / `--apk ` are transitional aliases: if +`bundle_path` is set and no callback was registered, the compiler +auto-falls-back to `post_link_module = "platform.bundle"`. The sx +bundler reads `bundle_path()` regardless of which flag the user used. +The callback returns `false` to fail the build. + +### BuildOptions surface + +`BuildOptions` is a `#compiler` struct in +[library/modules/compiler.sx](library/modules/compiler.sx). Setters +accumulate config in the compiler's `BuildConfig`; accessors read it +back inside the post-link callback. + +| Method | Read / write | Purpose | +|--------|--------------|---------| +| `add_link_flag(flag)` | write | extra linker flag | +| `add_framework(name)` | write | `-framework ` (Apple) | +| `set_output_path(path)` | write | linked binary path | +| `set_wasm_shell(path)` | write | custom WASM shell template | +| `add_asset_dir(src, dest)` | write | bundle a directory of runtime assets | +| `set_post_link_callback(cb)` | write | first-class callback (preferred) | +| `set_post_link_module(name)` | write | name-based callback fallback | +| `set_bundle_path(path)` | write | `.app` / `.apk` output | +| `set_bundle_id(id)` | write | iOS `CFBundleIdentifier` / Android package | +| `set_codesign_identity(name)` | write | Apple signing identity (`-` = ad-hoc) | +| `set_provisioning_profile(path)` | write | iOS device `.mobileprovision` | +| `set_manifest_path(path)` | write | Android AndroidManifest.xml override | +| `set_keystore_path(path)` | write | Android keystore override | +| `binary_path()` | read | path of the freshly-linked binary | +| `bundle_path() / bundle_id()` | read | mirror of the setters | +| `codesign_identity() / provisioning_profile()` | read | Apple codesign params | +| `manifest_path() / keystore_path()` | read | Android overrides | +| `target_triple()` | read | canonicalized target triple | +| `is_macos() / is_ios() / is_ios_device() / is_ios_simulator() / is_android()` | read | per-target predicates | +| `framework_count() / framework_at(i)` | read | linker `-framework` names (for `Frameworks/` embed) | +| `framework_path_count() / framework_path_at(i)` | read | linker `-F` search paths | +| `jni_main_count() / jni_main_foreign_path_at(i) / jni_main_java_source_at(i)` | read | `#jni_main` emissions for the APK bundler | +| `asset_dir_count() / asset_dir_src_at(i) / asset_dir_dest_at(i)` | read | iterate registered asset trees | + +Returned strings are `""` when unset; integer counts are `0`. Accessors +that read after-the-fact (`binary_path`, `bundle_path`, etc.) return +the value that was either set in `#run` or forwarded from a CLI flag. + +### `fs.sx` and `process.sx` stdlib modules + +The bundler is implemented in sx; its calls into `fs.sx` / `process.sx` +work both at runtime through the dynamic linker and at `#run` / post-link +through the host-FFI dispatch in +[src/ir/host_ffi.zig](src/ir/host_ffi.zig) (a `dlsym(RTLD_DEFAULT)` + +arity-switched cdecl trampoline). + +[library/modules/fs.sx](library/modules/fs.sx) (POSIX backend): + +| Function | Purpose | +|----------|---------| +| `open_file(path, mode) -> ?File` | open a handle | +| `read_file(path) -> ?string` | one-shot slurp | +| `write_file(path, data) -> bool` | create / truncate / write | +| `append_file(path, data) -> bool` | append | +| `copy_file(src, dst) -> bool` | byte copy (streamed through 64 KB buffer) | +| `delete_file(path) -> bool` | `unlink` | +| `delete_dir(path) -> bool` | `rmdir` (empty only) | +| `create_dir(path) -> bool` / `create_dir_all(path) -> bool` | `mkdir` / `mkdir -p` | +| `move(old, new) -> bool` | `rename` | +| `set_mode(path, mode) -> bool` | `chmod` | +| `exists(path) -> bool` | `access(F_OK)` | +| `basename(p) -> string` / `dirname(p) -> string` | text-only path split | + +`File` is a small value-typed handle wrapping a POSIX fd, with +methods `is_valid / close / read / write / seek`. Higher-level helpers +(`read_file`, `write_file`, `copy_file`) bypass `*File` methods and +call libc directly so they remain callable from the post-link IR +interpreter (which doesn't yet handle `*Self` method dispatch on +locally-unwrapped optionals). + +[library/modules/process.sx](library/modules/process.sx) (POSIX backend): + +| Function | Purpose | +|----------|---------| +| `run(cmd: [:0]u8) -> ?ProcessResult` | `popen` shell command, capture stdout + exit | +| `env(name: [:0]u8) -> ?string` | `getenv` (null if unset) | +| `find_executable(name) -> ?string` | `command -v ` via shell | + +`ProcessResult` is `{ exit_code: s32, stdout: string }`. The post-link +bundler invokes `codesign`, `plutil`, `security`, `aapt2`, `javac`, +`d8`, `keytool`, `apksigner`, etc. through `run`. + +### Apple `.app` flow (`bundle.sx::bundle_main`) + +`bundle_main` branches on `is_android()` first; the remaining body is +the Apple path. Per target: + +| Step | macOS | iOS sim | iOS device | +|------|-------|---------|------------| +| Stage `` (rm-rf + mkdir + copy binary + set exe bit) | ✓ | ✓ | ✓ | +| Write `Info.plist` | minimal `CFBundle*` | + `UIDeviceFamily` + `LSRequiresIPhoneOS` + `UIApplicationSceneManifest` + `DTPlatformName=iPhoneSimulator` | + same with `DTPlatformName=iPhoneOS` | +| Embed provisioning profile to `/embedded.mobileprovision` | — | — | when `provisioning_profile()` set | +| Embed `Frameworks/.framework/` (recursive `cp -R` per `-F` search path) | — | when present | when present | +| Extract entitlements (`security cms -D` + `plutil -extract Entitlements` + `plutil -extract ApplicationIdentifierPrefix.0` + `plutil -replace application-identifier` resolving `.*` → `.`) | — | — | when `provisioning_profile()` set | +| Codesign | ad-hoc (`-`) | ad-hoc | `--sign --entitlements ` | + +### Android `.apk` flow (`bundle.sx::android_bundle_main`) + +The Android branch: + +1. **Discover SDK** — `$ANDROID_HOME` → `$ANDROID_SDK_ROOT` → `$HOME/Library/Android/sdk`. +2. **Find highest `build-tools` / `platforms` subdir** — `process.run("ls -1 | sort -V | tail -1")`. +3. **Stage `.stage/lib/arm64-v8a/`** — `copy_file` from the linked output. +4. **Manifest** — user-supplied via `set_manifest_path()`, or synthesized: + - `NativeActivity` shape when no `#jni_main` is declared. + - `#jni_main` Activity shape with `android:name=""` + `android:hasCode="true"` otherwise. +5. **Compile `#jni_main` Java sources** — write each entry's `java_source` to `/java//.java`, run `javac --release 11 -classpath ` to `/classes/`, run `d8 --release --lib --output ` to produce `/classes.dex`. `javac` discovered via `$JAVA_HOME/bin/javac` then `command -v javac`. +6. **`aapt2 link -I --manifest -o `**. +7. **Append archives** — `zip -q -r lib/`, then `zip -q classes.dex` (if dex was produced), then `zip` each registered asset dir at its `dest` path. +8. **`zipalign -f 4 `**. +9. **Debug keystore** — `keytool -genkeypair -keystore ` on first use; defaults match Android Studio (`androiddebugkey` alias, password `android`). +10. **`apksigner sign --ks --ks-pass pass:android --key-pass pass:android --ks-key-alias androiddebugkey --out `**. +11. Clean intermediates (keep `.stage/` for inspection if it lasts the build). + +--- + ## 11. Program Structure A program is a sequence of top-level declarations and `#import` directives. Execution begins at `main`.