The Meson Build System
http://mesonbuild.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
247 lines
8.8 KiB
247 lines
8.8 KiB
#!/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', |
|
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()
|
|
|