#!/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. buildAllDefaultJaspModules.R shells out to buildModuleBundlesLocally.R # via system2(), which does NOT check exit status — so every failure below is # silent and the script still exits 0. The guards after it are what actually # catch a broken module build. # # GITHUB_PAT is required, not optional: buildModuleBundlesLocally.R does # 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" # buildModuleBundlesLocally.R calls devtools::install_github(), which since # devtools 2.5.0 is deprecated and delegates to the `remotes` package — but # remotes is no longer a hard dependency of devtools, so the call dies with # "The package \"remotes\" is required." Pre-seed it into the same library the # script uses, since that script 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: .../////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 # 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- 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 # buildModuleBundlesLocally.R calls unconditionally. 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" ./buildModuleBundlesLocally.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 staged_bundles="$(find "$STAGE_DIR$PREFIX/Modules" -maxdepth 1 -name '*.JASPModule' 2>/dev/null | wc -l)" [[ "$staged_bundles" -gt 0 ]] || { echo "error: staged tree has no module bundles" >&2; exit 1; } staged_so="$(find "$STAGE_DIR$PREFIX" -name '*.so' 2>/dev/null | wc -l)" log "staged: ${staged_bundles} module bundles, ${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" <