You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

build.py 8.7KB

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