From 930dafa31ff2f3c4afb1688f694061f9939b79c6 Mon Sep 17 00:00:00 2001 From: thetek Date: Fri, 23 Jun 2023 11:55:30 +0200 Subject: [PATCH] init: ready, set, go! --- .ccls | 13 +++ .gitignore | 4 + build/build.py | 230 ++++++++++++++++++++++++++++++++++++++++++++++++ build/config.py | 7 ++ inc/common.h | 100 +++++++++++++++++++++ inc/log.h | 39 ++++++++ src/common.c | 106 ++++++++++++++++++++++ src/log.c | 33 +++++++ src/main.c | 14 +++ 9 files changed, 546 insertions(+) create mode 100644 .ccls create mode 100644 .gitignore create mode 100755 build/build.py create mode 100644 build/config.py create mode 100644 inc/common.h create mode 100644 inc/log.h create mode 100644 src/common.c create mode 100644 src/log.c create mode 100644 src/main.c diff --git a/.ccls b/.ccls new file mode 100644 index 0000000..16b4310 --- /dev/null +++ b/.ccls @@ -0,0 +1,13 @@ +gcc +-std=c11 +-Iinc +-Wall +-Wextra +-Wconversion +-Wdouble-promotion +-Wshadow +-Wcast-qual +-Wmissing-prototypes +-Werror +-pedantic +-DDEBUG diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38c3217 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/bin +/obj +/.ccls-cache +__pycache__ diff --git a/build/build.py b/build/build.py new file mode 100755 index 0000000..58a00f0 --- /dev/null +++ b/build/build.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 + +import config +import dataclasses +import datetime +import enum +import os +import pathlib +import subprocess +import sys + + +@dataclasses.dataclass +class File: + ''' + stores source file, the path to the generated object file and a list of + header dependencies. + ''' + src: str + obj: str + deps: list[str] + + +class CompilationMode(enum.Enum): + ''' + used to specify if the program should be compiled in debug or release mode. + ''' + Debug = 'debug' + Release = 'release' + + +class Color(enum.Enum): + ''' + common colors used for colorizing terminal output. + ''' + Reset = '\x1b[0m' + Red = '\x1b[31m' + Green = '\x1b[32m' + + +def get_source_files() -> list[str]: + ''' + get a list of source files located in the `src/` folder. only matches files + ending in `*.c`. + ''' + path = pathlib.Path('src/') + files = [str(p) for p in path.rglob('*.c')] + return files + + +def get_object_location(src_file: str, compilation_mode: CompilationMode) -> str: + ''' + get the location of the resulting object file. for example, a source file + `src/foo/bar/baz.c` will have the object location `obj/debug/foo/bar/baz.c` + if compiled in debug mode. + ''' + result = src_file.replace('src/', f'obj/{compilation_mode.value}/') + result = result[:-2] + '.o' + return result + + +def get_dependencies(src_file: str) -> list[str]: + ''' + obtain a list of header dependencies for a source file by running `cc -M`. + since `cc -M` uses make as its output format, it needs to be converted to a + regular python list. + ''' + # obtain raw make-compatible output from `cc -M` and remove linebreaks + output_raw = subprocess.check_output([config.COMPILER, '-Iinc', '-M', '-MM', src_file]) + output = output_raw.decode('utf-8').replace('\n', '').replace('\\', '') + # remove make rule to get space-delimited list of dependencies + deps = output.split(':')[1].split(' ') + # filter for header files + deps = list(filter(lambda x: x.endswith('.h'), deps)) + return deps + + +def get_file_list(compilation_mode: CompilationMode) -> list[File]: + ''' + get a list of `File` objects that contain information about where source + files are, where their object files will be located and which headers they + depend on. + ''' + src_files = get_source_files() + files: list[File] = [] + + for src in src_files: + obj = get_object_location(src, compilation_mode) + deps = get_dependencies(src) + files.append(File(src, obj, deps)) + + return files + + +def log_message(color: Color, prefix: str, msg: str): + ''' + print a message to the terminal using a specific color and prefix. the + message will look like this: + PRFX message + where PRFX is the passed prefix. the prefix is colored according to the + parameter passed to this function. + ''' + print(f'{color.value}{prefix.ljust(4).upper()}{Color.Reset.value} {msg}') + + +def check_if_need_compile(file: File) -> bool: + ''' + checks if an object file needs to be re-compiled based on the modification + dates of its source file and header dependencies. + ''' + # if the object file does not exist, it needs to be compiled + obj_path = pathlib.Path(file.obj) + if not obj_path.exists(): + return True + + # if the object file is older than the source file, it needs to be compiled + src_path = pathlib.Path(file.src) + src_mod_time = datetime.datetime.fromtimestamp(src_path.stat().st_mtime, tz=datetime.timezone.utc) + obj_mod_time = datetime.datetime.fromtimestamp(obj_path.stat().st_mtime, tz=datetime.timezone.utc) + if obj_mod_time < src_mod_time: + return True + + # check all headers if they were modified since the object file was created + for dep in file.deps: + header_path = pathlib.Path(dep) + header_mod_time = datetime.datetime.fromtimestamp(header_path.stat().st_mtime, tz=datetime.timezone.utc) + if obj_mod_time < header_mod_time: + return True + + # file does not need to be compiled + return False + + +def compile_file(file: File, compilation_mode: CompilationMode) -> int: + ''' + compile a source file to an object file. + ''' + if not check_if_need_compile(file): + return 0 + + path = pathlib.Path(file.obj) + + obj_folder = str(path.parent) + os.makedirs(obj_folder, exist_ok=True) + + if compilation_mode == CompilationMode.Debug: + mode_specific_flags = config.FLAGS_DEBUG + elif compilation_mode == CompilationMode.Release: + mode_specific_flags = config.FLAGS_RELEASE + + log_message(Color.Green, 'cc', file.obj) + + return_code = subprocess.call([ + config.COMPILER, + *config.FLAGS.split(' '), + *config.FLAGS_WARN.split(' '), + *mode_specific_flags.split(' '), + '-Iinc', + '-c', file.src, + '-o', file.obj, + ]) + + return return_code + + +def link_program(files: list[File], compilation_mode: CompilationMode) -> int: + ''' + link the generated object files to an executable binary. + ''' + os.makedirs('bin/', exist_ok=True) + + objs = [file.obj for file in files] + + if compilation_mode == CompilationMode.Debug: + mode_specific_flags = config.FLAGS_DEBUG + elif compilation_mode == CompilationMode.Release: + mode_specific_flags = config.FLAGS_RELEASE + + target = f'bin/{config.TARGET}-{compilation_mode.value}' + + log_message(Color.Green, 'link', target) + + return_code = subprocess.call([ + config.COMPILER, + *config.FLAGS.split(' '), + *config.FLAGS_WARN.split(' '), + *mode_specific_flags.split(' '), + *config.LIBS, + '-Iinc', + *objs, + '-o', target, + ]) + + return return_code + + +def main(): + # parse command line arguments + if len(sys.argv) == 1: + compilation_mode = CompilationMode.Debug + elif sys.argv[1] == 'debug': + compilation_mode = CompilationMode.Debug + elif sys.argv[1] == 'release': + compilation_mode = CompilationMode.Release + elif sys.argv[1] == 'clean': + subprocess.call(['rm', '-rf', 'bin/', 'obj/']) + exit(0) + else: + log_message(Color.Red, 'err', f'unknown subcommand `{sys.argv[1]}`') + exit(1) + + # obtain source files + files = get_file_list(compilation_mode) + + # compile object files + for file in files: + success = compile_file(file, compilation_mode) + if success != 0: + log_message(Color.Red, 'err', f'failed to compile {file.obj}') + exit(success) + + # generate executable + success = link_program(files, compilation_mode) + if success != 0: + log_message(Color.Red, 'err', f'failed to link program') + exit(success) + + +if __name__ == '__main__': + main() diff --git a/build/config.py b/build/config.py new file mode 100644 index 0000000..53bffaa --- /dev/null +++ b/build/config.py @@ -0,0 +1,7 @@ +COMPILER = 'gcc' +FLAGS = '-std=c11' +FLAGS_WARN = '-Wall -Wextra -Wconversion -Wdouble-promotion -Wshadow -Wcast-qual -Wmissing-prototypes -Wmissing-noreturn -Wredundant-decls -Wdisabled-optimization -Wunsafe-loop-optimizations -Wcast-align=strict -Winline -Wvla -Wlogical-op -Wdate-time -Werror -pedantic' +FLAGS_DEBUG = '-O0 -ggdb3 -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer -fstack-protector -DDEBUG' +FLAGS_RELEASE = '-O3 -march=native -DNDEBUG' +LIBS = '' +TARGET = 'c-template' diff --git a/inc/common.h b/inc/common.h new file mode 100644 index 0000000..334656d --- /dev/null +++ b/inc/common.h @@ -0,0 +1,100 @@ +#ifndef COMMON_H_ +#define COMMON_H_ + + +#include +#include +#include +#include + + +/** macros ********************************************************************/ + +/// define debug macros + +#ifdef DEBUG +# define DEBUG_ALLOC +#endif + +/// c11 / c23 compatibility. many of these can be removed once the project fully +/// migrates to c23, but for now, some of these will have to be used. when +/// migrating, remember that the usage of macros like `_Noreturn` should be +/// replaced by c23-specific syntax, in this case the `[[noreturn]]` attribute. + +// if the program is compiled with c23 or newer, define `STDC23` as a feature +// test macro that is used for enabling features based on if c23 or c11 is used. + +#if __STDC_VERSION__ >= 202000L +# define STDC23 +#endif + +// enable C11-specific features + +#if !defined(STDC23) +# define nullptr NULL /* `nullptr` from C23 */ +#endif + +// enable C23-specific features + +#if defined(STDC23) +# define _Noreturn [[noreturn]] +#endif + + +/// function wrappers + +#ifdef DEBUG_ALLOC +# define smalloc(nmemb, size) __smalloc (nmemb, size, __FILE__, __LINE__) +# define scalloc(nmemb, size) __scalloc (nmemb, size, __FILE__, __LINE__) +# define srealloc(ptr, nmemb, size) __srealloc (ptr, nmemb, size, __FILE__, __LINE__) +#else +# define smalloc(nmemb, size) __smalloc (nmemb, size) +# define scalloc(nmemb, size) __scalloc (nmemb, size) +# define srealloc(ptr, nmemb, size) __srealloc (ptr, nmemb, size) +#endif + + +/** typedefs ******************************************************************/ + +/// common type abbreviations + +typedef signed char schar; +typedef signed long long llong; +typedef unsigned char uchar; +typedef unsigned short ushort; +typedef unsigned int uint; +typedef unsigned long ulong; +typedef unsigned long long ullong; +typedef int8_t i8; +typedef int16_t i16; +typedef int32_t i32; +typedef int64_t i64; +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef ssize_t isize; +typedef size_t usize; +typedef float f32; +typedef double f64; +typedef long double f128; + + +/** functions *****************************************************************/ + +/// safe memory allocation + +#ifdef DEBUG_ALLOC +void *__smalloc (const usize nmemb, const usize size, const char *const file, const usize line); +void *__scalloc (const usize nmemb, const usize size, const char *const file, const usize line); +void *__srealloc (void *ptr, const usize nmemb, const usize size, const char *const file, const usize line); +void sfree (void *ptr); +#else +void *__smalloc (const usize nmemb, const usize size); +void *__scalloc (const usize nmemb, const usize size); +void *__srealloc (void *ptr, const usize nmemb, const usize size); +void sfree (void *ptr); +#endif + + +#endif // COMMON_H_ diff --git a/inc/log.h b/inc/log.h new file mode 100644 index 0000000..4c87135 --- /dev/null +++ b/inc/log.h @@ -0,0 +1,39 @@ +#ifndef LOG_H_ +#define LOG_H_ + + +#include + + +/** macros ********************************************************************/ + +#define log_debug(...) __log_print (__Log_Level_Debug, __VA_ARGS__) +#define log_info(...) __log_print (__Log_Level_Info, __VA_ARGS__) +#define log_ok(...) __log_print (__Log_Level_Ok, __VA_ARGS__) +#define log_warn(...) __log_print (__Log_Level_Warn, __VA_ARGS__) +#define log_err(...) __log_print (__Log_Level_Err, __VA_ARGS__) + +#define log_die(...) \ + do { \ + __log_print(__Log_Level_Err, __VA_ARGS__); \ + exit(EXIT_FAILURE); \ + } while (0) + + +/** enums *********************************************************************/ + +enum __Log_Level { + __Log_Level_Debug, + __Log_Level_Info, + __Log_Level_Ok, + __Log_Level_Warn, + __Log_Level_Err, +}; + + +/** functions *****************************************************************/ + +void __log_print (enum __Log_Level level, const char *const fmt, ...) __attribute__ ((format (printf, 2, 3))); + + +#endif // LOG_H_ diff --git a/src/common.c b/src/common.c new file mode 100644 index 0000000..1c01886 --- /dev/null +++ b/src/common.c @@ -0,0 +1,106 @@ +#include "common.h" +#include +#include +#include "log.h" + + +/** functions *****************************************************************/ + +/** + * safe `malloc()` wrapper. instead of returning `nullptr` on error, this + * function will crash the program. returns a pointer to the allocated memory. + */ +void * +__smalloc (const usize nmemb, /* number of elements to allocate */ + const usize size /* size of each element */ +#ifdef DEBUG_ALLOC + , + const char *const file, /* __FILE__ */ + const usize line /* __LINE__ */ +#endif +) { + void *ptr; + + ptr = malloc (nmemb * size); + if (ptr == nullptr) { +#ifdef DEBUG_ALLOC + log_die ("\x1b[90m(\x1b[35m%s\x1b[90m:\x1b[34m%zu\x1b[90m)\x1b[0m " + "failed to allocate %zu bytes of memory.\n", + file, line, nmemb * size); +#else + log_die ("failed to allocate %zu bytes of memory.\n", nmemb * size); +#endif + } + + return ptr; +} + +/** + * safe `calloc()` wrapper. instead of returning `nullptr` on error, this + * function will crash the program. returns a pointer to the allocated memory. + */ +void * +__scalloc (const usize nmemb, /* number of elements to allocate */ + const usize size /* size of each element */ +#ifdef DEBUG_ALLOC + , + const char *const file, /* __FILE__ */ + const usize line /* __LINE__ */ +#endif +) { + void *ptr; + + ptr = calloc (nmemb, size); + if (ptr == nullptr) { +#ifdef DEBUG_ALLOC + log_die ("\x1b[90m(\x1b[35m%s\x1b[90m:\x1b[34m%zu\x1b[90m)\x1b[0m " + "failed to allocate %zu bytes of memory.\n", + file, line, nmemb * size); +#else + log_die ("failed to allocate %zu bytes of memory.\n", nmemb * size); +#endif + } + + return ptr; +} + +/** + * safe `realloc()` wrapper. instead of returning `nullptr` on error, this + * function will crash the program. returns a pointer to the reallocated memory. + */ +void * +__srealloc (void *ptr, /* the pointer to reallocate */ + const usize nmemb, /* number of elements to allocate */ + const usize size /* size of each element */ +#ifdef DEBUG_ALLOC + , + const char *const file, /* __FILE__ */ + const usize line /* __LINE__ */ +#endif +) { + ptr = realloc (ptr, nmemb * size); + if (ptr == nullptr) { +#ifdef DEBUG_ALLOC + log_die ("\x1b[90m(\x1b[35m%s\x1b[90m:\x1b[34m%zu\x1b[90m)\x1b[0m " + "failed to allocate %zu bytes of memory.\n", + file, line, nmemb * size); +#else + log_die ("failed to allocate %zu bytes of memory.\n", nmemb * size); +#endif + } + + return ptr; +} + +/** + * wrapper for `free()`. will not call `free()` on pointers that are `nullptr`. + * this is technically not necessary according to the c standard, but you never + * know. + */ +void +sfree (void *ptr) /* the pointer to free */ +{ + if (ptr != nullptr) { + free (ptr); + } +} diff --git a/src/log.c b/src/log.c new file mode 100644 index 0000000..6ce8db6 --- /dev/null +++ b/src/log.c @@ -0,0 +1,33 @@ +#include "log.h" +#include +#include +#include + + +/** functions *****************************************************************/ + +/** + * print a formatted message to stderr using printf formatting and a given log + * level. this functions is not intended to be used directly. the `log_*` macros + * should be used instead. + */ +__attribute__ ((format (printf, 2, 3))) +void +__log_print (enum __Log_Level level, /* log level */ + const char *const fmt, /* format string */ + ...) /* format parameters */ +{ + va_list ap; + + switch (level) { + case __Log_Level_Debug: { fprintf (stderr, "\x1b[90m[\x1b[35mdebug\x1b[90m]\x1b[0m "); break; } + case __Log_Level_Info: { fprintf (stderr, "\x1b[90m[\x1b[34minfo\x1b[90m]\x1b[0m " ); break; } + case __Log_Level_Ok: { fprintf (stderr, "\x1b[90m[\x1b[32mok\x1b[90m]\x1b[0m " ); break; } + case __Log_Level_Warn: { fprintf (stderr, "\x1b[90m[\x1b[33mwarn\x1b[90m]\x1b[0m " ); break; } + case __Log_Level_Err: { fprintf (stderr, "\x1b[90m[\x1b[31merr\x1b[90m]\x1b[0m " ); break; } + } + + va_start (ap, fmt); + vfprintf (stderr, fmt, ap); + va_end (ap); +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..b9b3c64 --- /dev/null +++ b/src/main.c @@ -0,0 +1,14 @@ +#include +#include "common.h" +#include "log.h" + +int +main (const int argc, const char *const argv[]) +{ + (void) argc; + (void) argv; + + log_debug ("Hello, World!\n"); + + return EXIT_SUCCESS; +}