231 lines
6.6 KiB
Python
Executable File
231 lines
6.6 KiB
Python
Executable File
#!/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()
|