#!/usr/bin/env bash # build.sh — Compile and run native-hello on macOS. # # Usage: # ./build.sh # compile and run # ./build.sh compile # compile only # ./build.sh clean # remove build artefacts # ./build.sh platforms # detect available platform bridges on this machine # # Requirements: # - elc in PATH (or ../../../lang/dist/platform/elc) # - clang with Xcode Command Line Tools (for Cocoa framework) # - macOS 12+ # # Multi-file compilation strategy: # elc does not resolve cross-directory imports (../../vessels/...) at bundle # time. The el-native vessel is compiled as a separate translation unit. # # el_seed.c and el_runtime.c MUST both be linked: # - el_seed.c = OS-boundary layer (__-prefixed primitives) + thin wrappers # - el_runtime.c = high-level el builtins (str_len, json_get_string, etc.) # They share many __-prefixed symbols. The macOS linker rejects duplicate # definitions, so we use nmedit to hide the el_runtime.c copies of the __ # symbols (keeping el_seed.c's versions canonical). # # el_native_target.h is a conflict-free header that adds: # - float_div / float_mul etc. (missing from current el_runtime.h) # - Native widget builtin declarations (#ifdef EL_TARGET_MACOS) # without redefining el_to_float / el_from_float from el_runtime.h. # # Vessel header generation: elc --emit-header includes function BODIES. # We extract only the forward declarations using awk (stop at first body). # NOTE: macOS awk does not support \s — use [[:space:]] instead. # # Vessel main(): the vessel's compiled .c file has its own main(). We use # nmedit to hide it from the vessel object before linking. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" EL_LANG_ROOT="${SCRIPT_DIR}/../../../lang" EL_UI_ROOT="${SCRIPT_DIR}/../.." EL_RUNTIME="${EL_LANG_ROOT}/el-compiler/runtime" EL_NATIVE_VESSEL="${EL_UI_ROOT}/vessels/el-native/src/main.el" BUILD_DIR="${SCRIPT_DIR}/build" # Locate elc if command -v elc &>/dev/null; then ELC="elc" elif [ -x "${EL_LANG_ROOT}/dist/platform/elc" ]; then ELC="${EL_LANG_ROOT}/dist/platform/elc" else echo "Error: elc not found. Add it to PATH or place it at:" echo " ${EL_LANG_ROOT}/dist/platform/elc" exit 1 fi CLANG_FLAGS_COMMON=( -std=c11 -DEL_TARGET_MACOS -I "${EL_RUNTIME}" ) clean() { rm -rf "${BUILD_DIR}" echo "Cleaned." } compile() { mkdir -p "${BUILD_DIR}" # ── 1. Vessel forward-declarations header ───────────────────────────────── # elc --emit-header emits full function bodies, not just prototypes. # Extract only the forward-declaration lines: stop at the first function # definition (a line that ends with ') {' opening a body block). # macOS awk does not support \s — use POSIX [[:space:]] instead. echo "==> Generating el-native vessel header (declarations only)..." "${ELC}" --emit-header "${EL_NATIVE_VESSEL}" \ | awk 'BEGIN{in_body=0} /^[a-zA-Z_].+\)[[:space:]]*\{/ {in_body=1} !in_body{print}' \ > "${BUILD_DIR}/el_native_vessel.h" # ── 2. Compile el-native vessel to C ────────────────────────────────────── echo "==> Compiling el-native vessel to C..." "${ELC}" "${EL_NATIVE_VESSEL}" \ > "${BUILD_DIR}/el_native_vessel.c" # ── 3. Compile app entry to C ───────────────────────────────────────────── echo "==> Compiling app entry to C..." EL_MANIFEST="${SCRIPT_DIR}/manifest.el" \ "${ELC}" "${SCRIPT_DIR}/src/main.el" \ > "${BUILD_DIR}/native_hello.c" # ── 4. Compile el_appkit.m (ObjC, MRC — not ARC) ───────────────────────── echo "==> Compiling el_appkit.m (AppKit bridge, -fno-objc-arc)..." clang \ -std=gnu11 \ -ObjC \ -fno-objc-arc \ -DEL_TARGET_MACOS \ -I "${EL_RUNTIME}" \ -c "${EL_RUNTIME}/el_appkit.m" \ -o "${BUILD_DIR}/el_appkit.o" # ── 5a. Compile el_seed.c ───────────────────────────────────────────────── # el_seed.c provides the OS-boundary __-prefixed primitives + thin wrappers # around el_runtime.c's higher-level functions. echo "==> Compiling el_seed.c..." clang "${CLANG_FLAGS_COMMON[@]}" \ -c "${EL_RUNTIME}/el_seed.c" \ -o "${BUILD_DIR}/el_seed.o" # ── 5b. Compile el_runtime.c and patch out duplicate __-prefixed symbols ── # el_runtime.c provides the high-level builtins (str_len, json_get_string, # int_to_str, println, env, exit_program, el_runtime_init_args, etc.) that # el_seed.c does NOT provide (they are single-name, no __ prefix). # # el_runtime.c also defines the same __-prefixed OS wrappers as el_seed.c. # We hide those duplicates using nmedit so the linker sees only one copy. echo "==> Compiling el_runtime.c..." clang "${CLANG_FLAGS_COMMON[@]}" \ -c "${EL_RUNTIME}/el_runtime.c" \ -o "${BUILD_DIR}/el_runtime.o" echo "==> Patching el_runtime.o (hiding __-prefixed symbols duplicated in el_seed.o)..." # Build the set of symbols that el_seed.o defines and el_runtime.o also # defines. We will hide el_runtime.o's copies, keeping el_seed.o canonical. nm "${BUILD_DIR}/el_seed.o" 2>/dev/null \ | awk '/^[0-9a-f]+ T _/{print $3}' | sort > "${BUILD_DIR}/.seed_T.txt" nm "${BUILD_DIR}/el_runtime.o" 2>/dev/null \ | awk '/^[0-9a-f]+ T _/{print $3}' | sort > "${BUILD_DIR}/.rt_T.txt" # Symbols to keep public in el_runtime.o = all T symbols MINUS the # duplicates (those will be supplied by el_seed.o). comm -23 "${BUILD_DIR}/.rt_T.txt" "${BUILD_DIR}/.seed_T.txt" \ > "${BUILD_DIR}/.rt_keep.txt" nmedit -s "${BUILD_DIR}/.rt_keep.txt" "${BUILD_DIR}/el_runtime.o" # ── 6. Compile el-native vessel object ──────────────────────────────────── # el_native_target.h adds float_div/mul/etc. + native widget declarations # without conflicting with el_runtime.h. echo "==> Compiling el-native vessel object..." clang "${CLANG_FLAGS_COMMON[@]}" \ -include "${EL_RUNTIME}/el_native_target.h" \ -c "${BUILD_DIR}/el_native_vessel.c" \ -o "${BUILD_DIR}/el_native_vessel.o" echo "==> Patching el_native_vessel.o (hiding vessel main)..." # The vessel's compiled C has its own main(). Hide it so it does not # conflict with the app's main in native_hello.o. nm "${BUILD_DIR}/el_native_vessel.o" 2>/dev/null \ | awk '/^[0-9a-f]+ T _/{print $3}' \ | grep -v '^_main$' \ > "${BUILD_DIR}/.vessel_keep.txt" nmedit -s "${BUILD_DIR}/.vessel_keep.txt" "${BUILD_DIR}/el_native_vessel.o" # ── 7. Compile app object ───────────────────────────────────────────────── # Inject el_native_target.h (float ops + native decls) and the vessel's # forward-declarations header so the vessel functions are resolvable. echo "==> Compiling app object..." clang "${CLANG_FLAGS_COMMON[@]}" \ -include "${EL_RUNTIME}/el_native_target.h" \ -include "${BUILD_DIR}/el_native_vessel.h" \ -c "${BUILD_DIR}/native_hello.c" \ -o "${BUILD_DIR}/native_hello.o" # ── 8. Link ─────────────────────────────────────────────────────────────── # el_seed.o = OS-boundary primitives (__-prefixed) # el_runtime.o (patched) = high-level builtins (str_len, json_*, etc.) # el_native_vessel.o (patched) = el-level widget API # el_appkit.o = AppKit C bridge (MRC) echo "==> Linking native-hello..." clang "${CLANG_FLAGS_COMMON[@]}" \ -framework Cocoa \ -lpthread \ -lcurl \ -ldl \ "${BUILD_DIR}/native_hello.o" \ "${BUILD_DIR}/el_native_vessel.o" \ "${BUILD_DIR}/el_appkit.o" \ "${BUILD_DIR}/el_runtime.o" \ "${BUILD_DIR}/el_seed.o" \ -o "${BUILD_DIR}/native-hello" echo "==> Build complete: ${BUILD_DIR}/native-hello" } run() { compile echo "==> Launching native-hello..." EL_MANIFEST="${SCRIPT_DIR}/manifest.el" \ "${BUILD_DIR}/native-hello" } case "${1:-run}" in clean) clean ;; compile) compile ;; platforms) "${EL_LANG_ROOT}/el-compiler/runtime/detect-platforms" ;; run|*) run ;; esac