aboutsummaryrefslogtreecommitdiffstats
path: root/meson/ci/ciimage/build.py
diff options
context:
space:
mode:
Diffstat (limited to 'meson/ci/ciimage/build.py')
-rwxr-xr-xmeson/ci/ciimage/build.py241
1 files changed, 241 insertions, 0 deletions
diff --git a/meson/ci/ciimage/build.py b/meson/ci/ciimage/build.py
new file mode 100755
index 000000000..1e1f23811
--- /dev/null
+++ b/meson/ci/ciimage/build.py
@@ -0,0 +1,241 @@
+#!/usr/bin/env python3
+
+import json
+import argparse
+import stat
+import textwrap
+import shutil
+import subprocess
+from tempfile import TemporaryDirectory
+from pathlib import Path
+import typing as T
+
+image_namespace = 'mesonbuild'
+
+image_def_file = 'image.json'
+install_script = 'install.sh'
+
+class ImageDef:
+ def __init__(self, image_dir: Path) -> None:
+ path = image_dir / image_def_file
+ data = json.loads(path.read_text(encoding='utf-8'))
+
+ assert isinstance(data, dict)
+ assert all([x in data for x in ['base_image', 'env']])
+ assert isinstance(data['base_image'], str)
+ assert isinstance(data['env'], dict)
+
+ self.base_image: str = data['base_image']
+ self.args: T.List[str] = data.get('args', [])
+ self.env: T.Dict[str, str] = data['env']
+
+class BuilderBase():
+ def __init__(self, data_dir: Path, temp_dir: Path) -> None:
+ self.data_dir = data_dir
+ self.temp_dir = temp_dir
+
+ self.common_sh = self.data_dir.parent / 'common.sh'
+ self.common_sh = self.common_sh.resolve(strict=True)
+ self.validate_data_dir()
+
+ self.image_def = ImageDef(self.data_dir)
+
+ self.docker = shutil.which('docker')
+ self.git = shutil.which('git')
+ if self.docker is None:
+ raise RuntimeError('Unable to find docker')
+ if self.git is None:
+ raise RuntimeError('Unable to find git')
+
+ def validate_data_dir(self) -> None:
+ files = [
+ self.data_dir / image_def_file,
+ self.data_dir / install_script,
+ ]
+ if not self.data_dir.exists():
+ raise RuntimeError(f'{self.data_dir.as_posix()} does not exist')
+ for i in files:
+ if not i.exists():
+ raise RuntimeError(f'{i.as_posix()} does not exist')
+ if not i.is_file():
+ raise RuntimeError(f'{i.as_posix()} is not a regular file')
+
+class Builder(BuilderBase):
+ def gen_bashrc(self) -> None:
+ out_file = self.temp_dir / 'env_vars.sh'
+ out_data = ''
+
+ # run_tests.py parameters
+ self.image_def.env['CI_ARGS'] = ' '.join(self.image_def.args)
+
+ for key, val in self.image_def.env.items():
+ out_data += f'export {key}="{val}"\n'
+
+ # Also add /ci to PATH
+ out_data += 'export PATH="/ci:$PATH"\n'
+
+ out_file.write_text(out_data, encoding='utf-8')
+
+ # make it executable
+ mode = out_file.stat().st_mode
+ out_file.chmod(mode | stat.S_IEXEC)
+
+ def gen_dockerfile(self) -> None:
+ out_file = self.temp_dir / 'Dockerfile'
+ out_data = textwrap.dedent(f'''\
+ FROM {self.image_def.base_image}
+
+ ADD install.sh /ci/install.sh
+ ADD common.sh /ci/common.sh
+ ADD env_vars.sh /ci/env_vars.sh
+ RUN /ci/install.sh
+ ''')
+
+ out_file.write_text(out_data, encoding='utf-8')
+
+ def do_build(self) -> None:
+ # copy files
+ for i in self.data_dir.iterdir():
+ shutil.copy(str(i), str(self.temp_dir))
+ shutil.copy(str(self.common_sh), str(self.temp_dir))
+
+ self.gen_bashrc()
+ self.gen_dockerfile()
+
+ cmd_git = [self.git, 'rev-parse', '--short', 'HEAD']
+ res = subprocess.run(cmd_git, cwd=self.data_dir, stdout=subprocess.PIPE)
+ if res.returncode != 0:
+ raise RuntimeError('Failed to get the current commit hash')
+ commit_hash = res.stdout.decode().strip()
+
+ cmd = [
+ self.docker, 'build',
+ '-t', f'{image_namespace}/{self.data_dir.name}:latest',
+ '-t', f'{image_namespace}/{self.data_dir.name}:{commit_hash}',
+ '--pull',
+ self.temp_dir.as_posix(),
+ ]
+ if subprocess.run(cmd).returncode != 0:
+ raise RuntimeError('Failed to build the docker image')
+
+class ImageTester(BuilderBase):
+ def __init__(self, data_dir: Path, temp_dir: Path, ci_root: Path) -> None:
+ super().__init__(data_dir, temp_dir)
+ self.meson_root = ci_root.parent.parent.resolve()
+
+ def gen_dockerfile(self) -> None:
+ out_file = self.temp_dir / 'Dockerfile'
+ out_data = textwrap.dedent(f'''\
+ FROM {image_namespace}/{self.data_dir.name}
+
+ ADD meson /meson
+ ''')
+
+ out_file.write_text(out_data, encoding='utf-8')
+
+ def copy_meson(self) -> None:
+ shutil.copytree(
+ self.meson_root,
+ self.temp_dir / 'meson',
+ ignore=shutil.ignore_patterns(
+ '.git',
+ '*_cache',
+ '__pycache__',
+ # 'work area',
+ self.temp_dir.name,
+ )
+ )
+
+ def do_test(self, tty: bool = False) -> None:
+ self.copy_meson()
+ self.gen_dockerfile()
+
+ try:
+ build_cmd = [
+ self.docker, 'build',
+ '-t', 'meson_test_image',
+ self.temp_dir.as_posix(),
+ ]
+ if subprocess.run(build_cmd).returncode != 0:
+ raise RuntimeError('Failed to build the test docker image')
+
+ test_cmd = []
+ if tty:
+ test_cmd = [
+ self.docker, 'run', '--rm', '-t', '-i', 'meson_test_image',
+ '/bin/bash', '-c', ''
+ + 'cd meson;'
+ + 'source /ci/env_vars.sh;'
+ + f'echo -e "\\n\\nInteractive test shell in the {image_namespace}/{self.data_dir.name} container with the current meson tree";'
+ + 'echo -e "The file ci/ciimage/user.sh will be sourced if it exists to enable user specific configurations";'
+ + 'echo -e "Run the following command to run all CI tests: ./run_tests.py $CI_ARGS\\n\\n";'
+ + '[ -f ci/ciimage/user.sh ] && exec /bin/bash --init-file ci/ciimage/user.sh;'
+ + 'exec /bin/bash;'
+ ]
+ else:
+ test_cmd = [
+ self.docker, 'run', '--rm', '-t', 'meson_test_image',
+ '/bin/bash', '-c', 'source /ci/env_vars.sh; cd meson; ./run_tests.py $CI_ARGS'
+ ]
+
+ if subprocess.run(test_cmd).returncode != 0 and not tty:
+ raise RuntimeError('Running tests failed')
+ finally:
+ cleanup_cmd = [self.docker, 'rmi', '-f', 'meson_test_image']
+ subprocess.run(cleanup_cmd).returncode
+
+class ImageTTY(BuilderBase):
+ def __init__(self, data_dir: Path, temp_dir: Path, ci_root: Path) -> None:
+ super().__init__(data_dir, temp_dir)
+ self.meson_root = ci_root.parent.parent.resolve()
+
+ def do_run(self) -> None:
+ try:
+ tty_cmd = [
+ self.docker, 'run',
+ '--name', 'meson_test_container', '-t', '-i', '-v', f'{self.meson_root.as_posix()}:/meson',
+ f'{image_namespace}/{self.data_dir.name}',
+ '/bin/bash', '-c', ''
+ + 'cd meson;'
+ + 'source /ci/env_vars.sh;'
+ + f'echo -e "\\n\\nInteractive test shell in the {image_namespace}/{self.data_dir.name} container with the current meson tree";'
+ + 'echo -e "The file ci/ciimage/user.sh will be sourced if it exists to enable user specific configurations";'
+ + 'echo -e "Run the following command to run all CI tests: ./run_tests.py $CI_ARGS\\n\\n";'
+ + '[ -f ci/ciimage/user.sh ] && exec /bin/bash --init-file ci/ciimage/user.sh;'
+ + 'exec /bin/bash;'
+ ]
+ subprocess.run(tty_cmd).returncode != 0
+ finally:
+ cleanup_cmd = [self.docker, 'rm', '-f', 'meson_test_container']
+ subprocess.run(cleanup_cmd).returncode
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description='Meson CI image builder')
+ parser.add_argument('what', type=str, help='Which image to build / test')
+ parser.add_argument('-t', '--type', choices=['build', 'test', 'testTTY', 'TTY'], help='What to do', required=True)
+
+ args = parser.parse_args()
+
+ ci_root = Path(__file__).parent
+ ci_data = ci_root / args.what
+
+ with TemporaryDirectory(prefix=f'{args.type}_{args.what}_', dir=ci_root) as td:
+ ci_build = Path(td)
+ print(f'Build dir: {ci_build}')
+
+ if args.type == 'build':
+ builder = Builder(ci_data, ci_build)
+ builder.do_build()
+ elif args.type == 'test':
+ tester = ImageTester(ci_data, ci_build, ci_root)
+ tester.do_test()
+ elif args.type == 'testTTY':
+ tester = ImageTester(ci_data, ci_build, ci_root)
+ tester.do_test(tty=True)
+ elif args.type == 'TTY':
+ tester = ImageTTY(ci_data, ci_build, ci_root)
+ tester.do_run()
+
+if __name__ == '__main__':
+ main()