specs: §10.5 Bundling and Post-Link Callbacks
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/<libfoo.so> →
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.
This commit is contained in:
157
specs.md
157
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 `<name>.bundle_main` post-link. |
|
||||
|
||||
CLI `--bundle <path>` / `--apk <path>` 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 <name>` (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 <name>` 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 `<bundle>` (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 `<bundle>/embedded.mobileprovision` | — | — | when `provisioning_profile()` set |
|
||||
| Embed `Frameworks/<Name>.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 `<TEAM>.*` → `<TEAM>.<bundle_id>`) | — | — | when `provisioning_profile()` set |
|
||||
| Codesign | ad-hoc (`-`) | ad-hoc | `--sign <identity> --entitlements <ent>` |
|
||||
|
||||
### 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 <parent> | sort -V | tail -1")`.
|
||||
3. **Stage `<apk>.stage/lib/arm64-v8a/<libfoo.so>`** — `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="<foreign_path_with_dots>"` + `android:hasCode="true"` otherwise.
|
||||
5. **Compile `#jni_main` Java sources** — write each entry's `java_source` to `<stage>/java/<pkg>/<Cls>.java`, run `javac --release 11 -classpath <android.jar>` to `<stage>/classes/`, run `d8 --release --lib <android.jar> --output <stage>` to produce `<stage>/classes.dex`. `javac` discovered via `$JAVA_HOME/bin/javac` then `command -v javac`.
|
||||
6. **`aapt2 link -I <android.jar> --manifest <m> -o <unaligned>`**.
|
||||
7. **Append archives** — `zip -q -r <unaligned> lib/`, then `zip -q <unaligned> classes.dex` (if dex was produced), then `zip` each registered asset dir at its `dest` path.
|
||||
8. **`zipalign -f 4 <unaligned> <aligned>`**.
|
||||
9. **Debug keystore** — `keytool -genkeypair -keystore <path>` on first use; defaults match Android Studio (`androiddebugkey` alias, password `android`).
|
||||
10. **`apksigner sign --ks <ks> --ks-pass pass:android --key-pass pass:android --ks-key-alias androiddebugkey --out <apk> <aligned>`**.
|
||||
11. Clean intermediates (keep `<apk>.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`.
|
||||
|
||||
Reference in New Issue
Block a user