Modernize code

* Add pyproject.toml
* Remove pylint/flake8 linting configuration
* Add ruff configuration
* Multiple code refactor
* Add prints when watch option is enable
This commit is contained in:
Corentin 2026-07-23 15:32:40 +09:00
commit 933dea2b9e
Signed by: corentin
GPG key ID: 48C87E27C6C917F4
6 changed files with 197 additions and 179 deletions

0
__init__.py Normal file
View file

31
make.py
View file

@ -1,36 +1,11 @@
#! python3 #! python3
import os from umake.umake import make, Config
from pathlib import Path
from umake import make
class Config:
CC = 'g++' # Compiler to call
APPS = ['app_name'] # Output binaries (path to source without extension)
IGNORE_APPS = []
JOB_COUNT = int(os.cpu_count() * 0.8) # Concurent jobs (multi-processing)
WATCH = False # Watch source modification for auto-compiling
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 = [filepath for filepath in SOURCE_DIR.rglob('*.cpp') if not filepath.name.startswith('.')]
def main(): def main():
make(Config) config = Config()
make(config)
if __name__ == '__main__': if __name__ == '__main__':

56
pyproject.toml Normal file
View file

@ -0,0 +1,56 @@
[project]
name = 'umake'
version = '1.0.0'
requires-python = ">=3.8"
[tool.ruff]
cache-dir = "/tmp/ruff"
exclude = [
".git",
".ruff_cache",
".venv"
]
line-length = 120
indent-width = 4
[tool.ruff.lint]
preview = true
select = ["A", "ARG", "B", "C", "E", "F", "FURB", "G", "I","ICN", "ISC", "PERF", "PIE", "PL", "PLE", "PT", "PTH",
"Q", "RET", "RSE", "RUF", "SLF", "SIM", "T20", "TCH", "UP", "W"]
ignore = ["implicit-return", "implicit-return-value", "missing-whitespace-after-keyword", "mutable-class-default",
"print", "redefined-loop-name", "reimplemented-starmap", "suppressible-exception", "try-except-in-loop",
"unsorted-imports"]
[tool.ruff.lint.flake8-quotes]
inline-quotes = "single"
[tool.ruff.lint.isort]
combine-as-imports = true
force-sort-within-sections = true
lines-after-imports = 2
[tool.ruff.lint.mccabe]
max-complexity = 40
[tool.ruff.lint.pylint]
max-args=16
max-branches=42
max-locals=64
max-nested-blocks=8
max-public-methods=64
max-returns=12
max-statements=128
max-statements-in-try=64
[tool.isort]
combine_as_imports = true
force_sort_within_sections = true
lexicographical = true
lines_after_imports = 2
multi_line_output = 4
no_sections = false
order_by_type = true
[tool.pyright]
exclude = ["**/__pycache__", "**/.venv", "**/services", "web_front"]

View file

@ -1,25 +0,0 @@
[flake8]
max-line-length=120
ignore=D10,D203,D204
[pycodestyle]
max-line-length=120
ignore=D10,D203,D204
[pylint.DESIGN]
max-args=16
min-public-methods=0
max-attributes=16
max-locals=64
[pylint.FORMAT]
max-line-length=120
[pylint.MESSAGE CONTROL]
disable=missing-module-docstring, missing-function-docstring, missing-class-docstring, relative-beyond-top-level, too-few-public-methods, import-error
[pylint.SIMILARITIES]
min-similarity-lines=6
[pydocstyle]
ignore=D10,D203,D204

248
umake.py
View file

