From e4cd4e5716875e993e05f0436c806bd8606d8379 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 14 Feb 2025 01:59:14 -0800 Subject: [PATCH] GUACAMOLE-1374: Migrate to modular, parallelizable Docker build. --- Dockerfile | 247 ++++++++++++++++++++++------- src/guacd-docker/bin/autobuild.sh | 144 +++++++++++++++++ src/guacd-docker/bin/build-all.sh | 136 ---------------- src/guacd-docker/bin/entrypoint.sh | 6 + 4 files changed, 336 insertions(+), 197 deletions(-) create mode 100755 src/guacd-docker/bin/autobuild.sh delete mode 100755 src/guacd-docker/bin/build-all.sh create mode 100755 src/guacd-docker/bin/entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 0288dcb5..63748246 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,46 +25,24 @@ # NOTE: Using 3.18 because the required openssl1.1-compat-dev package was # removed in more recent versions. ARG ALPINE_BASE_IMAGE=3.18 -FROM alpine:${ALPINE_BASE_IMAGE} AS builder + +# The target architecture of the build. Valid values are "ARM" and "X86". By +# default, this is detected automatically. +ARG BUILD_ARCHITECTURE + +# The number of processes that may run simultaneously during the build. By +# default, this is detected automatically. +ARG BUILD_JOBS + +# The directory that will house the guacamole-server source during the build +ARG BUILD_DIR=/tmp/guacamole-server # FreeRDP version (default to version 3) ARG FREERDP_VERSION=3 -# Install build dependencies -RUN apk add --no-cache \ - autoconf \ - automake \ - build-base \ - cairo-dev \ - cjson-dev \ - cmake \ - cunit-dev \ - git \ - grep \ - krb5-dev \ - libjpeg-turbo-dev \ - libpng-dev \ - libtool \ - libwebp-dev \ - make \ - openssl1.1-compat-dev \ - pango-dev \ - pulseaudio-dev \ - sdl2-dev \ - sdl2_ttf-dev \ - util-linux-dev \ - webkit2gtk-dev - -# Copy source to container for sake of build -ARG BUILD_DIR=/tmp/guacamole-server -COPY . ${BUILD_DIR} - -# -# Base directory for installed build artifacts. -# -# NOTE: Due to limitations of the Docker image build process, this value is -# duplicated in an ARG in the second stage of the build. -# +# The final install location for guacamole-server and all dependencies. NOTE: +# This value is hard-coded in the entrypoint. Any change to this value must be +# propagated there. ARG PREFIX_DIR=/opt/guacamole # @@ -84,6 +62,8 @@ ARG WITH_LIBWEBSOCKETS='v\d+(\.\d+)+' # options are needed) # +ARG FREERDP_ARM_OPTS="" + ARG FREERDP_OPTS="\ -DBUILTIN_CHANNELS=OFF \ -DCHANNEL_URBDRC=OFF \ @@ -112,7 +92,6 @@ ARG FREERDP_OPTS="\ -DWITH_SERVER_INTERFACE=OFF \ -DWITH_SHADOW_MAC=OFF \ -DWITH_SHADOW_X11=OFF \ - -DWITH_SSE2=ON \ -DWITH_SWSCALE=OFF \ -DWITH_WAYLAND=OFF \ -DWITH_X11=OFF \ @@ -127,19 +106,39 @@ ARG FREERDP_OPTS="\ -DWITH_XV=OFF \ -DWITH_ZLIB=ON" +ARG FREERDP_X86_OPTS="-DWITH_SSE2=ON" + +ARG GUACAMOLE_SERVER_ARM_OPTS="" + ARG GUACAMOLE_SERVER_OPTS="\ --disable-guaclog" +ARG GUACAMOLE_SERVER_X86_OPTS="" + +ARG LIBSSH2_ARM_OPTS="" + ARG LIBSSH2_OPTS="\ -DBUILD_EXAMPLES=OFF \ -DBUILD_SHARED_LIBS=ON" +ARG LIBSSH2_X86_OPTS="" + +ARG LIBTELNET_ARM_OPTS="" + ARG LIBTELNET_OPTS="\ --disable-static \ --disable-util" +ARG LIBTELNET_X86_OPTS="" + +ARG LIBVNCCLIENT_ARM_OPTS="" + ARG LIBVNCCLIENT_OPTS="" +ARG LIBVNCCLIENT_X86_OPTS="" + +ARG LIBWEBSOCKETS_ARM_OPTS="" + ARG LIBWEBSOCKETS_OPTS="\ -DDISABLE_WERROR=ON \ -DLWS_WITHOUT_SERVER=ON \ @@ -150,8 +149,143 @@ ARG LIBWEBSOCKETS_OPTS="\ -DLWS_WITHOUT_TEST_SERVER_EXTPOLL=ON \ -DLWS_WITH_STATIC=OFF" -# Build guacamole-server and its core protocol library dependencies -RUN ${BUILD_DIR}/src/guacd-docker/bin/build-all.sh +ARG LIBWEBSOCKETS_X86_OPTS="" + +# +# Base builder image that will be used by subsequent build stages, including +# for building dependencies of guacamole-server. +# + +FROM alpine:${ALPINE_BASE_IMAGE} AS builder +ARG BUILD_DIR + +# Install build dependencies +RUN apk add --no-cache \ + autoconf \ + automake \ + build-base \ + cairo-dev \ + cjson-dev \ + cmake \ + cunit-dev \ + git \ + grep \ + krb5-dev \ + libjpeg-turbo-dev \ + libpng-dev \ + libtool \ + libwebp-dev \ + make \ + openssl1.1-compat-dev \ + pango-dev \ + pulseaudio-dev \ + sdl2-dev \ + sdl2_ttf-dev \ + util-linux-dev \ + webkit2gtk-dev + +# Copy generic, automatic build script +COPY ./src/guacd-docker/bin/autobuild.sh ${BUILD_DIR}/src/guacd-docker/bin/ + +# +# Build dependency: libssh2 +# + +FROM builder AS libssh2 +ARG BUILD_DIR +ARG LIBSSH2_ARM_OPTS +ARG LIBSSH2_OPTS +ARG LIBSSH2_X86_OPTS +ARG PREFIX_DIR +ARG WITH_LIBSSH2 + +RUN ${BUILD_DIR}/src/guacd-docker/bin/autobuild.sh "LIBSSH2" \ + "https://github.com/libssh2/libssh2" + +# +# Build dependency: libtelnet +# + +FROM builder AS libtelnet +ARG BUILD_DIR +ARG LIBTELNET_ARM_OPTS +ARG LIBTELNET_OPTS +ARG LIBTELNET_X86_OPTS +ARG PREFIX_DIR +ARG WITH_LIBTELNET + +RUN ${BUILD_DIR}/src/guacd-docker/bin/autobuild.sh "LIBTELNET" \ + "https://github.com/seanmiddleditch/libtelnet" + +# +# Build dependency: libvncclient +# + +FROM builder AS libvncclient +ARG BUILD_DIR +ARG LIBVNCCLIENT_ARM_OPTS +ARG LIBVNCCLIENT_OPTS +ARG LIBVNCCLIENT_X86_OPTS +ARG PREFIX_DIR +ARG WITH_LIBVNCCLIENT + +RUN ${BUILD_DIR}/src/guacd-docker/bin/autobuild.sh "LIBVNCCLIENT" \ + "https://github.com/LibVNC/libvncserver" + +# +# Build dependency: libwebsockets +# + +FROM builder AS libwebsockets +ARG BUILD_DIR +ARG LIBWEBSOCKETS_ARM_OPTS +ARG LIBWEBSOCKETS_OPTS +ARG LIBWEBSOCKETS_X86_OPTS +ARG PREFIX_DIR +ARG WITH_LIBWEBSOCKETS + +RUN ${BUILD_DIR}/src/guacd-docker/bin/autobuild.sh "LIBWEBSOCKETS" \ + "https://github.com/warmcat/libwebsockets" + +# +# Build dependency: FreeRDP +# + +FROM builder AS freerdp +ARG BUILD_DIR +ARG FREERDP_ARM_OPTS +ARG FREERDP_OPTS +ARG FREERDP_X86_OPTS +ARG PREFIX_DIR +ARG WITH_FREERDP + +RUN ${BUILD_DIR}/src/guacd-docker/bin/autobuild.sh "FREERDP" \ + "https://github.com/FreeRDP/FreeRDP" + +# +# STAGE 7: Collect dependencies built by previous stages and build +# guacamole-server. +# + +FROM builder AS guacamole-server +ARG BUILD_DIR +ARG FREERDP_VERSION +ARG GUACAMOLE_SERVER_ARM_OPTS +ARG GUACAMOLE_SERVER_OPTS +ARG GUACAMOLE_SERVER_X86_OPTS +ARG PREFIX_DIR + +# Copy dependencies built in previous stages +COPY --from=freerdp ${PREFIX_DIR} ${PREFIX_DIR} +COPY --from=libssh2 ${PREFIX_DIR} ${PREFIX_DIR} +COPY --from=libtelnet ${PREFIX_DIR} ${PREFIX_DIR} +COPY --from=libvncclient ${PREFIX_DIR} ${PREFIX_DIR} +COPY --from=libwebsockets ${PREFIX_DIR} ${PREFIX_DIR} + +# Use guacamole-server source from build context +COPY . ${BUILD_DIR} + +RUN ${BUILD_DIR}/src/guacd-docker/bin/autobuild.sh "GUACAMOLE_SERVER" "${BUILD_DIR}" # Determine location of the FREERDP library based on the version. ARG FREERDP_LIB_PATH=${PREFIX_DIR}/lib/freerdp${FREERDP_VERSION} @@ -163,25 +297,16 @@ RUN ${BUILD_DIR}/src/guacd-docker/bin/list-dependencies.sh \ ${FREERDP_LIB_PATH}/*guac*.so \ > ${PREFIX_DIR}/DEPENDENCIES +# +# STAGE 8: Final, runtime image. +# + # Use same Alpine version as the base for the runtime image -FROM alpine:${ALPINE_BASE_IMAGE} - -# -# Base directory for installed build artifacts. See also the -# CMD directive at the end of this build stage. -# -# NOTE: Due to limitations of the Docker image build process, this value is -# duplicated in an ARG in the first stage of the build. -# -ARG PREFIX_DIR=/opt/guacamole - -# Runtime environment -ENV LC_ALL=C.UTF-8 -ENV LD_LIBRARY_PATH=${PREFIX_DIR}/lib -ENV GUACD_LOG_LEVEL=info +FROM alpine:${ALPINE_BASE_IMAGE} AS runtime +ARG PREFIX_DIR # Copy build artifacts into this stage -COPY --from=builder ${PREFIX_DIR} ${PREFIX_DIR} +COPY --from=guacamole-server ${PREFIX_DIR} ${PREFIX_DIR} # Bring runtime environment up to date and install runtime dependencies RUN apk add --no-cache \ @@ -196,6 +321,10 @@ RUN apk add --no-cache \ util-linux-login && \ xargs apk add --no-cache < ${PREFIX_DIR}/DEPENDENCIES +# Runtime environment +ENV LC_ALL=C.UTF-8 +ENV LD_LIBRARY_PATH=${PREFIX_DIR}/lib + # Checks the operating status every 5 minutes with a timeout of 5 seconds HEALTHCHECK --interval=5m --timeout=5s CMD nc -z 127.0.0.1 4822 || exit 1 @@ -211,9 +340,5 @@ USER guacd # Expose the default listener port EXPOSE 4822 -# Start guacd, listening on port 0.0.0.0:4822 -# -# Note the path here MUST correspond to the value specified in the -# PREFIX_DIR build argument. -# -CMD /opt/guacamole/sbin/guacd -b 0.0.0.0 -L $GUACD_LOG_LEVEL -f +COPY ./src/guacd-docker/bin/entrypoint.sh /opt/guacamole/ +ENTRYPOINT [ "/opt/guacamole/entrypoint.sh" ] diff --git a/src/guacd-docker/bin/autobuild.sh b/src/guacd-docker/bin/autobuild.sh new file mode 100755 index 00000000..8598c8ca --- /dev/null +++ b/src/guacd-docker/bin/autobuild.sh @@ -0,0 +1,144 @@ +#!/bin/sh -e +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +## +## @fn autobuild.sh +## +## Automatically builds the source of guacamole-server or its various core +## protocol library dependencies. Usage of GNU Autotools vs. CMake is +## automatically detected. Additional build options may be provided using +## environment variables, including architecture-specific options. +## +## @param VAR_BASE +## The text that should be used as the base for environment variables +## that control the build. The following variables will be read: +## +## - [VAR_BASE]_OPTS: Options that should be passed to the build regardless +## of architecture. +## +## - [VAR_BASE]_ARM_OPTS: Options that should be passed to the build only +## if built for ARM CPUs. +## +## - [VAR_BASE]_X86_OPTS: Options that should be passed to the build only +## if built for x86 (Intel, AMD) CPUs. +## +## - WITH_[VAR_BASE]: The git reference (tag, commit, etc.) that should be +## built, or a regular expression that matches the version tag that +## should be built (the largest-numbered tag will be chosen). This +## variable is only applicable if the source is being pulled from a git +## repository (the URL of a git repository is provided for LOCATION). +## +## @param LOCATION +## The location of the source to be used for the build. This may be a +## directory or the URL of a git repository. +## + +VAR_BASE="$1" +LOCATION="$2" + +# Pre-populate build control variables such that the custom build prefix is +# used for C headers, locating libraries, etc. +export CFLAGS="-I${PREFIX_DIR}/include" +export LDFLAGS="-L${PREFIX_DIR}/lib" +export PKG_CONFIG_PATH="${PREFIX_DIR}/lib/pkgconfig" + +# Ensure thread stack size will be 8 MB (glibc's default on Linux) rather than +# 128 KB (musl's default) +export LDFLAGS="$LDFLAGS -Wl,-z,stack-size=8388608" + +PATTERN_VAR="WITH_${VAR_BASE}" +PATTERN="$(printenv "${PATTERN_VAR}" || true)" + +# Allow dependencies to be manually omitted with the tag/commit pattern "NO" +if [ "$PATTERN" = "NO" ]; then + echo "NOT building $REPO_DIR (explicitly skipped)" + exit +fi + +# Clone repository and change to top-level directory of source +if echo "$LOCATION" | grep -q '://'; then + cd /tmp + git clone "$LOCATION" + SRC_DIR="$(basename "$LOCATION" .git)" +else + SRC_DIR="$LOCATION" +fi + +# Determine architecture that we're building on +if [ -z "$BUILD_ARCHITECTURE" ]; then + case "$(uname -m)" in + armv6l|armv7l|aarch64) + BUILD_ARCHITECTURE="ARM" + ;; + x86_64) + BUILD_ARCHITECTURE="X86" + ;; + *) + BUILD_ARCHITECTURE="UNKNOWN" + ;; + esac +fi + +# Determine how many procesess we can potentially run at once for the build +if [ -z "$BUILD_JOBS" ]; then + BUILD_JOBS="$(nproc)" +fi + +echo "Build architecture: $BUILD_ARCHITECTURE" +echo "Build parallelism: $BUILD_JOBS job(s)" +cd "$SRC_DIR/" + +OPTION_VAR="${VAR_BASE}_OPTS" +ARCH_OPTION_VAR="${VAR_BASE}_${BUILD_ARCHITECTURE}_OPTS" +BUILD_OPTS="$(printenv "${OPTION_VAR}" || true) $(printenv "${ARCH_OPTION_VAR}" || true)" + +# Download (if necessary) and log location of source +if [ -e .git ]; then + + # Locate tag/commit based on provided pattern + VERSION="$(git tag -l --sort=-v:refname | grep -Px -m1 "$PATTERN" \ + || echo "$PATTERN")" + + # Switch to desired version of source + echo "Building $SRC_DIR @ $VERSION ..." + git -c advice.detachedHead=false checkout "$VERSION" + +else + echo "Building $SRC_DIR ..." +fi + +# Configure build using CMake or GNU Autotools, whichever happens to be +# used by the library being built +if [ -e CMakeLists.txt ]; then + cmake \ + -B "${SRC_DIR}-build" -S . \ + -DCMAKE_INSTALL_PREFIX:PATH="$PREFIX_DIR" \ + $BUILD_OPTS + + # Build and install + cmake --build "${SRC_DIR}-build" --parallel "$BUILD_JOBS" + cmake --install "${SRC_DIR}-build" +else + [ -e configure ] || autoreconf -fi + ./configure --prefix="$PREFIX_DIR" $BUILD_OPTS + + # Build and install + make -j"$BUILD_JOBS" && make install +fi diff --git a/src/guacd-docker/bin/build-all.sh b/src/guacd-docker/bin/build-all.sh deleted file mode 100755 index 430419cf..00000000 --- a/src/guacd-docker/bin/build-all.sh +++ /dev/null @@ -1,136 +0,0 @@ -#!/bin/sh -e -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# - -## -## @fn build-all.sh -## -## Builds the source of guacamole-server and its various core protocol library -## dependencies. -## - -# Pre-populate build control variables such that the custom build prefix is -# used for C headers, locating libraries, etc. -export CFLAGS="-I${PREFIX_DIR}/include" -export LDFLAGS="-L${PREFIX_DIR}/lib" -export PKG_CONFIG_PATH="${PREFIX_DIR}/lib/pkgconfig" - -# Ensure thread stack size will be 8 MB (glibc's default on Linux) rather than -# 128 KB (musl's default) -export LDFLAGS="$LDFLAGS -Wl,-z,stack-size=8388608" - -## -## Builds and installs the source at the given git repository, automatically -## switching to the version of the source at the tag/commit that matches the -## given pattern. -## -## @param URL -## The URL of the git repository that the source should be downloaded from. -## -## @param PATTERN -## The Perl-compatible regular expression that the tag must match. If no -## tag matches the regular expression, the pattern is assumed to be an -## exact reference to a commit, branch, etc. acceptable by git checkout. -## -## @param ... -## Any additional command-line options that should be provided to CMake or -## the configure script. -## -install_from_git() { - - URL="$1" - PATTERN="$2" - shift 2 - - # Calculate top-level directory name of resulting repository from the - # provided URL - REPO_DIR="$(basename "$URL" .git)" - - # Allow dependencies to be manually omitted with the tag/commit pattern "NO" - if [ "$PATTERN" = "NO" ]; then - echo "NOT building $REPO_DIR (explicitly skipped)" - return - fi - - # Clone repository and change to top-level directory of source - cd /tmp - git clone "$URL" - cd $REPO_DIR/ - - # Locate tag/commit based on provided pattern - VERSION="$(git tag -l --sort=-v:refname | grep -Px -m1 "$PATTERN" \ - || echo "$PATTERN")" - - # Switch to desired version of source - echo "Building $REPO_DIR @ $VERSION ..." - git -c advice.detachedHead=false checkout "$VERSION" - - # Configure build using CMake or GNU Autotools, whichever happens to be - # used by the library being built - if [ -e CMakeLists.txt ]; then - cmake \ - -B "${REPO_DIR}-build" -S . \ - -DCMAKE_INSTALL_PREFIX:PATH="$PREFIX_DIR" \ - "$@" - - # Build and install - cmake --build "${REPO_DIR}-build" - cmake --install "${REPO_DIR}-build" - else - [ -e configure ] || autoreconf -fi - ./configure --prefix="$PREFIX_DIR" "$@" - - # Build and install - make && make install - fi -} - -# -# Determine any option overrides to guarantee successful build -# - -export BUILD_ARCHITECTURE="$(arch)" # Determine architecture building on -echo "Build architecture: $BUILD_ARCHITECTURE" - -case $BUILD_ARCHITECTURE in - armv6l|armv7l|aarch64) - export FREERDP_OPTS_OVERRIDES="-DWITH_SSE2=OFF" # Disable SSE2 on ARM - ;; - *) - export FREERDP_OPTS_OVERRIDES="" - ;; -esac - -# -# Build and install core protocol library dependencies -# - -install_from_git "https://github.com/FreeRDP/FreeRDP" "$WITH_FREERDP" $FREERDP_OPTS $FREERDP_OPTS_OVERRIDES -install_from_git "https://github.com/libssh2/libssh2" "$WITH_LIBSSH2" $LIBSSH2_OPTS -install_from_git "https://github.com/seanmiddleditch/libtelnet" "$WITH_LIBTELNET" $LIBTELNET_OPTS -install_from_git "https://github.com/LibVNC/libvncserver" "$WITH_LIBVNCCLIENT" $LIBVNCCLIENT_OPTS -install_from_git "https://github.com/warmcat/libwebsockets" "$WITH_LIBWEBSOCKETS" $LIBWEBSOCKETS_OPTS - -# -# Build guacamole-server -# - -cd "$BUILD_DIR" -autoreconf -fi && ./configure --prefix="$PREFIX_DIR" $GUACAMOLE_SERVER_OPTS -make && make check && make install diff --git a/src/guacd-docker/bin/entrypoint.sh b/src/guacd-docker/bin/entrypoint.sh new file mode 100755 index 00000000..3d9808ee --- /dev/null +++ b/src/guacd-docker/bin/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +# Listen on 0.0.0.0:4822, logging messages at the info level. Allow log level +# to be overridden with GUACD_LOG_LEVEL, and other behavior to be overridden +# with additional command-line options passed to Docker. +exec /opt/guacamole/sbin/guacd -f -b 0.0.0.0 -L "${GUACD_LOG_LEVEL:-info}" "$@"