#define CONFIG_FILE "IBUILD" #define MAX_STATEMENTS 50 #define MAX_TOKENS 200 #define MAX_FILES_IN_DIR 100 #define MAX_C_FILES 200 #define MAX_H_FILES 200 #define MAX_CPP_FILES 200 #include "deps/utils/src/arena_alloc.h" #include #include #include #include #include #include #include #include #include #define DIE(fmt, ...) die_t(__func__, __LINE__, fmt, ##__VA_ARGS__) #define LOG_ERROR(fmt, ...) log_write(__func__, fmt, ## __VA_ARGS__) #define LOG_USER(fmt, ...) log_write(NULL, fmt, ## __VA_ARGS__) /*** allocated memory ***/ // There shall never be another heap allocation outside of thy block. // Each system and/or type of allocation shall have its own memory. // There shall be no arena managed in this state, if the allocated memory is redundant and will not affect performance. // Arenas shall never be arena_free'd during program execution, only before exit. Use arena_reset if you wish to achieve // the same functionality without free() calls. typedef struct { Arena file_contents; Arena configuration; Arena strings; Arena parser; Arena discovery; } Memory; static Memory memory; void memory_free() { arena_free(&memory.file_contents); arena_free(&memory.configuration); arena_free(&memory.strings); arena_free(&memory.parser); arena_free(&memory.discovery); } /*** logging ***/ // Developer-reporting error interface, along with steps to reproduce the error. // Include this log in your issues void die_t(const char* func, int line, const char* fmt, ...) { assert(func != NULL); assert(fmt != NULL); va_list args; va_start(args, fmt); printf("ibuild exception(f:%s-l:%d): ", func, line); vprintf(fmt, args); printf("\n"); printf("******Last syscall error: %s\n", strerror(errno)); va_end(args); exit(1); } // User-reporting interface // func_name: MSG void log_write(const char* func, const char* fmt, ...) { if(fmt == NULL) DIE("fmt must not be null!"); va_list args; va_start(args, fmt); if(func != NULL) printf("%s: ", func); vprintf(fmt, args); printf("\n"); va_end(args); } /*** file management ***/ typedef struct { const char* path; unsigned char type; } DirEntry; int dirent_get_next(DIR *d, DirEntry *e, const char *p) { assert(d != NULL); struct dirent* file = readdir(d); if(file == NULL) return 1; switch(file->d_type) { case DT_DIR: e->type = DT_DIR; break; case DT_REG: e->type = DT_REG; break; default: return 1; } size_t fs = strlen(file->d_name); char* buf = (char*)arena_alloc(&memory.strings, fs + 1); memcpy(buf, file->d_name, fs + 1); buf[fs] = '\0'; e->path = buf; return 0; } size_t dirent_get_recursive(const char* p) { if(p == NULL) DIE("p must not be NULL"); DIR* dir = opendir(p); DirEntry current; size_t count = 0; while(dirent_get_next(dir, ¤t, p) == 0) { if(current.type == DT_DIR && (strcmp(current.path, ".") == 0 || strcmp(current.path, "..") == 0 || strcmp(current.path, ".git") == 0)) continue; DirEntry* ptr = (DirEntry*)arena_alloc(&memory.discovery, sizeof(DirEntry)); size_t size = strlen(current.path) + 2 + strlen(p); char* buf = (char*)arena_alloc(&memory.strings, size); snprintf(buf, size, "%s/%s", p, current.path); current.path = buf; *ptr = current; count++; if(current.type == DT_DIR) { count += dirent_get_recursive(buf); continue; } } closedir(dir); return count; } long file_get_size(FILE* fd) { if(fd == NULL) DIE("fd cannot be NULL."); if(fseek(fd, 0, SEEK_END) != 0) DIE("file exists, fseek SEEK_END fail."); long size = ftell(fd); if(fseek(fd, 0, SEEK_SET) != 0) DIE("file exists, fseek SEEK_SET fail."); return size; } char* file_get_content(Arena* a, const char* fp) { if(fp == NULL) DIE("fp cannot be NULL"); FILE* fd = fopen(fp, "r"); long size = file_get_size(fd); char* buf = (char*)arena_alloc(a, size + 1); size_t ret = fread(buf, size, 1, fd); if (ret != 1) DIE("fread returned %d", ret); buf[size] = '\0'; return buf; } bool file_has_r_access(const char* f) { if(access(f, F_OK | R_OK) == 0) return true; return false; } /*** discovery phase ***/ typedef struct { DirEntry** c_files; DirEntry** h_files; DirEntry** cpp_files; DirEntry** o_files; } Discovery; void discover(Discovery *d) { size_t amount = dirent_get_recursive("."); if(amount == 0) DIE("Directory is empty! If you have symlinks in the directory, ibuild does not support them."); size_t c_c = 0; size_t h_c = 0; size_t cpp_c = 0; for(size_t i = 0; i < amount; i++) { char* current = NULL; if((current = strrchr(((DirEntry*)memory.discovery.start)[i].path, '.')) == NULL) continue; DirEntry* current_dirent = &((DirEntry*)memory.discovery.start)[i]; if(strcmp(current, ".c") == 0) { if(c_c > MAX_C_FILES) DIE("Maximum C file limit was reached with the ibuild project. To extend, you must edit the source code and decide whether there is enough memory."); d->c_files[c_c] = current_dirent; c_c++; } else if(strcmp(current, ".h") == 0) { if(h_c > MAX_H_FILES) DIE("Maximum H file limit was reached with the ibuild project. To extend, you must edit the source code and decide whether there is enough memory."); d->h_files[h_c] = current_dirent; h_c++; }else if(strcmp(current, ".cpp") == 0) { if(cpp_c > MAX_CPP_FILES) DIE("Maximum CPP file limit was reached with the ibuild project. To extend, you must edit the source code and decide whether there is enough memory."); d->cpp_files[cpp_c] = current_dirent; cpp_c++; } } LOG_USER("Found %d C files, %d H files, %d CPP files.", c_c, h_c, cpp_c); } /*** compilation process ***/ typedef struct { char* compiler_path; char* build_dir; char* target_exec; char* src_dir; char** src_files; char* version; char* dep_searcher; char* dep_searcher_flags; } Configuration; void launch_compile(Configuration* co) { if(co == NULL) DIE("co has to be valid"); if(co->compiler_path == NULL) DIE("requires a valid compiler path"); int fildes[2]; pipe(fildes); pid_t p = fork(); if(p<0) { DIE("fork error"); } else if (p==0) { char* args[] = {co->compiler_path, "ibuild.c", "-o", co->target_exec, NULL}; close(STDOUT_FILENO); dup(fildes[1]); close(fildes[0]); close(fildes[1]); if((execvp(co->compiler_path, args)) == -1) DIE("launch_compile() execvp error"); exit(0); } } /*** configuration lexer ***/ typedef struct { char *src; } Tokenizer; typedef enum { T_INVALID = -1, T_IDENTIFIER = 0, T_STRING = 1, T_IS = 2, T_EOF = 3, } TokenType; typedef struct { TokenType type; char* value; int line; } Token; #define TOKEN_CONST {T_INVALID, NULL, 0} bool is_alpha_uppercase(char s) { return (s >= 'A' && s <= 'Z'); } bool is_alpha_lowercase(char s) { return (s >= 'a' && s <= 'z'); } bool is_whitespace(char s) { return s == ' ' || s == '\t' || s == '\r' || s == '\n'; } bool is_part_of_key(char s) { return is_alpha_uppercase(s) || s == '_'; } size_t skip_group(Tokenizer* t, bool (*func)(char)) { assert(func != NULL); size_t len = 0; while(func(*t->src)) { len++; t->src += 1; } return len; } Token tokenize_identifier(Tokenizer *t) { char* temp = t->src; size_t len = skip_group(t, &is_part_of_key); char* buf = (char*)arena_alloc(&memory.strings, sizeof(char) * len + 1); memcpy(buf, temp, len); buf[len] = '\0'; return (Token){T_IDENTIFIER, buf}; } Token tokenize_string(Tokenizer *t) { t->src += 1; char* temp = t->src; size_t len = 0; while(*t->src != '"' && *t->src != '\0') { len++; t->src += 1; } if(*t->src == '"') t->src++; char* buf = (char*)arena_alloc(&memory.strings, (sizeof(char) * len) + 1); memcpy(buf, temp, len); buf[len] = '\0'; return (Token){T_STRING, buf}; } Token tokenizer_next(Tokenizer* t) { if(is_whitespace(*t->src)) skip_group(t, &is_whitespace); if(is_alpha_uppercase(*t->src)) { return tokenize_identifier(t); } switch(*t->src) { case '"': return tokenize_string(t); case '=': t->src++; return (Token){T_IS, NULL}; } if(*t->src == '\0') return (Token){T_EOF, NULL}; t->src++; return (Token){T_INVALID, NULL}; } size_t tokenizer_tokenize(Tokenizer* t, Token* ts) { size_t t_count = 0; while(1) { if(t_count > MAX_TOKENS) DIE("Maximum number of tokens reached! \n Could not read configuration file."); Token token = tokenizer_next(t); if(token.type == T_INVALID) DIE("illegal token!"); ts[t_count] = token; t_count++; if(token.type == T_EOF) break; } return t_count; } /*** configuration parser ***/ typedef enum { K_NA = -1, K_UNKNOWN = 0, K_COMPILER_PATH = 1, K_SRC_DIR = 2, K_SRC_FILES = 3, K_BUILD_DIR = 4, K_TARGET_EXEC = 5, K_DEP_SEARCHER = 6, K_DEP_SEARCHER_FLAGS = 7, K_VERSION = 8, } Key; typedef enum { N_STRING = 0, N_ARRAY = 1, N_PAIR = 2, } NodeType; typedef struct { NodeType type; Key key; void* value; } Node; typedef struct { Token* tokens; size_t loc; size_t cap; } Parser; typedef struct { Key key; const char* value; } KeyMap; static KeyMap keyword_mappings[] = { {K_COMPILER_PATH, "COMPILER_PATH"}, {K_SRC_DIR, "SRC_DIR"}, {K_SRC_FILES, "SRC_FILES"}, {K_BUILD_DIR, "BUILD_DIR"}, {K_TARGET_EXEC, "TARGET_EXEC"}, {K_DEP_SEARCHER, "DEP_SEARCHER"}, {K_DEP_SEARCHER_FLAGS, "DEP_SEARCHER_FLAGS"}, {K_UNKNOWN, NULL}, }; Key key_lookup(char* s) { if(s == NULL) return K_UNKNOWN; for(const KeyMap* ptr = keyword_mappings; ptr->value != NULL; ptr++) { if(strcmp(ptr->value, s) == 0) { return ptr->key; } } return K_UNKNOWN; } Token parser_peek(Parser* p, size_t o) { if (p->loc + o >= p->cap) { return p->tokens[p->loc - 1]; } Token t = p->tokens[p->loc + o]; return t; } Token parser_previous(Parser* p) { if(p->loc - 1 < 0) { DIE("parser logic error."); } return p->tokens[p->loc - 1]; } Token parser_advance(Parser* p) { if(p->loc + 1 >= p->cap) { return p->tokens[p->loc]; } p->loc++; return p->tokens[p->loc]; } Token parser_expect(Parser* p, TokenType tt, char* s) { assert(p != NULL); assert(s != NULL); if(parser_peek(p, 1).type != tt) DIE(s); return parser_advance(p); } Node* parser_expression(Parser* p) { Token t = parser_advance(p); if(t.type == T_STRING) { Node* node = (Node*)arena_alloc(&memory.parser, sizeof(Node)); node->key = K_NA; node->value = (void*)t.value; node->type = N_STRING; parser_advance(p); return node; } DIE("Syntax error: Invalid expression."); } Node* parser_statement(Parser* p) { Token t = parser_peek(p, 0); if(t.type == T_IDENTIFIER) { Key k = key_lookup(t.value); if(k == K_UNKNOWN) DIE("Syntax error: Unexpected identifer encountered"); parser_expect(p, T_IS, "Syntax error: Expected '=' after identifier"); Node* expression = parser_expression(p); Node* node = (Node*)arena_alloc(&memory.parser, sizeof(Node)); node->key = k; node->value = (void*)expression; node->type = N_PAIR; return node; } else { DIE("Syntax error: expected an identifier, got: %d", t.type); } } Node* parser_parse_next(Parser* p) { return parser_statement(p); } size_t parser_parse(Parser* p, Node** s) { assert(p != NULL); assert(s != NULL); size_t p_count = 0; while(p_count < MAX_STATEMENTS && p->loc < p->cap - 1) { s[p_count] = parser_parse_next(p); p_count++; } return p_count; } #ifdef DEBUG void debug_parser(Node* n, int indent) { for(int i = 0; i < indent; i++) { printf(" "); } switch(n->type) { case N_PAIR: printf("PAIR STATEMENT"); printf(" Key: %10d\n", n->key); if(n->value != NULL) debug_parser((Node*)n->value, 2); break; case N_STRING: printf("STRING EXPRESSION"); printf(" Value: %s\n", (char*)n->value); break; } } #endif /*** configuration process ***/ void configuration_set_option(Configuration* co, Key k, char* val) { switch(k) { case K_NA: DIE("Node with an NA key hit configuration_set_option"); break; case K_UNKNOWN: DIE("Unknown node encountered!"); break; case K_COMPILER_PATH: co->compiler_path = val; break; case K_BUILD_DIR: co->build_dir = val; break; case K_TARGET_EXEC: co->target_exec = val; break; case K_DEP_SEARCHER: co->dep_searcher = val; break; case K_DEP_SEARCHER_FLAGS: co->dep_searcher_flags = val; break; case K_VERSION: co->version = val; break; case K_SRC_DIR: co->src_dir = val; break; case K_SRC_FILES: DIE("Not implemented!"); break; default: DIE("Invalid compiler option was supplied!"); } } void configuration_process_node(Configuration* co, Node* n) { switch(n->type) { case N_PAIR: Key k = n->key; configuration_set_option(co, k, (char*)((Node*)n->value)->value); break; } } Configuration configuration_struct_setup(int len, Node** st) { Configuration config; config.compiler_path = "/usr/bin/gcc"; config.build_dir = "."; config.src_dir = "."; config.target_exec = "iexec"; config.dep_searcher = "/usr/bin/gcc"; config.dep_searcher_flags = "-MM"; for (size_t i = 0; i < len; i++) configuration_process_node(&config, st[i]); LOG_USER("Configured IBUILD options."); return config; } /*** configuration file management ***/ Configuration process_config(const char* fn) { if(!file_has_r_access(fn)) { return configuration_struct_setup(0, NULL); } LOG_USER("IBUILD Configuration file detected.\n"); char* config_mem = file_get_content(&memory.file_contents, fn); Token tokens[MAX_TOKENS]; Tokenizer t = { .src = config_mem }; size_t t_count = tokenizer_tokenize(&t, tokens); if(t_count == 0) { return configuration_struct_setup(0, NULL); } Parser parser = {.tokens = tokens, .loc = 0, .cap = t_count }; Node* statements[MAX_STATEMENTS]; size_t p_count = parser_parse(&parser, statements); #ifdef DEBUG for (size_t i = 0; i < p_count; i++) debug_parser(statements[i], 0); #endif return configuration_struct_setup(p_count, statements); } #ifndef TESTING int main(int argc, char** argv) { LOG_USER("Build version: %s", VERSION); Configuration c = process_config(CONFIG_FILE); dirent_get_recursive("."); Discovery d; d.c_files = (DirEntry**)arena_alloc(&memory.discovery, MAX_C_FILES * sizeof(DirEntry*)); d.h_files = (DirEntry**)arena_alloc(&memory.discovery, MAX_H_FILES * sizeof(DirEntry*)); discover(&d); // Hand over discovery to the dependency checker. // Compile //launch_compile(&c); memory_free(); return 0; } #endif