231 lines
11 KiB
Python
Executable file
231 lines
11 KiB
Python
Executable file
#! python3
|
|
|
|
from argparse import ArgumentParser
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import shutil
|
|
import sys
|
|
|
|
|
|
class Config:
|
|
CC = 'g++' # Compiler to call
|
|
APPS = ['app_name'] # Output binaries (need to be found as .cpp directly in SOURCE_DIR)
|
|
JOB_COUNT = int(os.cpu_count() * 0.8) # Concurent jobs (multi-processing)
|
|
|
|
BIN_DIR = Path('bin') # Output directory (binaries)
|
|
INCLUDE_DIR = Path('include') # Include directory (header files)
|
|
OBJECT_DIR = Path('obj') # Temporary directory (object files)
|
|
SOURCE_DIR = Path('src') # Source directories
|
|
|
|
COMMON_FLAGS = '-std=c++17' # Flags used for comiling and linking
|
|
COMMON_DEBUG_FLAGS = '-g' # Flags added in debug mode
|
|
COMMON_RELEASE_FLAGS = '-O2 -flto' # Flags added in release mode
|
|
COMPILE_FLAGS = f'-Wall -I{INCLUDE_DIR}' # Flags added for compiling (recommandation : `pkg-config --cflags`)
|
|
LINK_FLAGS = '' # Flags added for linking (recommandation : `pkg-config --libs`)
|
|
|
|
PRE_COMPILE_FUNCTION = None # Function to run before compile (not call in --clean situation)
|
|
|
|
CPP_SOURCES = SOURCE_DIR.rglob('*.cpp')
|
|
|
|
|
|
class ConsoleColor:
|
|
"""Simple shortcut to use colors in console"""
|
|
HEADER = '\033[95m'
|
|
BLUE = '\033[94m'
|
|
GREEN = '\033[92m'
|
|
ORANGE = '\033[93m'
|
|
RED = '\033[91m'
|
|
ENDCOLOR = '\033[0m'
|
|
BOLD = '\033[1m'
|
|
UNDERLINE = '\033[4m'
|
|
|
|
|
|
def get_hash(path: Path) -> str:
|
|
with open(path, 'r') as hashing_file:
|
|
hash_obj = hashlib.md5()
|
|
hash_obj.update(hashing_file.read().encode())
|
|
return hash_obj.hexdigest()
|
|
|
|
|
|
def make(config: Config):
|
|
parser = ArgumentParser()
|
|
parser.add_argument('-j', type=int, default=config.JOB_COUNT, help='Jobs count (multi-processing)')
|
|
parser.add_argument('--type', default='production', help='Compilation type (release, debug). Default=release')
|
|
parser.add_argument('--clean', action='store_true', help='Clean all file instead of building')
|
|
arguments = parser.parse_args()
|
|
|
|
# Clean action
|
|
if arguments.clean:
|
|
shutil.rmtree(config.OBJECT_DIR, ignore_errors=True)
|
|
shutil.rmtree(config.BIN_DIR, ignore_errors=True)
|
|
return
|
|
|
|
if config.PRE_COMPILE_FUNCTION is not None:
|
|
config.PRE_COMPILE_FUNCTION()
|
|
|
|
# Update flags and directories for mode debug/release
|
|
if arguments.type == 'debug':
|
|
config.COMMON_FLAGS += ' ' + config.COMMON_DEBUG_FLAGS
|
|
config.OBJECT_DIR = config.OBJECT_DIR / 'debug'
|
|
else:
|
|
config.COMMON_FLAGS += ' ' + config.COMMON_RELEASE_FLAGS
|
|
config.OBJECT_DIR = config.OBJECT_DIR / 'release'
|
|
|
|
# Create list of source to process (tuple[source_path, object_path])
|
|
compile_list = [(Path(source_file),
|
|
config.OBJECT_DIR / source_file.parent.relative_to(config.SOURCE_DIR) / (source_file.stem + '.o'))
|
|
for source_file in config.CPP_SOURCES]
|
|
todo_list: list[tuple[Path, str]] = []
|
|
error_paths: list[tuple[Path, str]] = []
|
|
|
|
hash_dict = {}
|
|
if (config.OBJECT_DIR / 'hash.json').exists():
|
|
with open(config.OBJECT_DIR / 'hash.json', 'r') as hash_file:
|
|
hash_dict = json.loads(hash_file.read())
|
|
|
|
# Get source dependencies
|
|
dependency_dict = {}
|
|
for source_path, object_path in compile_list:
|
|
cmd = ' '.join([config.CC, config.COMMON_FLAGS, config.COMPILE_FLAGS, str(source_path), '-M'])
|
|
job = subprocess.run(cmd, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
universal_newlines=True, shell=True)
|
|
if job.returncode != 0:
|
|
error_paths.append((source_path, job.stdout + job.stderr))
|
|
break
|
|
dependency_dict[str(source_path)] = job.stdout.split('.o: ')[1].replace('\n', '').replace('\\ ', '').split(' ')
|
|
if error_paths:
|
|
for error_path, error_text in error_paths:
|
|
print(ConsoleColor.RED + f'Error checking dependencies for {error_path}:\n'
|
|
+ ConsoleColor.ENDCOLOR + error_text)
|
|
with open(config.OBJECT_DIR / 'hash.json', 'w') as hash_file:
|
|
hash_file.write(json.dumps(hash_dict, indent=1))
|
|
sys.exit(1)
|
|
|
|
# Check source file and generate compilation commands to execute
|
|
for source_path, object_path in compile_list:
|
|
cmd = ' '.join([config.CC, config.COMMON_FLAGS, config.COMPILE_FLAGS,
|
|
str(source_path), '-c', '-o', str(object_path)])
|
|
if not object_path.parent.exists():
|
|
object_path.parent.mkdir(parents=True)
|
|
source_hash = get_hash(source_path)
|
|
dependency_changed = False
|
|
for dependency_path in dependency_dict[str(source_path)]:
|
|
dependency_hash = get_hash(dependency_path)
|
|
if str(dependency_path) not in hash_dict or hash_dict[str(dependency_path)] != dependency_hash:
|
|
dependency_changed = True
|
|
print(ConsoleColor.ORANGE + f'Dependency changed for {source_path} : {dependency_path}'
|
|
+ ConsoleColor.ENDCOLOR)
|
|
break
|
|
if (dependency_changed or not object_path.exists()
|
|
or str(source_path) not in hash_dict or hash_dict[str(source_path)] != source_hash):
|
|
todo_list.append((source_path, cmd))
|
|
continue
|
|
|
|
if not todo_list and all([(config.BIN_DIR / app_path).exists() for app_path in config.APPS]):
|
|
print(ConsoleColor.GREEN + 'Nothing to do' + ConsoleColor.ENDCOLOR)
|
|
return
|
|
|
|
# Running compilation processes
|
|
if not os.path.exists(config.OBJECT_DIR):
|
|
os.makedirs(config.OBJECT_DIR)
|
|
if arguments.j > 1: # Multi-process
|
|
jobs: list[tuple[Path, subprocess.Popen]] = []
|
|
for source_path, cmd in todo_list:
|
|
print(ConsoleColor.BLUE + cmd + ConsoleColor.ENDCOLOR)
|
|
jobs.insert(0, (source_path, subprocess.Popen(
|
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True,
|
|
shell=True))) # FIFO style (will be poped)
|
|
if len(jobs) >= arguments.j: # If jobs count is maxed we wait for the oldest one to finished
|
|
job_path, oldest_job = jobs.pop()
|
|
oldest_job.wait()
|
|
if oldest_job.returncode != 0:
|
|
error_paths.append((job_path, oldest_job.stdout.read() + oldest_job.stderr.read()))
|
|
break
|
|
# Update hash if no error
|
|
for dependency_path in dependency_dict[str(job_path)]:
|
|
hash_dict[str(dependency_path)] = get_hash(dependency_path)
|
|
for job_path, job in jobs: # Wait the last jobs to finish
|
|
job.wait()
|
|
if job.returncode != 0:
|
|
error_paths.append((job_path, job.stdout.read() + job.stderr.read()))
|
|
else:
|
|
# Update hash if no error
|
|
for dependency_path in dependency_dict[str(job_path)]:
|
|
hash_dict[str(dependency_path)] = get_hash(dependency_path)
|
|
else: # Single-process
|
|
for source_path, source_hash, cmd in todo_list:
|
|
print(ConsoleColor.BLUE + cmd + ConsoleColor.ENDCOLOR)
|
|
job = subprocess.run(cmd, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
universal_newlines=True, shell=True)
|
|
if job.returncode != 0:
|
|
error_paths.append((source_path, job.stdout + job.stderr))
|
|
break
|
|
# Update hash if no error
|
|
for dependency_path in dependency_dict[str(source_path)]:
|
|
hash_dict[str(dependency_path)] = get_hash(dependency_path)
|
|
if error_paths:
|
|
for error_path, error_text in error_paths:
|
|
print(ConsoleColor.RED + f'Error compiling {error_path}:\n' + ConsoleColor.ENDCOLOR + error_text)
|
|
with open(config.OBJECT_DIR / 'hash.json', 'w') as hash_file:
|
|
hash_file.write(json.dumps(hash_dict, indent=1))
|
|
sys.exit(1)
|
|
|
|
# Running linking processes
|
|
if not config.BIN_DIR.exists():
|
|
config.BIN_DIR.mkdir(parents=True)
|
|
all_app_objects = [config.OBJECT_DIR / Path(app_path).parent / (Path(app_path).stem + '.o')
|
|
for app_path in config.APPS]
|
|
if arguments.j > 1: # Multi-process
|
|
jobs: list[tuple[Path, subprocess.Popen]] = []
|
|
for app_path, app_object_path in zip(config.APPS, all_app_objects):
|
|
if 'IGNORE_APPS' in config.__dict__ and app_path in config.IGNORE_APPS:
|
|
continue
|
|
bin_path = config.BIN_DIR / app_path
|
|
if not bin_path.parent.exists():
|
|
bin_path.parent.mkdir(parents=True)
|
|
object_files = [str(object_path) for _, object_path in compile_list
|
|
if object_path not in all_app_objects]
|
|
object_files.append(str(app_object_path))
|
|
cmd = ' '.join([config.CC, config.COMMON_FLAGS, *object_files, '-o', str(bin_path), config.LINK_FLAGS])
|
|
print(ConsoleColor.BLUE + cmd + ConsoleColor.ENDCOLOR)
|
|
jobs.insert(0, (app_path, subprocess.Popen(
|
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True,
|
|
shell=True))) # FIFO style (will be poped)
|
|
if len(jobs) >= arguments.j: # If jobs count is maxed we wait for the oldest one to finished
|
|
app_path, oldest_job = jobs.pop()
|
|
oldest_job.wait()
|
|
if oldest_job.returncode != 0:
|
|
error_paths.append((source_path, oldest_job.stdout.read() + oldest_job.stderr.read()))
|
|
break
|
|
for job_path, job in jobs: # Wait the last jobs to finish
|
|
job.wait()
|
|
if job.returncode != 0:
|
|
error_paths.append((source_path, job.stdout.read() + job.stderr.read()))
|
|
else: # Single-process
|
|
for app_path, app_object_path in zip(config.APPS, all_app_objects):
|
|
if 'IGNORE_APPS' in config.__dict__ and app_path in config.IGNORE_APPS:
|
|
continue
|
|
bin_path = config.BIN_DIR / app_path
|
|
if not bin_path.parent.exists():
|
|
bin_path.parent.mkdir(parents=True)
|
|
object_files = [str(object_path) for _, object_path in compile_list
|
|
if object_path not in all_app_objects]
|
|
object_files.append(str(app_object_path))
|
|
cmd = ' '.join([config.CC, config.COMMON_FLAGS, *object_files, '-o', str(bin_path), config.LINK_FLAGS])
|
|
print(ConsoleColor.BLUE + cmd + ConsoleColor.ENDCOLOR)
|
|
job = subprocess.run(cmd, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
universal_newlines=True, shell=True)
|
|
if job.returncode != 0:
|
|
error_paths.append((source_path, job.stdout + job.stderr))
|
|
if error_paths:
|
|
for error_path, error_text in error_paths:
|
|
print(ConsoleColor.RED + f'Error linking {error_path}:\n' + ConsoleColor.ENDCOLOR + error_text)
|
|
with open(os.path.join(config.OBJECT_DIR, 'hash.json'), 'w') as hash_file:
|
|
hash_file.write(json.dumps(hash_dict, indent=1))
|
|
sys.exit(1)
|
|
|
|
with open(config.OBJECT_DIR / 'hash.json', 'w') as hash_file:
|
|
hash_file.write(json.dumps(hash_dict, indent=1))
|