From cec42fc985941a1250a0b1facc96337f0e776a0a Mon Sep 17 00:00:00 2001 From: Marek Wydmuch Date: Wed, 11 Oct 2023 13:11:11 +0200 Subject: [PATCH] Bulding manylinux wheels using cibuildwheel (#73) --- .github/workflows/build-publish-linux.yml | 55 ------------ .github/workflows/build-publish.yml | 90 +++++++++++++++++++ .github/workflows/test-cpp.yml | 11 ++- .github/workflows/test-python.yml | 13 +-- .gitignore | 4 + CMakeLists.txt | 48 ++++++---- pyproject.toml | 21 +++++ setup.py | 40 +++++---- src/memory.h | 1 + tests/__init__.py | 0 tests/test_cibuildwheel/README.md | 10 +++ tests/test_cibuildwheel/apt-based.Dockerfile | 13 +++ .../test_cibuildwheel/conda-based.Dockerfile | 9 ++ tests/test_cibuildwheel/dnf-based.Dockerfile | 10 +++ .../install_and_test_wheel.sh | 29 ++++++ .../test_cibuildwheel_linux.sh | 68 ++++++++++++++ 16 files changed, 324 insertions(+), 98 deletions(-) delete mode 100644 .github/workflows/build-publish-linux.yml create mode 100644 .github/workflows/build-publish.yml create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/test_cibuildwheel/README.md create mode 100644 tests/test_cibuildwheel/apt-based.Dockerfile create mode 100644 tests/test_cibuildwheel/conda-based.Dockerfile create mode 100644 tests/test_cibuildwheel/dnf-based.Dockerfile create mode 100755 tests/test_cibuildwheel/install_and_test_wheel.sh create mode 100755 tests/test_cibuildwheel/test_cibuildwheel_linux.sh diff --git a/.github/workflows/build-publish-linux.yml b/.github/workflows/build-publish-linux.yml deleted file mode 100644 index 10a92a693..000000000 --- a/.github/workflows/build-publish-linux.yml +++ /dev/null @@ -1,55 +0,0 @@ -# This workflow will build and (if release) publish Python distributions to PyPI -# For more information see: -# - https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -# - https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ -# - Adapted from https://github.com/Farama-Foundation/PettingZoo/blob/e230f4d80a5df3baf9bd905149f6d4e8ce22be31/.github/workflows/build-publish.yml - -name: build-publish - -on: - push: - branches: [master] - pull_request: - branches: [master] - release: - types: [published] - -jobs: - build-wheels: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] - - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install system packages - run: sudo apt-get update && sudo apt-get install git cmake capnproto zlib1g-dev g++ build-essential python3-dev python3-pip pkg-config libzip-dev software-properties-common libbz2-dev - - name: Install dependencies - run: python3 -m pip install --upgrade setuptools wheel build - - name: Build wheels - run: python3 -m build --wheel --sdist - - name: Store wheels - uses: actions/upload-artifact@v2 - with: - path: dist - - publish: - runs-on: ubuntu-latest - needs: - - build-wheels - if: github.event_name == 'release' && github.event.action == 'published' - steps: - - name: Download dists - uses: actions/download-artifact@v2 - with: - name: artifact - path: dist - - name: Publish - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml new file mode 100644 index 000000000..bf21ff8b8 --- /dev/null +++ b/.github/workflows/build-publish.yml @@ -0,0 +1,90 @@ +# This workflow will build and (if release) publish Python distributions to PyPI +# For more information see: +# - https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions +# - https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ +# - Adapted from https://github.com/Farama-Foundation/PettingZoo/blob/e230f4d80a5df3baf9bd905149f6d4e8ce22be31/.github/workflows/build-publish.yml + +name: build-publish + +on: + workflow_dispatch: + push: + branches: [master] + pull_request: + branches: [master] + release: + types: [published] + +jobs: + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-22.04] #, macos-11] - disable macos, as it's not working for a moment + + steps: + - uses: actions/checkout@v3 + + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v2 + with: + platforms: all + + - name: Build wheels + uses: pypa/cibuildwheel@v2.15.0 + env: + # Configure cibuildwheel to build native archs, and some emulated ones + CIBW_ARCHS_LINUX: x86_64 # aarch64 - disable ARM, as it's not working for a moment + #CIBW_ARCHS_MACOS: x86_64 arm64 - macos is disabled for a moment + CIBW_BUILD_VERBOSITY: 3 # Increase verbosity to see what's going on + CIBW_REPAIR_WHEEL_COMMAND_LINUX: > # Print additional info from auditwheel + auditwheel show {wheel} && auditwheel repair -w {dest_dir} {wheel} + + - name: Report built wheels + run: | + ls -l ./wheelhouse/*.whl + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Build sdist + run: pipx run build --sdist + + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + path: dist/*.tar.gz + + upload_pypi: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + if: github.event_name == 'release' && github.event.action == 'published' + steps: + - name: Download all dists + uses: actions/download-artifact@v3 + with: + # Unpacks default artifact into dist/ + # If `name: artifact` is omitted, the action will create extra parent dir + name: artifact + path: dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + # To test: + # with: + # repository_url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/test-cpp.yml b/.github/workflows/test-cpp.yml index a53c11a7c..bc2a06848 100644 --- a/.github/workflows/test-cpp.yml +++ b/.github/workflows/test-cpp.yml @@ -12,14 +12,19 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.9 + - name: Install system packages run: | sudo apt-get update - sudo apt-get install git cmake capnproto zlib1g-dev g++ build-essential python3-dev python3-pip pkg-config libzip-dev software-properties-common libbz2-dev python3-opengl + sudo apt-get install git cmake capnproto zlib1g-dev build-essential pkg-config libzip-dev software-properties-common libbz2-dev python3-opengl - - name: Build test files + - name: Install pip package and build tests run: | - python3 -m pip install -e . + cmake . + make -j make -f tests/Makefile - name: Run tests diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 29e3a182a..487e1da04 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -10,24 +10,25 @@ jobs: runs-on: ubuntu-latest # todo, add more OS systems to see if they work as well strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] # '3.11' + python-version: ['3.8', '3.9', '3.10'] # '3.11' steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install system packages run: | sudo apt-get update - sudo apt-get install git cmake capnproto zlib1g-dev g++ build-essential python3-dev python3-pip pkg-config libzip-dev software-properties-common libbz2-dev xvfb python3-opengl + sudo apt-get install git cmake capnproto zlib1g-dev build-essential pkg-config libzip-dev software-properties-common libbz2-dev xvfb python3-opengl - name: Install pip packages run: | - python -m pip install --upgrade pip cmake wheel setuptools pytest - pip install -e . + python3 -m pip install --upgrade pip pytest + python3 -m pip install -e . - name: Run tests - run: xvfb-run -s '-screen 0 1024x768x24' pytest + run: | + xvfb-run -s '-screen 0 1024x768x24' pytest diff --git a/.gitignore b/.gitignore index 073cdd9d5..0107aabcb 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,7 @@ Temporary # VSCode config /.vscode/ + +# cibuildwheel +wheelhouse +tests/test_cibuildwheel/tmp_dockerfiles diff --git a/CMakeLists.txt b/CMakeLists.txt index 30965c298..9164afd50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,9 +4,32 @@ if(POLICY CMP0048) cmake_policy(SET CMP0048 NEW) endif() -find_package(Git QUIET) +if(LINUX) + set(PYEXT_SUFFIX + ".so" + CACHE STRING "Suffix for Python extension modules") + set(DYNLIB_SUFFIX + ".so" + CACHE STRING "Suffix for dynamic libraries") +elseif(APPLE) + set(PYEXT_SUFFIX + ".so" + CACHE STRING "Suffix for Python extension modules") + set(DYNLIB_SUFFIX + ".dylib" + CACHE STRING "Suffix for dynamic libraries") +elseif(WIN32) + set(PYEXT_SUFFIX + ".pyd" + CACHE STRING "Suffix for Python extension modules" FORCE) + set(DYNLIB_SUFFIX + ".dll" + CACHE STRING "Suffix for dynamic libraries") +else() + message(FATAL_ERROR "Unsupported platform") +endif() -find_package(PythonInterp REQUIRED) +find_package(Git QUIET) file(READ "${CMAKE_SOURCE_DIR}/retro/VERSION.txt" PROJECT_VERSION) string(REGEX REPLACE "\n$" "" PROJECT_VERSION "${PROJECT_VERSION}") @@ -45,7 +68,9 @@ set(BUILD_PYTHON mark_as_advanced(BUILD_PYTHON) if(BUILD_PYTHON) - find_package(PythonLibs REQUIRED) + find_package(Python 3 COMPONENTS Interpreter Development.Module) +else() + find_package(Python 3 COMPONENTS Interpreter) endif() if(WIN32 OR BUILD_MANYLINUX) @@ -103,16 +128,6 @@ else() WORKING_DIRECTORY "${LUA_INCLUDE_DIRS}") endif() -if(NOT WIN32) - set(PYEXT_SUFFIX - ".so" - CACHE STRING "Suffix for Python extension modules") -else() - set(PYEXT_SUFFIX - ".pyd" - CACHE STRING "Suffix for Python extension modules" FORCE) -endif() - if(CMAKE_C_COMPILER_ID STREQUAL "GNU") set(STATIC_LDFLAGS "-static-libstdc++ -Wl,--exclude-libs,ALL") endif() @@ -184,7 +199,8 @@ function(add_core platform core_name) ${CMAKE_COMMAND} -E env CFLAGS=${core_cflags} CXXFLAGS=${core_cxxflags} LDFLAGS=${core_ldflags} $(MAKE) -f ${makefile} CC="${CMAKE_C_COMPILER}" CXX="${CMAKE_CXX_COMPILER}" fpic=${core_fpic_flags} ${libretro_platform} - COMMAND ${CMAKE_COMMAND} -E copy "${core_name}_libretro*" "${TARGET_PATH}" + COMMAND ${CMAKE_COMMAND} -E copy "${core_name}_libretro${DYNLIB_SUFFIX}" + "${TARGET_PATH}" WORKING_DIRECTORY "cores/${platform}/${subdir}" DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/retro/cores/${core_name}-version") unset(core_ldflags) @@ -339,7 +355,7 @@ if(CAPN_PROTO_FOUND) endif() include_directories(src retro third-party/pybind11/include third-party - third-party/gtest/googletest/include ${PYTHON_INCLUDE_DIRS}) + third-party/gtest/googletest/include ${Python_INCLUDE_DIRS}) if(BUILD_PYTHON) add_library(retro SHARED src/retro.cpp) @@ -359,7 +375,7 @@ if(BUILD_PYTHON) add_definitions(-DMS_WIN64) endif() - set(PYBIND_LIBS "${PYTHON_LIBRARY}") + set(PYBIND_LIBS "${Python_LIBRARY}") endif() target_link_libraries(retro retro-base ${PYBIND_LIBS} ${STATIC_LDFLAGS}) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..293d88e1b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["cmake>=3.2.0", "setuptools", "wheel", "build"] + +[tool.cibuildwheel] +# We need to build for the following Python versions: +build = "cp{38,39,310,311}-*" + +[tool.cibuildwheel.linux] +# Only manylinux is supported (no musl) +build = "cp{38,39,310,311}-manylinux*" + +# For manylinux_2_28 we need to install the following dependencies using yum: +before-all = "yum install -y cmake git pkgconf-pkg-config zlib-devel libzip-devel bzip2-devel" + +# Only build for x86_64 and aarch64 are officially supported +archs = "x86_64 aarch64" +manylinux-x86_64-image = "manylinux_2_28" +manylinux-aarch64-image = "manylinux_2_28" + +[tool.cibuildwheel.macos] +before-all = "brew install pkg-config capnp lua@5.1 qt5" diff --git a/setup.py b/setup.py index 237bb3525..849e9fed9 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ import os import subprocess import sys -from distutils.spawn import find_executable +import sysconfig from setuptools import Extension, setup from setuptools.command.build_ext import build_ext @@ -18,33 +18,38 @@ class CMakeBuild(build_ext): def run(self): suffix = super().get_ext_filename("") - pyext_suffix = f"-DPYEXT_SUFFIX:STRING={suffix}" + pyext_suffix = f"-DPYEXT_SUFFIX={suffix}" pylib_dir = "" if not self.inplace: - pylib_dir = f"-DPYLIB_DIRECTORY:PATH={self.build_lib}" + pylib_dir = f"-DPYLIB_DIRECTORY={self.build_lib}" if self.debug: build_type = "-DCMAKE_BUILD_TYPE=Debug" else: build_type = "" - python_executable = f"-DPYTHON_EXECUTABLE:STRING={sys.executable}" - cmake_exe = find_executable("cmake") - if not cmake_exe: - try: - import cmake - except ImportError: - subprocess.check_call([sys.executable, "-m", "pip", "install", "cmake"]) - import cmake - cmake_exe = os.path.join(cmake.CMAKE_BIN_DIR, "cmake") + + # Provide hints to CMake about where to find Python (this should be enough for most cases) + python_root_dir = f"-DPython_ROOT_DIR={os.path.dirname(sys.executable)}" + python_find_strategy = "-DPython_FIND_STRATEGY=LOCATION" + + # These directly specify Python artifacts + python_executable = f"-DPython_EXECUTABLE={sys.executable}" + python_include_dir = f"-DPython_INCLUDE_DIR={sysconfig.get_path('include')}" + python_library = f"-DPython_LIBRARY={sysconfig.get_path('platlib')}" + subprocess.check_call( [ - cmake_exe, + "cmake", ".", "-G", "Unix Makefiles", build_type, pyext_suffix, pylib_dir, + python_root_dir, + python_find_strategy, python_executable, + python_include_dir, + python_library, ], ) if self.parallel: @@ -53,10 +58,8 @@ def run(self): import multiprocessing jobs = f"-j{multiprocessing.cpu_count():d}" - make_exe = find_executable("make") - if not make_exe: - raise RuntimeError("Could not find Make executable. Is it installed?") - subprocess.check_call([make_exe, jobs, "retro"]) + + subprocess.check_call(["make", jobs, "retro"]) platform_globs = [ @@ -88,7 +91,7 @@ def run(self): version=open(VERSION_PATH).read().strip(), license="MIT", install_requires=["gymnasium>=0.27.1", "pyglet>=1.3.2,==1.*"], - python_requires=">=3.6.0,<3.11", + python_requires=">=3.8.0,<3.11", ext_modules=[Extension("retro._retro", ["CMakeLists.txt", "src/*.cpp"])], cmdclass={"build_ext": CMakeBuild}, packages=[ @@ -100,6 +103,7 @@ def run(self): "retro.scripts", "retro.import", "retro.examples", + "retro.testing", ], package_data={ "retro": [ diff --git a/src/memory.h b/src/memory.h index f99bfb08d..dbd43235c 100644 --- a/src/memory.h +++ b/src/memory.h @@ -4,6 +4,7 @@ #include #include +#include #include #include diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_cibuildwheel/README.md b/tests/test_cibuildwheel/README.md new file mode 100644 index 000000000..862781be8 --- /dev/null +++ b/tests/test_cibuildwheel/README.md @@ -0,0 +1,10 @@ +# Test cibuildwheel + +This is a test of cibuildwheel, run `test_cibuildwheel.sh` to build the wheels locally, and test them on different Linux distros. Requires cibuildwheel and docker to be installed. + +## Usage + +Run manually: +```bash +./test_cibuildwheel.sh +``` diff --git a/tests/test_cibuildwheel/apt-based.Dockerfile b/tests/test_cibuildwheel/apt-based.Dockerfile new file mode 100644 index 000000000..f9f9dbe99 --- /dev/null +++ b/tests/test_cibuildwheel/apt-based.Dockerfile @@ -0,0 +1,13 @@ +FROM ubuntu:latest + +ARG DEBIAN_FRONTEND=noninteractive +ENV TZ=Europe/London + +WORKDIR /stable-retro + +# Install Python and pip, OpenGL, and Xvfb +RUN apt update && apt install -y python3-dev python3-pip python3-opengl freeglut3-dev xvfb + +COPY tests tests +COPY wheelhouse wheelhouse +CMD ["bash", "./tests/test_cibuildwheel/install_and_test_wheel.sh"] diff --git a/tests/test_cibuildwheel/conda-based.Dockerfile b/tests/test_cibuildwheel/conda-based.Dockerfile new file mode 100644 index 000000000..0da5736f8 --- /dev/null +++ b/tests/test_cibuildwheel/conda-based.Dockerfile @@ -0,0 +1,9 @@ +FROM continuumio/miniconda3:latest + +WORKDIR /stable-retro + +RUN apt update && apt install -y freeglut3-dev xvfb + +COPY tests tests +COPY wheelhouse wheelhouse +CMD ["bash", "./tests/test_cibuildwheel/install_and_test_wheel.sh"] diff --git a/tests/test_cibuildwheel/dnf-based.Dockerfile b/tests/test_cibuildwheel/dnf-based.Dockerfile new file mode 100644 index 000000000..597fae340 --- /dev/null +++ b/tests/test_cibuildwheel/dnf-based.Dockerfile @@ -0,0 +1,10 @@ +FROM fedora:latest + +WORKDIR /stable-retro + +# Install Python and pip, OpenGL, and Xvfb +RUN dnf update -y && dnf clean all && dnf install -y python3-devel python3-pip freeglut-devel xorg-x11-server-Xvfb + +COPY tests tests +COPY wheelhouse wheelhouse +CMD ["bash", "./tests/test_cibuildwheel/install_and_test_wheel.sh"] diff --git a/tests/test_cibuildwheel/install_and_test_wheel.sh b/tests/test_cibuildwheel/install_and_test_wheel.sh new file mode 100755 index 000000000..d5e6815db --- /dev/null +++ b/tests/test_cibuildwheel/install_and_test_wheel.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -e + +# Set working dir to the root of the repo +cd $( dirname "${BASH_SOURCE[0]}" )/../.. + +# Report directory +ls -lha . + +# Report python version +python3 --version +python3 -c "import sys; print('Python', sys.version)" + +# Find matching wheel file in wheelhouse +PYTHON_VERSION=$(python3 -c "import sys; print('{}{}'.format(sys.version_info.major, sys.version_info.minor))") +PYTHON_WHEEL=$(ls wheelhouse/stable_retro-*-cp${PYTHON_VERSION}-cp${PYTHON_VERSION}*.whl) + +# Updgrad pip and install test deps +python3 -m pip install --upgrade pip +python3 -m pip install pytest psutil + +# Install wheel +python3 -m pip install ${PYTHON_WHEEL} + +# Test import +python3 -c "import retro" + +# Run tests with xvfb and pytest +xvfb-run -s '-screen 0 1024x768x24' pytest diff --git a/tests/test_cibuildwheel/test_cibuildwheel_linux.sh b/tests/test_cibuildwheel/test_cibuildwheel_linux.sh new file mode 100755 index 000000000..071419bbd --- /dev/null +++ b/tests/test_cibuildwheel/test_cibuildwheel_linux.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -e + +NC='\033[0m' +RED='\033[0;31m' +GREEN='\033[0;32m' + +REPO_ROOT=$( dirname ${BASH_SOURCE[0]} )/../.. +DOCKERFILES_DIR=$( dirname ${BASH_SOURCE[0]} ) +GENERATED_DOCKERFILES_DIR=$( dirname ${BASH_SOURCE[0]} )/tmp_dockerfiles +IMAGE_PREFIX="stable-retro_wheels" + + +# Array in format " " +DOCKERFILES_TO_BUILD_AND_RUN=( + "debian:11.6 apt-based.Dockerfile ENV LANG C.UTF-8" # Python 3.9 + "ubuntu:20.04 apt-based.Dockerfile" # Python 3.8 + "ubuntu:22.04 apt-based.Dockerfile" # Python 3.10 + #"continuumio/miniconda3:latest conda-based.Dockerfile" # Python 3.11 - not supported at the moment + #"almalinux:9 dnf-based.Dockerfile" # Python 3.9 - test doesn't work becouse of pyglet requirement for X server + #"rockylinux:9 dnf-based.Dockerfile" # Python 3.9 - as above + #"fedora:36 dnf-based.Dockerfile" # Python 3.10 - as above + #"fedora:37 dnf-based.Dockerfile" # Python 3.11 - not supported at the moment +) + +# Clean local directory to avoid problems +cd $REPO_ROOT +rm -f CMakeCache.txt +rm -rf CMakeFiles +rm -f retro/*.so retro/cores/*.so retro/cores/*.json retro/cores/*-version +rm -f cores/*/*.so cores/snes/libretro/*.so +rm -rf build + +# Build wheels using cibuildwheel +#export CIBW_BUILD_VERBOSITY=3 # Uncomment to see full build logs +cibuildwheel --platform linux --arch $(uname -m) + +function create_dockerfile ( ) { + local all_args=("$@") + local base_image=$1 + local base_name=$( basename "$( echo ${base_image} | tr ':' '_' )" ) + local base_dockerfile=$2 + local add_commands=("${all_args[@]:2}") + + mkdir -p $GENERATED_DOCKERFILES_DIR + dockerfile=${GENERATED_DOCKERFILES_DIR}/${IMAGE_PREFIX}_${base_name}.Dockerfile + + echo "FROM $base_image" > $dockerfile + echo "" >> $dockerfile + echo -e "${add_commands[@]}" >> $dockerfile + cat ${DOCKERFILES_DIR}/$base_dockerfile | tail -n +2 >> $dockerfile +} + +for dockerfile_setting in "${DOCKERFILES_TO_BUILD_AND_RUN[@]}"; do + create_dockerfile $dockerfile_setting + + echo -n "Building and running $dockerfile, saving output to $dockerfile.log ... " + filename=$( basename "$dockerfile" ) + dockerfile_dir=$( dirname "$dockerfile" ) + without_ext="${filename%.*}" + tag="${without_ext}:latest" + log="${dockerfile_dir}/${without_ext}.log" + + docker build -t $tag -f $dockerfile . &> $log || ( echo -e "${RED}FAILED${NC}"; exit 1 ) + docker run -it $tag &>> $log || ( echo -e "${RED}FAILED${NC}"; exit 1 ) + + echo -e "${GREEN}OK${NC}" +done