// elb.el - El Build Coordinator // // The build system for El programs. Written in El. Builds El. // // Usage: // elb # build from manifest.el in current dir // elb --clean # remove generated artifacts and rebuild // elb --dry-run # print actions without executing // elb --jobs=N # parallel compile jobs (default: 4) // elb --out=DIR # output directory (default: dist) // elb --runtime=PATH # path to el_runtime.c // // How it works (the .NET model): // 1. Read manifest.el to find the entry file // 2. Walk the import graph depth-first, build topological order // 3. For each file: if .el is newer than .elh/.c, compile with elc --emit-header // 4. Link all .c files + el_runtime.c into the final binary // // Each module compiles independently - no 128K-line blobs. // Downstream compilations read .elh headers (function signatures only), // not source. Incremental: only recompile what changed. // -- Flags --------------------------------------------------------------------- fn flag_bool(argv: [String], name: String) -> Bool { let n: Int = native_list_len(argv) let i = 0 while i < n { let a: String = native_list_get(argv, i) if str_eq(a, name) { return true } let i = i + 1 } return false } fn flag_val(argv: [String], name: String, default_val: String) -> String { let n: Int = native_list_len(argv) let prefix: String = name + "=" let i = 0 while i < n { let a: String = native_list_get(argv, i) if str_starts_with(a, prefix) { return str_slice(a, str_len(prefix), str_len(a)) } let i = i + 1 } return default_val } // -- Manifest parsing ---------------------------------------------------------- // // Read the entry file from manifest.el: // build { entry "soul.el" } fn parse_manifest_entry(src: String) -> String { let lines: [String] = str_split(src, "\n") let n: Int = native_list_len(lines) let i = 0 while i < n { let line: String = native_list_get(lines, i) let t: String = str_trim(line) if str_starts_with(t, "entry ") { // entry "soul.el" let after: String = str_slice(t, 6, str_len(t)) let trimmed: String = str_trim(after) // strip surrounding quotes if str_starts_with(trimmed, "\"") { let inner: String = str_slice(trimmed, 1, str_len(trimmed)) let q: Int = str_index_of(inner, "\"") if q >= 0 { return str_slice(inner, 0, q) } } } let i = i + 1 } return "" } // parse_manifest_c_sources - collect all `c_source "path"` lines from the // build block. Returns a flat list of path strings. fn parse_manifest_c_sources(src: String) -> [String] { let result: [String] = native_list_empty() let lines: [String] = str_split(src, "\n") let n: Int = native_list_len(lines) let i = 0 while i < n { let line: String = native_list_get(lines, i) let t: String = str_trim(line) if str_starts_with(t, "c_source ") { let after: String = str_slice(t, 9, str_len(t)) let trimmed: String = str_trim(after) if str_starts_with(trimmed, "\"") { let inner: String = str_slice(trimmed, 1, str_len(trimmed)) let q: Int = str_index_of(inner, "\"") if q >= 0 { let path: String = str_slice(inner, 0, q) let result = native_list_append(result, path) } } } let i = i + 1 } return result } fn parse_manifest_name(src: String) -> String { let lines: [String] = str_split(src, "\n") let n: Int = native_list_len(lines) let i = 0 while i < n { let line: String = native_list_get(lines, i) let t: String = str_trim(line) if str_starts_with(t, "package ") { let after: String = str_slice(t, 8, str_len(t)) let trimmed: String = str_trim(after) if str_starts_with(trimmed, "\"") { let inner: String = str_slice(trimmed, 1, str_len(trimmed)) let q: Int = str_index_of(inner, "\"") if q >= 0 { return str_slice(inner, 0, q) } } } let i = i + 1 } return "out" } // -- Path helpers --------------------------------------------------------------- fn dirname_of(path: String) -> String { let n: Int = str_len(path) let i: Int = n - 1 while i >= 0 { let c: String = str_slice(path, i, i + 1) if str_eq(c, "/") { return str_slice(path, 0, i) } let i = i - 1 } return "." } fn basename_noext(path: String) -> String { // strip directory let n: Int = str_len(path) let last_slash: Int = -1 let i = 0 while i < n { let c: String = str_slice(path, i, i + 1) if str_eq(c, "/") { let last_slash = i } let i = i + 1 } let base: String = str_slice(path, last_slash + 1, n) // strip .el extension let bn: Int = str_len(base) if str_ends_with(base, ".el") { return str_slice(base, 0, bn - 3) } return base } fn path_with_ext(path: String, ext: String) -> String { let n: Int = str_len(path) if str_ends_with(path, ".el") { return str_slice(path, 0, n - 3) + ext } return path + ext } fn file_is_newer(a: String, b: String) -> Bool { // Returns true if file a is newer than file b, or if b doesn't exist. // Uses exec_capture with stat to compare modification times. let cmd: String = "test -f " + b + " && test " + a + " -nt " + b + " && echo yes || echo no" let result: String = str_trim(exec_capture(cmd)) if str_eq(result, "yes") { return true } // b doesn't exist - check with test -f let exist_cmd: String = "test -f " + b + " && echo exists || echo missing" let exist: String = str_trim(exec_capture(exist_cmd)) if str_eq(exist, "missing") { return true } return false } // -- Import graph walker -------------------------------------------------------- // // Walk import statements in each .el file to build the dependency graph. // Returns a list of absolute paths in topological order (deps before dependents). fn parse_import_path(line: String, dir: String) -> String { let t: String = str_trim(line) if str_starts_with(t, "import \"") { let after: String = str_slice(t, 8, str_len(t)) let q: Int = str_index_of(after, "\"") if q > 0 { let mod: String = str_slice(after, 0, q) return dir + "/" + mod } } if str_starts_with(t, "from ") { let after: String = str_slice(t, 5, str_len(t)) let sp: Int = str_index_of(after, " ") if sp > 0 { let mod: String = str_trim(str_slice(after, 0, sp)) if !str_eq(mod, "") { return dir + "/" + mod + ".el" } } } return "" } fn walk_imports(src_path: String, visited: [String], order: [String]) -> Map { // Dedup check let n: Int = native_list_len(visited) let i = 0 while i < n { let v: String = native_list_get(visited, i) if str_eq(v, src_path) { return { "visited": visited, "order": order } } let i = i + 1 } let visited = native_list_append(visited, src_path) let source: String = fs_read(src_path) if str_eq(source, "") { return { "visited": visited, "order": order } } let dir: String = dirname_of(src_path) let lines: [String] = str_split(source, "\n") let ln: Int = native_list_len(lines) let j = 0 while j < ln { let line: String = native_list_get(lines, j) let imp: String = parse_import_path(line, dir) if !str_eq(imp, "") { let r = walk_imports(imp, visited, order) let visited = r["visited"] let order = r["order"] } let j = j + 1 } // Add self after all deps let order = native_list_append(order, src_path) return { "visited": visited, "order": order } } // -- Build ---------------------------------------------------------------------- fn compile_module(src_path: String, out_dir: String, elc_bin: String, dry_run: Bool, verbose: Bool) -> Bool { let bname: String = basename_noext(src_path) let c_out: String = out_dir + "/" + bname + ".c" let elh_out: String = out_dir + "/" + bname + ".elh" let err_tmp: String = "/tmp/elb-err-" + bname + ".txt" // Check if recompile needed if !file_is_newer(src_path, c_out) { if verbose { println(" skip " + bname + ".el (up to date)") } return true } // elc streams C to stdout; redirect stderr to a temp file so we can // surface the actual error message on failure instead of swallowing it. let cmd: String = elc_bin + " --emit-header " + src_path + " > " + c_out + " 2>" + err_tmp println(" compile " + src_path) if dry_run { return true } let ret: Int = exec_command(cmd) if ret != 0 { // Surface the actual compiler error from stderr let err_msg: String = str_trim(fs_read(err_tmp)) if !str_eq(err_msg, "") { println(err_msg) } // Remove partial output so a retry starts clean exec_command("rm -f " + c_out + " " + err_tmp) println("elb: compile failed: " + src_path) return false } exec_command("rm -f " + err_tmp) // Move the generated .elh (written next to the source by elc) into // out_dir so that #include "module.elh" lines in the generated .c // files resolve correctly when cc is invoked with -I . let src_elh: String = path_with_ext(src_path, ".elh") let mv_cmd: String = "cp " + src_elh + " " + elh_out + " 2>/dev/null || true" exec_command(mv_cmd) return true } fn link_binary(c_files: [String], out_bin: String, runtime_path: String, out_dir: String, dry_run: Bool) -> Bool { let n: Int = native_list_len(c_files) let parts: [String] = native_list_empty() // Include both the runtime dir (for el_runtime.h) and the output dir // (for module.elh cross-module forward declarations). // Detect clang vs gcc: -fbracket-depth is clang-only; silently ignored // if unsupported but gcc rejects it with an error. let bracket_flag: String = "$(cc --version 2>&1 | grep -q clang && printf -- '-fbracket-depth=1024' || true)" // On macOS, OpenSSL is not on the default linker path. Detect homebrew // prefix and add it if present (no-op on Linux where libssl is in /usr/lib). let ossl_lib_flag: String = "$(brew --prefix openssl 2>/dev/null | xargs -I{} printf -- '-L{}/lib' 2>/dev/null || true)" let ossl_inc_flag: String = "$(brew --prefix openssl 2>/dev/null | xargs -I{} printf -- '-I{}/include' 2>/dev/null || true)" // Force-include the C-level master declarations header so every translation // unit sees all cross-module function signatures. Handles packages (like ELP) // where modules call each other without explicit El import statements. // The header is generated by elb --gen-decls or manually placed in out_dir. let master_decls: String = out_dir + "/elp-c-decls.h" let has_master: String = str_trim(exec_capture("test -f " + master_decls + " && echo yes || echo no")) let include_flag: String = if str_eq(has_master, "yes") { "-include " + master_decls } else { "" } let parts = native_list_append(parts, "cc -O2 " + bracket_flag + " " + ossl_inc_flag + " " + include_flag + " -I " + dirname_of(runtime_path) + " -I " + out_dir) let i = 0 while i < n { let f: String = native_list_get(c_files, i) let parts = native_list_append(parts, f) let i = i + 1 } let parts = native_list_append(parts, runtime_path) let parts = native_list_append(parts, ossl_lib_flag + " -lcurl -lssl -lcrypto -lpthread -lm") let parts = native_list_append(parts, "-o " + out_bin) let cmd: String = str_join(parts, " ") println(" link " + out_bin) if dry_run { return true } let ret: Int = exec_command(cmd) if ret != 0 { println("elb: link failed") return false } return true } // -- Main ----------------------------------------------------------------------- fn main() -> Void { let argv: [String] = args() let clean: Bool = flag_bool(argv, "--clean") let dry_run: Bool = flag_bool(argv, "--dry-run") let verbose: Bool = flag_bool(argv, "--verbose") let out_dir: String = flag_val(argv, "--out", "dist") let elc_bin: String = flag_val(argv, "--elc", "elc") let runtime: String = flag_val(argv, "--runtime", "") // Find manifest let manifest_src: String = fs_read("manifest.el") if str_eq(manifest_src, "") { println("elb: no manifest.el found in current directory") exit(1) } let pkg_name: String = parse_manifest_name(manifest_src) let entry: String = parse_manifest_entry(manifest_src) let extra_c: [String] = parse_manifest_c_sources(manifest_src) if str_eq(entry, "") { println("elb: manifest.el has no 'entry' declaration") exit(1) } println("elb: building " + pkg_name + " (entry: " + entry + ")") // Locate runtime let runtime_path: String = runtime if str_eq(runtime_path, "") { // Try to find el_runtime.c relative to elc binary let which_out: String = str_trim(exec_capture("which " + elc_bin + " 2>/dev/null")) if !str_eq(which_out, "") { let elc_dir: String = dirname_of(which_out) runtime_path = elc_dir + "/../el-compiler/runtime/el_runtime.c" } } // If --runtime points to a directory, auto-locate el_runtime.c inside it. // This lets both forms work: // --runtime=/opt/el/el-compiler/runtime (directory form) // --runtime=/opt/el/el-compiler/runtime/el_runtime.c (file form) if !str_eq(runtime_path, "") { let is_dir: String = str_trim(exec_capture("test -d " + runtime_path + " && echo dir || echo file")) if str_eq(is_dir, "dir") { let candidate: String = runtime_path + "/el_runtime.c" let has_file: String = str_trim(exec_capture("test -f " + candidate + " && echo yes || echo no")) if str_eq(has_file, "yes") { let runtime_path = candidate } } } if str_eq(runtime_path, "") { println("elb: cannot locate el_runtime.c - use --runtime=PATH") exit(1) } // Ensure output directory let mkdir_ret: Int = exec_command("mkdir -p " + out_dir) // Clean if requested if clean { println("elb: cleaning " + out_dir) if !dry_run { let rm_ret: Int = exec_command("rm -f " + out_dir + "/*.c " + out_dir + "/*.elh") } } // Walk import graph from entry file let empty_visited: [String] = native_list_empty() let empty_order: [String] = native_list_empty() let r = walk_imports(entry, empty_visited, empty_order) let order: [String] = r["order"] let total: Int = native_list_len(order) println("elb: " + native_int_to_str(total) + " modules in build graph") // Compile each module let c_files: [String] = native_list_empty() let i = 0 let ok = true while i < total { let src: String = native_list_get(order, i) let bname: String = basename_noext(src) let c_out: String = out_dir + "/" + bname + ".c" let compiled: Bool = compile_module(src, out_dir, elc_bin, dry_run, verbose) if !compiled { let ok = false let i = total } else { let c_files = native_list_append(c_files, c_out) } let i = i + 1 } if !ok { println("elb: build failed") exit(1) } // Append any extra C sources declared in the manifest (e.g. platform stubs) let ei = 0 let en: Int = native_list_len(extra_c) while ei < en { let ec: String = native_list_get(extra_c, ei) let c_files = native_list_append(c_files, ec) let ei = ei + 1 } // Link let out_bin: String = out_dir + "/" + pkg_name let linked: Bool = link_binary(c_files, out_bin, runtime_path, out_dir, dry_run) if !linked { println("elb: link failed") exit(1) } println("elb: done -> " + out_bin) }