Files
Jungfraujoch/.gitea/workflows/build_and_test.yml
T
leonarski_f 749db470ca
Build Packages / build:rpm (rocky9) (push) Successful in 19m56s
Build Packages / Unit tests (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 16m57s
Build Packages / build:windows:cuda (push) Successful in 19m18s
Build Packages / build:viewer-tgz:cpu (push) Successful in 14m48s
Build Packages / build:viewer-tgz:cuda (push) Successful in 16m18s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 14m19s
Build Packages / build:rugnux:windows (push) Successful in 10m34s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 8m49s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m55s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 17m4s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m48s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m15s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 24m26s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 20m32s
Build Packages / build:rpm (rocky8) (push) Successful in 23m39s
Build Packages / Generate python client (push) Successful in 46s
Build Packages / Build documentation (push) Successful in 1m45s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (durin plugin) (push) Successful in 11m3s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 11m30s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 20m10s
Build Packages / XDS test (neggia plugin) (push) Successful in 10m17s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 23m12s
Build Packages / DIALS test (push) Successful in 20m12s
v1.0.0-rc.164 (#74)
* rugnux now tells you whether a crystal diffracts anisotropically and how far it reaches in each direction, without a second program: a new `9. DIFFRACTION ANISOTROPY` section in `<prefix>_report.txt` and matching `_reflns.pdbx_aniso_B_tensor_*` / `_reflns.jfjoch_aniso_*` items in the merged mmCIF report the anisotropic deltaB, the diffraction limit along each principal direction, and a `NOT DETECTED` / `DETECTED` / `CANNOT DETERMINE` verdict measured against the data set's own systematic error. It is a description only - no intensity is corrected, no reflection is removed, and the merged data do not depend on direction.
* rugnux can hand its integrated observations to another scaling program: `--export-unmerged` writes `<prefix>_unmerged.mtz`, an unmerged MTZ readable by aimless, pointless, careless and `iotbx.merging_statistics`, in `--mode mx` and `--mode scale` alike. Each rotation reflection's partials are summed into one full; `--export-unmerged-partials` writes one row per image instead. Intensities carry the Lorentz-polarization factor and nothing else, since those programs scale the data themselves. Lattice-centring absences are not written; screw and glide absences are.
* rugnux integrates crystals with broad spots better - where it changes anything, per-shell mean I/sigma improves by up to 31% and R_meas by up to 24% - because on rotation data the integration signal radius is now taken from the crystal's own measured spot width instead of a fixed 4 px. `--adaptive-integration-radius=off` restores the fixed radius and an explicit `--integration-radius` still overrides both. The widened radius applies to the final integration pass only, and a pattern too dense for it is re-integrated at 4 px with a note in the log.
* rugnux discards fewer stills reflections for want of a background ring, improving per-shell R_meas over most of the signal-bearing range: the stills background ring now runs to 14 px instead of 12. The gain reverses in shells below a mean I/sigma of about 4.
* rugnux determines the space group with thresholds that mean the same thing on a weak crystal as on a strong one: symmetry operators are scored on resolution-normalised intensities (E squared) instead of raw merged intensities, and a reflection counts as genuinely present on its counting significance instead of on the merged I/sigma, which saturates at the merge's own ISa. The search resolution cut is no longer able to move the answer, and the twin-law H bound moves from 1.70 to 1.85, which stops one class of correct high-symmetry assignment being refused as twinning.
* rugnux says what the space-group search tested and what it could not: the twin-law disagreement H is printed for every operator together with the adopted point group's H ratio and its bound; alternatives that are not on the reported lattice are named with how their cell differs; and a lattice centring the data could not test - the crystal having been integrated on the primitive sub-cell, so the reflections it extinguishes were never measured - is marked `UNTESTED` and warned about where it is adopted, as coming from the lattice metric rather than from the intensities.
* rugnux `--mode scale` re-merges a `_process.h5` in the right symmetry without being told it: the file now records the space group on every run - a two-pass rotation run wrote none before, so re-merging defaulted to P1 - together with the change of basis under `/entry/MX/reindexMatrix` where the lattice was re-seated, and `--mode scale` also reports the Wilson B-factor estimate instead of `WILSON_B= nan`. A file written before this stops with a message naming the two cells and the override to use, instead of failing inside the merge. A third-party reader of a `_process.h5` must apply `reindexMatrix` where it is present.
* rugnux installs on its own, as a package called `rugnux` - `dnf install rugnux` or `apt install rugnux` - instead of arriving inside `jfjoch-viewer`. It pulls in none of the acquisition stack, so a machine that only processes data no longer has to carry the broker, the detector libraries or Qt to get it. Installing it over a `jfjoch-viewer` from rc.163 or earlier, which still owns `/usr/bin/rugnux`, upgrades cleanly rather than failing on the duplicate file.
* rugnux is also a standalone download, built for arm64 as well as x86_64: `rugnux-<version>-linux-{x86_64|aarch64}-cuda<major>.tgz` and `rugnux-<version>-win64-cuda<major>.zip` on the release page, for machines that are not managed by a package manager. The aarch64 build targets GH200 and DGX Spark, and is untested on hardware.
* Every portable Linux binary is now a single self-contained file: cuFFT is linked statically instead of being shipped beside the executable and found through an rpath, so `rugnux` and `jfjoch_viewer` need nothing but an NVIDIA driver, and only to use the GPU. The `.rpm`/`.deb` continue to take cuFFT from the distribution. The developer utilities `jfjoch_extract_hkl` and `jfjoch_recompress` are no longer packaged anywhere.
* Jungfraujoch needs six fewer shared libraries on the machine - libopenblas and libmetis, and libgfortran, libquadmath, libgomp and libz behind them - because the Ceres LAPACK, METIS and SuiteSparse back-ends are no longer built. Nothing in the code ever selected them, and results are unchanged.
* The PCIe driver DKMS package builds for the kernel it is being installed for instead of the running one, so a module built while a kernel update is being applied loads after the reboot.
* The PCIe driver builds on RHEL 9.5 and later, and on their CentOS Stream, Rocky and AlmaLinux equivalents, where the `vm_flags` kernel interface was backported into the 5.14 kernel.
* A data collection started with `async_start` that fails to start - a writer refusing to overwrite an existing file, for instance - is reported as an error by `/wait_until_running` and `/wait_till_done` instead of as a timeout and a successful collection respectively. The error message is the one the writer gave.
* A calibration that is cancelled or that fails to collect its pedestals is no longer reported as a successful one. The broker goes to `Inactive` with an error message and has to be initialized again, instead of sitting in `Idle` looking ready to measure while holding partial pedestals - data collected in that state was silently mis-converted.
* A failed `/initialize` is reported to `/wait_until_running` and `/wait_till_done` as soon as it happens, instead of when their timeout expires.
* `space_group_number` accepts space groups up to 230 in the API schema, so cubic space groups can be recorded. The broker always accepted them; the generated clients rejected them before the request was sent.
* The results report's `REPORT_VERSION` is 3, two sections having been added. Existing key names and table columns are unchanged.
* The merged statistics table has **9** resolution shells instead of 10, which is what XDS reports. The bins were already XDS's - equal steps in 1/d^2 between the lowest- and the highest-resolution reflection the merge kept - so at the same resolution limits the two tables now have the same shell boundaries and can be read row for row. `--resolution-shells` sets a different count.
* `rugnux --model` now settles the frame the merged reflections are written in, not only the frame the R-factors and the maps are computed in: the `.mtz`/`.cif`/`.hkl` come out in the model's indexing, and where the data were merged in the model's enantiomorph they take the model's hand and space group - which on anomalous data puts I(+) and I(-) the right way round. The indexing choice is logged with the winning R-free and the runner-up, so a decision made within noise is visible.
* `rugnux --model` can resolve the indexing ambiguity of a **serial stills** run, which a model could not do before: structure factors computed from the model become the per-image reference, the same role a reference MTZ plays. It needs the cell and space group up front (`-C` / `-S`). Without one or the other, a merohedral serial run still merges both hands together and says so.
* The rugnux documentation opens with a quick start - the default run, and runs with a reference MTZ, with a model, or with the space group and cell pinned - and explains the indexing ambiguity: what it costs on rotation and on serial data, and which of `-z` / `--model` resolves it in each case. The long reference pages now carry a table of contents.

Reviewed-on: #74
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-08-26 22:47:00 +02:00

705 lines
31 KiB
YAML

name: Build Packages
env:
# Architecture and link-time optimisation for every Linux configure. Both are deliberately absent
# from CMakeLists.txt so a site can pick its own (x86-64-v4 on an AVX-512 cluster, -march=native,
# or the plain baseline); CI pins what it ships.
#
# -flto=auto needs GCC >= 10 and both CI images put gcc-toolset-13 on PATH. Measured on rugnux
# against an otherwise identical build: 7-10% fewer retired instructions and a 9% smaller binary,
# but only ~1.5% off the wall clock, because the offline pipeline is GPU- and I/O-bound rather
# than CPU-instruction-bound. It costs about 3x on an INCREMENTAL rebuild (one file plus link,
# 9.8 s -> 30.1 s), which is why it belongs here and not in CMakeLists: CI always builds from
# scratch and ships the result, so it pays the link once and a developer never pays it at all.
#
# Not CMAKE_INTERPROCEDURAL_OPTIMIZATION, which is the tidier spelling and would cover MSVC too:
# built and linked fine (CUDA included, and it does not reach .cu either way), but measured 3%
# MORE retired instructions than -flto=auto - 396.9 G vs 384.9 G over three runs each against a
# 0.45% run-to-run spread, so the gap is real. It also leaves two dependencies (FFTW, libzmq) out
# of LTO that the flag reaches, and it did not switch CMAKE_AR to gcc-ar here, so the one
# correctness argument for it did not materialise either.
LINUX_CMAKE_FLAGS: '-DCMAKE_CXX_FLAGS="-march=x86-64-v3 -flto=auto" -DCMAKE_C_FLAGS="-march=x86-64-v3 -flto=auto"'
# MSVC has no spelling for the x86-64-v2 level; /arch:AVX is the nearest and implies SSE4.1/4.2,
# which is what matters here - without it Eigen has no vectorised round and falls back to a libm
# call per element. AVX is Sandy Bridge (2011) and up, a safe floor for a desktop viewer.
MSVC_ARCH_CMAKE_FLAGS: '-DCMAKE_CXX_FLAGS="/arch:AVX" -DCMAKE_C_FLAGS="/arch:AVX"'
on:
push:
branches:
- '**'
tags:
- '**'
pull_request:
workflow_dispatch:
inputs:
create_release:
type: boolean
description: 'Create release'
required: false
default: false
jobs:
unit-tests:
name: Unit tests
runs-on: jfjoch_rocky8
# Not on a tag, on purpose. Releasing is a two-step dance because artifacts do not carry between
# runs: the manual dispatch below runs the whole CI (this job included) and then creates the tag,
# and the tag push re-runs the workflow only to rebuild and upload. The tag run has therefore
# already been tested by the dispatch run it came from.
if: github.ref_type != 'tag'
container:
image: gitea.psi.ch/leonarski_f/jfjoch_rocky8:2511
options: --gpus all
timeout-minutes: 90
env:
CTEST_OUTPUT_ON_FAILURE: '1'
steps:
- uses: actions/checkout@v4
- name: Configure auth and fetch LFS
shell: bash
env:
GITEA_TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
run: |
git lfs install --local
AUTH=$(git config --local http.${{ github.server_url }}/.extraheader)
git config --local --unset http.${{ github.server_url }}/.extraheader
git config --local http.${{ github.server_url }}/${{ github.repository }}.git/info/lfs/objects/batch.extraheader "$AUTH"
git lfs pull
- name: Build tests
shell: bash
run: |
mkdir -p build
cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ${{ env.LINUX_CMAKE_FLAGS }} ..
ninja -j48 jfjoch_test jfjoch_hdf5_test jfjoch_hdf5_enospc_test enospc_shim
- name: Run unit tests
shell: bash
run: |
cd build/tests
./jfjoch_test
- name: HDF5 ENOSPC test
shell: bash
run: |
cd build/tests
LD_PRELOAD=./enospc_shim.so ./jfjoch_hdf5_enospc_test HDF5File_enospc
LD_PRELOAD=./enospc_shim.so ./jfjoch_hdf5_enospc_test FileWriter_enospc
- name: Run hdf5 test
shell: bash
run: |
cd build/tools
./jfjoch_hdf5_test ../../tests/test_data/compression_benchmark.h5
build-windows:
name: build:windows:${{ matrix.variant }}
runs-on: windows-11-cuda-qt
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- variant: cuda
use_cuda: 'ON'
- variant: nocuda
use_cuda: 'OFF'
steps:
- uses: actions/checkout@v4
- name: Configure viewer build
shell: cmd
run: |
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set "VSPATH=%%i"
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
cmake -G Ninja -B build -DJFJOCH_USE_CUDA=${{ matrix.use_cuda }} -DCMAKE_BUILD_TYPE=Release ${{ env.MSVC_ARCH_CMAKE_FLAGS }} -DCMAKE_PREFIX_PATH="C:/deps;C:/Qt/6.11.1/msvc2022_64"
- name: Build viewer
shell: cmd
run: |
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set "VSPATH=%%i"
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
cmake --build build
- name: Build installer (NSIS)
shell: cmd
run: |
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set "VSPATH=%%i"
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
cd build
cpack
- name: Upload installer to release
if: github.ref_type == 'tag'
shell: powershell
env:
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
run: |
# NSIS installer named jfjoch-viewer-<version>-win64-{cuda<major>|cpu}.exe (see CMakeLists.txt).
$files = Get-ChildItem -Path build -Filter 'jfjoch-viewer-*-win64-*.exe'
if ($files.Count -eq 0) { throw 'No Windows installer found in build/' }
foreach ($file in $files) {
python gitea_upload_file.py $file.FullName
if ($LASTEXITCODE -ne 0) { throw "Upload failed for $($file.Name)" }
}
build-viewer-tgz:
name: build:viewer-tgz:${{ matrix.variant }}
runs-on: jfjoch_rocky8
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- variant: cuda
use_cuda: 'ON'
- variant: cpu
use_cuda: 'OFF'
steps:
- uses: actions/checkout@v4
- name: Clean previous build
shell: bash
run: rm -rf build # self-hosted runners reuse the workspace volume; a stale build dir
# can leave the viewer linking a system libcurl instead of the vendored one
- name: Configure viewer build
shell: bash
run: |
mkdir -p build
cd build
cmake -G Ninja -DJFJOCH_VIEWER_ONLY=ON -DJFJOCH_USE_CUDA=${{ matrix.use_cuda }} ${{ env.LINUX_CMAKE_FLAGS }} -DCMAKE_BUILD_TYPE=Release ..
- name: Build viewer tgz
shell: bash
run: |
cd build
# The "viewer" component is now jfjoch_viewer alone -- rugnux ships as its own artifact
# (build:rugnux-tgz) and the two CLI tools are no longer packaged at all. The full tree is
# still built because the viewer links the Rugnux library for its in-process jobs.
ninja -j16
cpack
- name: Upload viewer tgz to release
if: github.ref_type == 'tag'
shell: bash
env:
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
run: |
set -euo pipefail
shopt -s nullglob
# CMakeLists.txt names the archive jfjoch_viewer-<version>-linux-{cuda<major>|cpu}.tar.gz;
# rename to .tgz (matching the Windows installer's release naming) before upload.
files=(build/jfjoch_viewer-*.tar.gz)
if [ ${#files[@]} -eq 0 ]; then
echo "No viewer archive found in build/"
exit 1
fi
for file in "${files[@]}"; do
tgz="${file%.tar.gz}.tgz"
mv "$file" "$tgz"
python3 gitea_upload_file.py "$tgz"
done
build-rugnux-tgz:
name: build:rugnux-tgz (x86_64)
runs-on: jfjoch_rocky8
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- name: Clean previous build
shell: bash
run: rm -rf build-rugnux # self-hosted runners reuse the workspace volume
- name: Configure rugnux build
shell: bash
run: |
cmake -G Ninja -S . -B build-rugnux -DJFJOCH_RUGNUX_ONLY=ON -DJFJOCH_USE_CUDA=ON \
${{ env.LINUX_CMAKE_FLAGS }} -DCMAKE_BUILD_TYPE=Release
- name: Build rugnux tgz
shell: bash
run: cmake --build build-rugnux --target package -j16
- name: Verify the artifact is self-contained
shell: bash
run: |
set -euo pipefail
rm -rf /tmp/rgx && mkdir -p /tmp/rgx
tar xf build-rugnux/rugnux-*.tar.gz -C /tmp/rgx
bin=$(find /tmp/rgx -name rugnux -type f)
# Each producer writes to a file and the greps read files, rather than piping into
# `grep -q`: grep exits at the first match, the producer then dies of SIGPIPE, and under
# `set -o pipefail` that becomes the pipeline's status -- which a leading `!` would turn
# into a pass, hiding the very thing being tested.
ldd "$bin" > /tmp/rgx.ldd 2>&1 || true
if grep -q cufft /tmp/rgx.ldd; then echo "cuFFT is linked dynamically"; exit 1; fi
find /tmp/rgx -name 'libcufft*' > /tmp/rgx.cufft
if [ -s /tmp/rgx.cufft ]; then echo "a libcufft is shipped in the tarball"; exit 1; fi
echo "self-contained: static cuFFT, nothing to ship beside it"
- name: Upload rugnux tgz to release
if: github.ref_type == 'tag'
shell: bash
env:
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
run: |
set -euo pipefail
shopt -s nullglob
# CMakeLists.txt names the archive rugnux-<version>-linux-{cuda<major>|cpu}.tar.gz;
# rename to .tgz before upload, as the viewer archive does.
files=(build-rugnux/rugnux-*.tar.gz)
if [ ${#files[@]} -eq 0 ]; then
echo "No rugnux archive found in build-rugnux/"
exit 1
fi
for file in "${files[@]}"; do
tgz="${file%.tar.gz}.tgz"
mv "$file" "$tgz"
python3 gitea_upload_file.py "$tgz"
done
build-rugnux-windows:
name: build:rugnux:windows
runs-on: windows-11-cuda-qt
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
# cmd, not bash: the Windows runner has no bash, and every cmake invocation needs the MSVC
# environment from vcvars64.bat first -- same shape as build-windows above. No Qt in
# CMAKE_PREFIX_PATH: JFJOCH_RUGNUX_ONLY is the portable subtree minus viewer/, so C:/deps
# (Eigen, ZLIB) is all it needs.
- name: Configure rugnux build
shell: cmd
run: |
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set "VSPATH=%%i"
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
cmake -G Ninja -B build-rugnux -DJFJOCH_RUGNUX_ONLY=ON -DJFJOCH_USE_CUDA=ON -DCMAKE_BUILD_TYPE=Release ${{ env.MSVC_ARCH_CMAKE_FLAGS }} -DCMAKE_PREFIX_PATH="C:/deps"
- name: Build rugnux
shell: cmd
run: |
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set "VSPATH=%%i"
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
cmake --build build-rugnux
- name: Build rugnux zip
shell: cmd
run: |
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set "VSPATH=%%i"
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat"
cd build-rugnux
cpack
- name: Upload rugnux zip to release
if: github.ref_type == 'tag'
shell: powershell
env:
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
run: |
# Zip named rugnux-<version>-win64-{cuda<major>|cpu}.zip (see CMakeLists.txt).
$files = Get-ChildItem -Path build-rugnux -Filter 'rugnux-*-win64-*.zip'
if ($files.Count -eq 0) { throw 'No rugnux zip found in build-rugnux/' }
foreach ($file in $files) {
python gitea_upload_file.py $file.FullName
if ($LASTEXITCODE -ne 0) { throw "Upload failed for $($file.Name)" }
}
build-rugnux-arm:
name: build:rugnux:aarch64 (cross)
# Cross-compiled on an x86_64 runner: nvcc, gcc and cmake all run native and only emit aarch64,
# so this costs about what a normal build costs. Uses the ordinary ubuntu2404 image, which now
# also carries the aarch64 toolchain and the CUDA sbsa cross tree.
#
# The toolchain file comes from the CHECKOUT, not from the image. It describes how to build this
# source, so it belongs with the source: baked into the image it could only be changed by
# rebuilding and re-pushing the image, and a fix to it would appear to have no effect.
runs-on: jfjoch_ubuntu2404
container:
image: gitea.psi.ch/leonarski_f/jfjoch_ubuntu2404:2508
steps:
- uses: actions/checkout@v4
- name: Clean previous build
shell: bash
run: rm -rf build-aarch64 # self-hosted runners reuse the workspace volume
- name: Configure (cross to aarch64 SBSA)
shell: bash
# NOT ${{ env.LINUX_CMAKE_FLAGS }}: that pins -march=x86-64-v3, which an aarch64 compiler
# rejects. The aarch64 baseline already has NEON and a vectorised round, so the Eigen gap
# that flag closes on x86 does not exist here.
# sm_90 = GH200 (Grace Hopper, e.g. ALPS), sm_121 = GB10 (DGX Spark). One tarball, both.
run: |
cmake -G Ninja -S . -B build-aarch64 \
-DCMAKE_TOOLCHAIN_FILE="$PWD/docker/ubuntu2404/aarch64-sbsa.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DJFJOCH_RUGNUX_ONLY=ON \
-DJFJOCH_USE_CUDA=ON \
-DJFJOCH_CUDA_ARCHITECTURES="90;121"
- name: Build and package
shell: bash
run: cmake --build build-aarch64 --target package -j16
# A cross build that silently produced x86_64 still packages cleanly, so assert the artifact.
- name: Verify the tarball is really aarch64
shell: bash
run: |
set -euo pipefail
tgz=$(ls build-aarch64/rugnux-*.tar.gz | head -1)
echo "checking $tgz"
rm -rf /tmp/tgzcheck && mkdir -p /tmp/tgzcheck
tar xf "$tgz" -C /tmp/tgzcheck
bin=$(find /tmp/tgzcheck -name rugnux -type f | head -1)
# Everything is captured to a file first and the greps read files. Piping a large
# producer into `grep -q` makes it die of SIGPIPE once grep exits, which `set -o pipefail`
# turns into a failure -- and on a negated check into a spurious PASS.
file "$bin" > /tmp/tc.bin
cat /tmp/tc.bin
if ! grep -q "ARM aarch64" /tmp/tc.bin; then echo "not an aarch64 binary"; exit 1; fi
# nothing x86 may have leaked in (libjpeg-turbo's ExternalProject is the classic culprit)
find /tmp/tgzcheck -type f -exec file {} + > /tmp/tc.arch
if grep -q "x86-64" /tmp/tc.arch; then echo "an x86-64 file leaked into the tarball"; exit 1; fi
# cuFFT must be linked statically: nothing to ship beside the binary, nothing to find
ldd "$bin" > /tmp/tc.ldd 2>&1 || true
if grep -q cufft /tmp/tc.ldd; then echo "cuFFT is linked dynamically"; exit 1; fi
# both GPU targets present in the fatbin: sm_90 = GH200, sm_121 = GB10
cuobjdump --list-elf "$bin" > /tmp/tc.elf
for arch in sm_90 sm_121; do
if ! grep -q "$arch" /tmp/tc.elf; then echo "fatbin is missing $arch"; exit 1; fi
done
echo "aarch64, self-contained, both GPU targets present"
- name: Upload tarball to release
if: github.ref_type == 'tag'
shell: bash
env:
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
run: |
set -euo pipefail
shopt -s nullglob
files=(build-aarch64/rugnux-*.tar.gz)
if [ ${#files[@]} -eq 0 ]; then
echo "No rugnux archive found in build-aarch64/"
exit 1
fi
for file in "${files[@]}"; do
tgz="${file%.tar.gz}.tgz"
mv "$file" "$tgz"
python3 gitea_upload_file.py "$tgz"
done
build-rpm:
name: build:rpm (${{ matrix.distro }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: jfjoch_rocky8
distro: rocky8
cmake_flags: -DJFJOCH_USE_CUDA=ON
pkg_glob: "*.rpm"
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el8/slsdet8-cuda12/upload
- runner: jfjoch_rocky8
distro: rocky8_sls9
cmake_flags: -DJFJOCH_USE_CUDA=ON -DSLS9=ON
pkg_glob: "*.rpm"
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el8/slsdet9-cuda12/upload
- runner: jfjoch_rocky8
distro: rocky8_nocuda
cmake_flags: -DJFJOCH_USE_CUDA=OFF
pkg_glob: "*.rpm"
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el8/slsdet8-nocuda/upload
- runner: jfjoch_rocky9
distro: rocky9
cmake_flags: -DJFJOCH_USE_CUDA=ON
pkg_glob: "*.rpm"
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el9/slsdet8-cuda13/upload
- runner: jfjoch_rocky9
distro: rocky9_sls9
cmake_flags: -DJFJOCH_USE_CUDA=ON -DSLS9=ON
pkg_glob: "*.rpm"
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el9/slsdet9-cuda13/upload
- runner: jfjoch_rocky9
distro: rocky9_nocuda
cmake_flags: -DJFJOCH_USE_CUDA=OFF
pkg_glob: "*.rpm"
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el9/slsdet8-nocuda/upload
- runner: jfjoch_ubuntu2204
distro: ubuntu2204
cmake_flags: -DJFJOCH_USE_CUDA=ON
pkg_glob: "*.deb"
upload_url: https://gitea.psi.ch/api/packages/mx/debian/pool/jammy/cuda13/upload
- runner: jfjoch_ubuntu2204
distro: ubuntu2204_nocuda
cmake_flags: -DJFJOCH_USE_CUDA=OFF
pkg_glob: "*.deb"
upload_url: https://gitea.psi.ch/api/packages/mx/debian/pool/jammy/nocuda/upload
- runner: jfjoch_ubuntu2404
distro: ubuntu2404
cmake_flags: -DJFJOCH_USE_CUDA=ON
pkg_glob: "*.deb"
upload_url: https://gitea.psi.ch/api/packages/mx/debian/pool/noble/cuda13/upload
- runner: jfjoch_ubuntu2404
distro: ubuntu2404_nocuda
cmake_flags: -DJFJOCH_USE_CUDA=OFF
pkg_glob: "*.deb"
upload_url: https://gitea.psi.ch/api/packages/mx/debian/pool/noble/nocuda/upload
steps:
- uses: actions/checkout@v4
- name: Clean previous build
shell: bash
run: rm -rf build # self-hosted runners reuse the workspace volume; a stale build dir
# can leave the viewer linking a system libcurl instead of the vendored one
- name: Setup build (cmake)
shell: bash
run: |
mkdir -p build
cd build
cmake -G Ninja -DJFJOCH_INSTALL_DRIVER_SOURCE=ON -DJFJOCH_VIEWER_BUILD=ON ${{ env.LINUX_CMAKE_FLAGS }} -DCMAKE_BUILD_TYPE=Release ${{ matrix.cmake_flags }} ..
- name: Build packages
shell: bash
run: |
cd build
ninja frontend
ninja -j16 package
- name: Upload packages
if: github.ref_type == 'tag'
shell: bash
env:
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
run: |
set -euo pipefail
cd build
shopt -s nullglob
files=(${{ matrix.pkg_glob }})
if [ ${#files[@]} -eq 0 ]; then
echo "No package files found for pattern: ${{ matrix.pkg_glob }}"
exit 1
fi
url="${{ matrix.upload_url }}"
if [[ "$url" == *"/rpm/"* ]]; then
url="${url}?sign=true"
fi
for file in "${files[@]}"; do
echo "Uploading $file -> $url"
curl --fail --user __token__:"$TOKEN" --upload-file "$file" "$url"
done
cd ..
# The viewer is shipped to the release as a Linux-version-agnostic .tgz by the
# build-viewer-tgz job; only the writer (and the XDS plugin .so on rocky8) go to the
# release as distro packages here.
if [ "${{ matrix.distro }}" = "rocky8_nocuda" ]; then
for file in build/jfjoch-writer*rpm build/xds-plugin/libjfjoch_xds_plugin.so.*; do
python3 gitea_upload_file.py "$file"
done
elif [ "${{ matrix.distro }}" = "rocky9_nocuda" ]; then
for file in build/jfjoch-writer*rpm; do
python3 gitea_upload_file.py "$file"
done
elif [ "${{ matrix.distro }}" = "ubuntu2204_nocuda" ]; then
for file in build/jfjoch*writer*.deb; do
python3 gitea_upload_file.py "$file"
done
fi
dials-test:
name: DIALS test
runs-on: jfjoch_rocky9
steps:
- uses: actions/checkout@v4
- name: Build processing test
shell: bash
run: |
mkdir -p build
cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ${{ env.LINUX_CMAKE_FLAGS }} ..
ninja -j16 jfjoch_hdf5_test
# dials.import before xia2.ssx, checking the image COUNT: dxtbx reports a short scan rather
# than failing when it cannot reach every image, and xia2.ssx still succeeds on the images it
# did get, so an exit code alone does not notice. Multi-file NXmxLegacy is exercised by the
# XDS jobs below but deliberately not here: dxtbx reads only the first data file of a legacy
# master (it takes one dataset out of NXdata and indexes it globally), so that combination
# cannot pass until the limitation is fixed upstream.
- name: Run DIALS processing on legacy format
shell: bash
run: |
source /opt/dials-v3-27-0/dials_env.sh
mkdir -p test01
cd test01
../build/tools/jfjoch_hdf5_test ../tests/test_data/compression_benchmark.h5 -n100 -o legacy
dials.import legacy_master.h5 | tee import.log
grep -q "num images: 100" import.log || { echo "DIALS did not read all 100 images"; exit 1; }
xia2.ssx image=legacy_master.h5 space_group=P43212 unit_cell=78.551,78.551,36.914,90.000,90.000,90.000
- name: Run DIALS processing on VDS (master + 4 linked image files)
shell: bash
run: |
source /opt/dials-v3-27-0/dials_env.sh
mkdir -p test02
cd test02
../build/tools/jfjoch_hdf5_test ../tests/test_data/compression_benchmark.h5 -n100 -f25 -V -o vds
dials.import vds_master.h5 | tee import.log
grep -q "num images: 100" import.log || { echo "DIALS did not read all 100 images"; exit 1; }
xia2.ssx image=vds_master.h5 space_group=P43212 unit_cell=78.551,78.551,36.914,90.000,90.000,90.000
- name: Run DIALS processing on single file format
shell: bash
run: |
source /opt/dials-v3-27-0/dials_env.sh
mkdir -p test03
cd test03
# -S writes <prefix>_master.h5, not <prefix>.h5 - naming this wrong meant xia2.ssx was
# pointed at a file that never existed, so the integrated format went untested.
../build/tools/jfjoch_hdf5_test ../tests/test_data/compression_benchmark.h5 -n100 -S -o single
dials.import single_master.h5 | tee import.log
grep -q "num images: 100" import.log || { echo "DIALS did not read all 100 images"; exit 1; }
xia2.ssx image=single_master.h5 space_group=P43212 unit_cell=78.551,78.551,36.914,90.000,90.000,90.000
# Everything above writes an UNTILTED detector, where every plausible encoding of the tilt
# agrees exactly - so none of it constrains the NXmx transformation chain. This does.
- name: Check tilted detector geometry against DIALS
shell: bash
run: |
source /opt/dials-v3-27-0/dials_env.sh
mkdir -p test04
cd test04
../build/tools/jfjoch_hdf5_test ../tests/test_data/compression_benchmark.h5 -n1 -S -o geom
dials.python ../tests/nxmx_geometry_dials_test.py geom_master.h5
xds-durin-test:
name: XDS test (durin plugin)
runs-on: jfjoch_rocky9
steps:
- uses: actions/checkout@v4
- name: Build processing test
shell: bash
run: |
mkdir -p build
cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ${{ env.LINUX_CMAKE_FLAGS }} ..
ninja -j16 jfjoch_hdf5_test
- name: Run XDS with Durin and legacy HDF5 format
shell: bash
run: |
cd tests/xds_durin
rm -rf *.h5 *.LP *.HKL
../../build/tools/jfjoch_hdf5_test ../../tests/test_data/compression_benchmark.h5 -n25 -f10
/opt/xds/xds_par
test -f IDXREF.LP
- name: Run XDS with Durin and VDS HDF5 format
shell: bash
run: |
cd tests/xds_durin
rm -rf *.h5 *.LP *.HKL
../../build/tools/jfjoch_hdf5_test ../../tests/test_data/compression_benchmark.h5 -n25 -f10 -V
/opt/xds/xds_par
test -f IDXREF.LP
- name: Run XDS with Durin and integrated HDF5 format
shell: bash
run: |
cd tests/xds_durin
rm -rf *.h5 *.LP *.HKL
../../build/tools/jfjoch_hdf5_test ../../tests/test_data/compression_benchmark.h5 -n25 -S
/opt/xds/xds_par
test -f IDXREF.LP
xds-test:
name: XDS test (JFJoch plugin)
runs-on: jfjoch_rocky9
steps:
- uses: actions/checkout@v4
- name: Build processing test
shell: bash
run: |
mkdir -p build
cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ${{ env.LINUX_CMAKE_FLAGS }} ..
ninja -j16 jfjoch_hdf5_test
ninja -j16 jfjoch_xds_plugin
- name: Run XDS with legacy HDF5 format
shell: bash
run: |
cd tests/xds
../../build/tools/jfjoch_hdf5_test ../../tests/test_data/compression_benchmark.h5 -n25 -f10
/opt/xds/xds_par
test -f IDXREF.LP
- name: Run XDS with VDS HDF5 format
shell: bash
run: |
cd tests/xds
rm -f *.h5 *.LP *.HKL
../../build/tools/jfjoch_hdf5_test ../../tests/test_data/compression_benchmark.h5 -n25 -f10 -V
/opt/xds/xds_par
test -f IDXREF.LP
- name: Run XDS with single HDF5 format
shell: bash
run: |
cd tests/xds
rm -f *.h5 *.LP *.HKL
../../build/tools/jfjoch_hdf5_test ../../tests/test_data/compression_benchmark.h5 -n25 -S
/opt/xds/xds_par
test -f IDXREF.LP
xds-neggia-test:
name: XDS test (neggia plugin)
runs-on: jfjoch_rocky9
steps:
- uses: actions/checkout@v4
- name: Build processing test
shell: bash
run: |
mkdir -p build
cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release ${{ env.LINUX_CMAKE_FLAGS }} ..
ninja -j16 jfjoch_hdf5_test
- name: Run XDS with Neggia and legacy HDF5 format
shell: bash
run: |
cd tests/xds_neggia
../../build/tools/jfjoch_hdf5_test ../../tests/test_data/compression_benchmark.h5 -n25 -f10
/opt/xds/xds_par
test -f IDXREF.LP
- name: Run XDS with Neggia and single HDF5 format
shell: bash
run: |
cd tests/xds_neggia
rm -f *.h5 *.LP *.HKL
../../build/tools/jfjoch_hdf5_test ../../tests/test_data/compression_benchmark.h5 -n25 -S
/opt/xds/xds_par
test -f IDXREF.LP
python-client:
name: Generate python client
runs-on: jfjoch_rocky8
steps:
- uses: actions/checkout@v4
- name: Build python client
shell: bash
run: bash gen_python_client.sh
- name: Upload Package to Gitea PyPI
if: github.ref_type == 'tag'
run: |
twine upload -u __token__ -p ${{ secrets.CI_PYPI_TOKEN }} --skip-existing dist/*
twine upload -u __token__ -p ${{ secrets.PIP_REPOSITORY_API_TOKEN }} --repository-url https://gitea.psi.ch/api/packages/mx/pypi dist/*
documentation:
name: Build documentation
runs-on: jfjoch_rocky8
steps:
- uses: actions/checkout@v4
- name: Build documentation
shell: bash
run: bash make_doc.sh
- name: Upload documentation
if: github.ref_type == 'tag'
shell: bash
run: |
git config --global user.email "filip.leonarski@psi.ch"
git config --global user.name "Filip Leonarski (Gitea)"
git checkout --orphan gitea-pages
git reset --hard
# Remove everything except .git and public
git clean -fdx -e public -e .git
# Move public contents to root and remove the now-empty public dir
shopt -s dotglob nullglob
mv public/* .
rm -rf public
git add .
git commit -m "Deploy site"
git push -f https://${{secrets.GITHUB_TOKEN}}@gitea.psi.ch/${{ github.repository }}.git gitea-pages
create-release:
name: Create release
runs-on: jfjoch_rocky8
if: github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true)
steps:
- uses: actions/checkout@v4
with:
persist-credentials: 'true' # Optional; should be the default
- name: Configure auth and fetch LFS
shell: bash
env:
GITEA_TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
run: |
git lfs install --local
AUTH=$(git config --local http.${{ github.server_url }}/.extraheader)
git config --local --unset http.${{ github.server_url }}/.extraheader
git config --local http.${{ github.server_url }}/${{ github.repository }}.git/info/lfs/objects/batch.extraheader "$AUTH"
git lfs pull
- name: Create release
env:
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
shell: bash
run: |
python3 gitea_create_release.py