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.

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