build.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. #!/usr/bin/env python3
  2. import json
  3. import argparse
  4. import stat
  5. import textwrap
  6. import shutil
  7. import subprocess
  8. from tempfile import TemporaryDirectory
  9. from pathlib import Path
  10. import typing as T
  11. image_namespace = 'mesonbuild'
  12. image_def_file = 'image.json'
  13. install_script = 'install.sh'
  14. class ImageDef:
  15. def __init__(self, image_dir: Path) -> None:
  16. path = image_dir / image_def_file
  17. data = json.loads(path.read_text())
  18. assert isinstance(data, dict)
  19. assert all([x in data for x in ['base_image', 'env']])
  20. assert isinstance(data['base_image'], str)
  21. assert isinstance(data['env'], dict)
  22. self.base_image: str = data['base_image']
  23. self.args: T.List[str] = data.get('args', [])
  24. self.env: T.Dict[str, str] = data['env']
  25. class BuilderBase():
  26. def __init__(self, data_dir: Path, temp_dir: Path) -> None:
  27. self.data_dir = data_dir
  28. self.temp_dir = temp_dir
  29. self.common_sh = self.data_dir.parent / 'common.sh'
  30. self.common_sh = self.common_sh.resolve(strict=True)
  31. self.validate_data_dir()
  32. self.image_def = ImageDef(self.data_dir)
  33. self.docker = shutil.which('docker')
  34. self.git = shutil.which('git')
  35. if self.docker is None:
  36. raise RuntimeError('Unable to find docker')
  37. if self.git is None:
  38. raise RuntimeError('Unable to find git')
  39. def validate_data_dir(self) -> None:
  40. files = [
  41. self.data_dir / image_def_file,
  42. self.data_dir / install_script,
  43. ]
  44. if not self.data_dir.exists():
  45. raise RuntimeError(f'{self.data_dir.as_posix()} does not exist')
  46. for i in files:
  47. if not i.exists():
  48. raise RuntimeError(f'{i.as_posix()} does not exist')
  49. if not i.is_file():
  50. raise RuntimeError(f'{i.as_posix()} is not a regular file')
  51. class Builder(BuilderBase):
  52. def gen_bashrc(self) -> None:
  53. out_file = self.temp_dir / 'env_vars.sh'
  54. out_data = ''
  55. # run_tests.py parameters
  56. self.image_def.env['CI_ARGS'] = ' '.join(self.image_def.args)
  57. for key, val in self.image_def.env.items():
  58. out_data += f'export {key}="{val}"\n'
  59. # Also add /ci to PATH
  60. out_data += 'export PATH="/ci:$PATH"\n'
  61. out_file.write_text(out_data)
  62. # make it executable
  63. mode = out_file.stat().st_mode
  64. out_file.chmod(mode | stat.S_IEXEC)
  65. def gen_dockerfile(self) -> None:
  66. out_file = self.temp_dir / 'Dockerfile'
  67. out_data = textwrap.dedent(f'''\
  68. FROM {self.image_def.base_image}
  69. ADD install.sh /ci/install.sh
  70. ADD common.sh /ci/common.sh
  71. ADD env_vars.sh /ci/env_vars.sh
  72. RUN /ci/install.sh
  73. ''')
  74. out_file.write_text(out_data)
  75. def do_build(self) -> None:
  76. # copy files
  77. for i in self.data_dir.iterdir():
  78. shutil.copy(str(i), str(self.temp_dir))
  79. shutil.copy(str(self.common_sh), str(self.temp_dir))
  80. self.gen_bashrc()
  81. self.gen_dockerfile()
  82. cmd_git = [self.git, 'rev-parse', '--short', 'HEAD']
  83. res = subprocess.run(cmd_git, cwd=self.data_dir, stdout=subprocess.PIPE)
  84. if res.returncode != 0:
  85. raise RuntimeError('Failed to get the current commit hash')
  86. commit_hash = res.stdout.decode().strip()
  87. cmd = [
  88. self.docker, 'build',
  89. '-t', f'{image_namespace}/{self.data_dir.name}:latest',
  90. '-t', f'{image_namespace}/{self.data_dir.name}:{commit_hash}',
  91. '--pull',
  92. self.temp_dir.as_posix(),
  93. ]
  94. if subprocess.run(cmd).returncode != 0:
  95. raise RuntimeError('Failed to build the docker image')
  96. class ImageTester(BuilderBase):
  97. def __init__(self, data_dir: Path, temp_dir: Path, ci_root: Path) -> None:
  98. super().__init__(data_dir, temp_dir)
  99. self.meson_root = ci_root.parent.parent.resolve()
  100. def gen_dockerfile(self) -> None:
  101. out_file = self.temp_dir / 'Dockerfile'
  102. out_data = textwrap.dedent(f'''\
  103. FROM {image_namespace}/{self.data_dir.name}
  104. ADD meson /meson
  105. ''')
  106. out_file.write_text(out_data)
  107. def copy_meson(self) -> None:
  108. shutil.copytree(
  109. self.meson_root,
  110. self.temp_dir / 'meson',
  111. ignore=shutil.ignore_patterns(
  112. '.git',
  113. '*_cache',
  114. 'work area',
  115. self.temp_dir.name,
  116. )
  117. )
  118. def do_test(self):
  119. self.copy_meson()
  120. self.gen_dockerfile()
  121. try:
  122. build_cmd = [
  123. self.docker, 'build',
  124. '-t', 'meson_test_image',
  125. self.temp_dir.as_posix(),
  126. ]
  127. if subprocess.run(build_cmd).returncode != 0:
  128. raise RuntimeError('Failed to build the test docker image')
  129. test_cmd = [
  130. self.docker, 'run', '--rm', '-t', 'meson_test_image',
  131. '/bin/bash', '-c', 'source /ci/env_vars.sh; cd meson; ./run_tests.py $CI_ARGS'
  132. ]
  133. if subprocess.run(test_cmd).returncode != 0:
  134. raise RuntimeError('Running tests failed')
  135. finally:
  136. cleanup_cmd = [self.docker, 'rmi', '-f', 'meson_test_image']
  137. subprocess.run(cleanup_cmd).returncode
  138. def main() -> None:
  139. parser = argparse.ArgumentParser(description='Meson CI image builder')
  140. parser.add_argument('what', type=str, help='Which image to build / test')
  141. parser.add_argument('-t', '--type', choices=['build', 'test'], help='What to do', required=True)
  142. args = parser.parse_args()
  143. ci_root = Path(__file__).parent
  144. ci_data = ci_root / args.what
  145. with TemporaryDirectory(prefix=f'{args.type}_{args.what}_', dir=ci_root) as td:
  146. ci_build = Path(td)
  147. print(f'Build dir: {ci_build}')
  148. if args.type == 'build':
  149. builder = Builder(ci_data, ci_build)
  150. builder.do_build()
  151. elif args.type == 'test':
  152. tester = ImageTester(ci_data, ci_build, ci_root)
  153. tester.do_test()
  154. if __name__ == '__main__':
  155. main()