Merge pull request #6843 from mensinda/ciJSONSCHEMA
CI: Maintain docker images with GitHub Actionspull/6901/head
commit
69a03fa94b
21 changed files with 417 additions and 100 deletions
@ -0,0 +1,54 @@ |
||||
name: CI image builder |
||||
|
||||
on: |
||||
push: |
||||
branches: |
||||
- master |
||||
paths: |
||||
- 'ci/ciimage/**' |
||||
- '.github/workflows/images.yml' |
||||
|
||||
pull_request: |
||||
branches: |
||||
- master |
||||
paths: |
||||
- 'ci/ciimage/**' |
||||
- '.github/workflows/images.yml' |
||||
|
||||
# Rebuild the images every week (Sunday) |
||||
schedule: |
||||
- cron: '0 0 * * 0' |
||||
|
||||
jobs: |
||||
build: |
||||
name: ${{ matrix.cfg.name }} |
||||
runs-on: ubuntu-latest |
||||
strategy: |
||||
fail-fast: false |
||||
matrix: |
||||
cfg: |
||||
- { name: Arch Linux, id: arch } |
||||
- { name: Fedora, id: fedora } |
||||
- { name: OpenSUSE, id: opensuse } |
||||
- { name: Ubuntu Eoan, id: eoan } |
||||
steps: |
||||
- uses: actions/checkout@v2 |
||||
|
||||
# Login to dockerhub |
||||
- name: Docker login |
||||
if: github.event_name == 'push' || github.event_name == 'schedule' |
||||
uses: azure/docker-login@v1 |
||||
with: |
||||
username: ${{ secrets.DOCKER_USERNAME }} |
||||
password: ${{ secrets.DOCKER_PASSWORD }} |
||||
|
||||
# Build and test |
||||
- name: Building the ${{ matrix.cfg.id }} image |
||||
run: ./ci/ciimage/build.py -t build ${{ matrix.cfg.id }} |
||||
- name: Testing the ${{ matrix.cfg.id }} image |
||||
run: ./ci/ciimage/build.py -t test ${{ matrix.cfg.id }} |
||||
|
||||
# Publish |
||||
- name: Push the ${{ matrix.cfg.id }} image |
||||
if: github.event_name == 'push' || github.event_name == 'schedule' |
||||
run: docker push mesonbuild/${{ matrix.cfg.id }} |
@ -0,0 +1,2 @@ |
||||
/build_* |
||||
/test_* |
@ -1,4 +0,0 @@ |
||||
FROM archlinux:latest |
||||
|
||||
ADD install.sh /usr/sbin/docker-arch-install |
||||
RUN docker-arch-install |
@ -0,0 +1,6 @@ |
||||
{ |
||||
"base_image": "archlinux:latest", |
||||
"env": { |
||||
"CI": "1" |
||||
} |
||||
} |
@ -0,0 +1,186 @@ |
||||
#!/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()) |
||||
|
||||
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.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 = '' |
||||
|
||||
for key, val in self.image_def.env.items(): |
||||
out_data += f'export {key}="{val}"\n' |
||||
|
||||
out_file.write_text(out_data) |
||||
|
||||
# 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) |
||||
|
||||
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('Failde 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) |
||||
|
||||
def copy_meson(self) -> None: |
||||
shutil.copytree( |
||||
self.meson_root, |
||||
self.temp_dir / 'meson', |
||||
ignore=shutil.ignore_patterns( |
||||
'.git', |
||||
'*_cache', |
||||
'work area', |
||||
self.temp_dir.name, |
||||
) |
||||
) |
||||
|
||||
def do_test(self): |
||||
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('Failde to build the test docker image') |
||||
|
||||
test_cmd = [ |
||||
self.docker, 'run', '--rm', '-t', 'meson_test_image', |
||||
'/usr/bin/bash', '-c', 'source /ci/env_vars.sh; cd meson; ./run_tests.py' |
||||
] |
||||
if subprocess.run(test_cmd).returncode != 0: |
||||
raise RuntimeError('Running tests failed') |
||||
finally: |
||||
cleanup_cmd = [self.docker, 'rmi', '-f', 'meson_test_image'] |
||||
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'], 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() |
||||
|
||||
if __name__ == '__main__': |
||||
main() |
@ -0,0 +1,22 @@ |
||||
#!/bin/bash |
||||
|
||||
### |
||||
### Common functions for CI builder files. |
||||
### All functions can be accessed in install.sh via: |
||||
### |
||||
### $ source /ci/common.sh |
||||
### |
||||
|
||||
set -e |
||||
|
||||
dub_fetch() { |
||||
set +e |
||||
for (( i=1; i<=24; ++i )); do |
||||
dub fetch "$@" |
||||
(( $? == 0 )) && break |
||||
|
||||
echo "Dub Fetch failed. Retrying in $((i*5))s" |
||||
sleep $((i*5)) |
||||
done |
||||
set -e |
||||
} |
@ -1,36 +0,0 @@ |
||||
FROM ubuntu:eoan |
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive |
||||
ENV LANG='C.UTF-8' |
||||
ENV DC=gdc |
||||
|
||||
RUN sed -i '/^#\sdeb-src /s/^#//' "/etc/apt/sources.list" \ |
||||
&& apt-get -y update && apt-get -y upgrade \ |
||||
&& apt-get -y install eatmydata \ |
||||
&& eatmydata apt-get -y build-dep meson \ |
||||
&& eatmydata apt-get -y install python3-pytest-xdist \ |
||||
&& eatmydata apt-get -y install python3-pip libxml2-dev libxslt1-dev libyaml-dev libjson-glib-dev \ |
||||
&& eatmydata python3 -m pip install hotdoc codecov \ |
||||
&& eatmydata apt-get -y install wget unzip \ |
||||
&& eatmydata apt-get -y install qt5-default clang \ |
||||
&& eatmydata apt-get -y install pkg-config-arm-linux-gnueabihf \ |
||||
&& eatmydata apt-get -y install qt4-linguist-tools \ |
||||
&& eatmydata apt-get -y install python-dev \ |
||||
&& eatmydata apt-get -y install libomp-dev \ |
||||
&& eatmydata apt-get -y install dub ldc \ |
||||
&& eatmydata apt-get -y install mingw-w64 mingw-w64-tools nim \ |
||||
&& eatmydata apt-get -y install --no-install-recommends wine-stable \ |
||||
&& eatmydata apt-get -y install libclang-dev \ |
||||
&& eatmydata apt-get -y install libgcrypt20-dev \ |
||||
&& eatmydata apt-get -y install libgpgme-dev \ |
||||
&& eatmydata apt-get -y install libhdf5-dev \ |
||||
&& eatmydata apt-get -y install libboost-python-dev libboost-regex-dev \ |
||||
&& eatmydata apt-get -y install libblocksruntime-dev \ |
||||
&& eatmydata apt-get -y install libperl-dev \ |
||||
&& eatmydata apt-get -y install liblapack-dev libscalapack-mpi-dev \ |
||||
&& eatmydata dub fetch urld && dub build urld --compiler=gdc \ |
||||
&& eatmydata dub fetch dubtestproject \ |
||||
&& eatmydata dub build dubtestproject:test1 --compiler=ldc2 \ |
||||
&& eatmydata dub build dubtestproject:test2 --compiler=ldc2 |
||||
# OpenSSH client is needed to run openmpi binaries. |
||||
|
@ -0,0 +1,7 @@ |
||||
{ |
||||
"base_image": "ubuntu:eoan", |
||||
"env": { |
||||
"CI": "1", |
||||
"DC": "gdc" |
||||
} |
||||
} |
@ -0,0 +1,55 @@ |
||||
#!/bin/bash |
||||
|
||||
set -e |
||||
|
||||
source /ci/common.sh |
||||
|
||||
export DEBIAN_FRONTEND=noninteractive |
||||
export LANG='C.UTF-8' |
||||
export DC=gdc |
||||
|
||||
pkgs=( |
||||
python3-pytest-xdist |
||||
python3-pip libxml2-dev libxslt1-dev libyaml-dev libjson-glib-dev |
||||
wget unzip |
||||
qt5-default clang |
||||
pkg-config-arm-linux-gnueabihf |
||||
qt4-linguist-tools |
||||
python-dev |
||||
libomp-dev |
||||
dub ldc |
||||
mingw-w64 mingw-w64-tools nim |
||||
libclang-dev |
||||
libgcrypt20-dev |
||||
libgpgme-dev |
||||
libhdf5-dev |
||||
libboost-python-dev libboost-regex-dev |
||||
libblocksruntime-dev |
||||
libperl-dev |
||||
liblapack-dev libscalapack-mpi-dev |
||||
) |
||||
|
||||
sed -i '/^#\sdeb-src /s/^#//' "/etc/apt/sources.list" |
||||
apt-get -y update |
||||
apt-get -y upgrade |
||||
apt-get -y install eatmydata |
||||
|
||||
# Base stuff |
||||
eatmydata apt-get -y build-dep meson |
||||
|
||||
# packages |
||||
eatmydata apt-get -y install "${pkgs[@]}" |
||||
eatmydata apt-get -y install --no-install-recommends wine-stable # Wine is special |
||||
|
||||
eatmydata python3 -m pip install hotdoc codecov jsonschema |
||||
|
||||
# dub stuff |
||||
dub_fetch urld |
||||
dub build urld --compiler=gdc |
||||
dub_fetch dubtestproject |
||||
dub build dubtestproject:test1 --compiler=ldc2 |
||||
dub build dubtestproject:test2 --compiler=ldc2 |
||||
|
||||
# cleanup |
||||
apt-get -y clean |
||||
apt-get -y autoclean |
@ -0,0 +1,12 @@ |
||||
#!/bin/bash |
||||
|
||||
set -e |
||||
|
||||
testFN() { |
||||
set +e |
||||
false |
||||
} |
||||
|
||||
testFN |
||||
false |
||||
exit 0 |
@ -1,4 +0,0 @@ |
||||
FROM fedora:latest |
||||
|
||||
ADD install.sh /usr/sbin/docker-fedora-install |
||||
RUN docker-fedora-install |
@ -0,0 +1,8 @@ |
||||
{ |
||||
"base_image": "fedora:latest", |
||||
"env": { |
||||
"CI": "1", |
||||
"SKIP_SCIENTIFIC": "1", |
||||
"SKIP_STATIC_BOOST": "1" |
||||
} |
||||
} |
@ -1,4 +0,0 @@ |
||||
FROM opensuse/tumbleweed:latest |
||||
|
||||
ADD install.sh /usr/sbin/docker-opensuse-install |
||||
RUN docker-opensuse-install |
@ -0,0 +1,9 @@ |
||||
{ |
||||
"base_image": "opensuse/tumbleweed:latest", |
||||
"env": { |
||||
"CI": "1", |
||||
"SKIP_SCIENTIFIC": "1", |
||||
"SKIP_STATIC_BOOST": "1", |
||||
"SINGLE_DUB_COMPILER": "1" |
||||
} |
||||
} |
@ -0,0 +1,9 @@ |
||||
{ |
||||
"matrix": { |
||||
"options": { |
||||
"warning_level": [ |
||||
{ "val": "1", "skip_on_env": [ "SINGLE_DUB_COMPILER" ] } |
||||
] |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue