includebuild/build.h
2026-03-11 14:57:45 -07:00

2488 lines
91 KiB
C

#ifndef INCLUDEBUILD_H
#define INCLUDEBUILD_H
#if defined(__linux__) && !defined(_POSIX_C_SOURCE)
#define _POSIX_C_SOURCE 200809L
#endif
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define IB_VERSION_MAJOR 2
#define IB_VERSION_MINOR 0
#define IB_VERSION_PATCH 0
#ifndef IB_INITIAL_LIST_CAPACITY
#define IB_INITIAL_LIST_CAPACITY 8
#endif
typedef struct ib_context ib_context;
typedef struct ib_project ib_project;
typedef struct ib_target ib_target;
typedef enum {
IB_OK = 0,
IB_ERR_INVALID,
IB_ERR_IO,
IB_ERR_TOOLCHAIN,
IB_ERR_BUILD,
IB_ERR_NOMEM,
IB_ERR_NOT_FOUND,
IB_ERR_DUPLICATE
} ib_status;
typedef enum {
IB_DIAG_ERROR = 0,
IB_DIAG_WARN = 1,
IB_DIAG_INFO = 2,
IB_DIAG_DEBUG = 3
} ib_diag_level;
typedef enum {
IB_TARGET_EXECUTABLE = 0,
IB_TARGET_STATIC_LIB = 1,
IB_TARGET_SHARED_LIB = 2
} ib_target_kind;
typedef enum {
IB_MODE_DEBUG = 0,
IB_MODE_RELEASE = 1,
IB_MODE_CUSTOM = 2
} ib_build_mode;
ib_context* ib_context_create(void);
void ib_context_destroy(ib_context* ctx);
void ib_context_set_log_level(ib_context* ctx, ib_diag_level level);
void ib_context_set_verbose(ib_context* ctx, bool verbose);
void ib_context_set_color_output(ib_context* ctx, bool enabled);
void ib_context_clear_diagnostics(ib_context* ctx);
ib_status ib_context_last_status(const ib_context* ctx);
const char* ib_context_last_message(const ib_context* ctx);
size_t ib_context_diagnostic_count(const ib_context* ctx);
ib_diag_level ib_context_diagnostic_level_at(const ib_context* ctx, size_t index);
ib_status ib_context_diagnostic_status_at(const ib_context* ctx, size_t index);
const char* ib_context_diagnostic_message_at(const ib_context* ctx, size_t index);
ib_project* ib_project_create(ib_context* ctx, const char* root_dir);
void ib_project_destroy(ib_project* project);
ib_status ib_project_set_c_compiler(ib_project* project, const char* compiler);
ib_status ib_project_set_cxx_compiler(ib_project* project, const char* compiler);
ib_status ib_project_set_archiver(ib_project* project, const char* archiver);
ib_status ib_project_set_output_dir(ib_project* project, const char* dir);
ib_status ib_project_set_state_dir(ib_project* project, const char* dir);
ib_status ib_project_set_mode_flags(ib_project* project, ib_build_mode mode, const char* c_flags, const char* cxx_flags);
ib_status ib_project_add_include_dir(ib_project* project, const char* path);
ib_status ib_project_add_shared_source(ib_project* project, const char* path);
ib_status ib_project_scan_shared_dir(ib_project* project, const char* dir, bool recursive);
ib_target* ib_project_add_target(ib_project* project, const char* name, ib_target_kind kind);
ib_status ib_target_set_entry(ib_target* target, const char* path);
ib_status ib_target_add_source(ib_target* target, const char* path);
ib_status ib_target_add_include_dir(ib_target* target, const char* path);
ib_status ib_target_add_cflags(ib_target* target, const char* flags);
ib_status ib_target_add_link_flags(ib_target* target, const char* flags);
ib_status ib_project_build(ib_project* project, ib_build_mode mode);
ib_status ib_project_clean(ib_project* project);
ib_status ib_project_write_compile_commands(ib_project* project, const char* out_path);
const char* ib_version(void);
#ifdef __cplusplus
}
#endif
#ifdef INCLUDEBUILD_IMPLEMENTATION
#include <ctype.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#include <direct.h>
#define IB_PATH_SEPARATOR '\\'
#if defined(_MSC_VER)
#define popen _popen
#define pclose _pclose
#endif
#else
#include <dirent.h>
#include <unistd.h>
#include <sys/wait.h>
extern FILE* popen(const char* command, const char* mode);
extern int pclose(FILE* stream);
#define IB_PATH_SEPARATOR '/'
#endif
typedef struct {
char** items;
size_t count;
size_t capacity;
} ib_strvec;
typedef struct {
char* data;
size_t length;
size_t capacity;
} ib_builder;
typedef struct {
ib_diag_level level;
ib_status status;
char* message;
} ib_diagnostic;
typedef struct {
ib_diagnostic* items;
size_t count;
size_t capacity;
} ib_diagvec;
struct ib_context {
ib_diagvec diagnostics;
ib_status last_status;
char* last_message;
ib_diag_level log_level;
bool verbose;
bool color_output;
};
struct ib_target {
ib_project* project;
char* name;
ib_target_kind kind;
char* entry_source;
ib_strvec sources;
ib_strvec include_dirs;
ib_strvec cflags;
ib_strvec link_flags;
};
struct ib_project {
ib_context* ctx;
char* root_dir;
char* output_dir;
char* state_dir;
char* compiler_c;
char* compiler_cxx;
char* archiver;
char* debug_cflags;
char* debug_cxxflags;
char* release_cflags;
char* release_cxxflags;
char* custom_cflags;
char* custom_cxxflags;
ib_strvec include_dirs;
ib_strvec shared_sources;
ib_target** targets;
size_t target_count;
size_t target_capacity;
};
typedef struct {
ib_target* target;
char* source_path;
char* source_arg;
bool is_cpp;
char* object_path;
char* object_arg;
char* depfile_path;
char* depfile_arg;
char* manifest_path;
char* command;
unsigned long long command_hash;
} ib_compile_action;
typedef struct {
ib_target* target;
char* output_path;
char* output_arg;
char* manifest_path;
char* command;
unsigned long long command_hash;
size_t compile_index;
size_t compile_count;
} ib_link_action;
typedef struct {
ib_compile_action* compile_actions;
size_t compile_count;
size_t compile_capacity;
ib_link_action* link_actions;
size_t link_count;
size_t link_capacity;
} ib_build_plan;
static char* ib_strdup_internal(const char* text);
static bool ib_status_failed(ib_status status);
static void ib_context_emit(ib_context* ctx, ib_diag_level level, ib_status status, const char* fmt, ...);
static void ib_strvec_init(ib_strvec* vec);
static void ib_strvec_free(ib_strvec* vec);
static bool ib_strvec_push_owned(ib_strvec* vec, char* value);
static bool ib_strvec_push_copy(ib_strvec* vec, const char* value);
static bool ib_strvec_push_unique_owned(ib_strvec* vec, char* value);
static bool ib_strvec_push_unique_copy(ib_strvec* vec, const char* value);
static bool ib_strvec_contains(const ib_strvec* vec, const char* value);
static void ib_strvec_sort(ib_strvec* vec);
static bool ib_builder_append(ib_builder* builder, const char* text);
static bool ib_builder_appendn(ib_builder* builder, const char* text, size_t length);
static bool ib_builder_append_char(ib_builder* builder, char ch);
static bool ib_builder_appendf(ib_builder* builder, const char* fmt, ...);
static char* ib_builder_take(ib_builder* builder);
static void ib_builder_free(ib_builder* builder);
static bool ib_char_is_sep(char ch);
static bool ib_is_absolute_path(const char* path);
static char* ib_get_cwd(ib_context* ctx);
static char* ib_path_join(ib_context* ctx, const char* a, const char* b);
static char* ib_path_normalize(ib_context* ctx, const char* base_abs, const char* input);
static char* ib_path_dirname(ib_context* ctx, const char* path);
static const char* ib_basename_ptr(const char* path);
static bool ib_path_exists(const char* path);
static bool ib_is_dir(const char* path);
static unsigned long long ib_file_mtime(const char* path);
static bool ib_path_is_within(const char* root, const char* path);
static char* ib_path_relative_to_root(ib_context* ctx, const char* root, const char* path);
static char* ib_path_display(ib_context* ctx, const char* root, const char* path);
static char* ib_path_replace_extension(ib_context* ctx, const char* path, const char* new_ext);
static char* ib_path_append_suffix(ib_context* ctx, const char* path, const char* suffix);
static bool ib_has_source_extension(const char* path);
static bool ib_is_cpp_source(const char* path);
static bool ib_should_skip_source_name(const char* name);
static bool ib_should_skip_dir_name(const char* name);
static ib_status ib_remove_recursive(ib_project* project, const char* path);
static ib_status ib_mkdirs(ib_project* project, const char* dir);
static ib_status ib_mkdirs_for_file(ib_project* project, const char* path);
static ib_status ib_collect_sources(ib_project* project, const char* dir, bool recursive, ib_strvec* out);
static unsigned long long ib_hash_string(const char* text);
static const char* ib_mode_name(ib_build_mode mode);
static const char* ib_project_mode_flags(const ib_project* project, bool is_cpp, ib_build_mode mode);
static ib_status ib_set_owned_string(ib_context* ctx, char** dst, const char* value);
static ib_status ib_normalize_existing_source(ib_project* project, const char* input, char** out);
static ib_status ib_normalize_existing_dir(ib_project* project, const char* input, char** out);
static ib_status ib_collect_target_sources(ib_project* project, ib_target* target, ib_strvec* out_sources);
static char* ib_source_object_relpath(ib_project* project, const char* source_path);
static char* ib_target_output_filename(ib_context* ctx, const ib_target* target);
static ib_status ib_target_output_path(ib_project* project, const ib_target* target, char** out_abs, char** out_arg);
static char* ib_shell_quote(ib_context* ctx, const char* text);
static ib_status ib_build_compile_command_for_mode(ib_project* project, ib_target* target, ib_build_mode mode, const char* source_arg, bool is_cpp, const char* object_arg, const char* depfile_arg, char** out_command);
static ib_status ib_build_link_command(ib_project* project, ib_build_plan* plan, ib_target* target, const char* output_arg, size_t compile_index, size_t compile_count, char** out_command);
static ib_status ib_plan_target(ib_project* project, ib_build_mode mode, ib_target* target, ib_build_plan* plan);
static ib_status ib_make_plan(ib_project* project, ib_build_mode mode, ib_build_plan* plan);
static bool ib_read_manifest_hash(const char* path, unsigned long long* out_hash);
static ib_status ib_write_manifest_hash(ib_project* project, const char* path, unsigned long long hash);
static ib_status ib_read_depfile(ib_project* project, const char* depfile_path, ib_strvec* deps);
static ib_status ib_run_command(ib_project* project, const char* command, int* exit_code, char** output);
static bool ib_compile_action_needs_rebuild(ib_project* project, const ib_compile_action* action);
static bool ib_link_action_needs_rebuild(ib_project* project, const ib_build_plan* plan, const ib_link_action* action);
static ib_status ib_execute_compile(ib_project* project, const ib_compile_action* action, bool* rebuilt);
static ib_status ib_execute_link(ib_project* project, const ib_build_plan* plan, const ib_link_action* action, bool* relinked);
static ib_status ib_write_json_string(FILE* fp, const char* text);
static void ib_build_plan_free(ib_build_plan* plan);
static char* ib_strdup_internal(const char* text) {
size_t len;
char* copy;
if (!text) {
text = "";
}
len = strlen(text);
copy = (char*)malloc(len + 1);
if (!copy) {
return NULL;
}
memcpy(copy, text, len + 1);
return copy;
}
static bool ib_status_failed(ib_status status) {
return status != IB_OK;
}
static const char* ib_diag_prefix(ib_diag_level level) {
switch (level) {
case IB_DIAG_ERROR: return "[ERROR] ";
case IB_DIAG_WARN: return "[WARN] ";
case IB_DIAG_INFO: return "[INFO] ";
case IB_DIAG_DEBUG: return "[DEBUG] ";
}
return "[INFO] ";
}
static const char* ib_diag_color(ib_diag_level level) {
switch (level) {
case IB_DIAG_ERROR: return "\x1b[31m";
case IB_DIAG_WARN: return "\x1b[33m";
case IB_DIAG_INFO: return "\x1b[32m";
case IB_DIAG_DEBUG: return "\x1b[36m";
}
return "";
}
static void ib_context_emit(ib_context* ctx, ib_diag_level level, ib_status status, const char* fmt, ...) {
va_list args;
va_list copy;
int needed;
char* message;
FILE* out;
if (!ctx || !fmt) {
return;
}
va_start(args, fmt);
va_copy(copy, args);
needed = vsnprintf(NULL, 0, fmt, copy);
va_end(copy);
if (needed < 0) {
va_end(args);
return;
}
message = (char*)malloc((size_t)needed + 1);
if (!message) {
va_end(args);
return;
}
vsnprintf(message, (size_t)needed + 1, fmt, args);
va_end(args);
if (ctx->diagnostics.count == ctx->diagnostics.capacity) {
size_t next_cap = ctx->diagnostics.capacity ? ctx->diagnostics.capacity * 2 : IB_INITIAL_LIST_CAPACITY;
ib_diagnostic* next = (ib_diagnostic*)realloc(ctx->diagnostics.items, next_cap * sizeof(*next));
if (next) {
ctx->diagnostics.items = next;
ctx->diagnostics.capacity = next_cap;
}
}
if (ctx->diagnostics.count < ctx->diagnostics.capacity) {
ib_diagnostic* diag = &ctx->diagnostics.items[ctx->diagnostics.count++];
diag->level = level;
diag->status = status;
diag->message = ib_strdup_internal(message);
}
ctx->last_status = status;
free(ctx->last_message);
ctx->last_message = ib_strdup_internal(message);
if (level <= ctx->log_level) {
out = (level == IB_DIAG_ERROR) ? stderr : stdout;
if (ctx->color_output) {
fprintf(out, "%s%s\x1b[0m%s\n", ib_diag_color(level), ib_diag_prefix(level), message);
} else {
fprintf(out, "%s%s\n", ib_diag_prefix(level), message);
}
}
free(message);
}
static bool ib_builder_reserve(ib_builder* builder, size_t extra) {
size_t needed = builder->length + extra + 1;
size_t capacity;
char* next;
if (needed <= builder->capacity) {
return true;
}
capacity = builder->capacity ? builder->capacity : 64;
while (capacity < needed) {
capacity *= 2;
}
next = (char*)realloc(builder->data, capacity);
if (!next) {
return false;
}
builder->data = next;
builder->capacity = capacity;
return true;
}
static bool ib_builder_appendn(ib_builder* builder, const char* text, size_t length) {
if (!length) {
return true;
}
if (!ib_builder_reserve(builder, length)) {
return false;
}
memcpy(builder->data + builder->length, text, length);
builder->length += length;
builder->data[builder->length] = '\0';
return true;
}
static bool ib_builder_append(ib_builder* builder, const char* text) {
return ib_builder_appendn(builder, text ? text : "", text ? strlen(text) : 0);
}
static bool ib_builder_append_char(ib_builder* builder, char ch) {
return ib_builder_appendn(builder, &ch, 1);
}
static bool ib_builder_appendf(ib_builder* builder, const char* fmt, ...) {
va_list args;
va_list copy;
int needed;
va_start(args, fmt);
va_copy(copy, args);
needed = vsnprintf(NULL, 0, fmt, copy);
va_end(copy);
if (needed < 0 || !ib_builder_reserve(builder, (size_t)needed)) {
va_end(args);
return false;
}
vsnprintf(builder->data + builder->length, builder->capacity - builder->length, fmt, args);
builder->length += (size_t)needed;
va_end(args);
return true;
}
static char* ib_builder_take(ib_builder* builder) {
char* data = builder->data;
if (!data) {
return ib_strdup_internal("");
}
builder->data = NULL;
builder->length = 0;
builder->capacity = 0;
return data;
}
static void ib_builder_free(ib_builder* builder) {
if (!builder) {
return;
}
free(builder->data);
builder->data = NULL;
builder->length = 0;
builder->capacity = 0;
}
static void ib_strvec_init(ib_strvec* vec) {
vec->items = NULL;
vec->count = 0;
vec->capacity = 0;
}
static bool ib_strvec_reserve(ib_strvec* vec, size_t needed) {
size_t capacity;
char** next;
if (needed <= vec->capacity) {
return true;
}
capacity = vec->capacity ? vec->capacity : IB_INITIAL_LIST_CAPACITY;
while (capacity < needed) {
capacity *= 2;
}
next = (char**)realloc(vec->items, capacity * sizeof(*next));
if (!next) {
return false;
}
vec->items = next;
vec->capacity = capacity;
return true;
}
static bool ib_strvec_push_owned(ib_strvec* vec, char* value) {
if (!ib_strvec_reserve(vec, vec->count + 1)) {
return false;
}
vec->items[vec->count++] = value;
return true;
}
static bool ib_strvec_push_copy(ib_strvec* vec, const char* value) {
char* copy = ib_strdup_internal(value);
if (!copy) {
return false;
}
if (!ib_strvec_push_owned(vec, copy)) {
free(copy);
return false;
}
return true;
}
static bool ib_strvec_contains(const ib_strvec* vec, const char* value) {
size_t i;
for (i = 0; vec && i < vec->count; ++i) {
if (strcmp(vec->items[i], value) == 0) {
return true;
}
}
return false;
}
static bool ib_strvec_push_unique_owned(ib_strvec* vec, char* value) {
if (ib_strvec_contains(vec, value)) {
free(value);
return true;
}
return ib_strvec_push_owned(vec, value);
}
static bool ib_strvec_push_unique_copy(ib_strvec* vec, const char* value) {
if (ib_strvec_contains(vec, value)) {
return true;
}
return ib_strvec_push_copy(vec, value);
}
static void ib_strvec_free(ib_strvec* vec) {
size_t i;
if (!vec) {
return;
}
for (i = 0; i < vec->count; ++i) {
free(vec->items[i]);
}
free(vec->items);
vec->items = NULL;
vec->count = 0;
vec->capacity = 0;
}
static int ib_qsort_strcmp(const void* lhs, const void* rhs) {
const char* const* a = (const char* const*)lhs;
const char* const* b = (const char* const*)rhs;
return strcmp(*a, *b);
}
static void ib_strvec_sort(ib_strvec* vec) {
if (vec && vec->count > 1) {
qsort(vec->items, vec->count, sizeof(*vec->items), ib_qsort_strcmp);
}
}
static bool ib_char_is_sep(char ch) {
return ch == '/' || ch == '\\';
}
static bool ib_is_absolute_path(const char* path) {
if (!path || !path[0]) {
return false;
}
if (ib_char_is_sep(path[0])) {
return true;
}
return isalpha((unsigned char)path[0]) && path[1] == ':' && ib_char_is_sep(path[2]);
}
static char* ib_get_cwd(ib_context* ctx) {
#ifdef _WIN32
DWORD needed = GetCurrentDirectoryA(0, NULL);
char* buffer;
if (needed == 0) {
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to get current working directory");
return NULL;
}
buffer = (char*)malloc((size_t)needed + 1);
if (!buffer) {
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while reading current working directory");
return NULL;
}
if (GetCurrentDirectoryA(needed + 1, buffer) == 0) {
free(buffer);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to get current working directory");
return NULL;
}
return buffer;
#else
size_t size = 256;
char* buffer = NULL;
while (1) {
char* next = (char*)realloc(buffer, size);
if (!next) {
free(buffer);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while reading current working directory");
return NULL;
}
buffer = next;
if (getcwd(buffer, size) != NULL) {
return buffer;
}
if (errno != ERANGE) {
free(buffer);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to get current working directory");
return NULL;
}
size *= 2;
}
#endif
}
static char* ib_path_root(const char* path, size_t* prefix_len) {
char* root;
if (isalpha((unsigned char)path[0]) && path[1] == ':' && ib_char_is_sep(path[2])) {
root = (char*)malloc(4);
if (!root) {
return NULL;
}
root[0] = path[0];
root[1] = ':';
root[2] = IB_PATH_SEPARATOR;
root[3] = '\0';
*prefix_len = 3;
return root;
}
if (ib_char_is_sep(path[0])) {
root = (char*)malloc(2);
if (!root) {
return NULL;
}
root[0] = IB_PATH_SEPARATOR;
root[1] = '\0';
*prefix_len = 1;
return root;
}
*prefix_len = 0;
return ib_strdup_internal("");
}
static char* ib_path_join(ib_context* ctx, const char* a, const char* b) {
ib_builder builder;
size_t len;
memset(&builder, 0, sizeof(builder));
if (!a || !a[0]) {
return ib_strdup_internal(b ? b : "");
}
if (!b || !b[0]) {
return ib_strdup_internal(a);
}
len = strlen(a);
if (!ib_builder_append(&builder, a)) {
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while joining paths");
return NULL;
}
if (a[len - 1] != IB_PATH_SEPARATOR) {
if (!ib_builder_append_char(&builder, IB_PATH_SEPARATOR)) {
ib_builder_free(&builder);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while joining paths");
return NULL;
}
}
while (*b && ib_char_is_sep(*b)) {
++b;
}
if (!ib_builder_append(&builder, b)) {
ib_builder_free(&builder);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while joining paths");
return NULL;
}
return ib_builder_take(&builder);
}
static char* ib_path_normalize(ib_context* ctx, const char* base_abs, const char* input) {
char* working;
char* absolute = NULL;
char* root;
size_t root_len = 0;
ib_strvec segments;
ib_builder result;
size_t i;
size_t start;
if (!input || !input[0]) {
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_INVALID, "Expected a non-empty path");
return NULL;
}
memset(&result, 0, sizeof(result));
ib_strvec_init(&segments);
working = ib_strdup_internal(input);
if (!working) {
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while normalizing path");
return NULL;
}
for (i = 0; working[i]; ++i) {
if (ib_char_is_sep(working[i])) {
working[i] = IB_PATH_SEPARATOR;
}
}
if (ib_is_absolute_path(working)) {
absolute = working;
working = NULL;
} else {
if (!base_abs || !base_abs[0]) {
free(working);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_INVALID, "Cannot resolve relative path '%s' without a base directory", input);
return NULL;
}
absolute = ib_path_join(ctx, base_abs, working);
free(working);
working = NULL;
if (!absolute) {
return NULL;
}
}
root = ib_path_root(absolute, &root_len);
if (!root) {
free(absolute);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while normalizing path");
return NULL;
}
start = root_len;
while (absolute[start]) {
size_t end;
size_t len;
char* segment;
while (absolute[start] == IB_PATH_SEPARATOR) {
++start;
}
if (!absolute[start]) {
break;
}
end = start;
while (absolute[end] && absolute[end] != IB_PATH_SEPARATOR) {
++end;
}
len = end - start;
segment = (char*)malloc(len + 1);
if (!segment) {
ib_strvec_free(&segments);
free(root);
free(absolute);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while normalizing path");
return NULL;
}
memcpy(segment, absolute + start, len);
segment[len] = '\0';
if (strcmp(segment, ".") == 0) {
free(segment);
} else if (strcmp(segment, "..") == 0) {
free(segment);
if (segments.count > 0) {
free(segments.items[--segments.count]);
}
} else if (!ib_strvec_push_owned(&segments, segment)) {
free(segment);
ib_strvec_free(&segments);
free(root);
free(absolute);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while normalizing path");
return NULL;
}
start = end;
}
if (!ib_builder_append(&result, root)) {
ib_builder_free(&result);
ib_strvec_free(&segments);
free(root);
free(absolute);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while normalizing path");
return NULL;
}
for (i = 0; i < segments.count; ++i) {
if (i > 0 || (root[0] && root[strlen(root) - 1] != IB_PATH_SEPARATOR)) {
if (!ib_builder_append_char(&result, IB_PATH_SEPARATOR)) {
ib_builder_free(&result);
ib_strvec_free(&segments);
free(root);
free(absolute);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while normalizing path");
return NULL;
}
}
if (!ib_builder_append(&result, segments.items[i])) {
ib_builder_free(&result);
ib_strvec_free(&segments);
free(root);
free(absolute);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while normalizing path");
return NULL;
}
}
ib_strvec_free(&segments);
free(root);
free(absolute);
return ib_builder_take(&result);
}
static char* ib_path_dirname(ib_context* ctx, const char* path) {
const char* last = strrchr(path ? path : "", IB_PATH_SEPARATOR);
char* out;
size_t len;
(void)ctx;
if (!path || !path[0] || !last) {
return ib_strdup_internal(".");
}
if (last == path) {
return ib_strdup_internal("/");
}
len = (size_t)(last - path);
out = (char*)malloc(len + 1);
if (!out) {
return NULL;
}
memcpy(out, path, len);
out[len] = '\0';
return out;
}
static const char* ib_basename_ptr(const char* path) {
const char* a = strrchr(path ? path : "", '/');
const char* b = strrchr(path ? path : "", '\\');
const char* last = a > b ? a : b;
return last ? last + 1 : (path ? path : "");
}
static bool ib_path_exists(const char* path) {
struct stat st;
return path && stat(path, &st) == 0;
}
static bool ib_is_dir(const char* path) {
struct stat st;
if (!path || stat(path, &st) != 0) {
return false;
}
#ifdef _WIN32
return (st.st_mode & _S_IFDIR) != 0;
#else
return S_ISDIR(st.st_mode);
#endif
}
static unsigned long long ib_file_mtime(const char* path) {
struct stat st;
if (!path || stat(path, &st) != 0) {
return 0;
}
#ifdef _WIN32
return (unsigned long long)st.st_mtime * 1000000000ULL;
#elif defined(__APPLE__)
return ((unsigned long long)st.st_mtimespec.tv_sec * 1000000000ULL) +
(unsigned long long)st.st_mtimespec.tv_nsec;
#elif defined(__linux__)
return ((unsigned long long)st.st_mtim.tv_sec * 1000000000ULL) +
(unsigned long long)st.st_mtim.tv_nsec;
#else
return (unsigned long long)st.st_mtime * 1000000000ULL;
#endif
}
static bool ib_path_component_equal(const char* a, const char* b, size_t len) {
#ifdef _WIN32
size_t i;
for (i = 0; i < len; ++i) {
if (tolower((unsigned char)a[i]) != tolower((unsigned char)b[i])) {
return false;
}
}
return true;
#else
return strncmp(a, b, len) == 0;
#endif
}
static bool ib_path_is_within(const char* root, const char* path) {
size_t root_len;
if (!root || !path) {
return false;
}
root_len = strlen(root);
if (root_len == 0 || !ib_path_component_equal(root, path, root_len)) {
return false;
}
return path[root_len] == '\0' || ib_char_is_sep(path[root_len]);
}
static char* ib_path_relative_to_root(ib_context* ctx, const char* root, const char* path) {
const char* start;
(void)ctx;
if (!ib_path_is_within(root, path)) {
return NULL;
}
start = path + strlen(root);
while (*start && ib_char_is_sep(*start)) {
++start;
}
return ib_strdup_internal(*start ? start : ".");
}
static char* ib_path_display(ib_context* ctx, const char* root, const char* path) {
char* rel = ib_path_relative_to_root(ctx, root, path);
return rel ? rel : ib_strdup_internal(path);
}
static char* ib_path_replace_extension(ib_context* ctx, const char* path, const char* new_ext) {
const char* last_sep = NULL;
const char* dot;
ib_builder builder;
memset(&builder, 0, sizeof(builder));
last_sep = strrchr(path, '/');
{
const char* back = strrchr(path, '\\');
if (back && (!last_sep || back > last_sep)) {
last_sep = back;
}
}
dot = strrchr(path, '.');
if (!dot || (last_sep && dot < last_sep)) {
dot = path + strlen(path);
}
if (!ib_builder_appendn(&builder, path, (size_t)(dot - path)) || !ib_builder_append(&builder, new_ext)) {
ib_builder_free(&builder);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while changing file extension");
return NULL;
}
return ib_builder_take(&builder);
}
static char* ib_path_append_suffix(ib_context* ctx, const char* path, const char* suffix) {
ib_builder builder;
memset(&builder, 0, sizeof(builder));
if (!ib_builder_append(&builder, path) || !ib_builder_append(&builder, suffix)) {
ib_builder_free(&builder);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while extending path");
return NULL;
}
return ib_builder_take(&builder);
}
static bool ib_has_source_extension(const char* path) {
const char* ext = strrchr(path, '.');
return ext && (strcmp(ext, ".c") == 0 || strcmp(ext, ".cpp") == 0 || strcmp(ext, ".cc") == 0 || strcmp(ext, ".cxx") == 0);
}
static bool ib_is_cpp_source(const char* path) {
const char* ext = strrchr(path, '.');
return ext && (strcmp(ext, ".cpp") == 0 || strcmp(ext, ".cc") == 0 || strcmp(ext, ".cxx") == 0);
}
static bool ib_should_skip_source_name(const char* name) {
return strcmp(name, "build.c") == 0 || strcmp(name, "build.cpp") == 0;
}
static bool ib_should_skip_dir_name(const char* name) {
return strcmp(name, ".git") == 0 || strcmp(name, ".svn") == 0 || strcmp(name, ".idea") == 0 ||
strcmp(name, ".vscode") == 0 || strcmp(name, ".ibuild") == 0 || strcmp(name, "node_modules") == 0;
}
static int ib_chdir_to(const char* path) {
#ifdef _WIN32
return _chdir(path);
#else
return chdir(path);
#endif
}
static int ib_remove_dir_single(const char* path) {
#ifdef _WIN32
return _rmdir(path);
#else
return rmdir(path);
#endif
}
static ib_status ib_mkdirs(ib_project* project, const char* dir) {
char* normalized;
char* cursor;
if (!dir || !dir[0]) {
return IB_OK;
}
normalized = ib_path_normalize(project->ctx, project->root_dir, dir);
if (!normalized) {
return project->ctx->last_status;
}
if (ib_path_exists(normalized)) {
free(normalized);
return IB_OK;
}
cursor = normalized;
while (*cursor) {
if (*cursor == ':' && cursor[1] == IB_PATH_SEPARATOR) {
cursor += 2;
continue;
}
if (*cursor == IB_PATH_SEPARATOR && cursor != normalized) {
char saved = *cursor;
*cursor = '\0';
if (normalized[0] && !ib_path_exists(normalized)) {
#ifdef _WIN32
if (_mkdir(normalized) != 0 && errno != EEXIST) {
#else
if (mkdir(normalized, 0755) != 0 && errno != EEXIST) {
#endif
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to create directory '%s': %s", normalized, strerror(errno));
*cursor = saved;
free(normalized);
return IB_ERR_IO;
}
}
*cursor = saved;
}
++cursor;
}
if (!ib_path_exists(normalized)) {
#ifdef _WIN32
if (_mkdir(normalized) != 0 && errno != EEXIST) {
#else
if (mkdir(normalized, 0755) != 0 && errno != EEXIST) {
#endif
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to create directory '%s': %s", normalized, strerror(errno));
free(normalized);
return IB_ERR_IO;
}
}
free(normalized);
return IB_OK;
}
static ib_status ib_mkdirs_for_file(ib_project* project, const char* path) {
char* dir = ib_path_dirname(project->ctx, path);
ib_status status;
if (!dir) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while preparing directories for '%s'", path);
return IB_ERR_NOMEM;
}
status = ib_mkdirs(project, dir);
free(dir);
return status;
}
static ib_status ib_remove_recursive(ib_project* project, const char* path) {
if (!ib_path_exists(path)) {
return IB_OK;
}
if (!ib_is_dir(path)) {
if (remove(path) != 0 && errno != ENOENT) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to remove '%s': %s", path, strerror(errno));
return IB_ERR_IO;
}
return IB_OK;
}
#ifdef _WIN32
{
WIN32_FIND_DATAA fd;
HANDLE handle;
char* pattern = ib_path_join(project->ctx, path, "*");
if (!pattern) {
return project->ctx->last_status;
}
handle = FindFirstFileA(pattern, &fd);
free(pattern);
if (handle != INVALID_HANDLE_VALUE) {
do {
char* child;
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) {
continue;
}
child = ib_path_join(project->ctx, path, fd.cFileName);
if (!child) {
FindClose(handle);
return project->ctx->last_status;
}
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
ib_status status = ib_remove_recursive(project, child);
free(child);
if (ib_status_failed(status)) {
FindClose(handle);
return status;
}
} else {
if (DeleteFileA(child) == 0) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to remove file '%s'", child);
free(child);
FindClose(handle);
return IB_ERR_IO;
}
free(child);
}
} while (FindNextFileA(handle, &fd));
FindClose(handle);
}
}
#else
{
DIR* dir = opendir(path);
struct dirent* ent;
if (!dir) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to open directory '%s': %s", path, strerror(errno));
return IB_ERR_IO;
}
while ((ent = readdir(dir)) != NULL) {
char* child;
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
continue;
}
child = ib_path_join(project->ctx, path, ent->d_name);
if (!child) {
closedir(dir);
return project->ctx->last_status;
}
if (ib_is_dir(child)) {
ib_status status = ib_remove_recursive(project, child);
free(child);
if (ib_status_failed(status)) {
closedir(dir);
return status;
}
} else {
if (remove(child) != 0 && errno != ENOENT) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to remove file '%s': %s", child, strerror(errno));
free(child);
closedir(dir);
return IB_ERR_IO;
}
free(child);
}
}
closedir(dir);
}
#endif
if (ib_remove_dir_single(path) != 0 && errno != ENOENT) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to remove directory '%s': %s", path, strerror(errno));
return IB_ERR_IO;
}
return IB_OK;
}
static ib_status ib_collect_sources(ib_project* project, const char* dir, bool recursive, ib_strvec* out) {
#ifdef _WIN32
WIN32_FIND_DATAA fd;
HANDLE handle;
char* pattern = ib_path_join(project->ctx, dir, "*");
if (!pattern) {
return project->ctx->last_status;
}
handle = FindFirstFileA(pattern, &fd);
free(pattern);
if (handle == INVALID_HANDLE_VALUE) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to scan directory '%s'", dir);
return IB_ERR_IO;
}
do {
char* child;
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0 || ib_should_skip_dir_name(fd.cFileName)) {
continue;
}
child = ib_path_join(project->ctx, dir, fd.cFileName);
if (!child) {
FindClose(handle);
return project->ctx->last_status;
}
if (ib_path_is_within(project->state_dir, child) || (strcmp(project->output_dir, project->root_dir) != 0 && ib_path_is_within(project->output_dir, child))) {
free(child);
continue;
}
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if (recursive) {
ib_status status = ib_collect_sources(project, child, recursive, out);
free(child);
if (ib_status_failed(status)) {
FindClose(handle);
return status;
}
} else {
free(child);
}
} else if (ib_has_source_extension(fd.cFileName) && !ib_should_skip_source_name(fd.cFileName)) {
if (!ib_strvec_push_unique_owned(out, child)) {
free(child);
FindClose(handle);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while collecting sources");
return IB_ERR_NOMEM;
}
} else {
free(child);
}
} while (FindNextFileA(handle, &fd));
FindClose(handle);
#else
DIR* handle = opendir(dir);
struct dirent* ent;
if (!handle) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to scan directory '%s': %s", dir, strerror(errno));
return IB_ERR_IO;
}
while ((ent = readdir(handle)) != NULL) {
char* child;
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0 || ib_should_skip_dir_name(ent->d_name)) {
continue;
}
child = ib_path_join(project->ctx, dir, ent->d_name);
if (!child) {
closedir(handle);
return project->ctx->last_status;
}
if (ib_path_is_within(project->state_dir, child) || (strcmp(project->output_dir, project->root_dir) != 0 && ib_path_is_within(project->output_dir, child))) {
free(child);
continue;
}
if (ib_is_dir(child)) {
if (recursive) {
ib_status status = ib_collect_sources(project, child, recursive, out);
free(child);
if (ib_status_failed(status)) {
closedir(handle);
return status;
}
} else {
free(child);
}
} else if (ib_has_source_extension(ent->d_name) && !ib_should_skip_source_name(ent->d_name)) {
if (!ib_strvec_push_unique_owned(out, child)) {
free(child);
closedir(handle);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while collecting sources");
return IB_ERR_NOMEM;
}
} else {
free(child);
}
}
closedir(handle);
#endif
return IB_OK;
}
static unsigned long long ib_hash_string(const char* text) {
unsigned long long hash = 1469598103934665603ULL;
while (text && *text) {
hash ^= (unsigned char)*text++;
hash *= 1099511628211ULL;
}
return hash;
}
static const char* ib_mode_name(ib_build_mode mode) {
switch (mode) {
case IB_MODE_DEBUG: return "debug";
case IB_MODE_RELEASE: return "release";
case IB_MODE_CUSTOM: return "custom";
}
return "custom";
}
static const char* ib_project_mode_flags(const ib_project* project, bool is_cpp, ib_build_mode mode) {
switch (mode) {
case IB_MODE_DEBUG: return is_cpp ? project->debug_cxxflags : project->debug_cflags;
case IB_MODE_RELEASE: return is_cpp ? project->release_cxxflags : project->release_cflags;
case IB_MODE_CUSTOM: return is_cpp ? project->custom_cxxflags : project->custom_cflags;
}
return "";
}
static ib_status ib_set_owned_string(ib_context* ctx, char** dst, const char* value) {
char* copy = ib_strdup_internal(value ? value : "");
if (!copy) {
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while updating string value");
return IB_ERR_NOMEM;
}
free(*dst);
*dst = copy;
return IB_OK;
}
static ib_status ib_normalize_existing_source(ib_project* project, const char* input, char** out) {
char* path = ib_path_normalize(project->ctx, project->root_dir, input);
if (!path) {
return project->ctx->last_status;
}
if (!ib_path_exists(path)) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOT_FOUND, "Source file '%s' does not exist", input);
free(path);
return IB_ERR_NOT_FOUND;
}
if (ib_is_dir(path) || !ib_has_source_extension(path)) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_INVALID, "Unsupported source path '%s'", input);
free(path);
return IB_ERR_INVALID;
}
*out = path;
return IB_OK;
}
static ib_status ib_normalize_existing_dir(ib_project* project, const char* input, char** out) {
char* path = ib_path_normalize(project->ctx, project->root_dir, input);
if (!path) {
return project->ctx->last_status;
}
if (!ib_path_exists(path) || !ib_is_dir(path)) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOT_FOUND, "Directory '%s' does not exist", input);
free(path);
return IB_ERR_NOT_FOUND;
}
*out = path;
return IB_OK;
}
static ib_status ib_collect_target_sources(ib_project* project, ib_target* target, ib_strvec* out_sources) {
size_t i;
ib_strvec_init(out_sources);
if (target->kind == IB_TARGET_EXECUTABLE && !target->entry_source) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_INVALID, "Target '%s' is missing an entry source", target->name);
return IB_ERR_INVALID;
}
if (target->entry_source && !ib_strvec_push_unique_copy(out_sources, target->entry_source)) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while collecting target sources");
ib_strvec_free(out_sources);
return IB_ERR_NOMEM;
}
for (i = 0; i < project->shared_sources.count; ++i) {
if (!ib_strvec_push_unique_copy(out_sources, project->shared_sources.items[i])) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while collecting target sources");
ib_strvec_free(out_sources);
return IB_ERR_NOMEM;
}
}
for (i = 0; i < target->sources.count; ++i) {
if (!ib_strvec_push_unique_copy(out_sources, target->sources.items[i])) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while collecting target sources");
ib_strvec_free(out_sources);
return IB_ERR_NOMEM;
}
}
if (out_sources->count == 0) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_INVALID, "Target '%s' does not have any sources", target->name);
ib_strvec_free(out_sources);
return IB_ERR_INVALID;
}
return IB_OK;
}
static char* ib_source_object_relpath(ib_project* project, const char* source_path) {
char* rel = ib_path_relative_to_root(project->ctx, project->root_dir, source_path);
if (!rel) {
ib_builder builder;
memset(&builder, 0, sizeof(builder));
if (!ib_builder_append(&builder, "external") ||
!ib_builder_append_char(&builder, IB_PATH_SEPARATOR) ||
!ib_builder_appendf(&builder, "%016llx_", ib_hash_string(source_path)) ||
!ib_builder_append(&builder, ib_basename_ptr(source_path))) {
ib_builder_free(&builder);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while mapping source path");
return NULL;
}
rel = ib_builder_take(&builder);
}
{
char* replaced = ib_path_replace_extension(project->ctx, rel, ".o");
free(rel);
return replaced;
}
}
static char* ib_target_output_filename(ib_context* ctx, const ib_target* target) {
ib_builder builder;
memset(&builder, 0, sizeof(builder));
switch (target->kind) {
case IB_TARGET_EXECUTABLE:
if (!ib_builder_append(&builder, target->name)) {
goto oom;
}
#ifdef _WIN32
if (!ib_builder_append(&builder, ".exe")) {
goto oom;
}
#endif
break;
case IB_TARGET_STATIC_LIB:
if (!ib_builder_append(&builder, "lib") || !ib_builder_append(&builder, target->name) || !ib_builder_append(&builder, ".a")) {
goto oom;
}
break;
case IB_TARGET_SHARED_LIB:
#ifdef _WIN32
if (!ib_builder_append(&builder, target->name) || !ib_builder_append(&builder, ".dll")) {
#elif defined(__APPLE__)
if (!ib_builder_append(&builder, "lib") || !ib_builder_append(&builder, target->name) || !ib_builder_append(&builder, ".dylib")) {
#else
if (!ib_builder_append(&builder, "lib") || !ib_builder_append(&builder, target->name) || !ib_builder_append(&builder, ".so")) {
#endif
goto oom;
}
break;
}
return ib_builder_take(&builder);
oom:
ib_builder_free(&builder);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while building output file name");
return NULL;
}
static ib_status ib_target_output_path(ib_project* project, const ib_target* target, char** out_abs, char** out_arg) {
char* filename = ib_target_output_filename(project->ctx, target);
char* abs_path;
char* arg_path;
if (!filename) {
return project->ctx->last_status;
}
abs_path = ib_path_join(project->ctx, project->output_dir, filename);
free(filename);
if (!abs_path) {
return project->ctx->last_status;
}
arg_path = ib_path_display(project->ctx, project->root_dir, abs_path);
if (!arg_path) {
free(abs_path);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while computing output path");
return IB_ERR_NOMEM;
}
*out_abs = abs_path;
*out_arg = arg_path;
return IB_OK;
}
static char* ib_shell_quote(ib_context* ctx, const char* text) {
ib_builder builder;
const char* p;
bool needs_quotes = false;
if (!text || !text[0]) {
needs_quotes = true;
}
for (p = text; !needs_quotes && p && *p; ++p) {
#ifdef _WIN32
if (isspace((unsigned char)*p) || strchr("\"&|<>()^%!", *p) != NULL) {
#else
if (isspace((unsigned char)*p) || strchr("'\"\\$`!&|;<>(){}[]*?", *p) != NULL) {
#endif
needs_quotes = true;
}
}
if (!needs_quotes && text) {
return ib_strdup_internal(text);
}
memset(&builder, 0, sizeof(builder));
#ifdef _WIN32
if (!ib_builder_append_char(&builder, '"')) {
goto oom;
}
for (p = text; p && *p; ++p) {
if (*p == '"') {
if (!ib_builder_append(&builder, "\\\"")) {
goto oom;
}
} else if (*p == '%') {
if (!ib_builder_append(&builder, "%%")) {
goto oom;
}
} else if (!ib_builder_append_char(&builder, *p)) {
goto oom;
}
}
if (!ib_builder_append_char(&builder, '"')) {
goto oom;
}
#else
if (!ib_builder_append_char(&builder, '\'')) {
goto oom;
}
for (p = text; p && *p; ++p) {
if (*p == '\'') {
if (!ib_builder_append(&builder, "'\"'\"'")) {
goto oom;
}
} else if (!ib_builder_append_char(&builder, *p)) {
goto oom;
}
}
if (!ib_builder_append_char(&builder, '\'')) {
goto oom;
}
#endif
return ib_builder_take(&builder);
oom:
ib_builder_free(&builder);
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while quoting shell argument");
return NULL;
}
static ib_status ib_build_compile_command_for_mode(ib_project* project, ib_target* target, ib_build_mode mode, const char* source_arg, bool is_cpp, const char* object_arg, const char* depfile_arg, char** out_command) {
ib_builder builder;
char* q_compiler = NULL;
char* q_depfile = NULL;
char* q_source = NULL;
char* q_object = NULL;
size_t i;
const char* compiler = is_cpp ? project->compiler_cxx : project->compiler_c;
const char* mode_flags = ib_project_mode_flags(project, is_cpp, mode);
memset(&builder, 0, sizeof(builder));
q_compiler = ib_shell_quote(project->ctx, compiler);
q_depfile = ib_shell_quote(project->ctx, depfile_arg);
q_source = ib_shell_quote(project->ctx, source_arg);
q_object = ib_shell_quote(project->ctx, object_arg);
if (!q_compiler || !q_depfile || !q_source || !q_object) {
free(q_compiler); free(q_depfile); free(q_source); free(q_object);
return project->ctx->last_status;
}
if (!ib_builder_append(&builder, q_compiler)) {
goto oom;
}
if (mode_flags && mode_flags[0]) {
if (!ib_builder_append_char(&builder, ' ') || !ib_builder_append(&builder, mode_flags)) {
goto oom;
}
}
for (i = 0; i < project->include_dirs.count; ++i) {
char* q_dir = ib_shell_quote(project->ctx, project->include_dirs.items[i]);
if (!q_dir || !ib_builder_append(&builder, " -I") || !ib_builder_append(&builder, q_dir)) {
free(q_dir);
goto oom;
}
free(q_dir);
}
for (i = 0; i < target->include_dirs.count; ++i) {
char* q_dir = ib_shell_quote(project->ctx, target->include_dirs.items[i]);
if (!q_dir || !ib_builder_append(&builder, " -I") || !ib_builder_append(&builder, q_dir)) {
free(q_dir);
goto oom;
}
free(q_dir);
}
#ifndef _WIN32
if (target->kind == IB_TARGET_SHARED_LIB) {
if (!ib_builder_append(&builder, " -fPIC")) {
goto oom;
}
}
#endif
for (i = 0; i < target->cflags.count; ++i) {
if (!ib_builder_append_char(&builder, ' ') || !ib_builder_append(&builder, target->cflags.items[i])) {
goto oom;
}
}
if (!ib_builder_append(&builder, " -MMD -MF ") ||
!ib_builder_append(&builder, q_depfile) ||
!ib_builder_append(&builder, " -c ") ||
!ib_builder_append(&builder, q_source) ||
!ib_builder_append(&builder, " -o ") ||
!ib_builder_append(&builder, q_object)) {
goto oom;
}
free(q_compiler); free(q_depfile); free(q_source); free(q_object);
*out_command = ib_builder_take(&builder);
return IB_OK;
oom:
ib_builder_free(&builder);
free(q_compiler); free(q_depfile); free(q_source); free(q_object);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while building compile command");
return IB_ERR_NOMEM;
}
static ib_status ib_build_link_command(ib_project* project, ib_build_plan* plan, ib_target* target, const char* output_arg, size_t compile_index, size_t compile_count, char** out_command) {
ib_builder builder;
char* q_output = NULL;
char* q_tool = NULL;
size_t i;
bool uses_cpp = false;
memset(&builder, 0, sizeof(builder));
q_output = ib_shell_quote(project->ctx, output_arg);
if (!q_output) {
return project->ctx->last_status;
}
if (target->kind == IB_TARGET_STATIC_LIB) {
q_tool = ib_shell_quote(project->ctx, project->archiver);
if (!q_tool || !ib_builder_append(&builder, q_tool) || !ib_builder_append(&builder, " rcs ") || !ib_builder_append(&builder, q_output)) {
goto oom;
}
} else {
for (i = 0; i < compile_count; ++i) {
if (plan->compile_actions[compile_index + i].is_cpp) {
uses_cpp = true;
break;
}
}
q_tool = ib_shell_quote(project->ctx, uses_cpp ? project->compiler_cxx : project->compiler_c);
if (!q_tool || !ib_builder_append(&builder, q_tool)) {
goto oom;
}
if (target->kind == IB_TARGET_SHARED_LIB) {
#ifdef __APPLE__
if (!ib_builder_append(&builder, " -dynamiclib")) {
goto oom;
}
#else
if (!ib_builder_append(&builder, " -shared")) {
goto oom;
}
#endif
}
if (!ib_builder_append(&builder, " -o ") || !ib_builder_append(&builder, q_output)) {
goto oom;
}
}
for (i = 0; i < compile_count; ++i) {
char* q_obj = ib_shell_quote(project->ctx, plan->compile_actions[compile_index + i].object_arg);
if (!q_obj || !ib_builder_append_char(&builder, ' ') || !ib_builder_append(&builder, q_obj)) {
free(q_obj);
goto oom;
}
free(q_obj);
}
if (target->kind != IB_TARGET_STATIC_LIB) {
for (i = 0; i < target->link_flags.count; ++i) {
if (!ib_builder_append_char(&builder, ' ') || !ib_builder_append(&builder, target->link_flags.items[i])) {
goto oom;
}
}
}
free(q_output);
free(q_tool);
*out_command = ib_builder_take(&builder);
return IB_OK;
oom:
ib_builder_free(&builder);
free(q_output);
free(q_tool);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while building link command");
return IB_ERR_NOMEM;
}
static bool ib_plan_compile_reserve(ib_build_plan* plan, size_t needed) {
size_t cap = plan->compile_capacity ? plan->compile_capacity : IB_INITIAL_LIST_CAPACITY;
ib_compile_action* next;
if (needed <= plan->compile_capacity) {
return true;
}
while (cap < needed) {
cap *= 2;
}
next = (ib_compile_action*)realloc(plan->compile_actions, cap * sizeof(*next));
if (!next) {
return false;
}
plan->compile_actions = next;
plan->compile_capacity = cap;
return true;
}
static bool ib_plan_link_reserve(ib_build_plan* plan, size_t needed) {
size_t cap = plan->link_capacity ? plan->link_capacity : IB_INITIAL_LIST_CAPACITY;
ib_link_action* next;
if (needed <= plan->link_capacity) {
return true;
}
while (cap < needed) {
cap *= 2;
}
next = (ib_link_action*)realloc(plan->link_actions, cap * sizeof(*next));
if (!next) {
return false;
}
plan->link_actions = next;
plan->link_capacity = cap;
return true;
}
static void ib_build_plan_free(ib_build_plan* plan) {
size_t i;
if (!plan) {
return;
}
for (i = 0; i < plan->compile_count; ++i) {
free(plan->compile_actions[i].source_path);
free(plan->compile_actions[i].source_arg);
free(plan->compile_actions[i].object_path);
free(plan->compile_actions[i].object_arg);
free(plan->compile_actions[i].depfile_path);
free(plan->compile_actions[i].depfile_arg);
free(plan->compile_actions[i].manifest_path);
free(plan->compile_actions[i].command);
}
for (i = 0; i < plan->link_count; ++i) {
free(plan->link_actions[i].output_path);
free(plan->link_actions[i].output_arg);
free(plan->link_actions[i].manifest_path);
free(plan->link_actions[i].command);
}
free(plan->compile_actions);
free(plan->link_actions);
memset(plan, 0, sizeof(*plan));
}
static ib_status ib_plan_target(ib_project* project, ib_build_mode mode, ib_target* target, ib_build_plan* plan) {
ib_strvec sources;
size_t start_index;
size_t i;
ib_status status = ib_collect_target_sources(project, target, &sources);
if (ib_status_failed(status)) {
return status;
}
start_index = plan->compile_count;
for (i = 0; i < sources.count; ++i) {
char* rel = ib_source_object_relpath(project, sources.items[i]);
char* obj_root;
char* obj_mode;
char* obj_target;
char* object_abs;
char* dep_abs;
char* manifest_abs;
char* source_arg;
char* object_arg;
char* dep_arg;
char* command = NULL;
ib_compile_action* action;
if (!rel) {
ib_strvec_free(&sources);
return project->ctx->last_status;
}
obj_root = ib_path_join(project->ctx, project->state_dir, "obj");
obj_mode = obj_root ? ib_path_join(project->ctx, obj_root, ib_mode_name(mode)) : NULL;
free(obj_root);
obj_target = obj_mode ? ib_path_join(project->ctx, obj_mode, target->name) : NULL;
free(obj_mode);
object_abs = obj_target ? ib_path_join(project->ctx, obj_target, rel) : NULL;
free(obj_target);
free(rel);
if (!object_abs) {
ib_strvec_free(&sources);
return project->ctx->last_status;
}
dep_abs = ib_path_append_suffix(project->ctx, object_abs, ".d");
manifest_abs = ib_path_append_suffix(project->ctx, object_abs, ".manifest");
source_arg = ib_path_display(project->ctx, project->root_dir, sources.items[i]);
object_arg = ib_path_display(project->ctx, project->root_dir, object_abs);
dep_arg = ib_path_display(project->ctx, project->root_dir, dep_abs);
if (!dep_abs || !manifest_abs || !source_arg || !object_arg || !dep_arg) {
free(object_abs); free(dep_abs); free(manifest_abs); free(source_arg); free(object_arg); free(dep_arg);
ib_strvec_free(&sources);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while planning target '%s'", target->name);
return IB_ERR_NOMEM;
}
status = ib_build_compile_command_for_mode(project, target, mode, source_arg, ib_is_cpp_source(sources.items[i]), object_arg, dep_arg, &command);
if (ib_status_failed(status)) {
free(object_abs); free(dep_abs); free(manifest_abs); free(source_arg); free(object_arg); free(dep_arg);
ib_strvec_free(&sources);
return status;
}
if (!ib_plan_compile_reserve(plan, plan->compile_count + 1)) {
free(object_abs); free(dep_abs); free(manifest_abs); free(source_arg); free(object_arg); free(dep_arg); free(command);
ib_strvec_free(&sources);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while planning compile actions");
return IB_ERR_NOMEM;
}
action = &plan->compile_actions[plan->compile_count++];
memset(action, 0, sizeof(*action));
action->target = target;
action->source_path = ib_strdup_internal(sources.items[i]);
action->source_arg = source_arg;
action->is_cpp = ib_is_cpp_source(sources.items[i]);
action->object_path = object_abs;
action->object_arg = object_arg;
action->depfile_path = dep_abs;
action->depfile_arg = dep_arg;
action->manifest_path = manifest_abs;
action->command = command;
action->command_hash = ib_hash_string(command);
if (!action->source_path) {
ib_strvec_free(&sources);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while planning compile actions");
return IB_ERR_NOMEM;
}
}
if (!ib_plan_link_reserve(plan, plan->link_count + 1)) {
ib_strvec_free(&sources);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while planning link actions");
return IB_ERR_NOMEM;
}
{
ib_link_action* link = &plan->link_actions[plan->link_count++];
char* output_abs = NULL;
char* output_arg = NULL;
char* link_root = NULL;
char* link_mode = NULL;
char* link_target = NULL;
memset(link, 0, sizeof(*link));
status = ib_target_output_path(project, target, &output_abs, &output_arg);
if (ib_status_failed(status)) {
ib_strvec_free(&sources);
return status;
}
link_root = ib_path_join(project->ctx, project->state_dir, "link");
link_mode = link_root ? ib_path_join(project->ctx, link_root, ib_mode_name(mode)) : NULL;
free(link_root);
link_target = link_mode ? ib_path_join(project->ctx, link_mode, target->name) : NULL;
free(link_mode);
if (!link_target) {
free(output_abs); free(output_arg);
ib_strvec_free(&sources);
return project->ctx->last_status;
}
link->target = target;
link->output_path = output_abs;
link->output_arg = output_arg;
link->manifest_path = ib_path_append_suffix(project->ctx, link_target, ".manifest");
free(link_target);
link->compile_index = start_index;
link->compile_count = plan->compile_count - start_index;
if (!link->manifest_path) {
ib_strvec_free(&sources);
return project->ctx->last_status;
}
status = ib_build_link_command(project, plan, target, output_arg, link->compile_index, link->compile_count, &link->command);
if (ib_status_failed(status)) {
ib_strvec_free(&sources);
return status;
}
link->command_hash = ib_hash_string(link->command);
}
ib_strvec_free(&sources);
return IB_OK;
}
static ib_status ib_make_plan(ib_project* project, ib_build_mode mode, ib_build_plan* plan) {
size_t i;
memset(plan, 0, sizeof(*plan));
if (!project || project->target_count == 0) {
ib_context_emit(project ? project->ctx : NULL, IB_DIAG_ERROR, IB_ERR_INVALID, "Project has no targets");
return IB_ERR_INVALID;
}
for (i = 0; i < project->target_count; ++i) {
ib_status status = ib_plan_target(project, mode, project->targets[i], plan);
if (ib_status_failed(status)) {
ib_build_plan_free(plan);
return status;
}
}
return IB_OK;
}
static ib_status ib_run_command(ib_project* project, const char* command, int* exit_code, char** output) {
char* saved_cwd = ib_get_cwd(project->ctx);
char* exec_command;
FILE* pipe;
ib_builder captured;
char buffer[4096];
int rc;
memset(&captured, 0, sizeof(captured));
if (!saved_cwd) {
return project->ctx->last_status;
}
if (ib_chdir_to(project->root_dir) != 0) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to change directory to '%s': %s", project->root_dir, strerror(errno));
free(saved_cwd);
return IB_ERR_IO;
}
exec_command = ib_path_append_suffix(project->ctx, command, " 2>&1");
if (!exec_command) {
ib_chdir_to(saved_cwd);
free(saved_cwd);
return project->ctx->last_status;
}
if (project->ctx->verbose) {
ib_context_emit(project->ctx, IB_DIAG_INFO, IB_OK, "Command: %s", command);
}
pipe = popen(exec_command, "r");
free(exec_command);
if (!pipe) {
ib_chdir_to(saved_cwd);
free(saved_cwd);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_TOOLCHAIN, "Failed to start command: %s", command);
return IB_ERR_TOOLCHAIN;
}
while (fgets(buffer, sizeof(buffer), pipe) != NULL) {
if (!ib_builder_append(&captured, buffer)) {
pclose(pipe);
ib_chdir_to(saved_cwd);
free(saved_cwd);
ib_builder_free(&captured);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while capturing command output");
return IB_ERR_NOMEM;
}
if (project->ctx->verbose) {
fputs(buffer, stdout);
}
}
rc = pclose(pipe);
#ifndef _WIN32
if (rc != -1 && WIFEXITED(rc)) {
rc = WEXITSTATUS(rc);
}
#endif
ib_chdir_to(saved_cwd);
free(saved_cwd);
if (exit_code) {
*exit_code = rc;
}
if (output) {
*output = ib_builder_take(&captured);
} else {
ib_builder_free(&captured);
}
return IB_OK;
}
static bool ib_read_manifest_hash(const char* path, unsigned long long* out_hash) {
FILE* fp = fopen(path, "r");
unsigned long long hash = 0;
if (!fp) {
return false;
}
if (fscanf(fp, "%llx", &hash) != 1) {
fclose(fp);
return false;
}
fclose(fp);
if (out_hash) {
*out_hash = hash;
}
return true;
}
static ib_status ib_write_manifest_hash(ib_project* project, const char* path, unsigned long long hash) {
FILE* fp;
ib_status status = ib_mkdirs_for_file(project, path);
if (ib_status_failed(status)) {
return status;
}
fp = fopen(path, "w");
if (!fp) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to write manifest '%s': %s", path, strerror(errno));
return IB_ERR_IO;
}
fprintf(fp, "%016llx\n", hash);
fclose(fp);
return IB_OK;
}
static ib_status ib_read_depfile(ib_project* project, const char* depfile_path, ib_strvec* deps) {
FILE* fp = fopen(depfile_path, "r");
ib_builder content;
char buffer[4096];
char* raw;
char* colon;
char* cursor;
ib_strvec_init(deps);
memset(&content, 0, sizeof(content));
if (!fp) {
return IB_ERR_IO;
}
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
if (!ib_builder_append(&content, buffer)) {
fclose(fp);
ib_builder_free(&content);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while parsing depfile");
return IB_ERR_NOMEM;
}
}
fclose(fp);
raw = ib_builder_take(&content);
if (!raw) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while parsing depfile");
return IB_ERR_NOMEM;
}
while ((cursor = strstr(raw, "\\\r\n")) != NULL) {
memmove(cursor, cursor + 3, strlen(cursor + 3) + 1);
*cursor = ' ';
}
while ((cursor = strstr(raw, "\\\n")) != NULL) {
memmove(cursor, cursor + 2, strlen(cursor + 2) + 1);
*cursor = ' ';
}
colon = NULL;
for (cursor = raw; *cursor; ++cursor) {
if (*cursor == ':' && (cursor[1] == '\0' || isspace((unsigned char)cursor[1]))) {
colon = cursor;
break;
}
}
if (!colon) {
free(raw);
return IB_ERR_IO;
}
cursor = colon + 1;
while (*cursor) {
ib_builder token;
char* text;
char* dep_abs;
memset(&token, 0, sizeof(token));
while (*cursor && isspace((unsigned char)*cursor)) {
++cursor;
}
if (!*cursor) {
break;
}
while (*cursor && !isspace((unsigned char)*cursor)) {
if (*cursor == '\\' && cursor[1] != '\0') {
if (!ib_builder_append_char(&token, cursor[1])) {
ib_builder_free(&token);
ib_strvec_free(deps);
free(raw);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while parsing depfile");
return IB_ERR_NOMEM;
}
cursor += 2;
} else if (!ib_builder_append_char(&token, *cursor++)) {
ib_builder_free(&token);
ib_strvec_free(deps);
free(raw);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while parsing depfile");
return IB_ERR_NOMEM;
}
}
text = ib_builder_take(&token);
if (!text || !text[0]) {
free(text);
continue;
}
dep_abs = ib_path_normalize(project->ctx, project->root_dir, text);
free(text);
if (!dep_abs) {
ib_strvec_free(deps);
free(raw);
return project->ctx->last_status;
}
if (!ib_strvec_push_unique_owned(deps, dep_abs)) {
free(dep_abs);
ib_strvec_free(deps);
free(raw);
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while parsing depfile");
return IB_ERR_NOMEM;
}
}
free(raw);
return IB_OK;
}
static bool ib_compile_action_needs_rebuild(ib_project* project, const ib_compile_action* action) {
unsigned long long manifest_hash = 0;
ib_strvec deps;
size_t i;
unsigned long long obj_mtime;
if (!ib_path_exists(action->object_path) || !ib_path_exists(action->depfile_path) || !ib_path_exists(action->manifest_path)) {
return true;
}
if (!ib_read_manifest_hash(action->manifest_path, &manifest_hash) || manifest_hash != action->command_hash) {
return true;
}
obj_mtime = ib_file_mtime(action->object_path);
if (obj_mtime == 0 || ib_status_failed(ib_read_depfile(project, action->depfile_path, &deps))) {
return true;
}
for (i = 0; i < deps.count; ++i) {
unsigned long long dep_mtime = ib_file_mtime(deps.items[i]);
if (dep_mtime == 0 || dep_mtime > obj_mtime) {
ib_strvec_free(&deps);
return true;
}
}
ib_strvec_free(&deps);
return false;
}
static bool ib_link_action_needs_rebuild(ib_project* project, const ib_build_plan* plan, const ib_link_action* action) {
unsigned long long manifest_hash = 0;
size_t i;
unsigned long long out_mtime;
if (!ib_path_exists(action->output_path) || !ib_path_exists(action->manifest_path)) {
return true;
}
if (!ib_read_manifest_hash(action->manifest_path, &manifest_hash) || manifest_hash != action->command_hash) {
return true;
}
out_mtime = ib_file_mtime(action->output_path);
if (out_mtime == 0) {
return true;
}
for (i = 0; i < action->compile_count; ++i) {
const ib_compile_action* compile = &plan->compile_actions[action->compile_index + i];
if (!ib_path_exists(compile->object_path) || ib_file_mtime(compile->object_path) > out_mtime) {
return true;
}
}
(void)project;
return false;
}
static ib_status ib_execute_compile(ib_project* project, const ib_compile_action* action, bool* rebuilt) {
int rc = 0;
char* output = NULL;
ib_status status;
if (rebuilt) {
*rebuilt = false;
}
if (!ib_compile_action_needs_rebuild(project, action)) {
return IB_OK;
}
status = ib_mkdirs_for_file(project, action->object_path);
if (ib_status_failed(status)) {
return status;
}
ib_context_emit(project->ctx, IB_DIAG_INFO, IB_OK, "Compiling %s for %s", action->source_arg, action->target->name);
status = ib_run_command(project, action->command, &rc, &output);
if (ib_status_failed(status)) {
free(output);
return status;
}
if (rc != 0) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_BUILD, "Compilation failed for %s (exit %d)%s%s", action->source_arg, rc, output && output[0] ? ":\n" : "", output && output[0] ? output : "");
free(output);
return IB_ERR_BUILD;
}
free(output);
status = ib_write_manifest_hash(project, action->manifest_path, action->command_hash);
if (!ib_status_failed(status) && rebuilt) {
*rebuilt = true;
}
return status;
}
static ib_status ib_execute_link(ib_project* project, const ib_build_plan* plan, const ib_link_action* action, bool* relinked) {
int rc = 0;
char* output = NULL;
ib_status status;
if (relinked) {
*relinked = false;
}
if (!ib_link_action_needs_rebuild(project, plan, action)) {
return IB_OK;
}
status = ib_mkdirs_for_file(project, action->output_path);
if (ib_status_failed(status)) {
return status;
}
ib_context_emit(project->ctx, IB_DIAG_INFO, IB_OK, "Linking %s", action->target->name);
status = ib_run_command(project, action->command, &rc, &output);
if (ib_status_failed(status)) {
free(output);
return status;
}
if (rc != 0) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_BUILD, "Link failed for %s (exit %d)%s%s", action->target->name, rc, output && output[0] ? ":\n" : "", output && output[0] ? output : "");
free(output);
return IB_ERR_BUILD;
}
free(output);
status = ib_write_manifest_hash(project, action->manifest_path, action->command_hash);
if (!ib_status_failed(status) && relinked) {
*relinked = true;
}
return status;
}
static ib_status ib_write_json_string(FILE* fp, const char* text) {
const unsigned char* p = (const unsigned char*)text;
fputc('"', fp);
while (*p) {
switch (*p) {
case '\\': fputs("\\\\", fp); break;
case '"': fputs("\\\"", fp); break;
case '\n': fputs("\\n", fp); break;
case '\r': fputs("\\r", fp); break;
case '\t': fputs("\\t", fp); break;
default:
if (*p < 0x20) fprintf(fp, "\\u%04x", *p);
else fputc(*p, fp);
break;
}
++p;
}
fputc('"', fp);
return IB_OK;
}
ib_context* ib_context_create(void) {
ib_context* ctx = (ib_context*)calloc(1, sizeof(*ctx));
if (!ctx) {
return NULL;
}
ctx->last_status = IB_OK;
ctx->last_message = ib_strdup_internal("");
ctx->log_level = IB_DIAG_INFO;
ctx->verbose = false;
ctx->color_output = true;
return ctx;
}
void ib_context_destroy(ib_context* ctx) {
size_t i;
if (!ctx) {
return;
}
for (i = 0; i < ctx->diagnostics.count; ++i) {
free(ctx->diagnostics.items[i].message);
}
free(ctx->diagnostics.items);
free(ctx->last_message);
free(ctx);
}
void ib_context_set_log_level(ib_context* ctx, ib_diag_level level) { if (ctx) ctx->log_level = level; }
void ib_context_set_verbose(ib_context* ctx, bool verbose) { if (ctx) ctx->verbose = verbose; }
void ib_context_set_color_output(ib_context* ctx, bool enabled) { if (ctx) ctx->color_output = enabled; }
void ib_context_clear_diagnostics(ib_context* ctx) {
size_t i;
if (!ctx) return;
for (i = 0; i < ctx->diagnostics.count; ++i) free(ctx->diagnostics.items[i].message);
ctx->diagnostics.count = 0;
ctx->last_status = IB_OK;
free(ctx->last_message);
ctx->last_message = ib_strdup_internal("");
}
ib_status ib_context_last_status(const ib_context* ctx) { return ctx ? ctx->last_status : IB_ERR_INVALID; }
const char* ib_context_last_message(const ib_context* ctx) { return (ctx && ctx->last_message) ? ctx->last_message : ""; }
size_t ib_context_diagnostic_count(const ib_context* ctx) { return ctx ? ctx->diagnostics.count : 0; }
ib_diag_level ib_context_diagnostic_level_at(const ib_context* ctx, size_t index) { return (!ctx || index >= ctx->diagnostics.count) ? IB_DIAG_ERROR : ctx->diagnostics.items[index].level; }
ib_status ib_context_diagnostic_status_at(const ib_context* ctx, size_t index) { return (!ctx || index >= ctx->diagnostics.count) ? IB_ERR_INVALID : ctx->diagnostics.items[index].status; }
const char* ib_context_diagnostic_message_at(const ib_context* ctx, size_t index) { return (!ctx || index >= ctx->diagnostics.count || !ctx->diagnostics.items[index].message) ? "" : ctx->diagnostics.items[index].message; }
ib_project* ib_project_create(ib_context* ctx, const char* root_dir) {
ib_project* project;
char* cwd;
if (!ctx) return NULL;
project = (ib_project*)calloc(1, sizeof(*project));
if (!project) {
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while creating project");
return NULL;
}
project->ctx = ctx;
cwd = ib_get_cwd(ctx);
if (!cwd) { free(project); return NULL; }
project->root_dir = ib_path_normalize(ctx, cwd, root_dir && root_dir[0] ? root_dir : ".");
free(cwd);
if (!project->root_dir || !ib_path_exists(project->root_dir) || !ib_is_dir(project->root_dir)) {
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOT_FOUND, "Project root '%s' does not exist", root_dir && root_dir[0] ? root_dir : ".");
ib_project_destroy(project);
return NULL;
}
project->output_dir = ib_strdup_internal(project->root_dir);
project->state_dir = ib_path_join(ctx, project->root_dir, ".ibuild");
#ifdef _WIN32
project->compiler_c = ib_strdup_internal("gcc");
project->compiler_cxx = ib_strdup_internal("g++");
#else
project->compiler_c = ib_strdup_internal("cc");
project->compiler_cxx = ib_strdup_internal("c++");
#endif
project->archiver = ib_strdup_internal("ar");
project->debug_cflags = ib_strdup_internal("-g -O0 -DDEBUG");
project->debug_cxxflags = ib_strdup_internal("-g -O0 -DDEBUG");
project->release_cflags = ib_strdup_internal("-O2 -DNDEBUG");
project->release_cxxflags = ib_strdup_internal("-O2 -DNDEBUG");
project->custom_cflags = ib_strdup_internal("");
project->custom_cxxflags = ib_strdup_internal("");
ib_strvec_init(&project->include_dirs);
ib_strvec_init(&project->shared_sources);
if (!project->output_dir || !project->state_dir || !project->compiler_c || !project->compiler_cxx || !project->archiver || !project->debug_cflags || !project->debug_cxxflags || !project->release_cflags || !project->release_cxxflags || !project->custom_cflags || !project->custom_cxxflags) {
ib_context_emit(ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while initializing project");
ib_project_destroy(project);
return NULL;
}
ib_context_emit(ctx, IB_DIAG_INFO, IB_OK, "IncludeBuild %s project created at %s", ib_version(), project->root_dir);
return project;
}
void ib_project_destroy(ib_project* project) {
size_t i;
if (!project) return;
for (i = 0; i < project->target_count; ++i) {
ib_target* target = project->targets[i];
if (!target) continue;
free(target->name);
free(target->entry_source);
ib_strvec_free(&target->sources);
ib_strvec_free(&target->include_dirs);
ib_strvec_free(&target->cflags);
ib_strvec_free(&target->link_flags);
free(target);
}
free(project->targets);
free(project->root_dir); free(project->output_dir); free(project->state_dir);
free(project->compiler_c); free(project->compiler_cxx); free(project->archiver);
free(project->debug_cflags); free(project->debug_cxxflags); free(project->release_cflags); free(project->release_cxxflags);
free(project->custom_cflags); free(project->custom_cxxflags);
ib_strvec_free(&project->include_dirs); ib_strvec_free(&project->shared_sources);
free(project);
}
ib_status ib_project_set_c_compiler(ib_project* project, const char* compiler) { return (!project || !compiler || !compiler[0]) ? IB_ERR_INVALID : ib_set_owned_string(project->ctx, &project->compiler_c, compiler); }
ib_status ib_project_set_cxx_compiler(ib_project* project, const char* compiler) { return (!project || !compiler || !compiler[0]) ? IB_ERR_INVALID : ib_set_owned_string(project->ctx, &project->compiler_cxx, compiler); }
ib_status ib_project_set_archiver(ib_project* project, const char* archiver) { return (!project || !archiver || !archiver[0]) ? IB_ERR_INVALID : ib_set_owned_string(project->ctx, &project->archiver, archiver); }
ib_status ib_project_set_output_dir(ib_project* project, const char* dir) {
char* normalized;
if (!project || !dir || !dir[0]) {
if (project) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_INVALID, "Output directory must be a non-empty path");
}
return IB_ERR_INVALID;
}
normalized = ib_path_normalize(project->ctx, project->root_dir, dir);
if (!normalized) return project->ctx->last_status;
free(project->output_dir);
project->output_dir = normalized;
return IB_OK;
}
ib_status ib_project_set_state_dir(ib_project* project, const char* dir) {
char* normalized;
if (!project || !dir || !dir[0]) {
if (project) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_INVALID, "State directory must be a non-empty path");
}
return IB_ERR_INVALID;
}
normalized = ib_path_normalize(project->ctx, project->root_dir, dir);
if (!normalized) return project->ctx->last_status;
free(project->state_dir);
project->state_dir = normalized;
return IB_OK;
}
ib_status ib_project_set_mode_flags(ib_project* project, ib_build_mode mode, const char* c_flags, const char* cxx_flags) {
ib_status status = IB_OK;
if (!project) return IB_ERR_INVALID;
switch (mode) {
case IB_MODE_DEBUG:
if (c_flags && ib_status_failed((status = ib_set_owned_string(project->ctx, &project->debug_cflags, c_flags)))) return status;
if (cxx_flags) status = ib_set_owned_string(project->ctx, &project->debug_cxxflags, cxx_flags);
return status;
case IB_MODE_RELEASE:
if (c_flags && ib_status_failed((status = ib_set_owned_string(project->ctx, &project->release_cflags, c_flags)))) return status;
if (cxx_flags) status = ib_set_owned_string(project->ctx, &project->release_cxxflags, cxx_flags);
return status;
case IB_MODE_CUSTOM:
if (c_flags && ib_status_failed((status = ib_set_owned_string(project->ctx, &project->custom_cflags, c_flags)))) return status;
if (cxx_flags) status = ib_set_owned_string(project->ctx, &project->custom_cxxflags, cxx_flags);
return status;
}
return IB_ERR_INVALID;
}
ib_status ib_project_add_include_dir(ib_project* project, const char* path) {
char* dir; ib_status status;
if (!project) return IB_ERR_INVALID;
status = ib_normalize_existing_dir(project, path, &dir);
if (ib_status_failed(status)) return status;
if (!ib_strvec_push_unique_owned(&project->include_dirs, dir)) {
free(dir); ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while adding include directory"); return IB_ERR_NOMEM;
}
return IB_OK;
}
ib_status ib_project_add_shared_source(ib_project* project, const char* path) {
char* source; ib_status status;
if (!project) return IB_ERR_INVALID;
status = ib_normalize_existing_source(project, path, &source);
if (ib_status_failed(status)) return status;
if (!ib_strvec_push_unique_owned(&project->shared_sources, source)) {
free(source); ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while adding shared source"); return IB_ERR_NOMEM;
}
return IB_OK;
}
ib_status ib_project_scan_shared_dir(ib_project* project, const char* dir, bool recursive) {
char* source_dir; ib_strvec found; size_t i; ib_status status;
if (!project) return IB_ERR_INVALID;
status = ib_normalize_existing_dir(project, dir, &source_dir);
if (ib_status_failed(status)) return status;
ib_strvec_init(&found);
status = ib_collect_sources(project, source_dir, recursive, &found);
free(source_dir);
if (ib_status_failed(status)) { ib_strvec_free(&found); return status; }
ib_strvec_sort(&found);
for (i = 0; i < found.count; ++i) {
if (!ib_strvec_push_unique_copy(&project->shared_sources, found.items[i])) {
ib_strvec_free(&found); ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while scanning shared sources"); return IB_ERR_NOMEM;
}
}
ib_strvec_free(&found);
return IB_OK;
}
static bool ib_targetvec_reserve(ib_project* project, size_t needed) {
size_t cap = project->target_capacity ? project->target_capacity : IB_INITIAL_LIST_CAPACITY; ib_target** next;
if (needed <= project->target_capacity) return true;
while (cap < needed) cap *= 2;
next = (ib_target**)realloc(project->targets, cap * sizeof(*next));
if (!next) return false;
project->targets = next; project->target_capacity = cap; return true;
}
ib_target* ib_project_add_target(ib_project* project, const char* name, ib_target_kind kind) {
ib_target* target; size_t i;
if (!project) return NULL;
if (!name || !name[0]) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_INVALID, "Target name must be non-empty");
return NULL;
}
if (strchr(name, '/') || strchr(name, '\\')) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_INVALID, "Target name '%s' must not contain path separators", name);
return NULL;
}
if (kind < IB_TARGET_EXECUTABLE || kind > IB_TARGET_SHARED_LIB) {
ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_INVALID, "Target '%s' uses an invalid target kind", name);
return NULL;
}
for (i = 0; i < project->target_count; ++i) if (strcmp(project->targets[i]->name, name) == 0) { ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_DUPLICATE, "Target '%s' already exists", name); return NULL; }
if (!ib_targetvec_reserve(project, project->target_count + 1)) { ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while creating target '%s'", name); return NULL; }
target = (ib_target*)calloc(1, sizeof(*target));
if (!target) { ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while creating target '%s'", name); return NULL; }
target->project = project; target->name = ib_strdup_internal(name); target->kind = kind;
ib_strvec_init(&target->sources); ib_strvec_init(&target->include_dirs); ib_strvec_init(&target->cflags); ib_strvec_init(&target->link_flags);
if (!target->name) { free(target); ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while creating target '%s'", name); return NULL; }
project->targets[project->target_count++] = target; return target;
}
ib_status ib_target_set_entry(ib_target* target, const char* path) { char* source; ib_status status; if (!target || !target->project) return IB_ERR_INVALID; status = ib_normalize_existing_source(target->project, path, &source); if (ib_status_failed(status)) return status; free(target->entry_source); target->entry_source = source; return IB_OK; }
ib_status ib_target_add_source(ib_target* target, const char* path) { char* source; ib_status status; if (!target || !target->project) return IB_ERR_INVALID; status = ib_normalize_existing_source(target->project, path, &source); if (ib_status_failed(status)) return status; if (!ib_strvec_push_unique_owned(&target->sources, source)) { free(source); ib_context_emit(target->project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while adding target source"); return IB_ERR_NOMEM; } return IB_OK; }
ib_status ib_target_add_include_dir(ib_target* target, const char* path) { char* dir; ib_status status; if (!target || !target->project) return IB_ERR_INVALID; status = ib_normalize_existing_dir(target->project, path, &dir); if (ib_status_failed(status)) return status; if (!ib_strvec_push_unique_owned(&target->include_dirs, dir)) { free(dir); ib_context_emit(target->project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while adding target include directory"); return IB_ERR_NOMEM; } return IB_OK; }
ib_status ib_target_add_cflags(ib_target* target, const char* flags) { if (!target || !target->project || !flags || !flags[0]) { if (target && target->project) ib_context_emit(target->project->ctx, IB_DIAG_ERROR, IB_ERR_INVALID, "Compile flags must be a non-empty string"); return IB_ERR_INVALID; } if (!ib_strvec_push_copy(&target->cflags, flags)) { ib_context_emit(target->project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while adding target compile flags"); return IB_ERR_NOMEM; } return IB_OK; }
ib_status ib_target_add_link_flags(ib_target* target, const char* flags) { if (!target || !target->project || !flags || !flags[0]) { if (target && target->project) ib_context_emit(target->project->ctx, IB_DIAG_ERROR, IB_ERR_INVALID, "Link flags must be a non-empty string"); return IB_ERR_INVALID; } if (!ib_strvec_push_copy(&target->link_flags, flags)) { ib_context_emit(target->project->ctx, IB_DIAG_ERROR, IB_ERR_NOMEM, "Out of memory while adding target link flags"); return IB_ERR_NOMEM; } return IB_OK; }
ib_status ib_project_build(ib_project* project, ib_build_mode mode) {
ib_build_plan plan; size_t i; size_t compiled = 0; size_t linked = 0; ib_status status;
if (!project) return IB_ERR_INVALID;
status = ib_make_plan(project, mode, &plan); if (ib_status_failed(status)) return status;
status = ib_mkdirs(project, project->state_dir); if (ib_status_failed(status)) { ib_build_plan_free(&plan); return status; }
status = ib_mkdirs(project, project->output_dir); if (ib_status_failed(status)) { ib_build_plan_free(&plan); return status; }
for (i = 0; i < plan.compile_count; ++i) { bool rebuilt = false; status = ib_execute_compile(project, &plan.compile_actions[i], &rebuilt); if (ib_status_failed(status)) { ib_build_plan_free(&plan); return status; } if (rebuilt) ++compiled; }
for (i = 0; i < plan.link_count; ++i) { bool relinked = false; status = ib_execute_link(project, &plan, &plan.link_actions[i], &relinked); if (ib_status_failed(status)) { ib_build_plan_free(&plan); return status; } if (relinked) ++linked; }
ib_context_emit(project->ctx, IB_DIAG_INFO, IB_OK, "Build complete. Recompiled %zu action(s), relinked %zu target(s).", compiled, linked);
ib_build_plan_free(&plan); return IB_OK;
}
ib_status ib_project_clean(ib_project* project) {
size_t i; ib_status status;
if (!project) return IB_ERR_INVALID;
status = ib_remove_recursive(project, project->state_dir); if (ib_status_failed(status)) return status;
for (i = 0; i < project->target_count; ++i) {
char* output_abs = NULL; char* output_arg = NULL;
status = ib_target_output_path(project, project->targets[i], &output_abs, &output_arg);
free(output_arg);
if (ib_status_failed(status)) { free(output_abs); return status; }
if (ib_path_exists(output_abs)) { status = ib_remove_recursive(project, output_abs); free(output_abs); if (ib_status_failed(status)) return status; } else free(output_abs);
}
ib_context_emit(project->ctx, IB_DIAG_INFO, IB_OK, "Cleaned build outputs"); return IB_OK;
}
ib_status ib_project_write_compile_commands(ib_project* project, const char* out_path) {
ib_build_plan plan; ib_status status; char* output_abs; FILE* fp; size_t i;
if (!project) return IB_ERR_INVALID;
output_abs = ib_path_normalize(project->ctx, project->root_dir, (out_path && out_path[0]) ? out_path : "compile_commands.json");
if (!output_abs) return project->ctx->last_status;
status = ib_make_plan(project, IB_MODE_DEBUG, &plan); if (ib_status_failed(status)) { free(output_abs); return status; }
status = ib_mkdirs_for_file(project, output_abs); if (ib_status_failed(status)) { ib_build_plan_free(&plan); free(output_abs); return status; }
fp = fopen(output_abs, "w");
if (!fp) { ib_build_plan_free(&plan); ib_context_emit(project->ctx, IB_DIAG_ERROR, IB_ERR_IO, "Failed to open '%s' for writing: %s", output_abs, strerror(errno)); free(output_abs); return IB_ERR_IO; }
fputs("[\n", fp);
for (i = 0; i < plan.compile_count; ++i) {
const ib_compile_action* action = &plan.compile_actions[i];
fputs(" {\n \"directory\": ", fp); ib_write_json_string(fp, project->root_dir);
fputs(",\n \"command\": ", fp); ib_write_json_string(fp, action->command);
fputs(",\n \"file\": ", fp); ib_write_json_string(fp, action->source_path);
fputs("\n }", fp); if (i + 1 < plan.compile_count) fputs(",", fp); fputs("\n", fp);
}
fputs("]\n", fp); fclose(fp); ib_build_plan_free(&plan); ib_context_emit(project->ctx, IB_DIAG_INFO, IB_OK, "Wrote compile_commands.json to %s", output_abs); free(output_abs); return IB_OK;
}
const char* ib_version(void) { return "2.0.0"; }
#endif /* INCLUDEBUILD_IMPLEMENTATION */
#endif /* INCLUDEBUILD_H */