#! python3 from argparse import ArgumentParser import hashlib import json import os import subprocess import shutil import sys from typing import List class Config: CC = 'g++' APPS = ['app_name'] JOB_COUNT = 1 BIN_DIR = 'bin' INCLUDE_DIR = 'include' OBJECT_DIR = 'obj' SOURCE_DIR = 'src' COMMON_FLAGS = '-std=c++17' COMMON_DEBUG_FLAGS = '-g' COMMON_RELEASE_FLAGS = '-O2 -flto' COMPILE_FLAGS = f'-Wall -I{INCLUDE_DIR}' LINK_FLAGS = '' CPP_HEADERS = [] CPP_SOURCES = [] def get_hash(path: str) -> 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 # Update flags and directories for mode debug/release if arguments.type == 'debug': config.COMMON_FLAGS += ' ' + config.COMMON_DEBUG_FLAGS config.OBJECT_DIR = os.path.join(config.OBJECT_DIR, 'debug') else: config.COMMON_FLAGS += ' ' + config.COMMON_RELEASE_FLAGS config.OBJECT_DIR = os.path.join(config.OBJECT_DIR, 'release') # Create list of source to process (tuple[source_path, object_path]) compile_list = [(source_file, source_file[:-4].replace(config.SOURCE_DIR, config.OBJECT_DIR) + '.o') for source_file in config.CPP_SOURCES] todo_list = [] hash_dict = {} if os.path.exists(os.path.join(config.OBJECT_DIR, 'hash.json')): with open(os.path.join(config.OBJECT_DIR, 'hash.json'), 'r') as hash_file: hash_dict = json.loads(hash_file.read()) # Check header files (if any modification is done all source file will be processed to avoid any problem) header_changed = False header_hash_dict = {} for header_path in config.CPP_HEADERS: header_hash = get_hash(header_path) if header_path not in hash_dict or hash_dict[header_path] != header_hash: header_hash_dict[header_path] = header_hash header_changed = True # 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, source_path, '-c', '-o', object_path]) if not os.path.exists(os.path.dirname(object_path)): os.makedirs(os.path.dirname(object_path)) source_hash = get_hash(source_path) if (header_changed or not os.path.exists(object_path) or source_path not in hash_dict or hash_dict[source_path] != source_hash): todo_list.append((source_path, source_hash, cmd)) continue # Running compilation processes if not os.path.exists(config.OBJECT_DIR): os.makedirs(config.OBJECT_DIR) error_path = None if arguments.j > 1: # Multi-process jobs: List[subprocess.Popen] = [] for source_path, source_hash, cmd in todo_list: print(cmd) jobs.insert(0, (source_path, source_hash, subprocess.Popen(cmd, 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, job_hash, oldest_job = jobs.pop() oldest_job.wait() if oldest_job.returncode != 0: error_path = job_path break hash_dict[job_path] = job_hash # Update hash if no error if error_path is None: for job_path, job_hash, job in jobs: # Wait the last jobs to finish job.wait() if job.returncode != 0: error_path = job_path break hash_dict[job_path] = job_hash # Update hash if no error else: # Single-process for source_path, source_hash, cmd in todo_list: print(cmd) complete = subprocess.run(cmd, check=False, shell=True) if complete.returncode != 0: error_path = source_path break hash_dict[source_path] = source_hash # Update hash if no error if error_path: print(f'Error compiling {error_path}') 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) # Running linking processes if not os.path.exists(config.BIN_DIR): os.makedirs(config.BIN_DIR) for app_name in config.APPS: app_path = os.path.join(config.BIN_DIR, app_name) app_objects = [file_name + '.o' for file_name in config.APPS if file_name != app_name] object_files = [object_path for _, object_path in compile_list if os.path.basename(object_path) not in app_objects] cmd = ' '.join([config.CC, config.COMMON_FLAGS, *object_files, '-o', app_path, config.LINK_FLAGS]) print(cmd) complete = subprocess.run(cmd, check=False, shell=True) if complete.returncode != 0: error_path = app_name if error_path: with open(os.path.join(config.OBJECT_DIR, 'hash.json'), 'w') as hash_file: hash_file.write(json.dumps(hash_dict, indent=1)) print('Error linking f{error_path}') sys.exit(1) # Updating header hashes only if everything compiled and linked correctly for header_path in header_hash_dict: hash_dict[header_path] = header_hash_dict[header_path] with open(os.path.join(config.OBJECT_DIR, 'hash.json'), 'w') as hash_file: hash_file.write(json.dumps(hash_dict, indent=1))