No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

305 líneas
8.3KB

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