## Motivation
R on the Alpine images was built without graphics support: `capabilities()` reports `cairo`, `png`, `jpeg` and `tiff` as `FALSE`, while Ubuntu reports all four `TRUE`.
The images install `R-dev` but never the graphics headers, and R's `configure` treats those as optional and does not fail when they are absent, so the build succeeds and ships an R that cannot draw.
Reproduced in a clean `alpine:3.22` container with the old package set:
```
Additional capabilities: PNG, NLS, ICU
Capabilities skipped: JPEG, TIFF, cairo
```
One correction to the original diagnosis: `--with-x=no` was not the cause, and libpng was in fact found.
R's bitmap and `pangocairo` checks run independently of `R_X11` (`configure.ac:2604` gates on `want_cairo`, not `use_X11`).
`capabilities("png")` needs a device *backend*, and with X11 off and cairo absent there was none, which is why all four read `FALSE` despite configure listing PNG.
So the fix is purely the missing `-dev` packages.
X11 stays off, which is correct for headless images, and cairo covers `png()` / `jpeg()` / `tiff()` / `svg()`.
## Changes
- `builder/Dockerfile.alpine-{320,321,322,323,324}` — install `pkgconf cairo-dev pango-dev fontconfig-dev freetype-dev libpng-dev jpeg-dev tiff-dev zlib-dev`; pass `--with-cairo --with-libpng --with-jpeglib --with-libtiff` explicitly so the intent sits next to `--with-x=no`; set `REQUIRED_CAPABILITIES`.
- `builder/build.sh` — add `verify_capabilities()` between `compile_r` and `package_r`. A build that loses a graphics device now fails instead of being packaged and uploaded. Opt-in via `REQUIRED_CAPABILITIES`, so every non-Alpine platform is untouched.
- `.crow/build.yaml` — add a `FORCE_REBUILD` escape hatch to the dedup step. Artifact filenames carry only the R version, so the HEAD probe would otherwise keep skipping every Alpine version and serving the stale broken binaries.
- `CLAUDE.md` — record the three gotchas.
## Verification
Compiled R 4.5.1 in the patched `alpine-322` image, through the real `build.sh`:
```
jpeg png tiff tcltk X11 aqua
TRUE TRUE TRUE TRUE FALSE FALSE
NLS Rprof profmem cairo ICU long.double
TRUE TRUE TRUE TRUE TRUE TRUE
All required capabilities are present.
```
The devices actually render, not just report `TRUE` (`default bitmapType: cairo`):
```
png -> t.png (2154 bytes)
jpeg -> t.jpeg (4489 bytes)
tiff -> t.tiff (120160 bytes)
svg -> t.svg (9381 bytes)
```
The guard was exercised in both directions against that real build: it passes on the four graphics capabilities, and returns non-zero naming the missing entry when additionally asked for `X11`.
`crow lint .crow/` passes and `bash -n builder/build.sh` is clean.
## Behaviour change
Alpine R binaries gain the cairo, png, jpeg and tiff devices, and link against `libcairo`, `libpangocairo`, `libpng`, `libjpeg` and `libtiff`.
The `.apk` metadata already declared those runtime dependencies, so consumer images need no change.
Any Alpine build that loses a graphics device now fails the pipeline rather than uploading silently.
**Needs a follow-up after merge:** the first run must have `FORCE_REBUILD` set, otherwise dedup skips every existing Alpine version and the broken artifacts stay in the bucket.
Reviewed-on: #9
283 lines
11 KiB
Shell
Executable file
283 lines
11 KiB
Shell
Executable file
#!/bin/bash
|
|
set -e
|
|
|
|
export CRAN=${CRAN-"https://cran.rstudio.com"}
|
|
export S3_BUCKET_PREFIX=${S3_BUCKET_PREFIX-""}
|
|
export OS_IDENTIFIER=${OS_IDENTIFIER-"unknown"}
|
|
export TARBALL_NAME="R-${R_VERSION}-${OS_IDENTIFIER}.tar.gz"
|
|
export R_INSTALL_PATH=${R_INSTALL_PATH:-"/opt/R/${R_VERSION}"}
|
|
|
|
# Some Dockerfiles may copy a `/env.sh` to set up environment variables
|
|
# that require command substitution. If this file exists, source it.
|
|
if [[ -f /env.sh ]]; then
|
|
echo "Sourcing environment variables"
|
|
source /env.sh
|
|
fi
|
|
|
|
# upload_r()
|
|
upload_r() {
|
|
baseName="r/${OS_IDENTIFIER}"
|
|
if [ -n "$S3_BUCKET" ] && [ "$S3_BUCKET" != "" ]; then
|
|
echo "Storing artifact on s3: ${S3_BUCKET}, tarball: ${TARBALL_NAME}"
|
|
s5cmd cp /tmp/${TARBALL_NAME} s3://${S3_BUCKET}/${S3_BUCKET_PREFIX}${baseName}/${TARBALL_NAME}
|
|
# check if PKG_FILE has been set by a packager script and act accordingly
|
|
if [ -n "$PKG_FILE" ] && [ "$PKG_FILE" != "" ]; then
|
|
if [ -f "$PKG_FILE" ]; then
|
|
s5cmd cp ${PKG_FILE} s3://${S3_BUCKET}/${S3_BUCKET_PREFIX}${baseName}/pkgs/$(basename ${PKG_FILE})
|
|
fi
|
|
fi
|
|
fi
|
|
if [ -n "$LOCAL_STORE" ] && [ "$LOCAL_STORE" != '' ]; then
|
|
echo "Storing artifact locally: ${LOCAL_STORE}, tarball: ${TARBALL_NAME}"
|
|
mkdir -p ${LOCAL_STORE}/${baseName}
|
|
cp /tmp/${TARBALL_NAME} ${LOCAL_STORE}/${baseName}/${TARBALL_NAME}
|
|
fi
|
|
}
|
|
|
|
# archive_r()
|
|
archive_r() {
|
|
dir=$(dirname "$R_INSTALL_PATH")
|
|
base=$(basename "$R_INSTALL_PATH")
|
|
tar czf "/tmp/${TARBALL_NAME}" --directory="$dir" "$base" --owner=0 --group=0
|
|
}
|
|
|
|
fetch_r_source() {
|
|
echo "Downloading R-${1}"
|
|
if [ -n "${R_TARBALL_URL}" ]; then
|
|
# Custom tarball URL for testing (e.g., R alpha and beta releases)
|
|
wget -q "${R_TARBALL_URL}" -O /tmp/R-${1}.tar.gz
|
|
elif [ "${1}" = devel ]; then
|
|
# Download the daily tarball of R devel
|
|
wget -q https://cran.r-project.org/src/base-prerelease/R-devel.tar.gz -O /tmp/R-devel.tar.gz
|
|
elif [ "${1}" = "next" ]; then
|
|
wget -q https://cran.r-project.org/src/base-prerelease/R-latest.tar.gz -O /tmp/R-next.tar.gz
|
|
else
|
|
wget -q "${CRAN}/src/base/R-`echo ${1}| awk 'BEGIN {FS="."} {print $1}'`/R-${1}.tar.gz" -O /tmp/R-${1}.tar.gz
|
|
fi
|
|
echo "Extracting R-${1}"
|
|
tar xf /tmp/R-${1}.tar.gz -C /tmp
|
|
# 'next' may contain R-patched/, R-alpha/, etc. make it R-next/
|
|
if [ "${1}" = "next" ]; then
|
|
dirname=`tar tzvf /tmp/R-next.tar.gz | head -1 | awk '{ print $NF }' | cut -d/ -f1`
|
|
mv /tmp/${dirname} /tmp/R-next
|
|
fi
|
|
rm /tmp/R-${1}.tar.gz
|
|
}
|
|
|
|
# Apply a patch for this R version if present. Typically for R-devel.
|
|
patch_r() {
|
|
cd /tmp/R-${1}
|
|
|
|
if [ -f "/patches/R-${1}.patch" ]; then
|
|
patch -p1 < "/patches/R-${1}.patch"
|
|
fi
|
|
|
|
if [ -f "/patches/R-${1}-${OS_IDENTIFIER}.patch" ]; then
|
|
patch -p1 < "/patches/R-${1}-${OS_IDENTIFIER}.patch"
|
|
fi
|
|
|
|
if [ -f "/patches/R-${1}-`arch`.patch" ]; then
|
|
patch -p1 < "/patches/R-${1}-`arch`.patch"
|
|
fi
|
|
}
|
|
|
|
# compile_r() - $1 as r version
|
|
compile_r() {
|
|
r_version=${1}
|
|
cd "/tmp/R-${r_version}"
|
|
|
|
# tools/config.guess in R versions older than 3.2.2 guess 'unknown' instead of 'pc'
|
|
# test the version and properly set the flag.
|
|
build_flag="--build=$(uname -m)-pc-linux-gnu"
|
|
if _version_is_greater_than "${r_version}" 3.2.2; then
|
|
build_flag=''
|
|
fi
|
|
|
|
# R 3.6.1 and below require additional compiler flags for GCC 10 and above
|
|
# (e.g., on Debian 11). Add -fcommon to CFLAGS to work around issues with new
|
|
# default of -fno-common for C (fixed in R 3.6.2). Add -fallow-argument-mismatch
|
|
# to FFLAGS to work around issues with changes to argument mismatch checking
|
|
# in Fortran (fixed in R 3.6.2, but not mentioned in NEWS).
|
|
# https://cran.r-project.org/doc/manuals/r-release/NEWS.3.html
|
|
# https://cran.r-project.org/doc/manuals/r-release/R-admin.html#Using-Fortran
|
|
# https://gcc.gnu.org/gcc-10/porting_to.html
|
|
gcc_major_version=$(gcc -dumpversion | cut -d '.' -f 1)
|
|
if _version_is_less_than "${r_version}" 3.6.2 && _version_is_greater_than "${gcc_major_version}" 9; then
|
|
# Default CFLAGS/FFLAGS for all R 3.x versions is '-g -O2' when using GCC
|
|
export CFLAGS='-g -O2 -fcommon'
|
|
export FFLAGS='-g -O2 -fallow-argument-mismatch'
|
|
echo "Setting CFLAGS: ${CFLAGS}"
|
|
echo "Setting FFLAGS: ${FFLAGS}"
|
|
fi
|
|
|
|
# R 4.3.x and below have K&R style XDR code that fails with GCC 14+
|
|
# GCC 14 treats () in prototypes as "zero args" instead of "unspecified args"
|
|
# Using -std=gnu17 restores the old behavior where () means unspecified args
|
|
# Fixed in R 4.4.0: https://bugs.r-project.org/show_bug.cgi?id=18755
|
|
echo "DEBUG: r_version=${r_version}, gcc_major_version=${gcc_major_version}"
|
|
echo "DEBUG: Checking if ${r_version} < 4.4.0 AND ${gcc_major_version} > 13"
|
|
if _version_is_less_than "${r_version}" 4.4.0 && _version_is_greater_than "${gcc_major_version}" 13; then
|
|
export CFLAGS="${CFLAGS:--g -O2} -std=gnu17"
|
|
echo "Setting CFLAGS for GCC 14+ compatibility: ${CFLAGS}"
|
|
else
|
|
echo "DEBUG: Condition not met, skipping GCC 14 workaround"
|
|
fi
|
|
|
|
# Avoid a PCRE2 dependency for R 3.5 and 3.6. R 3.x uses PCRE1, but R 3.5+
|
|
# will link against PCRE2 if present, although it is not actually used.
|
|
# Since there's no way to disable this in the configure script, and we need
|
|
# PCRE2 for R 4.x, we hide PCRE2 from the configure script by temporarily
|
|
# removing the pkg-config file and pcre2-config script.
|
|
#
|
|
# The INCLUDE_PCRE2_IN_R_3 environment variable can be set to include PCRE2
|
|
# in R 3.x builds, for distributions where PCRE2 is always required.
|
|
# In Debian 11/Ubuntu 22/RHEL 9, Pango now depends on PCRE2, so R 3.x will not be compiled with
|
|
# Pango support if the PCRE2 pkg-config file is missing.
|
|
if [[ "${r_version}" =~ ^3 ]] && pkg-config --exists libpcre2-8 && [ -z "$INCLUDE_PCRE2_IN_R_3" ]; then
|
|
mkdir -p /tmp/pcre2
|
|
pc_dir=$(pkg-config --variable pcfiledir libpcre2-8)
|
|
mv ${pc_dir}/libpcre2-8.pc /tmp/pcre2
|
|
config_bin=$(which pcre2-config)
|
|
mv ${config_bin} /tmp/pcre2
|
|
trap "{ mv /tmp/pcre2/libpcre2-8.pc ${pc_dir}; mv /tmp/pcre2/pcre2-config ${config_bin}; }" EXIT
|
|
fi
|
|
|
|
# Allow libcurl 8.x to be used in R 4.2 and below. Despite a change in major
|
|
# version number, libcurl 8 changes neither API nor ABI. This applies the same
|
|
# change to configure made in R 4.3.0, replacing exit(1) with exit(0) when
|
|
# LIBCURL_VERSION_MAJOR > 7.
|
|
# https://github.com/wch/r-source/commit/da6638896413bcbb5970b2335b92582853f94e3c
|
|
if _version_is_less_than "${r_version}" 4.3.0; then
|
|
sed -i -z 's/#if LIBCURL_VERSION_MAJOR > 7\n exit(1)/#if LIBCURL_VERSION_MAJOR > 7\n exit(0)/' configure
|
|
fi
|
|
|
|
# Default configure options. Some Dockerfiles override this with an ENV directive.
|
|
default_configure_options="\
|
|
--enable-R-shlib \
|
|
--with-tcltk \
|
|
--enable-memory-profiling \
|
|
--with-x \
|
|
--with-blas \
|
|
--with-lapack"
|
|
|
|
CONFIGURE_OPTIONS=${CONFIGURE_OPTIONS:-$default_configure_options}
|
|
|
|
# For RHEL 9, link to external FlexiBLAS starting from R 4.3.0 to align with EPEL 9
|
|
if _version_is_greater_than "${R_VERSION}" 4.2.10; then
|
|
if [[ "${OS_IDENTIFIER}" = "rhel-9" ]]; then
|
|
CONFIGURE_OPTIONS="$CONFIGURE_OPTIONS \
|
|
--with-blas=flexiblas --with-lapack=flexiblas"
|
|
fi
|
|
fi
|
|
|
|
echo "Using CONFIGURE_OPTIONS: ${CONFIGURE_OPTIONS}"
|
|
|
|
# set some common environment variables for the configure step
|
|
AWK=/usr/bin/awk \
|
|
LIBnn=lib \
|
|
PERL=/usr/bin/perl \
|
|
R_PDFVIEWER=xdg-open \
|
|
R_BROWSER=xdg-open \
|
|
R_PAPERSIZE=letter \
|
|
R_PRINTCMD=/usr/bin/lpr \
|
|
R_UNZIPCMD=/usr/bin/unzip \
|
|
R_ZIPCMD=/usr/bin/zip \
|
|
./configure \
|
|
--prefix="${R_INSTALL_PATH}" \
|
|
${CONFIGURE_OPTIONS} \
|
|
${build_flag}
|
|
make clean
|
|
make
|
|
make install
|
|
|
|
# Add OS identifier to the default HTTP user agent.
|
|
# Set this in the system Rprofile so it works when R is run with --vanilla.
|
|
cat <<EOF >> "${R_INSTALL_PATH}"/lib/R/library/base/R/Rprofile
|
|
## Set the default HTTP user agent
|
|
local({
|
|
os_identifier <- if (file.exists("/etc/os-release")) {
|
|
os <- readLines("/etc/os-release")
|
|
id <- gsub('^ID=|"', "", grep("^ID=", os, value = TRUE))
|
|
version <- gsub('^VERSION_ID=|"', "", grep("^VERSION_ID=", os, value = TRUE))
|
|
sprintf("%s-%s", id, version)
|
|
} else {
|
|
"${OS_IDENTIFIER}"
|
|
}
|
|
options(HTTPUserAgent = sprintf("R/%s (%s) R (%s)", getRversion(), os_identifier,
|
|
paste(getRversion(), R.version\$platform, R.version\$arch, R.version\$os)))
|
|
})
|
|
EOF
|
|
}
|
|
|
|
# verify_capabilities()
|
|
# Guard against a graphics device being silently compiled out. R's configure
|
|
# only enables the cairo/png/jpeg/tiff devices when the matching headers were
|
|
# present at compile time, and it does not fail when they are missing, so a
|
|
# missing -dev package produces a working R that simply cannot draw. Once that
|
|
# tarball is published, installing the runtime libraries on the target system
|
|
# cannot bring the devices back; the build has to be redone.
|
|
#
|
|
# Dockerfiles opt in by setting REQUIRED_CAPABILITIES to a comma-separated list
|
|
# of capabilities() names, e.g. "cairo,png,jpeg,tiff".
|
|
verify_capabilities() {
|
|
if [ -z "${REQUIRED_CAPABILITIES}" ]; then
|
|
return 0
|
|
fi
|
|
|
|
echo "Verifying required R capabilities: ${REQUIRED_CAPABILITIES}"
|
|
|
|
# The full capabilities() table goes to stderr so it shows up in the build
|
|
# log; stdout carries only the space-separated list of missing entries.
|
|
local missing
|
|
missing=$("${R_INSTALL_PATH}/bin/Rscript" --vanilla -e '
|
|
required <- trimws(strsplit(Sys.getenv("REQUIRED_CAPABILITIES"), ",")[[1]])
|
|
required <- required[nzchar(required)]
|
|
caps <- capabilities()
|
|
writeLines(capture.output(print(caps)), con = stderr())
|
|
ok <- (required %in% names(caps)) & (unname(caps[required]) %in% TRUE)
|
|
cat(paste(required[!ok], collapse = " "), "\n", sep = "")
|
|
') || {
|
|
echo "ERROR: could not query capabilities() from the freshly built R" >&2
|
|
return 1
|
|
}
|
|
|
|
if [ -n "${missing}" ]; then
|
|
echo "ERROR: R ${R_VERSION} for ${OS_IDENTIFIER} is missing required capabilities: ${missing}" >&2
|
|
echo "ERROR: install the matching -dev packages in builder/Dockerfile.${OS_IDENTIFIER} and rebuild." >&2
|
|
return 1
|
|
fi
|
|
|
|
echo "All required capabilities are present."
|
|
}
|
|
|
|
# check for packager script
|
|
## If it exists this build is ready for packaging with nFPM, so run the script
|
|
## else do nothing
|
|
package_r() {
|
|
if [[ -f /package.sh ]]; then
|
|
export R_VERSION=${1}
|
|
source /package.sh
|
|
fi
|
|
}
|
|
|
|
set_up_environment() {
|
|
mkdir -p "$R_INSTALL_PATH"
|
|
}
|
|
|
|
_version_is_greater_than() {
|
|
test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
|
|
}
|
|
|
|
_version_is_less_than() {
|
|
test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$2"
|
|
}
|
|
|
|
###### RUN R COMPILE PROCEDURE ######
|
|
set_up_environment
|
|
fetch_r_source $R_VERSION
|
|
patch_r $R_VERSION
|
|
compile_r $R_VERSION
|
|
verify_capabilities
|
|
package_r $R_VERSION
|
|
archive_r
|
|
upload_r $R_VERSION
|