| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
| import argparse | import argparse | ||||
| import os | |||||
| from pathlib import Path | from pathlib import Path | ||||
| import multiprocessing | import multiprocessing | ||||
| import itertools | import itertools | ||||
| from concurrent.futures import ThreadPoolExecutor | from concurrent.futures import ThreadPoolExecutor | ||||
| from typing import Sequence, Iterable, Dict, Tuple | |||||
| from typing import Sequence, Iterable, Dict, Tuple,List | |||||
| import subprocess | import subprocess | ||||
| import time | import time | ||||
| import sys | import sys | ||||
| INCLUDE_DIRS = [ | INCLUDE_DIRS = [ | ||||
| 'external/taywee-args/include', | 'external/taywee-args/include', | ||||
| 'external/spdlog/include', | 'external/spdlog/include', | ||||
| 'external/wil/include', | |||||
| ] | ] | ||||
| def is_msvc(cxx: Path) -> bool: | |||||
| return (not 'clang' in cxx.name) and 'cl' in cxx.name | |||||
| def _create_compile_command(cxx: Path, cpp_file: Path, obj_file: Path) -> List[str]: | |||||
| if not is_msvc(cxx): | |||||
| cmd = [ | |||||
| str(cxx), | |||||
| f'-I{HERE_DIR / "src"}', | |||||
| '-std=c++17', | |||||
| '-static', | |||||
| '-Wall', | |||||
| '-Wextra', | |||||
| '-Werror', | |||||
| '-Wshadow', | |||||
| '-Wconversion', | |||||
| '-fdiagnostics-color', | |||||
| '-pthread', | |||||
| '-g', | |||||
| '-c', | |||||
| '-O0', | |||||
| str(cpp_file), | |||||
| f'-o{obj_file}', | |||||
| ] | |||||
| cmd.extend( | |||||
| itertools.chain.from_iterable( | |||||
| ('-isystem', str(HERE_DIR / subdir)) for subdir in INCLUDE_DIRS)) | |||||
| return cmd | |||||
| else: | |||||
| cmd = [ | |||||
| str(cxx), | |||||
| # '/O2', | |||||
| '/Od', | |||||
| '/Z7', | |||||
| '/DEBUG', | |||||
| '/W4', | |||||
| '/WX', | |||||
| '/MT', | |||||
| '/nologo', | |||||
| # '/wd2220', | |||||
| '/EHsc', | |||||
| '/std:c++latest', | |||||
| f'/I{HERE_DIR / "src"}', | |||||
| str(cpp_file), | |||||
| '/c', | |||||
| f'/Fo{obj_file}', | |||||
| ] | |||||
| cmd.extend( | |||||
| f'/I{HERE_DIR / subdir}' for subdir in INCLUDE_DIRS) | |||||
| return cmd | |||||
| def _compile_src(cxx: Path, cpp_file: Path) -> Tuple[Path, Path]: | def _compile_src(cxx: Path, cpp_file: Path) -> Tuple[Path, Path]: | ||||
| build_dir = HERE_DIR / '_build' | build_dir = HERE_DIR / '_build' | ||||
| src_dir = HERE_DIR / 'src' | src_dir = HERE_DIR / 'src' | ||||
| relpath = cpp_file.relative_to(src_dir) | relpath = cpp_file.relative_to(src_dir) | ||||
| obj_path = build_dir / relpath.with_name(relpath.name + '.o') | |||||
| obj_path = build_dir / relpath.with_name(relpath.name + ('.obj' if is_msvc(cxx) else '.o')) | |||||
| obj_path.parent.mkdir(exist_ok=True, parents=True) | obj_path.parent.mkdir(exist_ok=True, parents=True) | ||||
| cmd = [ | |||||
| cxx, | |||||
| '-std=c++17', | |||||
| '-static', | |||||
| '-Wall', | |||||
| '-Wextra', | |||||
| '-Werror', | |||||
| '-Wshadow', | |||||
| '-Wconversion', | |||||
| '-fdiagnostics-color', | |||||
| '-pthread', | |||||
| '-g', | |||||
| '-c', | |||||
| '-O0', | |||||
| f'-I{src_dir}', | |||||
| cpp_file, | |||||
| f'-o{obj_path}', | |||||
| ] | |||||
| cmd.extend( | |||||
| itertools.chain.from_iterable( | |||||
| ('-isystem', HERE_DIR / subdir) for subdir in INCLUDE_DIRS)) | |||||
| cmd = _create_compile_command(cxx, cpp_file, obj_path) | |||||
| msg = f'Compile C++ file: {cpp_file}' | msg = f'Compile C++ file: {cpp_file}' | ||||
| print(msg) | print(msg) | ||||
| start = time.time() | start = time.time() | ||||
| raise RuntimeError( | raise RuntimeError( | ||||
| f'Compile command ({cmd}) failed for {cpp_file}:\n{res.stdout.decode()}' | f'Compile command ({cmd}) failed for {cpp_file}:\n{res.stdout.decode()}' | ||||
| ) | ) | ||||
| if res.stdout: | |||||
| print(res.stdout.decode()) | |||||
| end = time.time() | end = time.time() | ||||
| print(f'{msg} - Done: {end - start:.2}s') | print(f'{msg} - Done: {end - start:.2}s') | ||||
| return cpp_file, obj_path | return cpp_file, obj_path | ||||
| def compile_sources(cxx: Path, sources: Iterable[Path]) -> Dict[Path, Path]: | |||||
| pool = ThreadPoolExecutor(multiprocessing.cpu_count() + 2) | |||||
| def compile_sources(cxx: Path, sources: Iterable[Path], *, jobs: int) -> Dict[Path, Path]: | |||||
| pool = ThreadPoolExecutor(jobs) | |||||
| return { | return { | ||||
| src: obj | src: obj | ||||
| for src, obj in pool.map(lambda s: _compile_src(cxx, s), sources) | for src, obj in pool.map(lambda s: _compile_src(cxx, s), sources) | ||||
| } | } | ||||
| def _create_archive_command(objects: Iterable[Path]) -> List[str]: | |||||
| if os.name == 'nt': | |||||
| lib_file = HERE_DIR / '_build/libddslim.lib' | |||||
| cmd = ['lib', '/nologo', f'/OUT:{lib_file}', *map(str, objects)] | |||||
| return lib_file, cmd | |||||
| else: | |||||
| lib_file = HERE_DIR / '_build/libddslim.a' | |||||
| cmd = ['ar', 'rsc', str(lib_file), *objects] | |||||
| return lib_file, cmd | |||||
| def make_library(objects: Iterable[Path]) -> Path: | def make_library(objects: Iterable[Path]) -> Path: | ||||
| lib_file = HERE_DIR / '_build/libddslim.a' | |||||
| cmd = ['ar', 'rsc', lib_file] | |||||
| cmd.extend(objects) | |||||
| lib_file, cmd = _create_archive_command(objects) | |||||
| if lib_file.exists(): | if lib_file.exists(): | ||||
| lib_file.unlink() | lib_file.unlink() | ||||
| print(f'Creating static library {lib_file}') | print(f'Creating static library {lib_file}') | ||||
| return lib_file | return lib_file | ||||
| def _create_exe_link_command(cxx: Path, obj: Path, lib: Path, out: Path) -> List[str]: | |||||
| if not is_msvc(cxx): | |||||
| return [ | |||||
| str(cxx), | |||||
| '-static', | |||||
| '-pthread', | |||||
| # See: https://stackoverflow.com/questions/35116327/when-g-static-link-pthread-cause-segmentation-fault-why | |||||
| '-Wl,--whole-archive', | |||||
| '-lpthread', | |||||
| '-Wl,--no-whole-archive', | |||||
| str(obj), | |||||
| str(lib), | |||||
| '-lstdc++fs', | |||||
| f'-o{out}', | |||||
| ] | |||||
| else: | |||||
| return [ | |||||
| str(cxx), | |||||
| '/nologo', | |||||
| '/W4', | |||||
| '/WX', | |||||
| '/MT', | |||||
| '/Z7', | |||||
| '/DEBUG', | |||||
| f'/Fe{out}', | |||||
| str(lib), | |||||
| str(obj), | |||||
| ] | |||||
| def link_exe(cxx: Path, obj: Path, lib: Path, *, out: Path = None) -> Path: | def link_exe(cxx: Path, obj: Path, lib: Path, *, out: Path = None) -> Path: | ||||
| if out is None: | if out is None: | ||||
| basename = obj.stem | basename = obj.stem | ||||
| out.parent.mkdir(exist_ok=True, parents=True) | out.parent.mkdir(exist_ok=True, parents=True) | ||||
| print(f'Linking executable {out}') | print(f'Linking executable {out}') | ||||
| subprocess.check_call([ | |||||
| cxx, | |||||
| '-static', | |||||
| '-pthread', | |||||
| # See: https://stackoverflow.com/questions/35116327/when-g-static-link-pthread-cause-segmentation-fault-why | |||||
| '-Wl,--whole-archive', | |||||
| '-lpthread', | |||||
| '-Wl,--no-whole-archive', | |||||
| obj, | |||||
| lib, | |||||
| '-lstdc++fs', | |||||
| f'-o{out}', | |||||
| ]) | |||||
| cmd = _create_exe_link_command(cxx, obj, lib, out) | |||||
| subprocess.check_call(cmd) | |||||
| return out | return out | ||||
| '--test', action='store_true', help='Build and run tests') | '--test', action='store_true', help='Build and run tests') | ||||
| parser.add_argument( | parser.add_argument( | ||||
| '--cxx', help='Path/name of the C++ compiler to use.', required=True) | '--cxx', help='Path/name of the C++ compiler to use.', required=True) | ||||
| parser.add_argument('--jobs', '-j', type=int, help='Set number of parallel build threads', default=multiprocessing.cpu_count() + 2) | |||||
| args = parser.parse_args(argv) | args = parser.parse_args(argv) | ||||
| all_sources = set(HERE_DIR.glob('src/**/*.cpp')) | all_sources = set(HERE_DIR.glob('src/**/*.cpp')) | ||||
| lib_sources = (all_sources - test_sources) - main_sources | lib_sources = (all_sources - test_sources) - main_sources | ||||
| objects = compile_sources(Path(args.cxx), all_sources) | |||||
| objects = compile_sources(Path(args.cxx), all_sources, jobs=args.jobs) | |||||
| lib = make_library(objects[p] for p in lib_sources) | lib = make_library(objects[p] for p in lib_sources) | ||||
| test_objs = (objects[p] for p in test_sources) | test_objs = (objects[p] for p in test_sources) | ||||
| pool = ThreadPoolExecutor(multiprocessing.cpu_count() + 2) | |||||
| pool = ThreadPoolExecutor(args.jobs) | |||||
| test_exes = list( | test_exes = list( | ||||
| pool.map(lambda o: link_exe(Path(args.cxx), o, lib), test_objs)) | pool.map(lambda o: link_exe(Path(args.cxx), o, lib), test_objs)) | ||||