// build.h — Minimal C build system (stb-style single header) // // Define BUILD_IMPLEMENTATION in exactly one file before including this header. // // Bootstrap (one-time): // Windows: cl /nologo build.c // macOS: cc build.c -o build // After that, just run ./build (or build.exe) — it rebuilds itself. #ifndef BUILD_H #define BUILD_H #ifdef _MSC_VER # define _CRT_SECURE_NO_WARNINGS #endif #include #include #include #include #include #include #include #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN # include # include # include #else # include # include # include #endif //////////////////////////////// // Macros #define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) // Self-rebuild: call at the top of main(). Recompiles the build script // if the source file is newer than the running binary, then re-executes. #define GO_REBUILD_URSELF(argc, argv) \ go_rebuild_urself((argc), (argv), __FILE__) //////////////////////////////// // Logging typedef enum { LOG_INFO, LOG_WARNING, LOG_ERROR, } Log_Level; void build_log(Log_Level level, const char *fmt, ...); //////////////////////////////// // Temp allocator — ring buffer for short-lived sprintf results #ifndef TEMP_CAPACITY #define TEMP_CAPACITY (8 * 1024 * 1024) #endif char *temp_sprintf(const char *fmt, ...); void temp_reset(void); //////////////////////////////// // String builder — growable byte buffer typedef struct { char *items; size_t count; size_t capacity; } String_Builder; bool sb_read_file(String_Builder *sb, const char *path); void sb_free(String_Builder *sb); //////////////////////////////// // File I/O bool write_entire_file(const char *path, const void *data, size_t size); bool delete_file(const char *path); bool rename_file(const char *old_path, const char *new_path); bool mkdir_if_not_exists(const char *path); //////////////////////////////// // Rebuild checking — returns 1 if rebuild needed, 0 if up to date, -1 on error int needs_rebuild(const char *output, const char **inputs, size_t count); int needs_rebuild1(const char *output, const char *input); //////////////////////////////// // Command builder and runner typedef struct { const char **items; size_t count; size_t capacity; } Cmd; // Appends arguments to a command. Use via the macro which auto-counts args. void cmd__append(Cmd *cmd, size_t n, ...); void cmd__append_arr(Cmd *cmd, const char **args, size_t n); #define cmd_append(cmd, ...) do { \ const char *_cmd_args[] = {__VA_ARGS__}; \ cmd__append_arr((cmd), _cmd_args, sizeof(_cmd_args) / sizeof(_cmd_args[0])); \ } while(0) // Runs the command synchronously, resets cmd->count to 0, returns success. bool cmd_run(Cmd *cmd); // Frees the command's allocated memory. void cmd_free(Cmd *cmd); //////////////////////////////// // Self-rebuild void go_rebuild_urself(int argc, char **argv, const char *source); #endif // BUILD_H //////////////////////////////// // Implementation //////////////////////////////// #ifdef BUILD_IMPLEMENTATION //////////////////////////////// // Temp allocator static size_t g_temp_size = 0; static char g_temp[TEMP_CAPACITY]; void temp_reset(void) { g_temp_size = 0; } static char *temp_alloc(size_t size) { if (g_temp_size + size > TEMP_CAPACITY) { g_temp_size = 0; // wrap around } char *result = g_temp + g_temp_size; g_temp_size += size; return result; } char *temp_sprintf(const char *fmt, ...) { va_list args; va_start(args, fmt); va_list args2; va_copy(args2, args); int n = vsnprintf(NULL, 0, fmt, args); va_end(args); char *result = temp_alloc(n + 1); vsnprintf(result, n + 1, fmt, args2); va_end(args2); return result; } //////////////////////////////// // Logging void build_log(Log_Level level, const char *fmt, ...) { switch (level) { case LOG_INFO: fprintf(stderr, "[INFO] "); break; case LOG_WARNING: fprintf(stderr, "[WARNING] "); break; case LOG_ERROR: fprintf(stderr, "[ERROR] "); break; } va_list args; va_start(args, fmt); vfprintf(stderr, fmt, args); va_end(args); fprintf(stderr, "\n"); } //////////////////////////////// // String builder static void sb_ensure(String_Builder *sb, size_t needed) { if (sb->count + needed <= sb->capacity) return; size_t new_cap = sb->capacity ? sb->capacity * 2 : 256; while (new_cap < sb->count + needed) new_cap *= 2; sb->items = (char *)realloc(sb->items, new_cap); sb->capacity = new_cap; } bool sb_read_file(String_Builder *sb, const char *path) { FILE *f = fopen(path, "rb"); if (!f) { build_log(LOG_ERROR, "Could not open %s: %s", path, strerror(errno)); return false; } fseek(f, 0, SEEK_END); #ifdef _WIN32 long long m = _telli64(_fileno(f)); #else long long m = ftell(f); #endif if (m < 0) { build_log(LOG_ERROR, "Could not get size of %s: %s", path, strerror(errno)); fclose(f); return false; } fseek(f, 0, SEEK_SET); sb_ensure(sb, (size_t)m); fread(sb->items + sb->count, (size_t)m, 1, f); if (ferror(f)) { build_log(LOG_ERROR, "Could not read %s: %s", path, strerror(errno)); fclose(f); return false; } sb->count += (size_t)m; fclose(f); return true; } void sb_free(String_Builder *sb) { free(sb->items); sb->items = NULL; sb->count = 0; sb->capacity = 0; } //////////////////////////////// // File I/O bool write_entire_file(const char *path, const void *data, size_t size) { FILE *f = fopen(path, "wb"); if (!f) { build_log(LOG_ERROR, "Could not open %s for writing: %s", path, strerror(errno)); return false; } const char *buf = (const char *)data; while (size > 0) { size_t n = fwrite(buf, 1, size, f); if (ferror(f)) { build_log(LOG_ERROR, "Could not write to %s: %s", path, strerror(errno)); fclose(f); return false; } size -= n; buf += n; } fclose(f); return true; } bool delete_file(const char *path) { build_log(LOG_INFO, "Deleting %s", path); #ifdef _WIN32 DWORD attr = GetFileAttributesA(path); if (attr == INVALID_FILE_ATTRIBUTES) return true; // doesn't exist if (attr & FILE_ATTRIBUTE_DIRECTORY) { if (!RemoveDirectoryA(path)) { build_log(LOG_ERROR, "Could not delete directory %s", path); return false; } } else { if (!DeleteFileA(path)) { build_log(LOG_ERROR, "Could not delete file %s", path); return false; } } #else if (remove(path) < 0 && errno != ENOENT) { build_log(LOG_ERROR, "Could not delete %s: %s", path, strerror(errno)); return false; } #endif return true; } bool rename_file(const char *old_path, const char *new_path) { build_log(LOG_INFO, "Renaming %s -> %s", old_path, new_path); #ifdef _WIN32 if (!MoveFileEx(old_path, new_path, MOVEFILE_REPLACE_EXISTING)) { build_log(LOG_ERROR, "Could not rename %s to %s", old_path, new_path); return false; } #else if (rename(old_path, new_path) < 0) { build_log(LOG_ERROR, "Could not rename %s to %s: %s", old_path, new_path, strerror(errno)); return false; } #endif return true; } bool mkdir_if_not_exists(const char *path) { #ifdef _WIN32 int result = _mkdir(path); #else int result = mkdir(path, 0755); #endif if (result < 0) { if (errno == EEXIST) return true; build_log(LOG_ERROR, "Could not create directory %s: %s", path, strerror(errno)); return false; } build_log(LOG_INFO, "Created directory %s", path); return true; } //////////////////////////////// // Rebuild checking int needs_rebuild(const char *output, const char **inputs, size_t count) { #ifdef _WIN32 HANDLE out_h = CreateFile(output, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); if (out_h == INVALID_HANDLE_VALUE) { if (GetLastError() == ERROR_FILE_NOT_FOUND) return 1; build_log(LOG_ERROR, "Could not open %s", output); return -1; } FILETIME out_time; if (!GetFileTime(out_h, NULL, NULL, &out_time)) { CloseHandle(out_h); build_log(LOG_ERROR, "Could not get time of %s", output); return -1; } CloseHandle(out_h); for (size_t i = 0; i < count; i++) { HANDLE in_h = CreateFile(inputs[i], GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); if (in_h == INVALID_HANDLE_VALUE) { build_log(LOG_ERROR, "Could not open %s", inputs[i]); return -1; } FILETIME in_time; if (!GetFileTime(in_h, NULL, NULL, &in_time)) { CloseHandle(in_h); build_log(LOG_ERROR, "Could not get time of %s", inputs[i]); return -1; } CloseHandle(in_h); if (CompareFileTime(&in_time, &out_time) == 1) return 1; } return 0; #else struct stat sb; memset(&sb, 0, sizeof(sb)); if (stat(output, &sb) < 0) { if (errno == ENOENT) return 1; build_log(LOG_ERROR, "Could not stat %s: %s", output, strerror(errno)); return -1; } time_t out_time = sb.st_mtime; for (size_t i = 0; i < count; i++) { if (stat(inputs[i], &sb) < 0) { build_log(LOG_ERROR, "Could not stat %s: %s", inputs[i], strerror(errno)); return -1; } if (sb.st_mtime > out_time) return 1; } return 0; #endif } int needs_rebuild1(const char *output, const char *input) { return needs_rebuild(output, &input, 1); } //////////////////////////////// // Command builder and runner static void cmd__grow(Cmd *cmd) { size_t new_cap = cmd->capacity ? cmd->capacity * 2 : 32; cmd->items = (const char **)realloc(cmd->items, new_cap * sizeof(const char *)); cmd->capacity = new_cap; } void cmd__append(Cmd *cmd, size_t n, ...) { va_list args; va_start(args, n); for (size_t i = 0; i < n; i++) { const char *arg = va_arg(args, const char *); if (cmd->count >= cmd->capacity) cmd__grow(cmd); cmd->items[cmd->count++] = arg; } va_end(args); } void cmd__append_arr(Cmd *cmd, const char **args, size_t n) { for (size_t i = 0; i < n; i++) { if (cmd->count >= cmd->capacity) cmd__grow(cmd); cmd->items[cmd->count++] = args[i]; } } static void cmd_render(Cmd *cmd, char *buf, size_t buf_size) { size_t pos = 0; for (size_t i = 0; i < cmd->count && pos < buf_size - 1; i++) { if (i > 0 && pos < buf_size - 1) buf[pos++] = ' '; const char *arg = cmd->items[i]; size_t len = strlen(arg); if (pos + len < buf_size - 1) { memcpy(buf + pos, arg, len); pos += len; } } buf[pos] = '\0'; } #ifdef _WIN32 // Properly quote a command line for CreateProcess on Windows. static void cmd_quote_win32(Cmd *cmd, String_Builder *quoted) { for (size_t i = 0; i < cmd->count; i++) { const char *arg = cmd->items[i]; size_t len = strlen(arg); if (i > 0) { sb_ensure(quoted, 1); quoted->items[quoted->count++] = ' '; } if (len != 0 && strpbrk(arg, " \t\n\v\"") == NULL) { sb_ensure(quoted, len); memcpy(quoted->items + quoted->count, arg, len); quoted->count += len; } else { sb_ensure(quoted, len * 2 + 3); quoted->items[quoted->count++] = '"'; size_t backslashes = 0; for (size_t j = 0; j < len; j++) { char c = arg[j]; if (c == '\\') { backslashes++; } else { if (c == '"') { for (size_t k = 0; k < backslashes + 1; k++) quoted->items[quoted->count++] = '\\'; } backslashes = 0; } quoted->items[quoted->count++] = c; } for (size_t k = 0; k < backslashes; k++) quoted->items[quoted->count++] = '\\'; quoted->items[quoted->count++] = '"'; } } sb_ensure(quoted, 1); quoted->items[quoted->count] = '\0'; } #endif bool cmd_run(Cmd *cmd) { if (cmd->count == 0) { build_log(LOG_ERROR, "Cannot run empty command"); return false; } // Log the command { char render_buf[4096]; cmd_render(cmd, render_buf, sizeof(render_buf)); build_log(LOG_INFO, "CMD: %s", render_buf); } #ifdef _WIN32 STARTUPINFO si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); si.hStdError = GetStdHandle(STD_ERROR_HANDLE); si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); si.dwFlags |= STARTF_USESTDHANDLES; PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi)); String_Builder quoted = {0}; cmd_quote_win32(cmd, "ed); BOOL ok = CreateProcessA(NULL, quoted.items, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi); sb_free("ed); cmd->count = 0; if (!ok) { build_log(LOG_ERROR, "Could not create process for %s", cmd->items[0]); return false; } CloseHandle(pi.hThread); WaitForSingleObject(pi.hProcess, INFINITE); DWORD exit_code; if (!GetExitCodeProcess(pi.hProcess, &exit_code)) { build_log(LOG_ERROR, "Could not get exit code"); CloseHandle(pi.hProcess); return false; } CloseHandle(pi.hProcess); if (exit_code != 0) { build_log(LOG_ERROR, "Command exited with code %lu", exit_code); return false; } return true; #else pid_t pid = fork(); if (pid < 0) { build_log(LOG_ERROR, "Could not fork: %s", strerror(errno)); cmd->count = 0; return false; } if (pid == 0) { // Child: build null-terminated argv const char **argv_null = (const char **)malloc((cmd->count + 1) * sizeof(const char *)); memcpy(argv_null, cmd->items, cmd->count * sizeof(const char *)); argv_null[cmd->count] = NULL; execvp(argv_null[0], (char *const *)argv_null); build_log(LOG_ERROR, "Could not exec %s: %s", argv_null[0], strerror(errno)); exit(1); } cmd->count = 0; int wstatus; for (;;) { if (waitpid(pid, &wstatus, 0) < 0) { build_log(LOG_ERROR, "Could not wait on pid %d: %s", pid, strerror(errno)); return false; } if (WIFEXITED(wstatus)) { int code = WEXITSTATUS(wstatus); if (code != 0) { build_log(LOG_ERROR, "Command exited with code %d", code); return false; } return true; } if (WIFSIGNALED(wstatus)) { build_log(LOG_ERROR, "Command killed by signal %d", WTERMSIG(wstatus)); return false; } } #endif } void cmd_free(Cmd *cmd) { free(cmd->items); cmd->items = NULL; cmd->count = 0; cmd->capacity = 0; } //////////////////////////////// // Self-rebuild #ifdef _WIN32 # define REBUILD_CMD(binary, source) "cl.exe", "/nologo", temp_sprintf("/Fe:%s", binary), source #else # define REBUILD_CMD(binary, source) "cc", "-o", binary, source #endif void go_rebuild_urself(int argc, char **argv, const char *source) { const char *binary = argv[0]; #ifdef _WIN32 // Ensure .exe extension for Windows size_t len = strlen(binary); if (len < 4 || strcmp(binary + len - 4, ".exe") != 0) { binary = temp_sprintf("%s.exe", binary); } #endif int rebuild = needs_rebuild1(binary, source); if (rebuild < 0) exit(1); if (rebuild == 0) return; const char *old_binary = temp_sprintf("%s.old", binary); if (!rename_file(binary, old_binary)) exit(1); Cmd cmd = {0}; cmd_append(&cmd, REBUILD_CMD(binary, source)); if (!cmd_run(&cmd)) { rename_file(old_binary, binary); exit(1); } // Re-execute with the new binary cmd_append(&cmd, binary); { int i; for (i = 1; i < argc; i++) { cmd__append(&cmd, 1, argv[i]); } } if (!cmd_run(&cmd)) exit(1); exit(0); } #endif // BUILD_IMPLEMENTATION