From 369626c7647a80a8e6840ce66172fdcaca9d9651 Mon Sep 17 00:00:00 2001 From: David Liu Date: Mon, 29 Dec 2025 12:24:38 -0500 Subject: [PATCH 1/4] Improved robustness of tester installation scripts and Docker configuration --- Changelog.md | 2 + server/.dockerfiles/Dockerfile | 38 ++++++++++--------- server/autotest_server/testers/__init__.py | 1 + .../testers/haskell/requirements.system | 1 + .../autotest_server/testers/haskell/setup.py | 29 ++++++++------ .../testers/haskell/stack_permissions.sh | 3 ++ .../testers/java/requirements.system | 1 + server/autotest_server/testers/java/setup.py | 4 +- .../testers/r/requirements.system | 23 +++++++++++ server/autotest_server/testers/r/setup.py | 4 +- .../testers/racket/requirements.system | 1 + .../autotest_server/testers/racket/setup.py | 4 +- 12 files changed, 79 insertions(+), 32 deletions(-) diff --git a/Changelog.md b/Changelog.md index e60e0a15..9ad793cb 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,8 @@ All notable changes to this project will be documented here. ## [unreleased] - Fixed Haskell test results to only include the function name (#687) +- Improved robustness of tester installation scripts and Docker configuration (#688) +- Moved tidyverse installation steps from server Dockerfile into R tester requirements.system (#688) ## [v2.9.0] - Install stack with GHCup (#626) diff --git a/server/.dockerfiles/Dockerfile b/server/.dockerfiles/Dockerfile index 60ca5a48..4ff15f95 100644 --- a/server/.dockerfiles/Dockerfile +++ b/server/.dockerfiles/Dockerfile @@ -8,7 +8,9 @@ RUN userdel -r ubuntu ARG LOGIN_USER ARG WORKSPACE -RUN apt-get update -y && \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update -y && \ DEBIAN_FRONTEND=noninteractive apt-get -y install software-properties-common && \ DEBIAN_FRONTEND=noninteractive add-apt-repository -y ppa:deadsnakes/ppa && \ DEBIAN_FRONTEND=noninteractive apt-get -y install python3.11 \ @@ -21,18 +23,7 @@ RUN apt-get update -y && \ postgresql-client \ libpq-dev \ sudo \ - git \ - libfontconfig1-dev \ - libcurl4-openssl-dev \ - libfreetype6-dev \ - libpng-dev \ - libtiff5-dev \ - libjpeg-dev \ - libharfbuzz-dev \ - libfribidi-dev \ - libxml2-dev \ - libnuma-dev \ - r-base + git RUN useradd -ms /bin/bash $LOGIN_USER && \ usermod -aG sudo $LOGIN_USER && \ @@ -43,14 +34,25 @@ RUN useradd -ms /bin/bash $LOGIN_USER && \ done && \ chmod a+x /home/${LOGIN_USER} -COPY . /app +RUN mkdir -p ${WORKSPACE} && chown ${LOGIN_USER} ${WORKSPACE} && \ + mkdir -p /home/${LOGIN_USER}/markus_venv && chown ${LOGIN_USER} /home/${LOGIN_USER}/markus_venv -RUN find /app/autotest_server/testers -name requirements.system -exec {} \; -RUN echo "TZ=$( cat /etc/timezone )" >> /etc/R/Renviron.site +# Copy requirements.system files for all testers. These are copied separately from other files +# to avoid Docker cache invalidation of the subsequent RUN command if any other files are changed +# in markus-autotesting/server. +COPY --parents \ + ./autotest_server/testers/java/requirements.system \ + ./autotest_server/testers/haskell/requirements.system \ + ./autotest_server/testers/r/requirements.system \ + ./autotest_server/testers/racket/requirements.system \ + /app +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + --mount=type=cache,target=$WORKSPACE/.stack,sharing=locked \ + find /app/autotest_server/testers -name requirements.system -exec {} \; -RUN mkdir -p ${WORKSPACE} && chown ${LOGIN_USER} ${WORKSPACE} && \ - mkdir -p /home/${LOGIN_USER}/markus_venv && chown ${LOGIN_USER} /home/${LOGIN_USER}/markus_venv +COPY . /app WORKDIR /home/${LOGIN_USER} diff --git a/server/autotest_server/testers/__init__.py b/server/autotest_server/testers/__init__.py index 82826329..8aed26dc 100644 --- a/server/autotest_server/testers/__init__.py +++ b/server/autotest_server/testers/__init__.py @@ -10,6 +10,7 @@ def install(testers=_TESTERS): for tester in testers: mod = importlib.import_module(f".{tester}.setup", package="autotest_server.testers") try: + print(f"[AUTOTESTER] calling autotest_server.testers.{tester}.setup.install()") mod.install() except Exception as e: msg = ( diff --git a/server/autotest_server/testers/haskell/requirements.system b/server/autotest_server/testers/haskell/requirements.system index 7b493126..2aefb3a7 100755 --- a/server/autotest_server/testers/haskell/requirements.system +++ b/server/autotest_server/testers/haskell/requirements.system @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -euxo pipefail apt-get -y update diff --git a/server/autotest_server/testers/haskell/setup.py b/server/autotest_server/testers/haskell/setup.py index 48e4d3f7..66382057 100644 --- a/server/autotest_server/testers/haskell/setup.py +++ b/server/autotest_server/testers/haskell/setup.py @@ -20,26 +20,33 @@ def create_environment(_settings, _env_dir, default_env_dir): def install(): try: - subprocess.run( - os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.system"), - check=True, - capture_output=True, - text=True, - ) + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.system") + print(f"[AUTOTESTER] Running {path}", flush=True) + subprocess.run(path, check=True) except subprocess.CalledProcessError as e: raise RuntimeError(f"Error executing Haskell requirements.system: {e}") resolver = STACK_RESOLVER - cmd = ["stack", "build", "--resolver", resolver, "--system-ghc", *HASKELL_TEST_DEPS] + + cmd_update = ["stack", "update"] + print(f'[AUTOTESTER] Running {" ".join(cmd_update)}', flush=True) + try: + subprocess.run(cmd_update, check=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Error running {cmd_update}: {e}") + + cmd_build = ["stack", "build", "--resolver", resolver, "--system-ghc", *HASKELL_TEST_DEPS] + print(f'[AUTOTESTER] Running {" ".join(cmd_build)}', flush=True) try: - subprocess.run(cmd, check=True, capture_output=True) + subprocess.run(cmd_build, check=True) except subprocess.CalledProcessError as e: - raise RuntimeError(f"Error running {cmd}: {e}") + raise RuntimeError(f"Error running {cmd_build}: {e}") try: + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "stack_permissions.sh") + print(f"[AUTOTESTER] Running {path}", flush=True) subprocess.run( - os.path.join(os.path.dirname(os.path.realpath(__file__)), "stack_permissions.sh"), + path, check=True, shell=True, - capture_output=True, text=True, ) except subprocess.CalledProcessError as e: diff --git a/server/autotest_server/testers/haskell/stack_permissions.sh b/server/autotest_server/testers/haskell/stack_permissions.sh index 2bb5d2f3..f24bd8bd 100755 --- a/server/autotest_server/testers/haskell/stack_permissions.sh +++ b/server/autotest_server/testers/haskell/stack_permissions.sh @@ -1,3 +1,6 @@ +#!/usr/bin/env bash +set -euxo pipefail + echo "allow-different-user: true" >> $STACK_ROOT/config.yaml echo "recommend-stack-upgrade: false" >> $STACK_ROOT/config.yaml chmod a+w $STACK_ROOT/stack.sqlite3.pantry-write-lock diff --git a/server/autotest_server/testers/java/requirements.system b/server/autotest_server/testers/java/requirements.system index bbe96712..27082c3a 100755 --- a/server/autotest_server/testers/java/requirements.system +++ b/server/autotest_server/testers/java/requirements.system @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -euxo pipefail if ! dpkg -l openjdk-8-jdk wget &> /dev/null; then apt-get -y update diff --git a/server/autotest_server/testers/java/setup.py b/server/autotest_server/testers/java/setup.py index 26c8a018..fab85aa1 100644 --- a/server/autotest_server/testers/java/setup.py +++ b/server/autotest_server/testers/java/setup.py @@ -10,7 +10,9 @@ def create_environment(_settings, _env_dir, default_env_dir): def install(): this_dir = os.path.dirname(os.path.realpath(__file__)) - subprocess.run(os.path.join(this_dir, "requirements.system"), check=True) + path = os.path.join(this_dir, "requirements.system") + print(f"[AUTOTESTER] Running {path}", flush=True) + subprocess.run(path, check=True) url = ( "https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.7.0/junit-platform" "-console-standalone-1.7.0.jar" diff --git a/server/autotest_server/testers/r/requirements.system b/server/autotest_server/testers/r/requirements.system index 4e65d8aa..8ffc5ea3 100755 --- a/server/autotest_server/testers/r/requirements.system +++ b/server/autotest_server/testers/r/requirements.system @@ -1,6 +1,29 @@ #!/usr/bin/env bash +set -euxo pipefail if ! dpkg -l r-base &> /dev/null; then apt-get -y update DEBIAN_FRONTEND=noninteractive apt-get install -y -o 'Dpkg::Options::=--force-confdef' -o 'Dpkg::Options::=--force-confold' r-base + + # Set global R timezone + echo "TZ=$( cat /etc/timezone )" >> /etc/R/Renviron.site + + # Install system requirements for tidyverse. Obtained by running `pak::pkg_system_requirements("tidyverse")`. + DEBIAN_FRONTEND=noninteractive apt-get install -y -o 'Dpkg::Options::=--force-confdef' -o 'Dpkg::Options::=--force-confold' \ + libx11-dev \ + libcurl4-openssl-dev \ + libssl-dev \ + make \ + zlib1g-dev \ + pandoc \ + libfreetype6-dev \ + libjpeg-dev \ + libpng-dev \ + libtiff-dev \ + libwebp-dev \ + libicu-dev \ + libfontconfig1-dev \ + libfribidi-dev \ + libharfbuzz-dev \ + libxml2-dev fi diff --git a/server/autotest_server/testers/r/setup.py b/server/autotest_server/testers/r/setup.py index 6d418582..e07af2a6 100644 --- a/server/autotest_server/testers/r/setup.py +++ b/server/autotest_server/testers/r/setup.py @@ -39,4 +39,6 @@ def settings(): def install(): - subprocess.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.system"), check=True) + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.system") + print(f"[AUTOTESTER] Running {path}", flush=True) + subprocess.run(path, check=True) diff --git a/server/autotest_server/testers/racket/requirements.system b/server/autotest_server/testers/racket/requirements.system index dfd27b29..4dc70c06 100755 --- a/server/autotest_server/testers/racket/requirements.system +++ b/server/autotest_server/testers/racket/requirements.system @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -euxo pipefail if ! dpkg -l racket &> /dev/null; then apt-get -y update diff --git a/server/autotest_server/testers/racket/setup.py b/server/autotest_server/testers/racket/setup.py index a80bcc6a..218a611d 100644 --- a/server/autotest_server/testers/racket/setup.py +++ b/server/autotest_server/testers/racket/setup.py @@ -13,4 +13,6 @@ def settings(): def install(): - subprocess.run(os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.system"), check=True) + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.system") + print(f"[AUTOTESTER] Running {path}", flush=True) + subprocess.run(path, check=True) From 28be37912d814c32231687246c0fc0c334267cc5 Mon Sep 17 00:00:00 2001 From: David Liu Date: Mon, 29 Dec 2025 14:59:01 -0500 Subject: [PATCH 2/4] Resolved Dockerfile warning by switching CMD argument to JSON input --- client/.dockerfiles/Dockerfile | 2 +- server/.dockerfiles/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/.dockerfiles/Dockerfile b/client/.dockerfiles/Dockerfile index ebc4f1ca..efc4aed8 100644 --- a/client/.dockerfiles/Dockerfile +++ b/client/.dockerfiles/Dockerfile @@ -12,4 +12,4 @@ RUN apt-get update -y && \ WORKDIR /app -CMD /markus_venv/bin/python run.py +CMD ["/markus_venv/bin/python", "run.py"] diff --git a/server/.dockerfiles/Dockerfile b/server/.dockerfiles/Dockerfile index 4ff15f95..f9062ccc 100644 --- a/server/.dockerfiles/Dockerfile +++ b/server/.dockerfiles/Dockerfile @@ -58,4 +58,4 @@ WORKDIR /home/${LOGIN_USER} USER ${LOGIN_USER} -CMD /app/.dockerfiles/cmd-dev.sh +CMD ["/app/.dockerfiles/cmd-dev.sh"] From 0fbdab6c6878cde1b299429cf10d41eb45f5f26c Mon Sep 17 00:00:00 2001 From: David Liu Date: Mon, 29 Dec 2025 15:07:43 -0500 Subject: [PATCH 3/4] Fixed Haskell tester installation using ghcup to install stack system-wide --- Changelog.md | 1 + .../testers/haskell/haskell_tester.py | 3 --- .../testers/haskell/requirements.system | 25 +++++++++++++------ .../autotest_server/testers/haskell/setup.py | 3 --- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Changelog.md b/Changelog.md index 9ad793cb..9b88535e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented here. - Fixed Haskell test results to only include the function name (#687) - Improved robustness of tester installation scripts and Docker configuration (#688) - Moved tidyverse installation steps from server Dockerfile into R tester requirements.system (#688) +- Fixed Haskell tester installation using ghcup to install stack system-wide (#688) ## [v2.9.0] - Install stack with GHCup (#626) diff --git a/server/autotest_server/testers/haskell/haskell_tester.py b/server/autotest_server/testers/haskell/haskell_tester.py index 78010e53..48674fde 100644 --- a/server/autotest_server/testers/haskell/haskell_tester.py +++ b/server/autotest_server/testers/haskell/haskell_tester.py @@ -7,9 +7,6 @@ from ..tester import Tester, Test, TestError from ..specs import TestSpecs -home = os.getenv("HOME") -os.environ["PATH"] = f"{home}/.cabal/bin:{home}/.ghcup/bin:" + os.environ["PATH"] - class HaskellTest(Test): def __init__( diff --git a/server/autotest_server/testers/haskell/requirements.system b/server/autotest_server/testers/haskell/requirements.system index 2aefb3a7..8d23952a 100755 --- a/server/autotest_server/testers/haskell/requirements.system +++ b/server/autotest_server/testers/haskell/requirements.system @@ -1,16 +1,25 @@ #!/usr/bin/env bash set -euxo pipefail -apt-get -y update - +# Install a system-wide ghc, which can be used as a default version in the Haskell tester. +# This should be synchronized with the LTS version and dependencies in setup.py if ! dpkg -l ghc cabal-install &> /dev/null; then + apt-get -y update DEBIAN_FRONTEND=noninteractive apt-get install -y -o 'Dpkg::Options::=--force-confdef' -o 'Dpkg::Options::=--force-confold' ghc cabal-install fi -if [ ! -x "$HOME/.ghcup/bin/ghcup" ] && [ ! -x "/usr/local/bin/stack" ]; then - BOOTSTRAP_HASKELL_NONINTERACTIVE=1 BOOTSTRAP_HASKELL_INSTALL_NO_STACK=1 curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh - $HOME/.ghcup/bin/ghcup install stack recommended - if [ "$(id -u)" -eq 0 ]; then - cp $HOME/.ghcup/bin/stack /usr/local/bin/ - fi +if [ ! -x "/usr/local/bin/stack" ]; then + # We use ghcup to install stack rather than relying on system packages. This enables newer versions of Stack to be installed. + # Install ghcup dependencies: https://www.haskell.org/ghcup/install/#linux-ubuntu + apt-get -y update + DEBIAN_FRONTEND=noninteractive apt-get install -y -o 'Dpkg::Options::=--force-confdef' -o 'Dpkg::Options::=--force-confold' \ + build-essential \ + curl \ + libffi-dev \ + libffi8ubuntu1 \ + libgmp-dev \ + libgmp10 \ + libncurses-dev + curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 BOOTSTRAP_HASKELL_MINIMAL=1 sh + $HOME/.ghcup/bin/ghcup install stack recommended --isolate /usr/local/bin fi diff --git a/server/autotest_server/testers/haskell/setup.py b/server/autotest_server/testers/haskell/setup.py index 66382057..3e50aa33 100644 --- a/server/autotest_server/testers/haskell/setup.py +++ b/server/autotest_server/testers/haskell/setup.py @@ -5,9 +5,6 @@ HASKELL_TEST_DEPS = ["tasty-discover", "tasty-quickcheck", "tasty-hunit"] STACK_RESOLVER = "lts-21.21" -home = os.getenv("HOME") -os.environ["PATH"] = f"{home}/.cabal/bin:{home}/.ghcup/bin:" + os.environ["PATH"] - def create_environment(_settings, _env_dir, default_env_dir): env_data = _settings.get("env_data", {}) From 841efda80f696eff6f730b7f43462d5992714114 Mon Sep 17 00:00:00 2001 From: David Liu Date: Mon, 29 Dec 2025 20:18:01 -0500 Subject: [PATCH 4/4] Bump client and server docker image versions --- compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compose.yaml b/compose.yaml index 39e38ec8..d49eaddc 100644 --- a/compose.yaml +++ b/compose.yaml @@ -7,7 +7,7 @@ services: UBUNTU_VERSION: '24.04' LOGIN_USER: 'docker' WORKSPACE: '/home/docker/.autotesting' - image: markus-autotest-server-dev:1.4.0 + image: markus-autotest-server-dev:1.5.0 volumes: - ./server:/app:cached - venv_server:/home/docker/markus_venv @@ -29,7 +29,7 @@ services: dockerfile: ./.dockerfiles/Dockerfile args: UBUNTU_VERSION: '24.04' - image: markus-autotest-client-dev:1.4.0 + image: markus-autotest-client-dev:1.5.0 container_name: 'autotest-client' volumes: - ./client:/app:cached