/* * a simple routine to call out to a proper * compiler with our c source artifacts produced * by our bootstrapping transpiler to turn them * into real executable code. * currently we call out directly to the llvm-based * `clang` tool found in the system path to do * the real heavy lifting for us. :) * the catskill compiler does have an eventual goal * to achieve full self-hosting with real assembler * and linker implementations, however that is not * of interest to our bootstrap phase. * * Copyright (c) 2026, Mel G. * * SPDX-License-Identifier: MPL-2.0 */ #pragma once #include "catboot.h" #ifndef MUSL_LIB #error "MUSL_LIB not defined" #endif #ifndef MUSL_DEV #error "MUSL_DEV not defined" #endif bool create_build_directory(struct String* build_path) { // we can't directly call mkdtemp to create nested directories, // so we create the parent /tmp/catskill/ first. ascii build_dir_template[] = "/tmp/catskill/build_XXXXXX"; struct stat st; if (stat("/tmp/catskill", &st) == -1) { if (mkdir("/tmp/catskill", 0700) == -1) { log_error("failed to create /tmp/catskill/ directory\n"); return false; } } ascii* build_dir_path = mkdtemp(build_dir_template); if (!build_dir_path) { return false; } *build_path = string_from_c_string(build_dir_path); return true; } // TODO: clean up build directory after! struct Compiler_Command_Result { integer exit_code; struct String log; }; struct Compiler_Command_Result run_compiler_command(const ascii* command) { log_debug("running backend command: %s\n", command); FILE* command_pipe = popen(command, "r"); if (!command_pipe) return (struct Compiler_Command_Result){ .exit_code = -1 }; uint bytes_read; ascii log_buffer[8192] = { 0 }; while ((bytes_read = fread(log_buffer, 1, ARRAY_SIZE(log_buffer), command_pipe)) > 0) { if (bytes_read >= ARRAY_SIZE(log_buffer)) { log_error( "compiler output surpassed maximum output length... truncating to %lu bytes\n", ARRAY_SIZE(log_buffer) - 1); break; } } integer exit_code = pclose(command_pipe); struct String log = string_from_c_string(log_buffer); return (struct Compiler_Command_Result){ .exit_code = exit_code, .log = log }; } // TODO: move this over to common library bool write_build_file(struct String source, struct String input_path) { FILE* input_file = fopen(string_c_str(input_path), "w"); if (!input_file) { return false; } fwrite(source.data, 1, source.length, input_file); fclose(input_file); return true; } bool copy_runtime_library(struct String build_path) { const ascii core[] = CATBOOT_EMBED("./boot/runtime/core.c"); const ascii runtime[] = CATBOOT_EMBED("./boot/runtime/runtime.c"); const ascii stubs[] = CATBOOT_EMBED("./boot/runtime/stubs.c"); struct String build_runtime_path = string_append_c_str(build_path, "/runtime/"); if (mkdir(string_c_str(build_runtime_path), 0700) == -1) { log_error("failed to create build runtime directory\n"); return false; } struct String core_path = string_append_c_str(build_runtime_path, "core.c"), runtime_path = string_append_c_str(build_runtime_path, "runtime.c"), stubs_path = string_append_c_str(build_runtime_path, "stubs.c"); struct String core_source = string_from_c_string(core), runtime_source = string_from_c_string(runtime), stubs_source = string_from_c_string(stubs); if (!write_build_file(core_source, core_path)) return false; if (!write_build_file(runtime_source, runtime_path)) return false; if (!write_build_file(stubs_source, stubs_path)) return false; return true; } bool copy_file(struct String source_path, struct String dest_path) { FILE* source_file = fopen(string_c_str(source_path), "rb"); if (!source_file) { log_error("failed to open source file for copying: %s\n", source_path); return false; } // create destination file with the right permissions first if (!creat(string_c_str(dest_path), 0700)) { log_error("failed to create destination file for copying: %s\n", dest_path); fclose(source_file); return false; } FILE* dest_file = fopen(string_c_str(dest_path), "wb"); if (!dest_file) { log_error("failed to open destination file for copying: %s\n", dest_path); fclose(source_file); return false; } ascii buffer[4096]; size_t bytes; while ((bytes = fread(buffer, 1, sizeof(buffer), source_file)) > 0) { fwrite(buffer, 1, bytes, dest_file); } fclose(source_file); fclose(dest_file); return true; } struct Build_Result { bool success; integer compiler_exit_code; struct String compiler_log; struct String output_path; }; #ifdef CATBOOT_BACKEND_CLANG struct Build_Result compile_using_backend_clang( struct String build_path, struct String source_path, struct String output_path) { // probably too many flags for our purposes, but we kind of want // this simple bootstrapping output to be the eventual target // of our self-hosted compiler stack, so we avoid anything fancy // for an easier target to hit. struct String runtime_path = string_append_c_str(build_path, "/runtime/"); struct String stubs_path = string_append_c_str(runtime_path, "stubs.c"); const ascii* arguments[] = { "clang", // high-level compiler behavior "-O0", "-g", "-std=c99", // low-level compiler settings, which we optimize to generate // extremely simple and human-readable assembly in our final // executable. we want it to feel almost hand-written. "-fno-omit-frame-pointer", "-fno-stack-protector", "-fno-plt", "-fno-builtin", "-fno-inline", "-fno-common", "-fno-ident", "-fno-exceptions", "-fno-asynchronous-unwind-tables", // we rely on frame pointer instead // output options, for integrating clang output into ours "-fno-color-diagnostics", "-Wall", "-Wextra", // with -w flag or on error // linker and header options, static compilation w/ musl "-static", "-nostdlib", "-I", string_c_str(runtime_path), "-isystem", MUSL_DEV "/include", // linking must follow this exact order MUSL_LIB "/lib/crt1.o", MUSL_LIB "/lib/crti.o", string_c_str(stubs_path), // stub out some software float implementations string_c_str(source_path), MUSL_LIB "/lib/libc.a", MUSL_LIB "/lib/crtn.o", "-o", string_c_str(output_path), "2>&1", // errors are output to stderr, we want to capture them }; // append all flags to single command ascii command[1024] = { 0 }; for (uint i = 0; i < ARRAY_SIZE(arguments); ++i) { strcat(command, arguments[i]); strcat(command, " "); } struct Compiler_Command_Result result = run_compiler_command(command); return (struct Build_Result){ .success = result.exit_code == 0, .compiler_exit_code = result.exit_code, .compiler_log = result.log, .output_path = output_path, }; } #endif #ifdef CATBOOT_BACKEND_TCC #include #define BUILD_TCC_MAX_ERROR_LOG_LENGTH 65536 static struct String_Buffer tcc_errors; bool tcc_errors_truncated; void build_tcc_on_error(void* something, const ascii* message) { (void)something; if (tcc_errors_truncated) return; const ascii* log_prefix = ANSI_BOLD " tcc: " ANSI_NO_BOLD; const ascii* log_suffix = "\n"; uint added_length = strlen(message) + strlen(log_prefix) + strlen(log_suffix); uint free_space = string_buffer_capacity(&tcc_errors) - string_buffer_length(&tcc_errors); if (free_space <= added_length) { tcc_errors_truncated = true; return; } string_buffer_append_c_str(&tcc_errors, log_prefix); string_buffer_append_c_str(&tcc_errors, message); string_buffer_append_c_str(&tcc_errors, log_suffix); } void build_tcc_reset_errors() { if (tcc_errors.data == nil) tcc_errors = string_buffer_new(BUILD_TCC_MAX_ERROR_LOG_LENGTH); string_buffer_clear(&tcc_errors); tcc_errors_truncated = false; } struct Build_Result compile_using_backend_tcc( struct String build_path, struct String source_path, struct String output_path) { TCCState* tcc = tcc_new(); if (!tcc) { log_error("failed to initialize backend (TCC) compiler\n"); goto error; } build_tcc_reset_errors(); tcc_set_error_func(tcc, nil, build_tcc_on_error); // NOTE: this has to be called prior to `tcc_set_output_type`, apparently setting the output // to EXE pulls in some kind of default runtime which declares the _start & co. symbols before we can! tcc_set_options(tcc, "-static -nostdlib -nostdinc"); // we only want a simple executable output. // other options can create a dynamic library, or add the output // to our memory to run directly, like for a just-in-time compiler, pretty neat! tcc_set_output_type(tcc, TCC_OUTPUT_EXE); // add necessary include paths struct String runtime_path = string_append_c_str(build_path, "/runtime/"); tcc_add_include_path(tcc, string_c_str(runtime_path)); tcc_add_sysinclude_path(tcc, MUSL_DEV "/include"); // linking order, must be exact: // crt1.o -> crti.o -> stubs.c -> source.c -> libc.a -> crtn.o tcc_add_file(tcc, MUSL_LIB "/lib/crt1.o"); tcc_add_file(tcc, MUSL_LIB "/lib/crti.o"); struct String stubs_path = string_append_c_str(runtime_path, "stubs.c"); tcc_add_file(tcc, string_c_str(stubs_path)); if (tcc_add_file(tcc, string_c_str(source_path)) == -1) { log_error( "failed to add transpiled source to backend compiler (TCC), probably malformed?\n"); goto error; } tcc_add_file(tcc, MUSL_LIB "/lib/libc.a"); tcc_add_file(tcc, MUSL_LIB "/lib/crtn.o"); // now run the compilation, and output! if (tcc_output_file(tcc, string_c_str(output_path)) == -1) { log_error("error during backend (TCC) compilation!\n"); goto error; } tcc_delete(tcc); return (struct Build_Result){ .success = true }; error: tcc_delete(tcc); struct String log = string_buffer_to_string(&tcc_errors); return (struct Build_Result){ .success = false, .compiler_exit_code = -1, .compiler_log = log, }; } #endif struct Build_Result compile_using_backend( struct String build_path, struct String source_path, struct String output_path) { #if defined(CATBOOT_BACKEND_CLANG) log_debug("current backend: LLVM Clang (CLI)\n"); return compile_using_backend_clang(build_path, source_path, output_path); #elif defined(CATBOOT_BACKEND_TCC) log_debug("current backend: TCC (libtcc)\n"); return compile_using_backend_tcc(build_path, source_path, output_path); #else #error "no backend defined for compilation" #endif } struct Build_Result build_executable(struct String source, struct String output_path) { struct String build_path; if (!create_build_directory(&build_path)) { log_error("failed to create build directory\n"); return (struct Build_Result){}; } log_debug("temporary build directory for compilation: %s\n", build_path); // temporary input and output paths struct String build_input = string_append_c_str(build_path, "/input.c"); struct String build_output = string_append_c_str(build_path, "/output"); if (!write_build_file(source, build_input)) { log_error("failed to create input source file\n"); return (struct Build_Result){}; } // the runtime library files need to be present in the build directory // for inclusion within the generated c source. if (!copy_runtime_library(build_path)) { log_error("failed to copy runtime library files\n"); return (struct Build_Result){}; } struct Build_Result result = compile_using_backend(build_path, build_input, build_output); if (!result.success) { return result; } // copy the output executable to the desired location if (!copy_file(build_output, output_path)) { log_error("failed to copy final executable to %s\n", output_path); return (struct Build_Result){}; } result.output_path = output_path; return result; }