Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

306 lines
8.4KB

  1. #!/usr/bin/env python3
  2. import argparse
  3. import os
  4. from pathlib import Path
  5. import multiprocessing
  6. import itertools
  7. from concurrent.futures import ThreadPoolExecutor
  8. from typing import Sequence, Iterable, Dict, Tuple, List, NamedTuple
  9. import subprocess
  10. import time
  11. import re
  12. import sys
  13. ROOT = Path(__file__).parent.parent.absolute()
  14. INCLUDE_DIRS = [
  15. 'external/taywee-args/include',
  16. 'external/spdlog/include',
  17. 'external/wil/include',
  18. 'external/ranges-v3/include',
  19. 'external/nlohmann-json/include',
  20. ]
  21. class BuildOptions(NamedTuple):
  22. cxx: Path
  23. jobs: int
  24. static: bool
  25. debug: bool
  26. @property
  27. def is_msvc(self) -> bool:
  28. return is_msvc(self.cxx)
  29. @property
  30. def obj_suffix(self) -> str:
  31. return '.obj' if self.is_msvc else '.o'
  32. def is_msvc(cxx: Path) -> bool:
  33. return (not 'clang' in cxx.name) and 'cl' in cxx.name
  34. def have_ccache() -> bool:
  35. try:
  36. subprocess.check_output(['ccache', '--version'])
  37. return True
  38. except FileNotFoundError:
  39. return False
  40. def have_sccache() -> bool:
  41. try:
  42. subprocess.check_output(['sccache', '--version'])
  43. return True
  44. except FileNotFoundError:
  45. return False
  46. def _create_compile_command(opts: BuildOptions, cpp_file: Path,
  47. obj_file: Path) -> List[str]:
  48. if not opts.is_msvc:
  49. cmd = [
  50. str(opts.cxx),
  51. f'-I{ROOT / "src"}',
  52. '-std=c++17',
  53. '-Wall',
  54. '-Wextra',
  55. '-Werror',
  56. '-Wshadow',
  57. '-Wconversion',
  58. '-fdiagnostics-color',
  59. '-DFMT_HEADER_ONLY=1',
  60. '-pthread',
  61. '-c',
  62. str(cpp_file),
  63. f'-o{obj_file}',
  64. ]
  65. if have_ccache():
  66. cmd.insert(0, 'ccache')
  67. elif have_sccache():
  68. cmd.insert(0, 'sccache')
  69. if opts.static:
  70. cmd.append('-static')
  71. if opts.debug:
  72. cmd.extend(('-g', '-O0'))
  73. else:
  74. cmd.append('-O2')
  75. cmd.extend(
  76. itertools.chain.from_iterable(('-isystem', str(ROOT / subdir))
  77. for subdir in INCLUDE_DIRS))
  78. return cmd
  79. else:
  80. cmd = [
  81. str(opts.cxx),
  82. '/W4',
  83. '/WX',
  84. '/nologo',
  85. '/EHsc',
  86. '/std:c++latest',
  87. '/permissive-',
  88. '/experimental:preprocessor',
  89. '/wd5105', # winbase.h
  90. '/wd4459', # Shadowing
  91. '/DWIN32_LEAN_AND_MEAN',
  92. '/DNOMINMAX',
  93. '/DFMT_HEADER_ONLY=1',
  94. '/D_CRT_SECURE_NO_WARNINGS',
  95. '/diagnostics:caret',
  96. '/experimental:external',
  97. f'/I{ROOT / "src"}',
  98. str(cpp_file),
  99. '/c',
  100. f'/Fo{obj_file}',
  101. ]
  102. if have_ccache():
  103. cmd.insert(0, 'ccache')
  104. if opts.debug:
  105. cmd.extend(('/Od', '/DEBUG', '/Z7'))
  106. else:
  107. cmd.append('/O2')
  108. if opts.static:
  109. cmd.append('/MT')
  110. else:
  111. cmd.append('/MD')
  112. cmd.extend(f'/external:I{ROOT / subdir}' for subdir in INCLUDE_DIRS)
  113. return cmd
  114. def _compile_src(opts: BuildOptions, cpp_file: Path) -> Tuple[Path, Path]:
  115. build_dir = ROOT / '_build'
  116. src_dir = ROOT / 'src'
  117. relpath = cpp_file.relative_to(src_dir)
  118. obj_path = build_dir / '_obj' / relpath.with_name(relpath.name + opts.obj_suffix)
  119. obj_path.parent.mkdir(exist_ok=True, parents=True)
  120. cmd = _create_compile_command(opts, cpp_file, obj_path)
  121. msg = f'Compile C++ file: {cpp_file.relative_to(ROOT)}'
  122. print(msg)
  123. start = time.time()
  124. res = subprocess.run(
  125. cmd,
  126. stdout=subprocess.PIPE,
  127. stderr=subprocess.STDOUT,
  128. )
  129. if res.returncode != 0:
  130. raise RuntimeError(
  131. f'Compile command ({cmd}) failed for {cpp_file}:\n{res.stdout.decode()}'
  132. )
  133. stdout: str = res.stdout.decode()
  134. stdout = re.sub(r'^{cpp_file.name}\r?\n', stdout, '')
  135. fname_head = f'{cpp_file.name}\r\n'
  136. if stdout:
  137. print(stdout, end='')
  138. end = time.time()
  139. print(f'{msg} - Done: {end - start:.2n}s')
  140. return cpp_file, obj_path
  141. def compile_sources(opts: BuildOptions,
  142. sources: Iterable[Path]) -> Dict[Path, Path]:
  143. pool = ThreadPoolExecutor(opts.jobs)
  144. return {
  145. src: obj
  146. for src, obj in pool.map(lambda s: _compile_src(opts, s), sources)
  147. }
  148. def _create_archive_command(opts: BuildOptions,
  149. objects: Iterable[Path]) -> Tuple[Path, List[str]]:
  150. if opts.is_msvc:
  151. lib_file = ROOT / '_build/libdds.lib'
  152. cmd = ['lib', '/nologo', f'/OUT:{lib_file}', *map(str, objects)]
  153. return lib_file, cmd
  154. else:
  155. lib_file = ROOT / '_build/libdds.a'
  156. cmd = ['ar', 'rsc', str(lib_file), *map(str, objects)]
  157. return lib_file, cmd
  158. def make_library(opts: BuildOptions, objects: Iterable[Path]) -> Path:
  159. lib_file, cmd = _create_archive_command(opts, objects)
  160. if lib_file.exists():
  161. lib_file.unlink()
  162. print(f'Creating static library {lib_file}')
  163. subprocess.check_call(cmd)
  164. return lib_file
  165. def _create_exe_link_command(opts: BuildOptions, obj: Path, lib: Path,
  166. out: Path) -> List[str]:
  167. if not opts.is_msvc:
  168. cmd = [
  169. str(opts.cxx),
  170. '-pthread',
  171. str(obj),
  172. str(lib),
  173. '-lstdc++fs',
  174. f'-o{out}',
  175. ]
  176. if opts.static:
  177. cmd.extend((
  178. '-static',
  179. # See: https://stackoverflow.com/questions/35116327/when-g-static-link-pthread-cause-segmentation-fault-why
  180. '-Wl,--whole-archive',
  181. '-lpthread',
  182. '-Wl,--no-whole-archive',
  183. ))
  184. return cmd
  185. else:
  186. cmd = [
  187. str(opts.cxx),
  188. '/nologo',
  189. '/W4',
  190. '/WX',
  191. '/Z7',
  192. '/DEBUG',
  193. 'rpcrt4.lib',
  194. f'/Fe{out}',
  195. str(lib),
  196. str(obj),
  197. ]
  198. if opts.debug:
  199. cmd.append('/DEBUG')
  200. if opts.static:
  201. cmd.append('/MT')
  202. else:
  203. cmd.append('/MD')
  204. return cmd
  205. def link_exe(opts: BuildOptions, obj: Path, lib: Path, *,
  206. out: Path = None) -> Path:
  207. if out is None:
  208. basename = obj.stem
  209. out = ROOT / '_build/test' / (basename + '.exe')
  210. out.parent.mkdir(exist_ok=True, parents=True)
  211. print(f'Linking executable {out}')
  212. cmd = _create_exe_link_command(opts, obj, lib, out)
  213. subprocess.check_call(cmd)
  214. return out
  215. def run_test(exe: Path) -> None:
  216. print(f'Running test: {exe}')
  217. subprocess.check_call([str(exe)])
  218. def main(argv: Sequence[str]) -> int:
  219. parser = argparse.ArgumentParser()
  220. parser.add_argument(
  221. '--test', action='store_true', help='Build and run tests')
  222. parser.add_argument(
  223. '--cxx', help='Path/name of the C++ compiler to use.', required=True)
  224. parser.add_argument(
  225. '--jobs',
  226. '-j',
  227. type=int,
  228. help='Set number of parallel build threads',
  229. default=multiprocessing.cpu_count() + 2)
  230. parser.add_argument(
  231. '--debug',
  232. action='store_true',
  233. help='Build with debug information and disable optimizations')
  234. parser.add_argument(
  235. '--static', action='store_true', help='Build a static executable')
  236. args = parser.parse_args(argv)
  237. all_sources = set(ROOT.glob('src/**/*.cpp'))
  238. test_sources = set(ROOT.glob('src/**/*.test.cpp'))
  239. main_sources = set(ROOT.glob('src/**/*.main.cpp'))
  240. lib_sources = (all_sources - test_sources) - main_sources
  241. build_opts = BuildOptions(
  242. cxx=Path(args.cxx),
  243. jobs=args.jobs,
  244. static=args.static,
  245. debug=args.debug)
  246. objects = compile_sources(build_opts, sorted(all_sources))
  247. lib = make_library(build_opts, (objects[p] for p in lib_sources))
  248. test_objs = (objects[p] for p in test_sources)
  249. pool = ThreadPoolExecutor(build_opts.jobs)
  250. test_exes = list(
  251. pool.map(lambda o: link_exe(build_opts, o, lib), test_objs))
  252. main_exe = link_exe(
  253. build_opts,
  254. objects[next(iter(main_sources))],
  255. lib,
  256. out=ROOT / '_build/dds')
  257. if args.test:
  258. list(pool.map(run_test, test_exes))
  259. print(f'Main executable generated at {main_exe}')
  260. return 0
  261. if __name__ == "__main__":
  262. sys.exit(main(sys.argv[1:]))