Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

217 linhas
7.6KB

  1. import argparse
  2. import json
  3. from contextlib import contextmanager
  4. import enum
  5. import multiprocessing
  6. import pytest
  7. from pathlib import Path
  8. from concurrent import futures
  9. import sys
  10. import os
  11. from typing import NoReturn, Sequence, Optional, Iterator
  12. from typing_extensions import Protocol
  13. import subprocess
  14. import json5
  15. from . import paths
  16. from .dds import DDSWrapper
  17. from .bootstrap import BootstrapMode, get_bootstrap_exe
  18. def make_argparser() -> argparse.ArgumentParser:
  19. """Create an argument parser for the dds-ci command-line"""
  20. parser = argparse.ArgumentParser()
  21. parser.add_argument('-B',
  22. '--bootstrap-with',
  23. help='How are we to obtain a bootstrapped DDS executable?',
  24. metavar='{download,build,skip,lazy}',
  25. type=BootstrapMode,
  26. default=BootstrapMode.Lazy)
  27. parser.add_argument('--rapid', help='Run CI for fast development iterations', action='store_true')
  28. parser.add_argument('--test-toolchain',
  29. '-TT',
  30. type=Path,
  31. metavar='<toolchain-file>',
  32. help='The toolchain to use for the first build, which will be passed through the tests')
  33. parser.add_argument('--main-toolchain',
  34. '-T',
  35. type=Path,
  36. dest='toolchain',
  37. metavar='<toolchain-file>',
  38. help='The toolchain to use for the final build')
  39. parser.add_argument('--jobs',
  40. '-j',
  41. type=int,
  42. help='Number of parallel jobs to use when building and testing',
  43. default=multiprocessing.cpu_count() + 2)
  44. parser.add_argument('--build-only', action='store_true', help='Only build the dds executable, do not run tests')
  45. parser.add_argument('--clean', action='store_true', help="Don't remove prior build/deps results")
  46. parser.add_argument('--no-test',
  47. action='store_false',
  48. dest='do_test',
  49. help='Skip testing and just build the final result')
  50. return parser
  51. class CommandArguments(Protocol):
  52. """
  53. The result of parsing argv with the dds-ci argument parser.
  54. """
  55. #: Whether the user wants us to clean result before building
  56. clean: bool
  57. #: The bootstrap method the user has requested
  58. bootstrap_with: BootstrapMode
  59. #: The toolchain to use when building the 'dds' executable that will be tested.
  60. test_toolchain: Optional[Path]
  61. #: The toolchain to use when building the main 'dds' executable to publish
  62. toolchain: Optional[Path]
  63. #: The maximum number of parallel jobs for build and test
  64. jobs: int
  65. #: Whether we should run the pytest tests
  66. do_test: bool
  67. #: Rapid-CI is for 'dds' development purposes
  68. rapid: bool
  69. def parse_argv(argv: Sequence[str]) -> CommandArguments:
  70. """Parse the given dds-ci command-line argument list"""
  71. return make_argparser().parse_args(argv)
  72. @contextmanager
  73. def fixup_toolchain(json_file: Path) -> Iterator[Path]:
  74. """
  75. Augment the toolchain at the given path by adding 'ccache' or -fuse-ld=lld,
  76. if those tools are available on the system. Yields a new toolchain file
  77. based on 'json_file'
  78. """
  79. data = json5.loads(json_file.read_text())
  80. # Check if we can add ccache
  81. ccache = paths.find_exe('ccache')
  82. if ccache:
  83. print('Found ccache:', ccache)
  84. data['compiler_launcher'] = [str(ccache)]
  85. # Check for lld for use with GCC/Clang
  86. if paths.find_exe('ld.lld') and data.get('compiler_id') in ('gnu', 'clang'):
  87. print('Linking with `-fuse-ld=lld`')
  88. data.setdefault('link_flags', []).append('-fuse-ld=lld')
  89. # Save the new toolchain data
  90. with paths.new_tempdir() as tdir:
  91. new_json = tdir / json_file.name
  92. new_json.write_text(json.dumps(data))
  93. yield new_json
  94. def get_default_test_toolchain() -> Path:
  95. """
  96. Get the default toolchain that should be used for dev and test based on the
  97. host platform.
  98. """
  99. if sys.platform == 'win32':
  100. return paths.TOOLS_DIR / 'msvc-audit.jsonc'
  101. elif sys.platform in 'linux':
  102. return paths.TOOLS_DIR / 'gcc-9-audit.jsonc'
  103. elif sys.platform == 'darwin':
  104. return paths.TOOLS_DIR / 'gcc-9-audit-macos.jsonc'
  105. else:
  106. raise RuntimeError(f'Unable to determine the default toolchain (sys.platform is {sys.platform!r})')
  107. def get_default_toolchain() -> Path:
  108. """
  109. Get the default toolchain that should be used to generate the release executable
  110. based on the host platform.
  111. """
  112. if sys.platform == 'win32':
  113. return paths.TOOLS_DIR / 'msvc-rel.jsonc'
  114. elif sys.platform == 'linux':
  115. return paths.TOOLS_DIR / 'gcc-9-rel.jsonc'
  116. elif sys.platform == 'darwin':
  117. return paths.TOOLS_DIR / 'gcc-9-rel-macos.jsonc'
  118. else:
  119. raise RuntimeError(f'Unable to determine the default toolchain (sys.platform is {sys.platform!r})')
  120. def test_build(dds: DDSWrapper, args: CommandArguments) -> DDSWrapper:
  121. """
  122. Execute the build that generates the test-mode executable. Uses the given 'dds'
  123. to build the new dds. Returns a DDSWrapper around the generated test executable.
  124. """
  125. test_tc = args.test_toolchain or get_default_test_toolchain()
  126. build_dir = paths.BUILD_DIR / '_ci-test'
  127. with fixup_toolchain(test_tc) as new_tc:
  128. dds.build(toolchain=new_tc, root=paths.PROJECT_ROOT, build_root=build_dir, jobs=args.jobs)
  129. return DDSWrapper(build_dir / ('dds' + paths.EXE_SUFFIX))
  130. def run_pytest(dds: DDSWrapper, args: CommandArguments) -> int:
  131. """
  132. Execute pytest, testing against the given 'test_dds' executable. Returns
  133. the exit code of pytest.
  134. """
  135. basetemp = Path('/tmp/dds-ci')
  136. basetemp.mkdir(exist_ok=True, parents=True)
  137. return pytest.main([
  138. '-v',
  139. '--durations=10',
  140. '-n',
  141. str(args.jobs),
  142. f'--basetemp={basetemp}',
  143. f'--dds-exe={dds.path}',
  144. str(paths.PROJECT_ROOT / 'tests/'),
  145. ])
  146. def main_build(dds: DDSWrapper, args: CommandArguments) -> int:
  147. """
  148. Execute the main build of dds using the given 'dds' executable to build itself.
  149. """
  150. main_tc = args.toolchain or (
  151. # If we are in rapid-dev mode, use the test toolchain, which had audit/debug enabled
  152. get_default_toolchain() if not args.rapid else get_default_test_toolchain())
  153. with fixup_toolchain(main_tc) as new_tc:
  154. try:
  155. dds.build(toolchain=new_tc, root=paths.PROJECT_ROOT, build_root=paths.BUILD_DIR, jobs=args.jobs)
  156. except subprocess.CalledProcessError as e:
  157. if args.rapid:
  158. return e.returncode
  159. raise
  160. return 0
  161. def ci_with_dds(dds: DDSWrapper, args: CommandArguments) -> int:
  162. """
  163. Execute CI using the given prior 'dds' executable.
  164. """
  165. if args.clean:
  166. dds.clean(build_dir=paths.BUILD_DIR)
  167. dds.catalog_json_import(paths.PROJECT_ROOT / 'old-catalog.json')
  168. pool = futures.ThreadPoolExecutor()
  169. test_fut = pool.submit(lambda: 0)
  170. if args.do_test and not args.rapid:
  171. test_dds = test_build(dds, args)
  172. test_fut = pool.submit(lambda: run_pytest(test_dds, args))
  173. main_fut = pool.submit(lambda: main_build(dds, args))
  174. for fut in futures.as_completed({test_fut, main_fut}):
  175. if fut.result():
  176. return fut.result()
  177. return 0
  178. def main(argv: Sequence[str]) -> int:
  179. args = parse_argv(argv)
  180. with get_bootstrap_exe(args.bootstrap_with) as f:
  181. return ci_with_dds(f, args)
  182. def start():
  183. sys.exit(main(sys.argv[1:]))
  184. if __name__ == "__main__":
  185. start()