601 lines
16 KiB
C
601 lines
16 KiB
C
// 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 <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <errno.h>
|
|
|
|
#ifdef _WIN32
|
|
# define WIN32_LEAN_AND_MEAN
|
|
# include <windows.h>
|
|
# include <direct.h>
|
|
# include <io.h>
|
|
#else
|
|
# include <sys/stat.h>
|
|
# include <sys/wait.h>
|
|
# include <unistd.h>
|
|
#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
|