Automated Arch binary builds of JASP for the AUR
Upstream JASP ships no Linux binary, so the existing AUR package builds everything from source including several hundred R packages. This builds that once in CI and publishes the result as jasp-desktop-bin. JASP links against the system R and Qt6 rather than bundling them, so a rebuild is triggered by an Arch r/qt6-base bump as well as by a new JASP release, and the generated PKGBUILD pins depends to the versions actually built against.
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
name: Build JASP binary and publish to AUR
|
||||
|
||||
# JASP publishes no Linux binary of its own, so this builds one on Arch and
|
||||
# ships it as jasp-desktop-bin. Because JASP links against the system R and Qt6,
|
||||
# a rebuild is needed both when JASP releases and when Arch bumps R or Qt6 —
|
||||
# check-upstream.py decides which.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
jasp_tag:
|
||||
description: 'Specific JASP tag to build (blank = latest release)'
|
||||
required: false
|
||||
default: ''
|
||||
force:
|
||||
description: 'Build even if nothing changed'
|
||||
type: boolean
|
||||
default: false
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/build.yml'
|
||||
- 'build/**'
|
||||
- 'packaging/**'
|
||||
- 'scripts/**'
|
||||
|
||||
concurrency:
|
||||
group: jasp-build
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
GH_OWNER: ${{ vars.GH_OWNER }}
|
||||
GH_REPO: ${{ vars.GH_REPO }}
|
||||
AUR_MAINTAINER: ${{ vars.AUR_MAINTAINER }}
|
||||
WORK_DIR: /srv/jasp-build
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
needs_build: ${{ steps.decide.outputs.needs_build }}
|
||||
jasp_tag: ${{ steps.decide.outputs.jasp_tag }}
|
||||
reasons: ${{ steps.decide.outputs.reasons }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Decide whether a rebuild is needed
|
||||
id: decide
|
||||
env:
|
||||
JASP_TAG: ${{ inputs.jasp_tag }}
|
||||
FORCE_BUILD: ${{ inputs.force }}
|
||||
run: python3 scripts/check-upstream.py
|
||||
|
||||
build:
|
||||
needs: check
|
||||
if: needs.check.outputs.needs_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
# The R module tree is several hundred packages compiled from source; the
|
||||
# full build runs for hours. The runner and Gitea job timeouts must allow
|
||||
# for this too (see README).
|
||||
timeout-minutes: 900
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Report plan
|
||||
run: |
|
||||
echo "Building JASP ${{ needs.check.outputs.jasp_tag }}"
|
||||
echo "Reasons: ${{ needs.check.outputs.reasons }}"
|
||||
|
||||
- name: Prepare work directory
|
||||
run: |
|
||||
sudo rm -rf "${WORK_DIR}"
|
||||
sudo mkdir -p "${WORK_DIR}/out"
|
||||
sudo cp -a build packaging scripts "${WORK_DIR}/"
|
||||
df -h /
|
||||
|
||||
- name: Build JASP in an Arch container
|
||||
run: |
|
||||
docker rm -f jasp-build 2>/dev/null || true
|
||||
docker run --rm --name jasp-build \
|
||||
-v "${WORK_DIR}:/work" \
|
||||
-e JASP_TAG="${{ needs.check.outputs.jasp_tag }}" \
|
||||
-e OUT_DIR=/work/out \
|
||||
-e JOBS=5 \
|
||||
archlinux:base-devel \
|
||||
bash /work/build/build-jasp.sh
|
||||
|
||||
- name: Show build result
|
||||
run: |
|
||||
cat "${WORK_DIR}/out/build-info.json"
|
||||
ls -lh "${WORK_DIR}/out/"
|
||||
|
||||
- name: Generate PKGBUILD
|
||||
run: |
|
||||
python3 scripts/gen-pkgbuild.py \
|
||||
--build-info "${WORK_DIR}/out/build-info.json" \
|
||||
--template packaging/PKGBUILD.in \
|
||||
--out "${WORK_DIR}/out/PKGBUILD" \
|
||||
--gh-owner "${GH_OWNER}" \
|
||||
--gh-repo "${GH_REPO}" \
|
||||
--maintainer "${AUR_MAINTAINER}"
|
||||
cat "${WORK_DIR}/out/PKGBUILD"
|
||||
|
||||
- name: Publish tarball to GitHub Releases
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
TAG="$(python3 -c 'import json;print(json.load(open("release-meta.json"))["tag"])')"
|
||||
ASSET="$(python3 -c 'import json;print(json.load(open("release-meta.json"))["tarball"])')"
|
||||
RELEASE_NOTES="Automated build of JASP ${{ needs.check.outputs.jasp_tag }} for Arch Linux.
|
||||
Consumed by the AUR package jasp-desktop-bin.
|
||||
Trigger: ${{ needs.check.outputs.reasons }}" \
|
||||
TAG="$TAG" ASSET="${WORK_DIR}/out/${ASSET}" \
|
||||
bash scripts/publish-github-release.sh
|
||||
|
||||
- name: Push to AUR
|
||||
env:
|
||||
AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }}
|
||||
run: bash scripts/push-aur.sh "${WORK_DIR}/out/PKGBUILD"
|
||||
|
||||
- name: Record built state
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: bash scripts/record-state.sh
|
||||
|
||||
- name: Clean up build tree
|
||||
if: always()
|
||||
run: |
|
||||
sudo rm -rf "${WORK_DIR}/jasp-desktop" "${WORK_DIR}/build" "${WORK_DIR}/stage" || true
|
||||
df -h /
|
||||
@@ -0,0 +1,3 @@
|
||||
release-meta.json
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -0,0 +1,109 @@
|
||||
# jasp-desktop-bin
|
||||
|
||||
Automated binary builds of [JASP](https://jasp-stats.org/) for Arch Linux, published
|
||||
to the AUR as `jasp-desktop-bin`.
|
||||
|
||||
## Why this exists
|
||||
|
||||
Upstream JASP ships installers for Windows and macOS only — there is **no official
|
||||
Linux binary**. The official Linux channel is the Flathub build. The existing AUR
|
||||
package `jasp-desktop` therefore builds everything from source, including
|
||||
`buildAllDefaultJaspModules.R`, which compiles several hundred R packages. That is
|
||||
the multi-hour build this repo exists to avoid: the compile happens once, here, and
|
||||
users download the result.
|
||||
|
||||
## The constraint that shapes everything
|
||||
|
||||
JASP does **not** bundle R or Qt6 on Linux. `Tools/CMake/R.cmake` only fetches
|
||||
`R.framework` on macOS; on Linux JASP links against the distribution's R and Qt6,
|
||||
and the R module library it ships is compiled against one specific R minor version.
|
||||
|
||||
A binary is therefore only valid for the R minor and Qt6 minor it was built
|
||||
against. Two consequences:
|
||||
|
||||
1. A rebuild is triggered by a new JASP release **or** by Arch bumping `r` or
|
||||
`qt6-base`. `scripts/check-upstream.py` checks all three.
|
||||
2. The generated PKGBUILD pins `depends` to those versions
|
||||
(`r>=4.6 r<4.7`, `qt6-base>=6.11 qt6-base<6.12`). If Arch moves ahead before a
|
||||
rebuild lands, pacman refuses the combination rather than leaving users with a
|
||||
JASP that fails to start.
|
||||
|
||||
`jags` and `librdata-git` are AUR-only, and JASP needs `freexl >= 2.0.99` while Arch
|
||||
ships 2.0.0 — so the build compiles those itself, matching what upstream CI does.
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | Purpose |
|
||||
| --- | --- |
|
||||
| `build/build-jasp.sh` | The actual build. Runs in an `archlinux:base-devel` container, emits the tarball plus `build-info.json`. |
|
||||
| `scripts/check-upstream.py` | Compares upstream JASP / Arch R / Arch Qt6 against `state.json`; decides whether to build. |
|
||||
| `scripts/gen-pkgbuild.py` | Fills `packaging/PKGBUILD.in` from `build-info.json`; derives `pkgrel` from what is on the AUR. |
|
||||
| `scripts/publish-github-release.sh` | Creates the GitHub release and uploads the tarball. |
|
||||
| `scripts/push-aur.sh` | Generates `.SRCINFO` and pushes to `aur.archlinux.org`. |
|
||||
| `scripts/record-state.sh` | Commits `state.json` so the next nightly run knows what was built. |
|
||||
| `packaging/PKGBUILD.in` | Template for the AUR package. |
|
||||
| `state.json` | Last successfully built combination. |
|
||||
|
||||
## Flow
|
||||
|
||||
```
|
||||
nightly (03:00) ─► check-upstream.py
|
||||
│ new JASP tag, or Arch bumped r / qt6-base?
|
||||
▼
|
||||
build-jasp.sh (Arch container, hours)
|
||||
│ jasp-desktop-<ver>-x86_64.tar.zst + build-info.json
|
||||
▼
|
||||
GitHub release ──► gen-pkgbuild.py ──► push to AUR
|
||||
│
|
||||
▼
|
||||
state.json committed back
|
||||
```
|
||||
|
||||
## Required configuration
|
||||
|
||||
Set these in the Gitea repo before the first run.
|
||||
|
||||
**Variables** (Settings → Actions → Variables):
|
||||
|
||||
| Name | Example | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `GH_OWNER` | `asgerw` | GitHub account hosting the release repo |
|
||||
| `GH_REPO` | `jasp-desktop-bin` | Release repo name |
|
||||
| `AUR_MAINTAINER` | `Asger W <cla@weircon.dk>` | Written into the PKGBUILD header |
|
||||
|
||||
**Secrets** (Settings → Actions → Secrets):
|
||||
|
||||
| Name | Meaning |
|
||||
| --- | --- |
|
||||
| `GH_TOKEN` | GitHub PAT with `contents: write` on the release repo |
|
||||
| `AUR_SSH_KEY` | Private SSH key registered with your AUR account |
|
||||
| `GITEA_TOKEN` | Token with write access to this repo, for committing `state.json` |
|
||||
|
||||
The GitHub repo `GH_OWNER/GH_REPO` must exist and be public — the AUR PKGBUILD
|
||||
downloads the tarball from its releases, so it has to be reachable anonymously.
|
||||
|
||||
## Runner requirements
|
||||
|
||||
The build runs for hours and needs disk. On the Gitea runner host:
|
||||
|
||||
- `runner.timeout` in `/var/lib/gitea-worker/config.yaml` must exceed the build
|
||||
time (set to `16h`).
|
||||
- Gitea's own job timeout (`[actions] DEFAULT_ACTIONS_TIMEOUT` in `app.ini`) must
|
||||
match, otherwise Gitea kills the job at 3h regardless of the runner.
|
||||
- Roughly 40–60 GB of free disk for the build tree and staged install.
|
||||
|
||||
## Running it by hand
|
||||
|
||||
```
|
||||
# Build a specific tag without waiting for the schedule
|
||||
Actions → Build JASP binary and publish to AUR → Run workflow
|
||||
jasp_tag = v0.98.1 force = true
|
||||
```
|
||||
|
||||
## Caveat worth knowing
|
||||
|
||||
AUR convention is that a `-bin` package repackages binaries published by upstream.
|
||||
Here upstream publishes no Linux binary, so this one is built by us. That is
|
||||
unusual, and a maintainer or TU may question it. It is not against the rules, but
|
||||
the package description and this README should stay honest about where the binary
|
||||
comes from.
|
||||
Executable
+185
@@ -0,0 +1,185 @@
|
||||
#!/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, so this
|
||||
# script also records the exact versions of the Arch libraries JASP ends up
|
||||
# linked against — the PKGBUILD pins `depends` to them.
|
||||
#
|
||||
# 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)
|
||||
#
|
||||
# JASP links against the distro's R and Qt6 rather than bundling them
|
||||
# (Tools/CMake/R.cmake only fetches R.framework on macOS), so a build is only
|
||||
# valid for the R minor and Qt6 minor present at build time.
|
||||
|
||||
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)}"
|
||||
|
||||
SRC_DIR=/build/jasp-desktop
|
||||
BUILD_DIR=/build/build
|
||||
STAGE_DIR=/build/stage
|
||||
PREFIX=/opt/jasp-desktop
|
||||
|
||||
mkdir -p "$OUT_DIR" /build
|
||||
|
||||
log() { echo ":: $* "; }
|
||||
|
||||
# ---------------------------------------------------------------- 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 r glpk jsoncpp libarchive openssl curl sqlite zlib \
|
||||
readstat netcdf mpfr cairo fontconfig harfbuzz fribidi \
|
||||
libpng libtiff libjpeg-turbo minizip libxkbcommon libxkbcommon-x11 \
|
||||
wget unzip
|
||||
|
||||
# Stock makepkg.conf leaves MAKEFLAGS at -j1, which makes the AUR dependencies
|
||||
# (JAGS especially) crawl. R's Makeconf picks this up for module builds too.
|
||||
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
|
||||
|
||||
# ------------------------------------------------------------------- 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=ON \
|
||||
-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 expects these directories to exist.
|
||||
log "building R modules — this is the slow part"
|
||||
mkdir -p "$BUILD_DIR"/Modules/{binary_pkgs,manifests,module_libs,Tools}
|
||||
cd "$SRC_DIR/Tools"
|
||||
Rscript buildAllDefaultJaspModules.R
|
||||
|
||||
# -------------------------------------------------------------------- staging
|
||||
log "staging install tree"
|
||||
rm -rf "$STAGE_DIR"
|
||||
mkdir -p "$STAGE_DIR"
|
||||
DESTDIR="$STAGE_DIR" ninja -C "$BUILD_DIR" install
|
||||
|
||||
install -d "$STAGE_DIR$PREFIX/Modules"
|
||||
cp -a "$SRC_DIR/Tools/ModuleBundleBuildDir/Modules/." "$STAGE_DIR$PREFIX/Modules/"
|
||||
|
||||
if [[ -d "$BUILD_DIR/R/library" ]]; then
|
||||
install -d "$STAGE_DIR$PREFIX/R/library"
|
||||
cp -a "$BUILD_DIR/R/library/." "$STAGE_DIR$PREFIX/R/library/"
|
||||
fi
|
||||
|
||||
# Desktop entry + icons, so the tarball is a complete install tree.
|
||||
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 -maxdepth 2 -name 'LICENSE*' -type f \
|
||||
| while read -r f; do install -m644 "$f" "$STAGE_DIR/usr/share/licenses/jasp-desktop-bin/"; done
|
||||
|
||||
# Strip build-host paths out of the staged tree so the tarball is relocatable.
|
||||
find "$STAGE_DIR" -name '*.o' -delete 2>/dev/null || true
|
||||
|
||||
# ------------------------------------------------------------------- metadata
|
||||
# Pin data: the PKGBUILD turns these into versioned `depends` entries, so an
|
||||
# Arch R or Qt6 bump makes the package rebuild rather than silently break.
|
||||
pkgver_of() { pacman -Q "$1" 2>/dev/null | awk '{print $2}'; }
|
||||
|
||||
R_FULL="$(pkgver_of r)"
|
||||
R_MINOR="$(R --version | head -1 | grep -oE '[0-9]+\.[0-9]+' | head -1)"
|
||||
QT_FULL="$(pkgver_of qt6-base)"
|
||||
QT_MINOR="$(echo "$QT_FULL" | grep -oE '^[0-9]+\.[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},
|
||||
"r_version": "${R_FULL}",
|
||||
"r_minor": "${R_MINOR}",
|
||||
"qt6_version": "${QT_FULL}",
|
||||
"qt6_minor": "${QT_MINOR}",
|
||||
"boost_version": "${BOOST_FULL}"
|
||||
}
|
||||
EOF
|
||||
|
||||
log "done"
|
||||
cat "$OUT_DIR/build-info.json"
|
||||
@@ -0,0 +1,43 @@
|
||||
# Maintainer: @MAINTAINER@
|
||||
#
|
||||
# Upstream JASP publishes installers for Windows and macOS only. The binary this
|
||||
# package installs is therefore not an upstream release artifact: it is built
|
||||
# from the tagged JASP sources on Arch by
|
||||
# https://gitea.weircon.dk/agw/jasp-desktop-bin and published to GitHub Releases.
|
||||
|
||||
pkgname=jasp-desktop-bin
|
||||
pkgver=@JASP_VERSION@
|
||||
pkgrel=@PKGREL@
|
||||
pkgdesc="JASP Desktop - a fresh way to do statistics (prebuilt binary)"
|
||||
arch=('x86_64')
|
||||
url="https://jasp-stats.org/"
|
||||
license=('AGPL-3.0-or-later')
|
||||
|
||||
provides=("jasp-desktop=${pkgver}")
|
||||
conflicts=('jasp-desktop')
|
||||
|
||||
# JASP links against the system R and Qt6 rather than bundling them, and the
|
||||
# bundled R module library is compiled against one specific R minor. These are
|
||||
# pinned to what this binary was actually built against — an Arch R or Qt6 bump
|
||||
# makes pacman refuse the combination instead of leaving JASP silently broken.
|
||||
depends=(
|
||||
"r>=@R_MINOR@" "r<@R_NEXT_MINOR@"
|
||||
"qt6-base>=@QT_MINOR@" "qt6-base<@QT_NEXT_MINOR@"
|
||||
'qt6-declarative' 'qt6-svg' 'qt6-positioning' 'qt6-webchannel'
|
||||
'qt6-webengine' 'qt6-5compat' 'qt6-httpserver'
|
||||
'boost-libs' 'glpk' 'jsoncpp' 'libarchive' 'openssl' 'curl' 'sqlite'
|
||||
'zlib' 'readstat' 'blas' 'lapack'
|
||||
'jags' 'librdata-git'
|
||||
)
|
||||
|
||||
# The tarball is a complete /opt + /usr install tree; there is nothing to
|
||||
# compile or strip here.
|
||||
options=('!strip' '!debug')
|
||||
|
||||
source=("jasp-desktop-${pkgver}-${pkgrel}-x86_64.tar.zst::@DOWNLOAD_URL@")
|
||||
sha256sums=('@SHA256@')
|
||||
|
||||
package() {
|
||||
cp -a "${srcdir}/opt" "${pkgdir}/opt"
|
||||
cp -a "${srcdir}/usr" "${pkgdir}/usr"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[Desktop Entry]
|
||||
Name=JASP
|
||||
Comment=Statistical analysis package
|
||||
Keywords=statistics;data
|
||||
Exec=/opt/jasp-desktop/bin/JASP %f
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=application/x-jaspstats-jasp;application/x-spss-sav;
|
||||
StartupWMClass=org.jasp-stats.JASP
|
||||
StartupNotify=true
|
||||
Icon=org.jaspstats.JASP
|
||||
Categories=Science;Education;DataVisualization
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Decide whether a rebuild is needed.
|
||||
|
||||
A JASP binary is only valid for the R minor and Qt6 minor it was linked against,
|
||||
so a rebuild is triggered by an upstream JASP release *or* by Arch moving R or
|
||||
Qt6 — not just the former.
|
||||
|
||||
Writes GITHUB_OUTPUT-style key=value pairs to stdout (and to $GITHUB_OUTPUT).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
STATE_FILE = "state.json"
|
||||
GH_LATEST = "https://api.github.com/repos/jasp-stats/jasp-desktop/releases/latest"
|
||||
ARCH_PKG = "https://archlinux.org/packages/search/json/?name={}&repo=Extra"
|
||||
|
||||
|
||||
def fetch_json(url: str) -> dict:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "jasp-desktop-bin-ci"})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return json.load(r)
|
||||
|
||||
|
||||
def arch_version(pkg: str) -> str:
|
||||
results = fetch_json(ARCH_PKG.format(pkg)).get("results", [])
|
||||
if not results:
|
||||
raise RuntimeError(f"package {pkg} not found in Arch Extra")
|
||||
return results[0]["pkgver"]
|
||||
|
||||
|
||||
def minor(version: str) -> str:
|
||||
return ".".join(version.split(".")[:2])
|
||||
|
||||
|
||||
def main() -> int:
|
||||
forced = os.environ.get("FORCE_BUILD", "").lower() == "true"
|
||||
pinned_tag = os.environ.get("JASP_TAG", "").strip()
|
||||
|
||||
jasp_tag = pinned_tag or fetch_json(GH_LATEST)["tag_name"]
|
||||
r_minor = minor(arch_version("r"))
|
||||
qt_minor = minor(arch_version("qt6-base"))
|
||||
|
||||
try:
|
||||
state = json.loads(open(STATE_FILE).read())
|
||||
except FileNotFoundError:
|
||||
state = {}
|
||||
|
||||
reasons = []
|
||||
if forced:
|
||||
reasons.append("manually forced")
|
||||
if state.get("jasp_tag") != jasp_tag:
|
||||
reasons.append(f"JASP {state.get('jasp_tag', 'none')} -> {jasp_tag}")
|
||||
if state.get("r_minor") != r_minor:
|
||||
reasons.append(f"R {state.get('r_minor', 'none')} -> {r_minor}")
|
||||
if state.get("qt6_minor") != qt_minor:
|
||||
reasons.append(f"Qt6 {state.get('qt6_minor', 'none')} -> {qt_minor}")
|
||||
|
||||
needs_build = bool(reasons)
|
||||
|
||||
print(f"jasp_tag = {jasp_tag}")
|
||||
print(f"r_minor = {r_minor}")
|
||||
print(f"qt6_minor = {qt_minor}")
|
||||
print(f"decision = {'BUILD' if needs_build else 'up to date'}")
|
||||
for reason in reasons:
|
||||
print(f" reason: {reason}")
|
||||
|
||||
out = os.environ.get("GITHUB_OUTPUT")
|
||||
if out:
|
||||
with open(out, "a") as fh:
|
||||
fh.write(f"needs_build={'true' if needs_build else 'false'}\n")
|
||||
fh.write(f"jasp_tag={jasp_tag}\n")
|
||||
fh.write(f"r_minor={r_minor}\n")
|
||||
fh.write(f"qt6_minor={qt_minor}\n")
|
||||
fh.write(f"reasons={'; '.join(reasons)}\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Turn build-info.json into a ready-to-publish PKGBUILD.
|
||||
|
||||
pkgrel is derived from what is currently on the AUR: same pkgver means this is a
|
||||
rebuild (against a new R/Qt6), so pkgrel increments; a new pkgver resets it to 1.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
AUR_RPC = "https://aur.archlinux.org/rpc/v5/info?arg[]={}"
|
||||
|
||||
|
||||
def next_minor(version: str) -> str:
|
||||
"""4.6 -> 4.7, 6.11 -> 6.12 — the upper bound of the compatible range."""
|
||||
major, minor = version.split(".")[:2]
|
||||
return f"{major}.{int(minor) + 1}"
|
||||
|
||||
|
||||
def current_aur_version(pkgname: str) -> tuple[str, int] | None:
|
||||
"""(pkgver, pkgrel) currently published on the AUR, or None if absent."""
|
||||
try:
|
||||
with urllib.request.urlopen(AUR_RPC.format(pkgname), timeout=30) as r:
|
||||
results = json.load(r).get("results", [])
|
||||
except Exception as exc: # network trouble must not silently reset pkgrel
|
||||
print(f"warning: could not query AUR: {exc}", file=sys.stderr)
|
||||
return None
|
||||
if not results:
|
||||
return None
|
||||
full = results[0]["Version"] # e.g. "0.98.1-2"
|
||||
ver, _, rel = full.rpartition("-")
|
||||
return ver, int(rel) if rel.isdigit() else 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--build-info", required=True)
|
||||
ap.add_argument("--template", required=True)
|
||||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--gh-owner", required=True)
|
||||
ap.add_argument("--gh-repo", default="jasp-desktop-bin")
|
||||
ap.add_argument("--maintainer", required=True)
|
||||
ap.add_argument("--pkgname", default="jasp-desktop-bin")
|
||||
args = ap.parse_args()
|
||||
|
||||
info = json.loads(open(args.build_info).read())
|
||||
jasp_version = info["jasp_version"]
|
||||
|
||||
published = current_aur_version(args.pkgname)
|
||||
if published and published[0] == jasp_version:
|
||||
pkgrel = published[1] + 1
|
||||
print(f"rebuild of {jasp_version}: pkgrel {published[1]} -> {pkgrel}")
|
||||
else:
|
||||
pkgrel = 1
|
||||
print(f"new version {jasp_version}: pkgrel 1")
|
||||
|
||||
tag = f"v{jasp_version}-{pkgrel}"
|
||||
download_url = (
|
||||
f"https://github.com/{args.gh_owner}/{args.gh_repo}"
|
||||
f"/releases/download/{tag}/{info['tarball']}"
|
||||
)
|
||||
|
||||
r_minor = info["r_minor"]
|
||||
qt_minor = info["qt6_minor"]
|
||||
|
||||
substitutions = {
|
||||
"@MAINTAINER@": args.maintainer,
|
||||
"@JASP_VERSION@": jasp_version,
|
||||
"@PKGREL@": str(pkgrel),
|
||||
"@R_MINOR@": r_minor,
|
||||
"@R_NEXT_MINOR@": next_minor(r_minor),
|
||||
"@QT_MINOR@": qt_minor,
|
||||
"@QT_NEXT_MINOR@": next_minor(qt_minor),
|
||||
"@DOWNLOAD_URL@": download_url,
|
||||
"@SHA256@": info["sha256"],
|
||||
}
|
||||
|
||||
text = open(args.template).read()
|
||||
for key, value in substitutions.items():
|
||||
text = text.replace(key, value)
|
||||
|
||||
leftover = re.findall(r"@[A-Z0-9_]+@", text)
|
||||
if leftover:
|
||||
print(f"error: unsubstituted placeholders: {sorted(set(leftover))}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
with open(args.out, "w") as fh:
|
||||
fh.write(text)
|
||||
|
||||
# Consumed by the release + AUR-push steps of the workflow.
|
||||
with open("release-meta.json", "w") as fh:
|
||||
json.dump({"tag": tag, "pkgrel": pkgrel, "download_url": download_url, **info}, fh, indent=2)
|
||||
|
||||
print(f"wrote {args.out} ({args.pkgname} {jasp_version}-{pkgrel})")
|
||||
print(f" R pinned to >={r_minor} <{next_minor(r_minor)}")
|
||||
print(f" Qt pinned to >={qt_minor} <{next_minor(qt_minor)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# Create (or reuse) a GitHub release and upload the JASP tarball to it.
|
||||
# Uses the REST API directly so the workflow does not depend on `gh` being
|
||||
# present in the runner image.
|
||||
#
|
||||
# GH_TOKEN PAT with `contents: write` on the target repo (required)
|
||||
# GH_OWNER GitHub account owning the release repo (required)
|
||||
# GH_REPO release repo name (default jasp-desktop-bin)
|
||||
# TAG release tag, e.g. v0.98.1-1 (required)
|
||||
# ASSET path to the tarball to upload (required)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${GH_TOKEN:?GH_TOKEN is required}"
|
||||
: "${GH_OWNER:?GH_OWNER is required}"
|
||||
: "${TAG:?TAG is required}"
|
||||
: "${ASSET:?ASSET is required}"
|
||||
GH_REPO="${GH_REPO:-jasp-desktop-bin}"
|
||||
|
||||
API="https://api.github.com/repos/${GH_OWNER}/${GH_REPO}"
|
||||
UPLOADS="https://uploads.github.com/repos/${GH_OWNER}/${GH_REPO}"
|
||||
AUTH=(-H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json")
|
||||
|
||||
asset_name="$(basename "$ASSET")"
|
||||
|
||||
echo ":: looking for existing release $TAG"
|
||||
release_id="$(curl -sSf "${AUTH[@]}" "${API}/releases/tags/${TAG}" 2>/dev/null \
|
||||
| python3 -c 'import json,sys; print(json.load(sys.stdin).get("id",""))' 2>/dev/null || true)"
|
||||
|
||||
if [[ -z "$release_id" ]]; then
|
||||
echo ":: creating release $TAG"
|
||||
release_id="$(curl -sSf -X POST "${AUTH[@]}" "${API}/releases" \
|
||||
-d "$(python3 -c '
|
||||
import json, os
|
||||
print(json.dumps({
|
||||
"tag_name": os.environ["TAG"],
|
||||
"name": "JASP " + os.environ["TAG"],
|
||||
"body": os.environ.get("RELEASE_NOTES", ""),
|
||||
"draft": False,
|
||||
"prerelease": False,
|
||||
}))')" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')"
|
||||
else
|
||||
echo ":: reusing release $release_id"
|
||||
# Replace a same-named asset so re-runs are idempotent.
|
||||
old="$(curl -sSf "${AUTH[@]}" "${API}/releases/${release_id}/assets" \
|
||||
| python3 -c "
|
||||
import json, sys
|
||||
name = '${asset_name}'
|
||||
print(next((a['id'] for a in json.load(sys.stdin) if a['name'] == name), ''))
|
||||
")"
|
||||
if [[ -n "$old" ]]; then
|
||||
echo ":: deleting previous asset $old"
|
||||
curl -sSf -X DELETE "${AUTH[@]}" "${API}/releases/assets/${old}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ":: uploading $asset_name ($(du -h "$ASSET" | cut -f1))"
|
||||
curl -sSf -X POST "${AUTH[@]}" \
|
||||
-H "Content-Type: application/zstd" \
|
||||
--data-binary "@${ASSET}" \
|
||||
"${UPLOADS}/releases/${release_id}/assets?name=${asset_name}" >/dev/null
|
||||
|
||||
echo ":: uploaded to https://github.com/${GH_OWNER}/${GH_REPO}/releases/tag/${TAG}"
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# Publish the generated PKGBUILD to the AUR.
|
||||
#
|
||||
# AUR_SSH_KEY private key registered with aur.archlinux.org (required)
|
||||
# $1 path to the generated PKGBUILD
|
||||
#
|
||||
# .SRCINFO is generated with makepkg inside an Arch container, because makepkg
|
||||
# does not exist on the Ubuntu runner image.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PKGBUILD_PATH="${1:?path to PKGBUILD is required}"
|
||||
: "${AUR_SSH_KEY:?AUR_SSH_KEY is required}"
|
||||
PKGNAME="${PKGNAME:-jasp-desktop-bin}"
|
||||
|
||||
workdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$workdir"' EXIT
|
||||
|
||||
install -m600 /dev/stdin "$workdir/aur_key" <<<"$AUR_SSH_KEY"
|
||||
|
||||
export GIT_SSH_COMMAND="ssh -i $workdir/aur_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
|
||||
|
||||
echo ":: cloning AUR repo $PKGNAME"
|
||||
# A package that does not exist yet clones as an empty repo, which is fine.
|
||||
git clone "ssh://aur@aur.archlinux.org/${PKGNAME}.git" "$workdir/aur"
|
||||
|
||||
cp "$PKGBUILD_PATH" "$workdir/aur/PKGBUILD"
|
||||
|
||||
echo ":: generating .SRCINFO"
|
||||
docker run --rm -v "$workdir/aur:/pkg" -w /pkg archlinux:base-devel bash -euc '
|
||||
useradd -m builder
|
||||
chown -R builder /pkg
|
||||
sudo -u builder makepkg --printsrcinfo > /pkg/.SRCINFO
|
||||
'
|
||||
|
||||
cd "$workdir/aur"
|
||||
git config user.name "${AUR_GIT_NAME:-JASP bin autobuilder}"
|
||||
git config user.email "${AUR_GIT_EMAIL:-noreply@weircon.dk}"
|
||||
|
||||
if git diff --quiet && git diff --cached --quiet && [[ -z "$(git status --porcelain)" ]]; then
|
||||
echo ":: AUR already up to date, nothing to push"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
version="$(awk -F= '/^pkgver=/{v=$2} /^pkgrel=/{r=$2} END{print v"-"r}' PKGBUILD)"
|
||||
git add PKGBUILD .SRCINFO
|
||||
git commit -m "Update to ${version}"
|
||||
git push origin HEAD:master
|
||||
|
||||
echo ":: pushed ${PKGNAME} ${version} to the AUR"
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
# Commit state.json back to this repo so the next scheduled run knows what was
|
||||
# already built. Without this the daily check would rebuild every night.
|
||||
#
|
||||
# GITEA_TOKEN token with write access to this repo (required)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${GITEA_TOKEN:?GITEA_TOKEN is required}"
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
|
||||
meta = json.load(open("release-meta.json"))
|
||||
state = {
|
||||
"jasp_tag": meta["jasp_tag"],
|
||||
"jasp_version": meta["jasp_version"],
|
||||
"pkgrel": meta["pkgrel"],
|
||||
"r_minor": meta["r_minor"],
|
||||
"qt6_minor": meta["qt6_minor"],
|
||||
"sha256": meta["sha256"],
|
||||
"download_url": meta["download_url"],
|
||||
}
|
||||
with open("state.json", "w") as fh:
|
||||
json.dump(state, fh, indent=2)
|
||||
fh.write("\n")
|
||||
print(json.dumps(state, indent=2))
|
||||
PY
|
||||
|
||||
if git diff --quiet -- state.json; then
|
||||
echo ":: state.json unchanged"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "jasp-bin autobuilder"
|
||||
git config user.email "noreply@weircon.dk"
|
||||
git add state.json
|
||||
git commit -m "Record build of ${GITHUB_REF_NAME:-build}: $(python3 -c 'import json;print(json.load(open("state.json"))["jasp_version"])')"
|
||||
|
||||
origin="$(git remote get-url origin)"
|
||||
host_path="${origin#*://}"
|
||||
host_path="${host_path#*@}"
|
||||
git push "https://x-access-token:${GITEA_TOKEN}@${host_path}" HEAD:"${GITHUB_REF_NAME:-master}"
|
||||
|
||||
echo ":: state recorded"
|
||||
Reference in New Issue
Block a user