blob: b9d3181584119d7ba3dc2763c00b19eb3e586e8f [file] [log] [blame]
#!/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_data += '''
if [ -f "$HOME/.cargo/env" ]; then
source "$HOME/.cargo/env"
fi
'''
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',
symlinks=True,
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', '-xc', '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()