# build.ludic — turning a .ludic file into an executable. # # compile_app is the one place that happens, shared by `ludic build` / `ludic # run` and by the contributor tool, so a user's build and the repo's own example # builds cannot drift apart. Everything the toolchain owns — the compiler, the # engine runtime it splices — is addressed through ludic_home(), so the same code # path works from a checkout and from an installed ~/.ludic. # ensure bin/ludicc exists and is current with the seed. Built from the checked-in # IR seed with clang alone — no C compiler is ever involved. function ensure_ludicc() -> void { if not in_toolchain_repo() { return } shell("mkdir -p bin build") let seed = seed_file() let bin = `bin/{exe_name("ludicc")}` if (not is_exec(bin)) or newer(seed, bin) { print(`cc: {seed} -> {bin} (from the IR seed, no C compiler)`) if not shq(`{cc()} {seed} -o {bin}`) { err("ludic: could not assemble the seed\n"); exit(1) } } } # ---- compiling one program to a native binary ------------------------------- # # mode 1 = windowed (a game opens a real window), 2 = headless (the last frame is # rendered to build/out.ppm from piped input — deterministic output for tests). # Returns true on success. `ludic build`, `ludic run` and the contributor tool all # go through here. # the native libraries (phase 15) the last compile_app linked, for `ludic bundle` to place var g_built_natives: []pointer = new []pointer function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool { ensure_ludicc() # in a checkout: assemble the seed if bin/ludicc is missing or stale let home = ludic_home() let pbf = prebuilt_link_flags() # #64: link resolved prebuilt module dylibs, if any let odir = dir_of_path(out) if len(odir) > 0 { shell(`mkdir -p {odir}`) } shell("mkdir -p build") # The IR is a scratch file: it goes to this run's own temp directory, never beside the # binary, so a project's tree never holds one and two builds at once cannot delete each # other's. --save-temps asks to read it, and keeps it at {out}.ll. var ll = `{out}.ll` if not save { ll = tmp_path(`{file_of_path(out)}.ll`) } # On Windows the compiler links too: ludicc knows the Windows runtime's files and # libraries (win32.ll, xaudio2, opengl32 ...), so saying them again here would be a # second copy to keep in step. It writes {out}.exe. if host_windows() { var flags = "--windowed" if mode == 2 { flags = "--headless" } if save { flags = flags + " --save-temps" } return shq(`{ludicc()} {flags}{title_flag()}{unsafe_flag()}{release_flag()} {src} -o {out}`) } if mode == 2 { if not shq(`{ludicc()} --headless{title_flag()}{unsafe_flag()}{release_flag()} {src} --emit-llvm -o {ll}`) { return false } let objs = split_objs(ll, out) # on every core when it can (split.ludic) var input = ll if objs != "" { input = objs } let linked = shq(`{cc()} -O{split_opt()} {input}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{native_link_flags(ll)}{pbf} -o {out}`) if objs != "" { shell(`rm -f {objs}`) } if not linked { return false } g_built_natives = native_libs_of(ll) if not save { shell(`rm -f {ll}`) } return true } if not shq(`{ludicc()} --windowed{title_flag()}{unsafe_flag()}{release_flag()} {src} --emit-llvm -o {ll}`) { return false } # audio.ll (#22) is always linked here — unused snd_* are dead-stripped; the # canonical `ludicc -o` path links it only when Audio.* is used. let cocoa = `{home}runtime/native/cocoa.ll` let audio = `{home}runtime/native/audio.ll` let objs = split_objs(ll, out) var input = ll if objs != "" { input = objs } let linked = shq(`{cc()} -O{split_opt()} {input} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{native_link_flags(ll)}{pbf} -o {out}`) if objs != "" { shell(`rm -f {objs}`) } if not linked { return false } g_built_natives = native_libs_of(ll) if not save { shell(`rm -f {ll}`) } return true } # ` --title ""` when the project's package.ludic names the app, so the window # opens under that name rather than the `program` name; "" otherwise, and for a name the # shell line could not carry safely. # ` --unsafe` when the build said so: the project's own files may write `unsafe` (L7) # ludic bundle: what ships is a release, and Build.schema_hash() is 0 in it var g_release_build: bool = false function release_flag() -> pointer { if g_release_build { return " --release" } return "" } var g_unsafe_build: bool = false function unsafe_flag() -> pointer { if g_unsafe_build { return " --unsafe" } return "" } function title_flag() -> pointer { let name = manifest_app(read_root_manifest(), "name") if name == "" { return "" } var i = 0 while name[i] != 0 { let c = name[i] if c == '"' or c == '`' or c == '$' or c == 92 or c == '%' or c < ' ' { return "" } i += 1 } return ` --title "{name}"` } # A program that uses Gl.* references the @lgl_* thunks; link the OpenGL backend # (gl.ll + gl_thunks.ll + OpenGL.framework) only then, so other builds are untouched. function gl_link_flags(ll: pointer) -> pointer { if not shq(`grep -q "@lgl_" {ll}`) { return "" } let home = ludic_home() return ` {home}runtime/native/gl.ll {home}runtime/native/gl_thunks.ll -framework OpenGL` } # A program that uses Vk.* references the @lvk_* thunks; link them and the loader that # fills them at run time (vk_mac.ll opens libvulkan.1.dylib - MoltenVK - when it is # there). No Vulkan library is linked, so the program still starts on a Mac without one. function vk_link_flags(ll: pointer) -> pointer { if not shq(`grep -q "@lvk_" {ll}`) { return "" } let home = ludic_home() # QuartzCore: the CAMetalLayer a window presents through (cocoa.ll finds the class by name) return ` {home}runtime/native/vk_thunks.ll {home}runtime/native/vk_mac.ll -Wl,-needed_framework,QuartzCore` } # A package that carries a native library (phase 15) is written into the IR by the compiler as # `; ludic-native: ` lines: link each, with an rpath to its directory for a run from the # build and one to Contents/Frameworks, where `ludic bundle` puts it. function native_libs_of(ll: pointer) -> []pointer { let out = new []pointer let lines = split_lines(capture(`grep "^; ludic-native: " {ll} 2>/dev/null`)) var i = 0 while i < len(lines) { if s_starts(lines[i], "; ludic-native: ") { push(out, sslice(lines[i], 16, slen(lines[i]))) } i += 1 } return out } function native_link_flags(ll: pointer) -> pointer { let libs = native_libs_of(ll) var f = "" var i = 0 while i < len(libs) { f = f + ` "{libs[i]}" -Wl,-rpath,{dir_of_path(libs[i])} -Wl,-rpath,@executable_path/../Frameworks` i += 1 } return f } # A program that uses Http.* calls the @hs_* transport; link http.ll and Foundation only # then, in both modes, as `ludicc -o` does (selfhost/main.ludic). Without it a Http.* # program built through `ludic build` failed to link on every hs_* symbol. function http_link_flags(ll: pointer) -> pointer { # a CALL, not the name: every program's header declares the hs_* transport, so grepping # for "@hs_" linked http.ll and Foundation into everything - harmless on macOS, a failed # link on Linux. Only the Http runtime a program actually uses calls hs_send. if not shq(`grep -q "call void @hs_send" {ll}`) { return "" } return ` {ludic_home()}runtime/native/http.ll -Wl,-needed_framework,Foundation` } # A program that uses Udp.* calls the lu_udp_* socket layer; link udp.ll only then, as # `ludicc -o` does (Windows links through ludicc, with udp_win.ll and ws2_32). function udp_link_flags(ll: pointer) -> pointer { if not shq(`grep -q "call .*@lu_udp_" {ll}`) { return "" } return ` {ludic_home()}runtime/native/udp.ll` } # A program that uses Process.* calls the lu_proc_* layer; link process.ll only then, as # `ludicc -o` does (Windows links through ludicc, with process_win.ll). function process_link_flags(ll: pointer) -> pointer { if not shq(`grep -q "call .*@lu_proc_" {ll}`) { return "" } return ` {ludic_home()}runtime/native/process.ll` } # A program that uses Job.* / Promise.* / Sync.* calls the thr_* OS-thread runtime; link # threads.ll only then, as `ludicc -o` does. function threads_link_flags(ll: pointer) -> pointer { if not shq(`grep -q "call .*@thr_" {ll}`) { return "" } if host_windows() { return ` {ludic_home()}runtime/native/threads_win.ll` } return ` {ludic_home()}runtime/native/threads.ll` } # the file part of a path: everything after the last '/' function file_of_path(p: pointer) -> string { let d = dir_of_path(p) if len(d) == 0 { return p } return p[len(d) + 1..len(p)] } # the directory part of a path, without the trailing '/' ("" when there is none) function dir_of_path(p: pointer) -> pointer { var last = -1 var i = 0 while p[i] != 0 { if p[i] == '/' { last = i }; i += 1 } if last < 0 { return "" } return p[0..last] } # Remove every generated artifact and leave the tracked source untouched: the # whole build/ tree (IR, objects, compiled apps, the headless render at # build/out.ppm), and any stray *.tmp/*.ll left behind by a failed build. A # legacy out.ppm in the root (from an older toolchain) is swept too. Binaries in # bin/ are kept so the running CLI survives; in the toolchain repo, rebuild them # any time with `ludic-dev build`. function cmd_clean() -> int { shell("rm -rf build") shell("rm -f out.ppm bin/*.tmp") print("cleaned: build/ (incl. build/out.ppm) and stray artifacts (bin/ is left alone)") return 0 }