Upstream's buildModuleBundlesLocally.R sapply()s over the modules, so the first failure silently abandons every module after it. The resumed build stopped at 26/36 this way: jaspPower failed on V8, and the nine modules behind it were never attempted in any of the four passes. build/build-modules.R compiles each module under tryCatch, reports what failed and carries on, and also skips reinstalling devtools and jaspModuleTools on every call. V8 needs libv8, which Arch does not package; DOWNLOAD_STATIC_LIBV8=1 makes its configure download upstream's static build. Claude-Session: https://claude.ai/code/session_01GGe7kUhooMFuzY6GaDb2w9
578 lines
24 KiB
Bash
Executable File
578 lines
24 KiB
Bash
Executable File
#!/bin/bash
|
|
# Build JASP Desktop on Arch Linux and emit a relocatable binary tarball.
|
|
#
|
|
# Runs as root inside an `archlinux:base-devel` container. The resulting tarball
|
|
# is what the AUR `jasp-desktop-bin` package downloads and unpacks.
|
|
#
|
|
# JASP_TAG upstream tag to build, e.g. v0.98.1 (required)
|
|
# OUT_DIR where the tarball + metadata land (default /work/out)
|
|
# JOBS compile parallelism (default nproc)
|
|
# R_VERSION R to build and bundle (default 4.5.3)
|
|
# GITHUB_PAT token for R package downloads from GitHub (optional but
|
|
# strongly recommended — several JASP modules are installed from
|
|
# GitHub and anonymous API rate limits will fail the build)
|
|
#
|
|
# Why we build our own R instead of using Arch's:
|
|
#
|
|
# JASP pins its R package set with renv lockfiles against a frozen CRAN snapshot
|
|
# (Modules/Rcpp_RInside.lock points at the 2026-03-31 Posit snapshot for 0.98.1).
|
|
# That set targets R 4.5. Building against Arch's R 4.6 fails immediately —
|
|
# Rcpp 1.1.1 does not compile there (R_NamespaceRegistry is gone in R 4.6), and
|
|
# the pinned snapshot predates the Rcpp release that fixes it. Upstream's own CI
|
|
# installs a specific R and points R_HOME at it, which is what we do here.
|
|
#
|
|
# Bundling R also means the package does not depend on Arch's `r` at all, so an
|
|
# Arch R bump can no longer break an installed JASP.
|
|
|
|
set -euo pipefail
|
|
|
|
JASP_TAG="${JASP_TAG:?JASP_TAG is required, e.g. v0.98.1}"
|
|
OUT_DIR="${OUT_DIR:-/work/out}"
|
|
JOBS="${JOBS:-$(nproc)}"
|
|
R_VERSION="${R_VERSION:-4.5.3}"
|
|
|
|
SRC_DIR=/build/jasp-desktop
|
|
BUILD_DIR=/build/build
|
|
STAGE_DIR=/build/stage
|
|
PREFIX=/opt/jasp-desktop
|
|
R_HOME_DIR="${PREFIX}/R"
|
|
|
|
mkdir -p "$OUT_DIR" /build
|
|
|
|
log() { echo ":: $* "; }
|
|
|
|
# ------------------------------------------------------------- build caches
|
|
# /cache is a directory on the runner host that outlives the container. Without
|
|
# it every build recompiles R, JASP and 1000+ R packages from nothing, which is
|
|
# days of CPU on the runner; with it a rebuild only compiles what changed.
|
|
# /cache/ccache compiler cache for R and JASP's own C++
|
|
# /cache/renv-cache compiled R packages, shared by every module
|
|
# /cache/renv-root renv's download cache (source tarballs)
|
|
CACHE_DIR="${CACHE_DIR:-/cache}"
|
|
mkdir -p "$CACHE_DIR"/{ccache,renv-cache,renv-root}
|
|
export CCACHE_DIR="$CACHE_DIR/ccache"
|
|
export CCACHE_MAXSIZE="${CCACHE_MAXSIZE:-15G}"
|
|
# renv files its cache under the OS release, and the archlinux image stamps its
|
|
# build date there (linux-arch-20260809.0.570793), so every newer image would
|
|
# start from an empty cache. A rolling distro has no release to key on; staleness
|
|
# is handled by purge_stale_renv_cache below instead.
|
|
export RENV_PATHS_PREFIX=linux-arch
|
|
|
|
# ---------------------------------------------------------------- system deps
|
|
log "installing Arch dependencies"
|
|
pacman -Syu --noconfirm --needed \
|
|
base-devel git cmake ninja gcc gcc-fortran make pkgconf patchelf \
|
|
autoconf automake libtool bison flex gettext blas lapack ccache \
|
|
qt6-base qt6-declarative qt6-svg qt6-positioning qt6-webchannel \
|
|
qt6-webengine qt6-5compat qt6-shadertools qt6-websockets qt6-tools \
|
|
qt6-httpserver vulkan-headers \
|
|
boost boost-libs glpk jsoncpp libarchive openssl curl sqlite zlib \
|
|
readstat netcdf mpfr cairo pango fontconfig harfbuzz fribidi icu pcre2 \
|
|
readline bzip2 xz libpng libtiff libjpeg-turbo minizip \
|
|
libxkbcommon libxkbcommon-x11 libxt wget unzip python \
|
|
gsl libsodium gmp nlopt libxml2 fftw
|
|
|
|
# Stock makepkg.conf leaves MAKEFLAGS at -j1, which makes the AUR dependencies
|
|
# (JAGS especially) crawl.
|
|
sed -i "s|^#\?MAKEFLAGS=.*|MAKEFLAGS=\"-j${JOBS}\"|" /etc/makepkg.conf
|
|
export MAKEFLAGS="-j${JOBS}"
|
|
|
|
# makepkg refuses to run as root.
|
|
if ! id builder &>/dev/null; then
|
|
useradd -m builder
|
|
echo "builder ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/builder
|
|
fi
|
|
|
|
# jags and librdata are AUR-only; build them the same way a user would.
|
|
build_aur() {
|
|
local pkg="$1"
|
|
log "building AUR package $pkg"
|
|
sudo -u builder bash -euc "
|
|
cd /home/builder
|
|
rm -rf '$pkg'
|
|
git clone --depth 1 'https://aur.archlinux.org/$pkg.git'
|
|
cd '$pkg'
|
|
makepkg -si --noconfirm --needed
|
|
"
|
|
}
|
|
build_aur jags
|
|
build_aur librdata-git
|
|
|
|
# JASP requires freexl >= 2.0.99; Arch ships 2.0.0, so use upstream's own fork
|
|
# exactly as the JASP CI does.
|
|
log "building jasp-stats/freexl"
|
|
rm -rf /build/freexl
|
|
git clone --depth 1 https://github.com/jasp-stats/freexl.git /build/freexl
|
|
cd /build/freexl
|
|
./configure --prefix=/usr
|
|
make -j"$JOBS"
|
|
make install
|
|
|
|
# ------------------------------------------------------- R that we ship along
|
|
log "building R ${R_VERSION} into ${R_HOME_DIR}"
|
|
rm -rf "/build/R-${R_VERSION}"
|
|
cd /build
|
|
wget -q "https://cran.r-project.org/src/base/R-4/R-${R_VERSION}.tar.gz"
|
|
tar -xzf "R-${R_VERSION}.tar.gz"
|
|
cd "R-${R_VERSION}"
|
|
|
|
# --enable-R-shlib is required: JASP embeds R through RInside/libR.
|
|
# --libdir is pinned so R lands in lib/R rather than the lib64/R that Arch's
|
|
# config.site would otherwise pick, which keeps the rpath below predictable.
|
|
# The install prefix equals the final install path, so the rpath stays valid
|
|
# once the tarball is unpacked on the user's machine.
|
|
#
|
|
# --without-blas/--without-lapack is deliberate and load-bearing. With
|
|
# --with-blas, R links Arch's external BLAS and does NOT build libRblas.so or
|
|
# libRlapack.so. The JASP module bundles expect those files to exist inside
|
|
# R_HOME (that is how a conventionally built R, and the Flatpak, ship), so
|
|
# linking externally leaves the modules unable to resolve them. Reference BLAS
|
|
# is slower than OpenBLAS, but a working install beats a fast broken one.
|
|
./configure \
|
|
--prefix="${R_HOME_DIR}" \
|
|
--libdir="${R_HOME_DIR}/lib" \
|
|
--enable-R-shlib \
|
|
--enable-memory-profiling \
|
|
--without-blas \
|
|
--without-lapack \
|
|
--with-x=no \
|
|
--with-tcltk=no \
|
|
LDFLAGS="-Wl,-rpath,${R_HOME_DIR}/lib/R/lib"
|
|
# ccache through PATH rather than CC="ccache gcc": configure would bake the
|
|
# latter into the shipped Makeconf, and every package a user installs into
|
|
# JASP's R would then need ccache.
|
|
PATH="/usr/lib/ccache/bin:$PATH" make -j"$JOBS"
|
|
make install
|
|
|
|
# Guard the above: a silently external-BLAS build produces a JASP whose modules
|
|
# fail at runtime, which is exactly the class of failure this package exists to
|
|
# avoid. Better to stop here than ship it.
|
|
for lib in libR.so libRblas.so libRlapack.so; do
|
|
if [[ ! -f "${R_HOME_DIR}/lib/R/lib/${lib}" ]]; then
|
|
echo "error: ${lib} missing from the bundled R — R linked an external BLAS?" >&2
|
|
ls -la "${R_HOME_DIR}/lib/R/lib/" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
log "bundled R provides its own libR/libRblas/libRlapack"
|
|
|
|
export PATH="${R_HOME_DIR}/bin:${PATH}"
|
|
# Ask R where it actually put itself rather than assuming lib vs lib64.
|
|
# Deliberately NOT exported: R.cmake notes that a set R_HOME makes R print
|
|
# "WARNING: ignoring environment value of R_HOME", which corrupts the output it
|
|
# parses. CUSTOM_R_PATH is the supported way to point JASP at a specific R.
|
|
JASP_R_HOME="$("${R_HOME_DIR}/bin/R" RHOME)"
|
|
log "bundled R_HOME is ${JASP_R_HOME}"
|
|
"${R_HOME_DIR}/bin/R" --version | sed -n 1p || true
|
|
|
|
# ------------------------------------------------------------------- checkout
|
|
log "cloning jasp-desktop $JASP_TAG"
|
|
rm -rf "$SRC_DIR"
|
|
git clone --depth 1 --branch "$JASP_TAG" --recurse-submodules --shallow-submodules \
|
|
https://github.com/jasp-stats/jasp-desktop.git "$SRC_DIR"
|
|
|
|
cd "$SRC_DIR"
|
|
JASP_COMMIT="$(git rev-parse HEAD)"
|
|
|
|
# ---------------------------------------------------------------------- build
|
|
log "configuring"
|
|
export CMAKE_C_COMPILER_LAUNCHER=ccache
|
|
export CMAKE_CXX_COMPILER_LAUNCHER=ccache
|
|
export CMAKE_Fortran_COMPILER_LAUNCHER=ccache
|
|
export CFLAGS="${CFLAGS:-} -fPIC"
|
|
export CXXFLAGS="${CXXFLAGS:-} -fPIC"
|
|
export FCFLAGS="${FCFLAGS:-} -fPIC"
|
|
export FFLAGS="${FFLAGS:-} -fPIC"
|
|
|
|
rm -rf "$BUILD_DIR"
|
|
cmake -S "$SRC_DIR" -B "$BUILD_DIR" -G Ninja \
|
|
-DCMAKE_BUILD_TYPE=Release \
|
|
-DCMAKE_INSTALL_PREFIX="$PREFIX" \
|
|
-DLINUX_LOCAL_BUILD=OFF \
|
|
-DCUSTOM_R_PATH="${JASP_R_HOME}" \
|
|
-Wno-dev
|
|
|
|
log "compiling JASP (-j$JOBS)"
|
|
ninja -C "$BUILD_DIR" -j"$JOBS"
|
|
|
|
# The R modules are the long pole: several hundred R packages compiled from
|
|
# source. build/build-modules.R reports a failed module and carries on rather
|
|
# than exiting non-zero, so the guards after it are what actually catch a
|
|
# broken module build.
|
|
#
|
|
# GITHUB_PAT is required, not optional: jaspModuleTools is installed with
|
|
# install_github('jasp-stats/jaspModuleTools'), and anonymous GitHub API rate
|
|
# limits make that fail. When it fails, no bundles are produced at all and the
|
|
# build otherwise looks successful.
|
|
if [[ -z "${GITHUB_PAT:-}" ]]; then
|
|
echo "error: GITHUB_PAT is required — jaspModuleTools is installed from GitHub" >&2
|
|
echo " without it the module build silently produces nothing" >&2
|
|
exit 1
|
|
fi
|
|
export GITHUB_PAT
|
|
export GITHUB_TOKEN="${GITHUB_PAT}"
|
|
|
|
log "building R modules — this is the slow part"
|
|
mkdir -p "$BUILD_DIR"/Modules/{binary_pkgs,manifests,module_libs,Tools}
|
|
cd "$SRC_DIR/Tools"
|
|
|
|
# jaspModuleTools is installed from GitHub with `remotes`, which devtools >= 2.5
|
|
# no longer pulls in ("The package \"remotes\" is required."). Pre-seed it into
|
|
# the library build-modules.R uses, since that does .libPaths(new=userLibrary)
|
|
# and would not see remotes anywhere else.
|
|
MODULE_LIBRARY="$SRC_DIR/Tools/ModuleBundleBuildDir/LibraryR"
|
|
MODULE_BUNDLES="$SRC_DIR/Modules/local"
|
|
|
|
log "pre-installing remotes into the module build library"
|
|
mkdir -p "$MODULE_LIBRARY"
|
|
Rscript -e "
|
|
lib <- '${MODULE_LIBRARY}'
|
|
.libPaths(new = lib)
|
|
install.packages('remotes', lib = lib, repos = 'https://cran.r-project.org')
|
|
if (!requireNamespace('remotes', lib.loc = lib, quietly = TRUE))
|
|
stop('failed to install remotes')
|
|
"
|
|
|
|
# Some of the versions JASP pins in its module renv.lock files predate the
|
|
# toolchain on a current Arch and cannot compile. Bump only those, by name, with
|
|
# the reason recorded — never a blanket "use latest", which would silently
|
|
# change the analysis code users get.
|
|
#
|
|
# later 1.4.6 bundles a tinycthread whose `#define once_flag pthread_once_t`
|
|
# collides with the C11 once_flag that glibc >= 2.43 now declares under
|
|
# C23, so it fails with "conflicting types for pthread_once_t".
|
|
# Fixed upstream in later 1.4.7 (r-lib/later#257). The 2026-04-22 RSPM
|
|
# snapshot the lockfiles already reference carries 1.4.8.
|
|
log "patching module lockfile pins that cannot build on this toolchain"
|
|
python3 - <<'PY'
|
|
import json, pathlib
|
|
|
|
OVERRIDES = {"later": "1.4.8"}
|
|
|
|
root = pathlib.Path("ModuleBundleBuildDir/Modules")
|
|
patched = 0
|
|
for lock in root.glob("*/renv.lock"):
|
|
data = json.loads(lock.read_text())
|
|
packages = data.get("Packages", {})
|
|
changed = False
|
|
for name, want in OVERRIDES.items():
|
|
pkg = packages.get(name)
|
|
if pkg and pkg.get("Version") != want:
|
|
print(f" {lock.parent.name}: {name} {pkg['Version']} -> {want}")
|
|
pkg["Version"] = want
|
|
# The recorded hash no longer matches the bumped version.
|
|
pkg.pop("Hash", None)
|
|
changed = True
|
|
if changed:
|
|
lock.write_text(json.dumps(data, indent=2) + "\n")
|
|
patched += 1
|
|
print(f"patched {patched} lockfiles")
|
|
PY
|
|
|
|
# renv restores packages concurrently and its ordering is not always
|
|
# dependency-correct: a module can be lazy-loaded while a package it needs is
|
|
# still being built, e.g. "forecast" failing with "there is no package called
|
|
# 'vctrs'" six log lines before vctrs finishes compiling. That aborts the whole
|
|
# sapply() loop, so a single race costs every remaining module.
|
|
#
|
|
# Retrying is the cheap fix: renv skips what is already installed, so each pass
|
|
# starts from more complete state and converges. Passes are capped so a genuine
|
|
# failure cannot loop forever.
|
|
# OpenMx instantiates enough Eigen templates that a single cc1plus can take
|
|
# several GB. At the top-level -j the compiler gets OOM-killed
|
|
# ("g++: fatal error: Killed signal terminated program cc1plus"), which is not
|
|
# a race — it stalls at the same module count on every retry. Compile R packages
|
|
# with less parallelism than the C++ build uses.
|
|
R_JOBS="${R_JOBS:-2}"
|
|
export MAKEFLAGS="-j${R_JOBS}"
|
|
|
|
# R's default flags are -g -O2. Debug info in the Eigen/Stan-heavy packages
|
|
# (rstanarm, OpenMx) costs compile time and GBs of cc1plus memory, and nobody
|
|
# debugs them. User Makevars only affects this build, not the shipped Makeconf.
|
|
mkdir -p ~/.R
|
|
cat >~/.R/Makevars <<'EOF'
|
|
CFLAGS=-O2 -pipe
|
|
CXXFLAGS=-O2 -pipe
|
|
CXX11FLAGS=-O2 -pipe
|
|
CXX14FLAGS=-O2 -pipe
|
|
CXX17FLAGS=-O2 -pipe
|
|
CXX20FLAGS=-O2 -pipe
|
|
FFLAGS=-O2 -pipe
|
|
FCFLAGS=-O2 -pipe
|
|
EOF
|
|
|
|
# jaspModuleTools::compile() hard-codes RENV_PATHS_CACHE and RENV_PATHS_ROOT to
|
|
# directories under its workdir, so the persistent cache is linked in there.
|
|
MODULE_WORKDIR="$SRC_DIR/Tools/ModuleBundleBuildDir/build_dir"
|
|
mkdir -p "$MODULE_WORKDIR"
|
|
ln -sfn "$CACHE_DIR/renv-cache" "$MODULE_WORKDIR/renv-cache"
|
|
ln -sfn "$CACHE_DIR/renv-root" "$MODULE_WORKDIR/renv-root"
|
|
|
|
# A cached package is only reusable while the system libraries it linked
|
|
# against still exist. Arch rolls (an ICU or gsl soname bump), and renv's cache
|
|
# key knows nothing about that, so drop every entry with an unresolved library
|
|
# and let renv rebuild just those. Layout: .../<pkg>/<ver>/<hash>/<pkg>/libs/*.so
|
|
purge_stale_renv_cache() {
|
|
local so entry purged=0
|
|
while IFS= read -r so; do
|
|
if LD_LIBRARY_PATH="${JASP_R_HOME}/lib" ldd "$so" 2>/dev/null | grep -q 'not found'; then
|
|
entry="$(dirname "$(dirname "$(dirname "$so")")")"
|
|
[[ -d "$entry" ]] || continue
|
|
rm -rf "$entry"
|
|
purged=$((purged + 1))
|
|
fi
|
|
done < <(find "$CACHE_DIR/renv-cache/" -path '*/libs/*.so' 2>/dev/null)
|
|
log "renv cache: purged ${purged} entries with missing system libraries"
|
|
}
|
|
purge_stale_renv_cache
|
|
|
|
# The R package V8 (jaspPower, via gt and juicyjuice) needs libv8, which Arch
|
|
# does not package; this makes its configure fetch upstream's static build.
|
|
export DOWNLOAD_STATIC_LIBV8=1
|
|
|
|
# ccache for R package compiles too: cheap insurance for when renv reinstalls a
|
|
# package whose compiler inputs did not actually change.
|
|
export PATH="/usr/lib/ccache/bin:$PATH"
|
|
# R leaves 00LOCK-<pkg> directories behind when an install is interrupted, and
|
|
# refuses to touch the library afterwards ("failed to lock directory ... for
|
|
# modifying"). A build that is killed mid-install - a timeout, an OOM, a stopped
|
|
# container - therefore poisons the library for every later run.
|
|
log "clearing stale R install locks"
|
|
rm -rf "${MODULE_LIBRARY}"/00LOCK-* 2>/dev/null || true
|
|
|
|
# The interrupted install can also leave the library without devtools, which
|
|
# the jaspModuleTools install relies on. Reinstall whatever is
|
|
# missing rather than assuming a clean slate.
|
|
Rscript -e "
|
|
lib <- '${MODULE_LIBRARY}'
|
|
dir.create(lib, recursive = TRUE, showWarnings = FALSE)
|
|
.libPaths(new = lib)
|
|
for (pkg in c('remotes', 'devtools')) {
|
|
if (!requireNamespace(pkg, lib.loc = lib, quietly = TRUE)) {
|
|
message('installing missing ', pkg)
|
|
install.packages(pkg, lib = lib, repos = 'https://cran.r-project.org')
|
|
}
|
|
if (!requireNamespace(pkg, lib.loc = lib, quietly = TRUE))
|
|
stop('failed to install ', pkg)
|
|
}
|
|
"
|
|
|
|
log "compiling R packages with -j${R_JOBS} (OpenMx needs the headroom)"
|
|
|
|
# Build only the modules JASP actually ships. Modules/modules-settings.json
|
|
# splits them into "common" (bundled with the application) and "extra"
|
|
# (fetched on demand at runtime, into ~/.local/share/JASP).
|
|
#
|
|
# buildAllDefaultJaspModules.R is a *developer* helper that builds 22 modules
|
|
# regardless of that split — its name says "all default modules", not "what is
|
|
# released". Building from it produces a ~9 GB tree, most of it modules the user
|
|
# would have downloaded on demand anyway, and drags in system dependencies that
|
|
# only the extras need (gsl for jaspNetwork, OpenMx for jaspSem).
|
|
# Build every module JASP ships a definition for, not just the "common" seven.
|
|
#
|
|
# The extras are normally downloaded at runtime — but those bundles are built
|
|
# against the Flatpak runtime, and on Arch they fail on sonames that do not
|
|
# exist here (the first one hit was libjpeg.so.62, which Arch replaced with
|
|
# libjpeg.so.8 long ago). A downloaded module is therefore never reliable in
|
|
# this package; anything the user might enable has to be built against the same
|
|
# Arch userland as the application itself.
|
|
#
|
|
# common modules are mandatory: without them the package is not JASP.
|
|
# extras are best-effort: a few need dependencies that are not packaged for
|
|
# Arch at all (jaspProcess wants V8), and losing one of those is better than
|
|
# losing the build.
|
|
mapfile -t COMMON_MODULES < <(
|
|
python3 -c "
|
|
import json
|
|
print('\n'.join(json.load(open('../Modules/modules-settings.json'))['common']))
|
|
"
|
|
)
|
|
mapfile -t EXTRA_MODULES < <(
|
|
python3 -c "
|
|
import json
|
|
s = json.load(open('../Modules/modules-settings.json'))
|
|
print('\n'.join(m for m in s['extra'] if m != 'jaspTestModule'))
|
|
"
|
|
)
|
|
ALL_MODULES=("${COMMON_MODULES[@]}" "${EXTRA_MODULES[@]}")
|
|
log "building ${#ALL_MODULES[@]} modules (${#COMMON_MODULES[@]} common + ${#EXTRA_MODULES[@]} extra)"
|
|
|
|
mkdir -p ModuleBundleBuildDir/Modules
|
|
for module in "${ALL_MODULES[@]}"; do
|
|
if [[ ! -d "ModuleBundleBuildDir/Modules/${module}" ]]; then
|
|
git clone --depth 1 "https://github.com/jasp-stats/${module}" \
|
|
"ModuleBundleBuildDir/Modules/${module}" 2>/dev/null || log "no repo for ${module}, skipping"
|
|
fi
|
|
done
|
|
|
|
for attempt in 1 2 3; do
|
|
# Hand over only modules without a bundle yet: the script rebuilds everything
|
|
# it is given, so a retry would otherwise redo every finished module.
|
|
MODULE_PATHS=()
|
|
for module in "${ALL_MODULES[@]}"; do
|
|
[[ -f "${MODULE_BUNDLES}/${module}.JASPModule" ]] && continue
|
|
[[ -d "ModuleBundleBuildDir/Modules/${module}" ]] && \
|
|
MODULE_PATHS+=("ModuleBundleBuildDir/Modules/${module}")
|
|
done
|
|
[[ ${#MODULE_PATHS[@]} -eq 0 ]] && break
|
|
log "module build pass ${attempt}/3: ${#MODULE_PATHS[@]} modules left"
|
|
Rscript /work/build/build-modules.R "${MODULE_PATHS[@]}" || true
|
|
built=0
|
|
for module in "${ALL_MODULES[@]}"; do
|
|
[[ -f "${MODULE_BUNDLES}/${module}.JASPModule" ]] && built=$((built + 1))
|
|
done
|
|
log "pass ${attempt} finished: ${built}/${#ALL_MODULES[@]} modules"
|
|
[[ "$built" -ge "${#ALL_MODULES[@]}" ]] && break
|
|
done
|
|
|
|
if [[ ! -d "$MODULE_LIBRARY/jaspModuleTools" ]]; then
|
|
echo "error: jaspModuleTools was not installed — install_github failed" >&2
|
|
echo " check the install_github output above: a missing 'remotes'," >&2
|
|
echo " a GitHub rate limit, or a PAT without read access all do this" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Guard on the expected count, not merely "non-empty". An earlier run shipped a
|
|
# package containing 2 of 22 modules because a non-empty check passed.
|
|
missing_common=()
|
|
for module in "${COMMON_MODULES[@]}"; do
|
|
[[ -f "${MODULE_BUNDLES}/${module}.JASPModule" ]] || missing_common+=("$module")
|
|
done
|
|
if [[ ${#missing_common[@]} -gt 0 ]]; then
|
|
echo "error: mandatory common modules were not built" >&2
|
|
printf ' missing: %s\n' "${missing_common[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
missing_extra=()
|
|
for module in "${EXTRA_MODULES[@]}"; do
|
|
[[ -f "${MODULE_BUNDLES}/${module}.JASPModule" ]] || missing_extra+=("$module")
|
|
done
|
|
built_total=$(find "$MODULE_BUNDLES" -maxdepth 1 -name '*.JASPModule' | wc -l)
|
|
log "built ${built_total} modules; all ${#COMMON_MODULES[@]} common present"
|
|
if [[ ${#missing_extra[@]} -gt 0 ]]; then
|
|
log "extras that did not build (users will fall back to downloading these):"
|
|
printf ' %s\n' "${missing_extra[@]}"
|
|
fi
|
|
|
|
# -------------------------------------------------------------------- staging
|
|
log "staging install tree"
|
|
rm -rf "$STAGE_DIR"
|
|
mkdir -p "$STAGE_DIR"
|
|
DESTDIR="$STAGE_DIR" ninja -C "$BUILD_DIR" install
|
|
|
|
# The bundled R was installed straight to $PREFIX/R on the build host; copy it
|
|
# into the staged tree so it ends up in the tarball.
|
|
install -d "$STAGE_DIR$PREFIX"
|
|
cp -a "$R_HOME_DIR" "$STAGE_DIR$PREFIX/"
|
|
|
|
# Everything the module build produced has to travel with the tarball, or JASP
|
|
# falls back to downloading bundles at runtime — which is the failure mode this
|
|
# package is meant to remove.
|
|
#
|
|
# Modules/local the compiled module bundles themselves
|
|
#
|
|
# ModuleBundleBuildDir/LibraryR is deliberately NOT shipped: it is the *build*
|
|
# toolchain (devtools, roxygen2, testthat, pkgdown, rcmdcheck, jaspModuleTools),
|
|
# not anything JASP loads at runtime. Each .JASPModule already carries its own
|
|
# dependency closure — jaspTTests alone unpacks to 139 content-addressed package
|
|
# directories — so the library would be 283 MB of pure duplication.
|
|
#
|
|
# ModuleBundleBuildDir/Modules is deliberately NOT copied: it is the cloned
|
|
# module *sources* (no compiled objects at all), several hundred MB of git
|
|
# checkouts that would bloat the tarball without being usable at runtime.
|
|
install -d "$STAGE_DIR$PREFIX/Modules"
|
|
|
|
# Deliberately do NOT copy Modules/local/*.JASPModule here. `ninja install`
|
|
# has already placed the same packages in Modules/binary_pkgs as a
|
|
# deduplicated, content-addressed store — 677 package directories, in which
|
|
# Rcpp, ggplot2 and the rest of the shared base appear once rather than once
|
|
# per module. The .JASPModule files are the *distribution* form used for
|
|
# on-demand downloads; shipping them alongside the installed form sends the
|
|
# same content twice and took the tarball from 1.86 GB to 4.3 GB.
|
|
|
|
|
|
install -Dm644 /work/packaging/org.jaspstats.JASP.desktop \
|
|
"$STAGE_DIR/usr/share/applications/org.jaspstats.JASP.desktop"
|
|
|
|
if [[ -d "$STAGE_DIR$PREFIX/share/icons/hicolor" ]]; then
|
|
install -d "$STAGE_DIR/usr/share/icons/hicolor"
|
|
cp -a "$STAGE_DIR$PREFIX/share/icons/hicolor/." "$STAGE_DIR/usr/share/icons/hicolor/"
|
|
fi
|
|
|
|
install -d "$STAGE_DIR/usr/share/licenses/jasp-desktop-bin"
|
|
cd "$SRC_DIR"
|
|
find . -maxdepth 2 \( -name 'COPYING*' -o -name 'LICENSE*' \) -type f \
|
|
-exec install -m644 {} "$STAGE_DIR/usr/share/licenses/jasp-desktop-bin/" \;
|
|
|
|
find "$STAGE_DIR" -name '*.o' -delete 2>/dev/null || true
|
|
|
|
# Make the tree independent of Arch's rolling libraries (ICU, jsoncpp, glpk,
|
|
# JAGS, readstat, librdata ...). Qt6 stays a system dependency; BLAS/LAPACK now
|
|
# come from the bundled R itself.
|
|
log "bundling non-system shared libraries"
|
|
bash /work/build/bundle-libs.sh "$STAGE_DIR" "$PREFIX"
|
|
|
|
# Final sanity check on what is about to be shipped. Each of these has already
|
|
# been shipped missing once.
|
|
log "verifying the staged tree"
|
|
staged_r="$STAGE_DIR$PREFIX/R/lib/R/lib"
|
|
for lib in libR.so libRblas.so libRlapack.so; do
|
|
[[ -f "$staged_r/$lib" ]] || { echo "error: staged tree lacks $lib" >&2; exit 1; }
|
|
done
|
|
# Modules ship installed (module_libs/<module> over the binary_pkgs store), not
|
|
# as .JASPModule bundles, so check that every module that built got installed.
|
|
unstaged=()
|
|
for bundle in "$MODULE_BUNDLES"/*.JASPModule; do
|
|
module="$(basename "$bundle" .JASPModule)"
|
|
[[ -d "$STAGE_DIR$PREFIX/Modules/module_libs/$module" ]] || unstaged+=("$module")
|
|
done
|
|
if [[ ${#unstaged[@]} -gt 0 ]]; then
|
|
echo "error: built modules missing from the staged module_libs: ${unstaged[*]}" >&2
|
|
exit 1
|
|
fi
|
|
staged_modules="$(find "$STAGE_DIR$PREFIX/Modules/module_libs" -mindepth 1 -maxdepth 1 -type d | wc -l)"
|
|
staged_so="$(find "$STAGE_DIR$PREFIX" -name '*.so' 2>/dev/null | wc -l)"
|
|
log "staged: ${staged_modules} modules, ${staged_so} shared objects"
|
|
du -sh "$STAGE_DIR$PREFIX"/* 2>/dev/null | sort -rh | head -8
|
|
|
|
# ------------------------------------------------------------------- metadata
|
|
pkgver_of() { pacman -Q "$1" 2>/dev/null | awk '{print $2}'; }
|
|
|
|
QT_FULL="$(pkgver_of qt6-base)"
|
|
QT_MINOR="$(echo "$QT_FULL" | grep -oE '^[0-9]+\.[0-9]+')"
|
|
QT_MAJOR="$(echo "$QT_FULL" | grep -oE '^[0-9]+')"
|
|
ICU_FULL="$(pkgver_of icu)"
|
|
ICU_MAJOR="$(echo "$ICU_FULL" | grep -oE '^[0-9]+')"
|
|
BOOST_FULL="$(pkgver_of boost-libs)"
|
|
|
|
JASP_VER="${JASP_TAG#v}"
|
|
TARBALL="jasp-desktop-${JASP_VER}-x86_64.tar.zst"
|
|
|
|
log "creating $TARBALL"
|
|
cd "$STAGE_DIR"
|
|
tar --zstd -cf "$OUT_DIR/$TARBALL" .
|
|
SHA256="$(sha256sum "$OUT_DIR/$TARBALL" | awk '{print $1}')"
|
|
SIZE="$(stat -c%s "$OUT_DIR/$TARBALL")"
|
|
|
|
cat >"$OUT_DIR/build-info.json" <<EOF
|
|
{
|
|
"jasp_version": "${JASP_VER}",
|
|
"jasp_tag": "${JASP_TAG}",
|
|
"jasp_commit": "${JASP_COMMIT}",
|
|
"tarball": "${TARBALL}",
|
|
"sha256": "${SHA256}",
|
|
"size_bytes": ${SIZE},
|
|
"bundled_r_version": "${R_VERSION}",
|
|
"qt6_version": "${QT_FULL}",
|
|
"qt6_minor": "${QT_MINOR}",
|
|
"qt6_major": "${QT_MAJOR}",
|
|
"icu_version": "${ICU_FULL}",
|
|
"icu_major": "${ICU_MAJOR}",
|
|
"boost_version": "${BOOST_FULL}"
|
|
}
|
|
EOF
|
|
|
|
log "done"
|
|
cat "$OUT_DIR/build-info.json"
|