@ -1,6 +1,7 @@
#! python3 #! python3
from argparse import ArgumentParser from argparse import ArgumentParser
from dataclasses import dataclass, field
import hashlib import hashlib
import json import json
import os import os
@ -8,29 +9,34 @@ from pathlib import Path
import subprocess import subprocess
import shutil import shutil
import sys import sys
from typing import Any, Callable, Iterable
@dataclass
class Config: class Config:
CC = 'g++' # Compiler to call cc: str = 'g++' # Compiler to call
APPS = ['app_name'] # Output binaries (need to be found as .cpp directly in SOURCE_DIR) # Output binaries (need to be found as .cpp directly in source_dir)
IGNORE_APPS = [] apps: Iterable[str] = field(default_factory=lambda: ['app_name'])
JOB_COUNT = int(os.cpu_count() * 0.8) # Concurent jobs (multi-processing) ignore_apps: Iterable[str] = field(default_factory=list)
WATCH = False # Watch source modification for auto-compiling job_count: int = int((os.cpu_count() or 1) * 0.8) # Concurent jobs (multi-processing)
watch: bool = False # Watch source modification for auto-compiling
BIN_DIR = Path('bin') # Output directory (binaries) bin_dir: Path = Path('bin') # Output directory (binaries)
INCLUDE_DIR = Path('include') # Include directory (header files) include_dir: Path = Path('include') # Include directory (header files)
OBJECT_DIR = Path('obj') # Temporary directory (object files) object_dir: Path = Path('obj') # Temporary directory (object files)
SOURCE_DIR = Path('src') # Source directories source_dir: Path = Path('src') # Source directories
COMMON_FLAGS = '-std=c++17' # Flags used for comiling and linking common_flags: str = '-std=c++17' # Flags used for comiling and linking
COMMON_DEBUG_FLAGS = '-g' # Flags added in debug mode common_debug_flags: str = '-g' # Flags added in debug mode
COMMON_RELEASE_FLAGS = '-O2 -flto' # Flags added in release mode common_release_flags: str = '-O2 -flto' # Flags added in release mode
COMPILE_FLAGS = f'-Wall -I{INCLUDE_DIR}' # Flags added for compiling (recommandation : `pkg-config --cflags`) compile_flags: str = f'-Wall -I{include_dir}' # Flags added for compiling (recommandation : `pkg-config --cflags`)
LINK_FLAGS = '' # Flags added for linking (recommandation : `pkg-config --libs`) link_flags: str = '' # Flags added for linking (recommandation : `pkg-config --libs`)
PRE_COMPILE_FUNCTION = None # Function to run before compile (not call in --clean situation) cpp_sources: Iterable[Path] = field(default_factory=lambda: [
filepath for filepath in Path('src').rglob('*.cpp') if not filepath.name.startswith('.')])
CPP_SOURCES = SOURCE_DIR.rglob('*.cpp') # Function to run before compile (not call in --clean situation)
pre_compile_function: Callable[[], Any] | None = None
class ConsoleColor: class ConsoleColor:
@ -46,45 +52,42 @@ class ConsoleColor:
UNDERLINE = '\033[4m' UNDERLINE = '\033[4m'
def get_hash(path: Path) -> str: def get_hash(hash_path: Path) -> str:
hash_obj = hashlib.md5() hash_obj = hashlib.md5()
with open(path, 'r', encoding='utf-8') as hashing_file: hash_obj.update(hash_path.read_text(encoding='utf-8').encode())
hash_obj.update(hashing_file.read().encode())
return hash_obj.hexdigest() return hash_obj.hexdigest()
def make(config: Config): def make(config: Config):
parser = ArgumentParser() parser = ArgumentParser()
parser.add_argument('-j', type=int, default=config.JOB_COUNT, help='Jobs count (multi-processing)') 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('--type', default='release', help='Compilation type (release, debug). Default=release')
parser.add_argument('--clean', action='store_true', help='Clean all file instead of building') parser.add_argument('--clean', action='store_true', help='Clean all file instead of building')
arguments = parser.parse_args() arguments = parser.parse_args()
# Clean action # Clean action
if arguments.clean: if arguments.clean:
if config.OBJECT_DIR.exists(): if config.object_dir.exists():
for object_entry in config.OBJECT_DIR.iterdir(): for object_entry in config.object_dir.iterdir():
shutil.rmtree(object_entry, ignore_errors=True) shutil.rmtree(object_entry, ignore_errors=True)
if config.BIN_DIR.exists(): if config.bin_dir.exists():
for binary_entry in config.BIN_DIR.iterdir(): for binary_entry in config.bin_dir.iterdir():
shutil.rmtree(binary_entry, ignore_errors=True) shutil.rmtree(binary_entry, ignore_errors=True)
return return
if config.PRE_COMPILE_FUNCTION is not None: if config.pre_compile_function is not None:
config.PRE_COMPILE_FUNCTION() config.pre_compile_function()
if 'IGNORE_APPS' in config.__dict__:
config.IGNORE_APPS = []
# Update flags and directories for mode debug/release # Update flags and directories for mode debug/release
if arguments.type == 'debug': if arguments.type == 'debug':
config.COMMON_FLAGS += ' ' + config.COMMON_DEBUG_FLAGS config.common_flags += ' ' + config.common_debug_flags
config.OBJECT_DIR = config.OBJECT_DIR / 'debug' config.object_dir /= 'debug'
else: else:
config.COMMON_FLAGS += ' ' + config.COMMON_RELEASE_FLAGS config.common_flags += ' ' + config.common_release_flags
config.OBJECT_DIR = config.OBJECT_DIR / 'release' config.object_dir /= 'release'
# Update job count # Update job count
config.JOB_COUNT = arguments.j config.job_count = arguments.j
Builder(config).make() Builder(config).make()
@ -95,51 +98,55 @@ class Builder:
# Create list of source to process (tuple[source_path, object_path]) # Create list of source to process (tuple[source_path, object_path])
self._compile_dict: dict[Path, Path] = { self._compile_dict: dict[Path, Path] = {
Path(source_file): (self._config.OBJECT_DIR / source_file.parent.relative_to(self._config.SOURCE_DIR) Path(source_file): (self._config.object_dir / source_file.parent.relative_to(self._config.source_dir)
/ (source_file.stem + '.o')) / (source_file.stem + '.o'))
for source_file in self._config.CPP_SOURCES} for source_file in self._config.cpp_sources}
self._todo_dict: dict[Path, str] = {} self._todo_dict: dict[Path, str] = {}
self._hash_dict: dict[str, str] = {} self._hash_dict: dict[Path, str] = {}
if (self._config.OBJECT_DIR / 'hash.json').exists(): hash_dict_path = self._config.object_dir / 'hash.json'
with open(self._config.OBJECT_DIR / 'hash.json', 'r', encoding='utf-8') as hash_file: if hash_dict_path.exists():
self._hash_dict = json.loads(hash_file.read()) try:
self._hash_dict = {
Path(path): value for path, value in json.loads(hash_dict_path.read_text(encoding='utf-8')).items()}
except json.JSONDecodeError:
pass
# Get source dependencies # Get source dependencies
error_paths: list[tuple[Path, str]] = [] error_paths: list[tuple[Path, str]] = []
self._dependency_dict: dict[str, list[str]] = {} self._dependency_dict: dict[Path, list[Path]] = {}
for source_path, _object_path in self._compile_dict.items(): for source_path in self._compile_dict:
cmd = ' '.join( cmd = ' '.join(
[self._config.CC, self._config.COMMON_FLAGS, self._config.COMPILE_FLAGS, str(source_path), '-M']) [self._config.cc, self._config.common_flags, self._config.compile_flags, str(source_path), '-M'])
job = subprocess.run(cmd, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, job = subprocess.run(cmd, check=False, capture_output=True, text=True, shell=True)
universal_newlines=True, shell=True)
if job.returncode != 0: if job.returncode != 0:
error_paths.append((source_path, job.stdout + job.stderr)) error_paths.append((source_path, job.stdout + job.stderr))
break break
self._dependency_dict[str(source_path)] = [ self._dependency_dict[source_path] = [
line for line in job.stdout.split('.o: ')[1].replace('\n', '').replace('\\ ', '').split(' ') if line] Path(line) for line in job.stdout.split('.o: ')[1].replace('\n', '').replace('\\ ', '').split(' ')
if line]
if error_paths: if error_paths:
for error_path, error_text in error_paths: for error_path, error_text in error_paths:
print(f'{ConsoleColor.RED}Error checking dependencies for {error_path}:' print(f'{ConsoleColor.RED}Error checking dependencies for {error_path}:{ConsoleColor.ENDCOLOR}'
f'\n{ConsoleColor.ENDCOLOR + error_text}') f'\n{error_text}')
with open(self._config.OBJECT_DIR / 'hash.json', 'w', encoding='utf-8') as hash_file:
hash_file.write(json.dumps(self._hash_dict, indent=1))
sys.exit(1) sys.exit(1)
def make(self): def make(self):
self._populate_todo_dict() self._populate_todo_dict()
if self._config.WATCH: if self._config.watch:
from watch import Watcher, WatchFlag # pylint: disable=import-outside-toplevel from watch import Watcher, WatchFlag # ruff:ignore[import-outside-top-level]
def callback(path: Path, _flags: WatchFlag): def callback(path: Path, _: WatchFlag):
source_hash = get_hash(path) source_hash = get_hash(path)
if str(path) not in self._hash_dict or source_hash != self._hash_dict[str(path)]: if path not in self._hash_dict or source_hash != self._hash_dict[path]:
print(f'{ConsoleColor.ORANGE}Source file changed : {path} {ConsoleColor.ENDCOLOR}') print(f'{ConsoleColor.ORANGE}Source file changed : {path} {ConsoleColor.ENDCOLOR}')
self._populate_todo_dict() self._populate_todo_dict()
self._compile_sources() self._compile_sources()
print(f'{ConsoleColor.GREEN}Watching for changes{ConsoleColor.ENDCOLOR}')
self._compile_sources() self._compile_sources()
print(f'{ConsoleColor.GREEN}Watching for changes{ConsoleColor.ENDCOLOR}')
watcher = Watcher() watcher = Watcher()
for source_path in self._compile_dict: for source_path in self._compile_dict:
watcher.register(source_path, WatchFlag.MODIFY, callback) watcher.register(source_path, WatchFlag.MODIFY, callback)
@ -150,6 +157,10 @@ class Builder:
elif not self._compile_sources(): elif not self._compile_sources():
sys.exit(1) sys.exit(1)
def _save_hash_dict(self):
(self._config.object_dir / 'hash.json').write_text(
json.dumps({str(path): value for path, value in self._hash_dict.items()}, indent=1), encoding='utf-8')
def _populate_todo_dict(self): def _populate_todo_dict(self):
"""Check source file and generate compilation commands to execute.""" """Check source file and generate compilation commands to execute."""
for source_path, object_path in self._compile_dict.items(): for source_path, object_path in self._compile_dict.items():
@ -157,51 +168,53 @@ class Builder:
object_path.parent.mkdir(parents=True) object_path.parent.mkdir(parents=True)
source_hash = get_hash(source_path) source_hash = get_hash(source_path)
dependency_changed = False dependency_changed = False
for path in self._dependency_dict[str(source_path)]: for path in self._dependency_dict[source_path]:
dependency_hash = get_hash(path) dependency_hash = get_hash(path)
if str(path) not in self._hash_dict or self._hash_dict[str(path)] != dependency_hash: if path not in self._hash_dict or self._hash_dict[path] != dependency_hash:
dependency_changed = True dependency_changed = True
print(f'{ConsoleColor.ORANGE}Dependency changed for {source_path} : {path} {ConsoleColor.ENDCOLOR}') print(f'{ConsoleColor.ORANGE}Dependency changed for {source_path} : {path} {ConsoleColor.ENDCOLOR}')
break break
if (dependency_changed or not object_path.exists() if (dependency_changed or not object_path.exists()
or str(source_path) not in self._hash_dict or self._hash_dict[str(source_path)] != source_hash): or source_path not in self._hash_dict or self._hash_dict[source_path] != source_hash):
self._todo_dict[source_path] = self._generate_compilation_command(source_path, object_path) self._todo_dict[source_path] = self._generate_compilation_command(source_path, object_path)
continue continue
def _generate_compilation_command(self, source_path: Path, object_path: Path) -> str: def _generate_compilation_command(self, source_path: Path, object_path: Path) -> str:
return ' '.join([self._config.CC, self._config.COMMON_FLAGS, self._config.COMPILE_FLAGS, return ' '.join([self._config.cc, self._config.common_flags, self._config.compile_flags,
str(source_path), '-c', '-o', str(object_path)]) str(source_path), '-c', '-o', str(object_path)])
def _compile_sources(self) -> bool: def _compile_sources(self) -> bool:
if not self._todo_dict and all( if not self._todo_dict and all(
(self._config.BIN_DIR / app_path).exists() (self._config.bin_dir / app_path).exists()
for app_path in self._config.APPS if app_path not in self._config.IGNORE_APPS): for app_path in self._config.apps if app_path not in self._config.ignore_apps):
print(f'{ConsoleColor.GREEN}Nothing to do{ConsoleColor.ENDCOLOR}') print(f'{ConsoleColor.GREEN}Nothing to do{ConsoleColor.ENDCOLOR}')
return True return True
# Running compilation processes # Running compilation processes
if not os.path.exists(self._config.OBJECT_DIR): if not self._config.object_dir.exists:
os.makedirs(self._config.OBJECT_DIR) self._config.object_dir.mkdir(parents=True)
error_paths: list[tuple[Path, str]] = [] error_paths: list[tuple[Path, str]] = []
if self._config.JOB_COUNT > 1: # Multi-process if self._config.job_count > 1: # Multi-process
jobs: list[tuple[Path, subprocess.Popen]] = [] jobs: list[tuple[Path, subprocess.Popen]] = []
completed_paths: list[Path] = [] completed_paths: list[Path] = []
for source_path, cmd in self._todo_dict.items(): for source_path, cmd in self._todo_dict.items():
print(ConsoleColor.BLUE + cmd + ConsoleColor.ENDCOLOR) print(f'{ConsoleColor.BLUE}{cmd}{ConsoleColor.ENDCOLOR}')
jobs.insert(0, (source_path, subprocess.Popen( jobs.insert(0, (source_path, subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True,
shell=True))) # FIFO style (will be poped) shell=True))) # FIFO style (will be poped)
if len(jobs) >= self._config.JOB_COUNT: # If jobs count is maxed we wait for the oldest one to finished if len(jobs) >= self._config.job_count: # If jobs count is maxed we wait for the oldest one to finished
job_path, oldest_job = jobs.pop() job_path, oldest_job = jobs.pop()
oldest_job.wait() oldest_job.wait()
if oldest_job.returncode != 0: if oldest_job.returncode != 0:
if oldest_job.stdout is None or oldest_job.stderr is None:
raise RuntimeError('Unexpected error: job without stdout/stdin')
error_paths.append((job_path, oldest_job.stdout.read() + oldest_job.stderr.read())) error_paths.append((job_path, oldest_job.stdout.read() + oldest_job.stderr.read()))
if str(job_path) in self._hash_dict: if job_path in self._hash_dict:
del self._hash_dict[str(job_path)] del self._hash_dict[job_path]
break break
# Update hash if no error # Update hash if no error
for dependency_path in self._dependency_dict[str(job_path)]: for dependency_path in self._dependency_dict[job_path]:
self._hash_dict[str(dependency_path)] = get_hash(dependency_path) self._hash_dict[dependency_path] = get_hash(dependency_path)
completed_paths.append(job_path) completed_paths.append(job_path)
for source_path in completed_paths: for source_path in completed_paths:
del self._todo_dict[source_path] del self._todo_dict[source_path]
@ -209,97 +222,96 @@ class Builder:
for job_path, job in jobs: # Wait the last jobs to finish for job_path, job in jobs: # Wait the last jobs to finish
job.wait() job.wait()
if job.returncode != 0: if job.returncode != 0:
if job.stdout is None or job.stderr is None:
raise RuntimeError('Unexpected error: job without stdout/stdin')
error_paths.append((job_path, job.stdout.read() + job.stderr.read())) error_paths.append((job_path, job.stdout.read() + job.stderr.read()))
if str(job_path) in self._hash_dict: if job_path in self._hash_dict:
del self._hash_dict[str(job_path)] del self._hash_dict[job_path]
else: else:
# Update hash if no error # Update hash if no error
for dependency_path in self._dependency_dict[str(job_path)]: for dependency_path in self._dependency_dict[job_path]:
self._hash_dict[str(dependency_path)] = get_hash(dependency_path) self._hash_dict[dependency_path] = get_hash(dependency_path)
del self._todo_dict[job_path] del self._todo_dict[job_path]
else: # Single-process else: # Single-process
completed_paths: list[Path] = [] completed_paths: list[Path] = []
for source_path, cmd in self._todo_dict.items(): for source_path, cmd in self._todo_dict.items():
print(ConsoleColor.BLUE + cmd + ConsoleColor.ENDCOLOR) print(f'{ConsoleColor.BLUE}{cmd}{ConsoleColor.ENDCOLOR}')
job = subprocess.run(cmd, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, job = subprocess.run(cmd, check=False, capture_output=True, text=True, shell=True)
universal_newlines=True, shell=True)
if job.returncode != 0: if job.returncode != 0:
error_paths.append((source_path, job.stdout + job.stderr)) error_paths.append((source_path, job.stdout + job.stderr))
if str(source_path) in self._hash_dict: if source_path in self._hash_dict:
del self._hash_dict[str(source_path)] del self._hash_dict[source_path]
break break
# Update hash if no error # Update hash if no error
for dependency_path in self._dependency_dict[str(source_path)]: for dependency_path in self._dependency_dict[source_path]:
self._hash_dict[str(dependency_path)] = get_hash(dependency_path) self._hash_dict[dependency_path] = get_hash(dependency_path)
completed_paths.append(source_path) completed_paths.append(source_path)
for source_path in completed_paths: for source_path in completed_paths:
del self._todo_dict[source_path] del self._todo_dict[source_path]
if error_paths: if error_paths:
for error_path, error_text in error_paths: for error_path, error_text in error_paths:
print(ConsoleColor.RED + f'Error compiling {error_path}:\n' + ConsoleColor.ENDCOLOR + error_text) print(f'{ConsoleColor.RED}Error compiling {error_path}:{ConsoleColor.ENDCOLOR}\n{error_text}')
with open(self._config.OBJECT_DIR / 'hash.json', 'w', encoding='utf-8') as hash_file: self._save_hash_dict()
hash_file.write(json.dumps(self._hash_dict, indent=1))
return False return False
# Running linking processes # Running linking processes
if not self._config.BIN_DIR.exists(): if not self._config.bin_dir.exists():
self._config.BIN_DIR.mkdir(parents=True) self._config.bin_dir.mkdir(parents=True)
all_app_objects = [self._config.OBJECT_DIR / Path(app_path).parent / (Path(app_path).stem + '.o') all_app_objects = [self._config.object_dir / Path(app_path).parent / (Path(app_path).stem + '.o')
for app_path in self._config.APPS] for app_path in self._config.apps]
if self._config.JOB_COUNT > 1: # Multi-process if self._config.job_count > 1: # Multi-process
jobs: list[tuple[Path, subprocess.Popen]] = [] jobs: list[tuple[Path, subprocess.Popen]] = []
for app_path, app_object_path in zip(self._config.APPS, all_app_objects): for app_path, app_object_path in zip(self._config.apps, all_app_objects):
if app_path in self._config.IGNORE_APPS: if app_path in self._config.ignore_apps:
continue continue
bin_path = self._config.BIN_DIR / app_path bin_path = self._config.bin_dir / app_path
if not bin_path.parent.exists(): if not bin_path.parent.exists():
bin_path.parent.mkdir(parents=True) bin_path.parent.mkdir(parents=True)
object_files = [str(object_path) for object_path in self._compile_dict.values() object_files: list[str] = [str(app_object_path), *[
if object_path not in all_app_objects] str(object_path) for object_path in self._compile_dict.values()
object_files.append(str(app_object_path)) if object_path not in all_app_objects]]
cmd = ' '.join([self._config.CC, self._config.COMMON_FLAGS, *object_files, '-o', str(bin_path), cmd = ' '.join([self._config.cc, self._config.common_flags, *object_files, '-o', str(bin_path),
self._config.LINK_FLAGS]) self._config.link_flags])
print(ConsoleColor.BLUE + cmd + ConsoleColor.ENDCOLOR) print(f'{ConsoleColor.BLUE}{cmd}{ConsoleColor.ENDCOLOR}')
jobs.insert(0, (app_path, subprocess.Popen( jobs.insert(0, (Path(app_path), subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True,
shell=True))) # FIFO style (will be poped) shell=True))) # FIFO style (will be poped)
if len(jobs) >= self._config.JOB_COUNT: # If jobs count is maxed we wait for the oldest one to finished if len(jobs) >= self._config.job_count: # If jobs count is maxed we wait for the oldest one to finished
app_path, oldest_job = jobs.pop() app_path, oldest_job = jobs.pop()
oldest_job.wait() oldest_job.wait()
if oldest_job.returncode != 0: if oldest_job.returncode != 0:
if oldest_job.stdout is None or oldest_job.stderr is None:
raise RuntimeError('Unexpected error: job without stdout/stdin')
error_paths.append((app_path, oldest_job.stdout.read() + oldest_job.stderr.read())) error_paths.append((app_path, oldest_job.stdout.read() + oldest_job.stderr.read()))
break break
for job_path, job in jobs: # Wait the last jobs to finish for job_path, job in jobs: # Wait the last jobs to finish
job.wait() job.wait()
if job.returncode != 0: if job.returncode != 0:
if job.stdout is None or job.stderr is None:
raise RuntimeError('Unexpected error: job without stdout/stdin')
error_paths.append((job_path, job.stdout.read() + job.stderr.read())) error_paths.append((job_path, job.stdout.read() + job.stderr.read()))
else: # Single-process else: # Single-process
for app_path, app_object_path in zip(self._config.APPS, all_app_objects): for app_path, app_object_path in zip(self._config.apps, all_app_objects):
if app_path in self._config.IGNORE_APPS: if app_path in self._config.ignore_apps:
continue continue
bin_path = self._config.BIN_DIR / app_path bin_path = self._config.bin_dir / app_path
if not bin_path.parent.exists(): if not bin_path.parent.exists():
bin_path.parent.mkdir(parents=True) bin_path.parent.mkdir(parents=True)
object_files = [str(object_path) for object_path in self._compile_dict.values() object_files: list[str] = [str(app_object_path), *[
if object_path not in all_app_objects] str(object_path) for object_path in self._compile_dict.values()
object_files.append(str(app_object_path)) if object_path not in all_app_objects]]
cmd = ' '.join([self._config.CC, self._config.COMMON_FLAGS, *object_files, '-o', str(bin_path), cmd = ' '.join([self._config.cc, self._config.common_flags, *object_files, '-o', str(bin_path),
self._config.LINK_FLAGS]) self._config.link_flags])
print(ConsoleColor.BLUE + cmd + ConsoleColor.ENDCOLOR) print(f'{ConsoleColor.BLUE}{cmd}{ConsoleColor.ENDCOLOR}')
job = subprocess.run(cmd, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, job = subprocess.run(cmd, check=False, capture_output=True, text=True, shell=True)
universal_newlines=True, shell=True)
if job.returncode != 0: if job.returncode != 0:
error_paths.append((app_path, job.stdout + job.stderr)) error_paths.append((Path(app_path), job.stdout + job.stderr))
self._save_hash_dict()
if error_paths: if error_paths:
for error_path, error_text in error_paths: for error_path, error_text in error_paths:
print(ConsoleColor.RED + f'Error linking {error_path}:\n' + ConsoleColor.ENDCOLOR + error_text) print(f'{ConsoleColor.RED}Error linking {error_path}:{ConsoleColor.ENDCOLOR}\n{error_text}')
with open(os.path.join(self._config.OBJECT_DIR, 'hash.json'), 'w', encoding='utf-8') as hash_file:
hash_file.write(json.dumps(self._hash_dict, indent=1))
return False return False
with open(self._config.OBJECT_DIR / 'hash.json', 'w', encoding='utf-8') as hash_file:
hash_file.write(json.dumps(self._hash_dict, indent=1))
print(f'{ConsoleColor.GREEN}Compilation done{ConsoleColor.ENDCOLOR}') print(f'{ConsoleColor.GREEN}Compilation done{ConsoleColor.ENDCOLOR}')
return True return True

View file

@ -68,15 +68,15 @@ class Watcher:
libc_path = 'libc.so.6' libc_path = 'libc.so.6'
self._instance = ctypes.cdll.LoadLibrary(libc_path) self._instance = ctypes.cdll.LoadLibrary(libc_path)
self._inotify_init: ctypes.CDLL._FuncPtr = self._instance.inotify_init self._inotify_init = self._instance.inotify_init
self._inotify_init.argtypes = [] self._inotify_init.argtypes = []
self._inotify_init.restype = self._check_nonnegative self._inotify_init.restype = self._check_nonnegative
self._inotify_add_watch: ctypes.CDLL._FuncPtr = self._instance.inotify_add_watch self._inotify_add_watch = self._instance.inotify_add_watch
self._inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32] self._inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32]
self._inotify_add_watch.restype = self._check_nonnegative self._inotify_add_watch.restype = self._check_nonnegative
self._inotify_rm_watch: ctypes.CDLL._FuncPtr = self._instance.inotify_rm_watch self._inotify_rm_watch = self._instance.inotify_rm_watch
self._inotify_rm_watch.argtypes = [ctypes.c_int, ctypes.c_int] self._inotify_rm_watch.argtypes = [ctypes.c_int, ctypes.c_int]
self._inotify_rm_watch.restype = self._check_nonnegative self._inotify_rm_watch.restype = self._check_nonnegative
@ -105,17 +105,17 @@ class Watcher:
def watch(self): def watch(self):
while True: while True:
results = self._poll.poll() results = self._poll.poll()
for poll_fd, _event in results: for poll_fd, _ in results:
watch_buffer = os.read(poll_fd, self._EVENT_SIZE) watch_buffer = os.read(poll_fd, self._EVENT_SIZE)
if len(watch_buffer) < self._EVENT_SIZE: if len(watch_buffer) < self._EVENT_SIZE:
continue continue
watch_fd, mask, _cookie, _namesize = unpack(self._EVENT_FORMAT, watch_buffer) watch_fd, mask, _, _ = unpack(self._EVENT_FORMAT, watch_buffer)
watch_path, callback = self._watch_info[watch_fd] watch_path, callback = self._watch_info[watch_fd]
callback(watch_path, WatchFlag(mask)) callback(watch_path, WatchFlag(mask))
def __del__(self): def __del__(self):
self._poll.unregister(self._inotify_fd) self._poll.unregister(self._inotify_fd)
for _watch_path, watch_fd in self._watch_fds.items(): for watch_fd in self._watch_fds.values():
self._inotify_rm_watch(self._inotify_fd, watch_fd) self._inotify_rm_watch(self._inotify_fd, watch_fd)
os.close(self._inotify_fd) os.close(self._inotify_fd)
@ -127,7 +127,7 @@ class Watcher:
def main(): def main():
from argparse import ArgumentParser # pylint: disable=import-outside-toplevel from argparse import ArgumentParser # ruff:ignore[import-outside-top-level]
parser = ArgumentParser() parser = ArgumentParser()
parser.add_argument('paths', nargs='*', type=Path) parser.add_argument('paths', nargs='*', type=Path)
@ -135,7 +135,7 @@ def main():
paths: list[Path] = arguments.paths paths: list[Path] = arguments.paths
def callback(path: Path, _flags: WatchFlag): def callback(path: Path, _: WatchFlag):
print(f'{path} has been modified') print(f'{path} has been modified')
watcher = Watcher() watcher = Watcher()