init: ready, set, go!

This commit is contained in:
thetek 2023-06-23 11:55:30 +02:00
commit 930dafa31f
9 changed files with 546 additions and 0 deletions

13
.ccls Normal file
View File

@ -0,0 +1,13 @@
gcc
-std=c11
-Iinc
-Wall
-Wextra
-Wconversion
-Wdouble-promotion
-Wshadow
-Wcast-qual
-Wmissing-prototypes
-Werror
-pedantic
-DDEBUG

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/bin
/obj
/.ccls-cache
__pycache__

230
build/build.py Executable file
View File

@ -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()

7
build/config.py Normal file
View File

@ -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'

100
inc/common.h Normal file
View File

@ -0,0 +1,100 @@
#ifndef COMMON_H_
#define COMMON_H_
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <sys/types.h>
/** 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_

39
inc/log.h Normal file
View File

@ -0,0 +1,39 @@
#ifndef LOG_H_
#define LOG_H_
#include <stdlib.h>
/** 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_

106
src/common.c Normal file
View File

@ -0,0 +1,106 @@
#include "common.h"
#include <stdarg.h>
#include <stdlib.h>
#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);
}
}

33
src/log.c Normal file
View File

@ -0,0 +1,33 @@
#include "log.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
/** 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);
}

14
src/main.c Normal file
View File

@ -0,0 +1,14 @@
#include <stdlib.h>
#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;
}