init: ready, set, go!
This commit is contained in:
commit
930dafa31f
|
@ -0,0 +1,13 @@
|
|||
gcc
|
||||
-std=c11
|
||||
-Iinc
|
||||
-Wall
|
||||
-Wextra
|
||||
-Wconversion
|
||||
-Wdouble-promotion
|
||||
-Wshadow
|
||||
-Wcast-qual
|
||||
-Wmissing-prototypes
|
||||
-Werror
|
||||
-pedantic
|
||||
-DDEBUG
|
|
@ -0,0 +1,4 @@
|
|||
/bin
|
||||
/obj
|
||||
/.ccls-cache
|
||||
__pycache__
|
|
@ -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()
|
|
@ -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'
|
|
@ -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_
|
|
@ -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_
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue