Files
el/lang/elb.el
T

403 lines
14 KiB
EmacsLisp

// 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 ""
}
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<String, Any> {
// 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 <out_dir>.
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).
let parts = native_list_append(parts, "cc -O2 -fbracket-depth=1024 -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, "-lcurl -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)
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)
}
// 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)
}