#!/usr/bin/env modbuild # Olex2 - official open-source (BSD) Linux x86_64 distribution from OlexSys. # https://www.olexsys.org/olex2/docs/getting-started/installing-olex2/ # # Upstream ships NO installer and NOTHING to compile for Linux: a single # olex2-linux64.zip that already contains # olex2 GUI (wxWidgets) olex2c console/"headless" Olex2 # unirun vendor auto-updater (NOT used here) start vendor launcher (NOT used here) # cctbx/ lib/ (private CPython 3.8 + libs) ilib/ util/ etc/ sample_data/ ... # # BUT: since 2026 OlexSys builds the Linux binaries on an Ubuntu 24.04-class # toolchain: olex2c/olex2/unirun/NoSpherA2/libopenblas/cctbx *.so need # GLIBC_2.38 + GLIBCXX_3.4.32, while RHEL9 has glibc 2.34 / GLIBCXX_3.4.29 # (RHEL8: 2.28 / 3.4.25). A newer glibc cannot be supplied via LD_LIBRARY_PATH # (the dynamic loader is hard-wired into each ELF), so this recipe ships a # private glibc 2.39 + GCC 14 libstdc++/libgcc under $PREFIX/sysroot (frozen # Fedora 40 rpms from kojipkgs) and re-points the ELF executables at that # loader with patchelf (--set-interpreter + DT_RPATH). GTK2/X11/GL still come # from the host: older libs run fine on a newer glibc. The sysroot libc dir is # NEVER put on LD_LIBRARY_PATH - that would crash every system tool Olex2 spawns. # # Default pbuild::prep downloads every file (urls: in files/config.yaml), # checks the pinned sha256 (shasums:) and copies it into SRC_DIR (unpacker: # none). configure unpacks, install copies the vendor tree 1:1 into $PREFIX # (Olex2's BaseDir() is wherever the binaries sit) and adds $PREFIX/bin launchers. # # No errexit in modbuild hooks -> every critical step is die-guarded. # files fetched by prep (names must match urls:/shasums: in files/config.yaml) OLEX2_SYSROOT_RPMS=( glibc-2.39-37.fc40.x86_64.rpm libstdc++-14.2.1-3.fc40.x86_64.rpm libgcc-14.2.1-3.fc40.x86_64.rpm readline-8.2-8.fc40.x86_64.rpm # libreadline.so.8: RHEL8 has .so.7, vendor lib/ .so.6 libwebp-1.3.2-5.fc40.x86_64.rpm # libwebp.so.7: PIL + libtiff libdeflate-1.19-2.fc40.x86_64.rpm # libdeflate.so.0: vendor libtiff.so.5 (absent on RHEL8) libtiff-4.6.0-2.fc40.x86_64.rpm # libtiff.so.6: GUI binary (RHEL ships .so.5) + its deps: libjpeg-turbo-3.0.2-1.fc40.x86_64.rpm jbigkit-libs-2.1-29.fc40.x86_64.rpm liblerc-4.0.0-6.fc40.x86_64.rpm ) OLEX2_PATCHELF_TGZ="patchelf-0.18.0-x86_64.tar.gz" # ELF executables in the vendor tree that get the bundled loader (static ones, # e.g. hart, are detected and skipped) OLEX2_PATCH_EXES=(olex2c olex2 unirun NoSpherA2 hart_mpi) pbuild::configure() { local -r zip="${SRC_DIR}/olex2-${V}-linux64.zip" if [[ -f "${SRC_DIR}/olex2/olex2c" && -f "${SRC_DIR}/olex2/olex2" ]]; then # re-run after a failed configure/install: prep's stamp is set, the # tree is already unpacked - reuse it (the zip may be gone from SRC_DIR) echo "olex2: reusing unpacked tree in ${SRC_DIR}/olex2" else [[ -s "${zip}" ]] || std::die 42 "olex2: ${zip} not found - prep did not fetch it. \ Pre-stage the archive by hand as /olex2-${V}-linux64.zip (see README) and re-run (-f)." command -v unzip >/dev/null || std::die 42 "olex2: 'unzip' not on PATH on this build host" # partial downloads reused from distfiles are the classic failure: test the archive first unzip -tq "${zip}" >/dev/null || \ std::die 42 "olex2: archive is corrupt/truncated - rm /olex2-${V}-linux64.zip and re-run" # ~1 GB unpacked (cctbx + python + sample data); SRC_DIR is under --tmpdir unzip -q -o "${zip}" -d "${SRC_DIR}" || std::die 42 "olex2: unzip failed (disk/quota at --tmpdir?)" [[ -f "${SRC_DIR}/olex2/olex2c" && -f "${SRC_DIR}/olex2/olex2" ]] || \ std::die 42 "olex2: unexpected archive layout - expected ${SRC_DIR}/olex2/{olex2,olex2c}" rm -f "${zip}" # 255 MB copy; the distfiles cache keeps the original fi # --- private glibc 2.39 + libstdc++ 14 sysroot (Fedora 40 rpms) ---------- command -v rpm2cpio >/dev/null && command -v cpio >/dev/null || \ std::die 42 "olex2: need rpm2cpio and cpio on the build host (dnf install rpm cpio)" local -r sysroot="${SRC_DIR}/sysroot" local rpm rm -rf "${sysroot}" mkdir -p "${sysroot}" for rpm in "${OLEX2_SYSROOT_RPMS[@]}"; do [[ -s "${SRC_DIR}/${rpm}" ]] || std::die 42 "olex2: ${rpm} missing in SRC_DIR - prep did not fetch it" ( cd "${sysroot}" && rpm2cpio "${SRC_DIR}/${rpm}" | cpio -idm --quiet ) || \ std::die 42 "olex2: extracting ${rpm} failed" done # Fedora's glibc + libstdc++ install to /usr/lib64 but libgcc still to # /lib64, which on a real system is the filesystem package's symlink to # usr/lib64 - we do not extract that package, so recreate the merge here. if [[ -d "${sysroot}/lib64" && ! -L "${sysroot}/lib64" ]]; then mkdir -p "${sysroot}/usr/lib64" cp -r --preserve=mode,timestamps,links "${sysroot}/lib64/." "${sysroot}/usr/lib64/" && \ rm -rf "${sysroot}/lib64" && ln -s usr/lib64 "${sysroot}/lib64" || \ std::die 42 "olex2: merging sysroot/lib64 into sysroot/usr/lib64 failed" fi local ld libc libdir ld="$(find "${sysroot}" -type f -name 'ld-linux-x86-64.so.2' -print -quit)" libc="$(find "${sysroot}" -type f -name 'libc.so.6' -print -quit)" [[ -n "${ld}" && -n "${libc}" ]] || std::die 42 "olex2: ld-linux-x86-64.so.2 / libc.so.6 not found in the extracted glibc rpm" libdir="$(dirname "${libc}")" [[ -e "${libdir}/libstdc++.so.6" && -e "${libdir}/libgcc_s.so.1" ]] || \ std::die 42 "olex2: libstdc++.so.6 / libgcc_s.so.1 not next to libc.so.6 in ${libdir}" # the loader is static: it must run on any x86_64 host "${ld}" --version > "${SRC_DIR}/ldso.version" 2>&1 || \ std::die 42 "olex2: the bundled ld.so does not run on this host" echo "olex2: bundled loader: $(head -n1 "${SRC_DIR}/ldso.version")" # --- patchelf (static release binary) -------------------------------------- [[ -s "${SRC_DIR}/${OLEX2_PATCHELF_TGZ}" ]] || std::die 42 "olex2: ${OLEX2_PATCHELF_TGZ} missing in SRC_DIR - prep did not fetch it" mkdir -p "${SRC_DIR}/patchelf" tar -xzf "${SRC_DIR}/${OLEX2_PATCHELF_TGZ}" -C "${SRC_DIR}/patchelf" || std::die 42 "olex2: unpacking patchelf failed" "${SRC_DIR}/patchelf/bin/patchelf" --version >/dev/null || std::die 42 "olex2: patchelf binary does not run on this host" } pbuild::compile() { : } pbuild::install() { local -r src="${SRC_DIR}/olex2" local -r patchelf="${SRC_DIR}/patchelf/bin/patchelf" # 1. vendor tree, flat into $PREFIX (= BaseDir). Slow on /opt/psi: 12k files. # NOT `cp -a`: on /opt/psi chown is refused, so -a ends with thousands of # "failed to preserve ownership ... Permission denied" and a non-zero exit # (that killed the first build). Keep mode/timestamps/links only. cp -r --preserve=mode,timestamps,links "${src}/." "${PREFIX}/" || \ std::die 42 "olex2: copying the vendor tree into ${PREFIX} failed" # 2. private glibc/libstdc++ sysroot rm -rf "${PREFIX}/sysroot" cp -r --preserve=mode,timestamps,links "${SRC_DIR}/sysroot" "${PREFIX}/sysroot" || \ std::die 42 "olex2: copying the sysroot into ${PREFIX} failed" # 3. zip loses nothing, but make sure the ELF binaries are executable and # everything is world-readable on the shared install chmod 755 "${PREFIX}"/{olex2,olex2c,unirun,start,NoSpherA2,hart,hart_mpi} 2>/dev/null || true chmod -R a+rX "${PREFIX}" || true # 4. never auto-update a shared, read-only install (vendor default is # update=Always + ask_update=true, driven by unirun). Best effort - the # launchers never call unirun anyway. if [[ -f "${PREFIX}/usettings.dat" ]]; then sed -i -e 's/^update=.*/update=Never/' -e 's/^ask_update=.*/ask_update=false/' \ "${PREFIX}/usettings.dat" || true fi # 5. re-point the ELF executables at the bundled loader. DT_RPATH (not # RUNPATH, hence --force-rpath) so that everything dlopen()ed later by the # embedded Python (cctbx *.so, lib-dynload, _ac7util) resolves libc, # libstdc++, libopenblas ... through the same list. Host dirs (/lib64) # follow via the loader's defaults + /etc/ld.so.cache. local ld libdir rpath exe ld="$(find "${PREFIX}/sysroot" -type f -name 'ld-linux-x86-64.so.2' -print -quit)" libdir="$(dirname "$(find "${PREFIX}/sysroot" -type f -name 'libc.so.6' -print -quit)")" [[ -x "${ld}" && -d "${libdir}" ]] || std::die 42 "olex2: sysroot loader/libdir not found under ${PREFIX}/sysroot" rpath="${libdir}:${PREFIX}/lib:${PREFIX}/ilib:${PREFIX}/cctbx/cctbx_build/lib:${PREFIX}:\$ORIGIN" for exe in "${OLEX2_PATCH_EXES[@]}"; do [[ -f "${PREFIX}/${exe}" ]] || continue if ! "${patchelf}" --print-interpreter "${PREFIX}/${exe}" >/dev/null 2>&1; then echo "olex2: ${exe} is statically linked - left untouched" continue fi "${patchelf}" --set-interpreter "${ld}" --force-rpath --set-rpath "${rpath}" "${PREFIX}/${exe}" || \ std::die 42 "olex2: patchelf failed on ${exe}" echo "olex2: ${exe} -> interpreter ${ld}" done # 6. launchers: $PREFIX/bin/olex2 (GUI) and $PREFIX/bin/olex2c (console). # Two copies of one script (it dispatches on basename $0, so no symlinks). # Only $PREFIX/bin goes on PATH - the raw binaries and the vendor # 'start' (auto-updater) stay off it. mkdir -p "${PREFIX}/bin" install -m 755 "${BUILDBLOCK_DIR}/files/olex2-launcher" "${PREFIX}/bin/olex2" || \ std::die 42 "olex2: installing bin/olex2 launcher failed" install -m 755 "${BUILDBLOCK_DIR}/files/olex2-launcher" "${PREFIX}/bin/olex2c" || \ std::die 42 "olex2: installing bin/olex2c launcher failed" # 7. prove the patched olex2c resolves through the bundled glibc. The # build host (Ra, RHEL8) is not the execution host (RHEL9): a HOST lib # missing here ("=> not found", or a hard "cannot open shared object # file" that aborts the listing) is reported, not fatal. Anything else # (segfault, page alignment, GLIBC_PRIVATE ...) means patchelf/sysroot # is broken and the build must stop. local -r ldd="${SRC_DIR}/olex2c.ldd" local rc=0 "${ld}" --list "${PREFIX}/olex2c" > "${ldd}" 2>&1 || rc=$? echo "olex2: --- ${ld} --list olex2c (rc=${rc}) ---" cat "${ldd}" echo "olex2: --- end of listing ---" if grep -q "${libdir}/libc.so.6" "${ldd}"; then if grep -q 'not found' "${ldd}"; then echo "olex2: WARNING - host libraries missing on THIS host; dnf install them on the execution hosts:" grep 'not found' "${ldd}" fi elif grep -q 'cannot open shared object file' "${ldd}"; then echo "olex2: WARNING - cannot verify the loader on THIS host, a host library is missing here:" grep 'cannot open shared object file' "${ldd}" echo "olex2: install it on the execution hosts (or bundle it in the sysroot) and re-check with" echo "olex2: ${ld} --list ${PREFIX}/olex2c" else std::die 42 "olex2: olex2c does not resolve through the bundled glibc (rc=${rc}, listing above) - patchelf/sysroot step broken" fi # 8. what the modulefile / launchers rely on local f for f in olex2 olex2c cctbx/cctbx_build/lib lib/libpython3.8.so.1.0 lib/python3.8 util/pyUtil/initpy.py bin/olex2c \ sysroot/usr/lib64/libc.so.6 sysroot/usr/lib64/libstdc++.so.6 sysroot/usr/lib64/libgcc_s.so.1 \ sysroot/usr/lib64/libreadline.so.8 sysroot/usr/lib64/libwebp.so.7 sysroot/usr/lib64/libdeflate.so.0 \ sysroot/usr/lib64/libtiff.so.6 sysroot/usr/lib64/libLerc.so.4; do [[ -e "${PREFIX}/${f}" ]] || std::die 42 "olex2: ${PREFIX}/${f} missing after install - module would be broken" done # 9. cctbx first-run setup. On its FIRST start Olex2 regenerates # cctbx/cctbx_build/libtbx_env (+ dispatchers) by running libtbx's # configure.py into BaseDir (util/pyUtil/PyToolLib/path_utils.py, # cold_start). Users cannot write there, so do it now as the owner. # Upstream quirk: the vendor ships SCons in its site-packages, so # libtbx believes it can build and the scitbx refresh then fails on # Fortran sources that are not shipped ("[Errno 2] ... scitbx/lbfgs/ # sdrive.f"). That happens AFTER libtbx_env is pickled (env_config.py), # so the first run printing "There is a problem with the cctbx" is # expected; what matters is that the second start is clean. local -r home="${SRC_DIR}/home" # keep ~/.olex2 of the build user out of it mkdir -p "${home}" printf 'exit\n' | HOME="${home}" timeout 900 "${PREFIX}/bin/olex2c" > "${SRC_DIR}/olex2c.firstrun.log" 2>&1 || true if [[ ! -f "${PREFIX}/cctbx/cctbx_build/libtbx_env" ]]; then cat "${SRC_DIR}/olex2c.firstrun.log" std::die 42 "olex2: first start did not create cctbx/cctbx_build/libtbx_env (log above) - host libs missing? see step 7" fi # 10. second start must be clean, and refine the shipped sucrose sample # (real cctbx/smtbx use: libgomp, openblas, PIL ... all exercised) local -r smoke="${SRC_DIR}/smoke" rm -rf "${smoke}" mkdir -p "${smoke}" cp "${PREFIX}/sample_data/sucrose/"* "${smoke}/" ( cd "${smoke}" && printf 'reap sucrose.res\nrefine 2\nexit\n' | \ HOME="${home}" timeout 900 "${PREFIX}/bin/olex2c" > "${SRC_DIR}/olex2c.smoke.log" 2>&1 ) || true echo "olex2: --- second start + refine smoke test, tail of ${SRC_DIR}/olex2c.smoke.log ---" tail -n 40 "${SRC_DIR}/olex2c.smoke.log" echo "olex2: --- end ---" grep -q 'problem with the cctbx' "${SRC_DIR}/olex2c.smoke.log" && \ std::die 42 "olex2: cctbx still not usable on the second start - full log: ${SRC_DIR}/olex2c.smoke.log" if grep -qE 'ImportError|cannot open shared object|Traceback' "${SRC_DIR}/olex2c.smoke.log"; then echo "olex2: WARNING - errors during the smoke test on THIS host (full log: ${SRC_DIR}/olex2c.smoke.log):" grep -E 'ImportError|cannot open shared object|Error' "${SRC_DIR}/olex2c.smoke.log" | head -n 20 fi # the first run wrote libtbx_env, dispatchers, .pyc files as the build user chmod -R a+rX "${PREFIX}" || true echo "olex2: installed upstream build '$(cat "${PREFIX}/version.txt" 2>/dev/null)' (tag $(cat "${PREFIX}/olex2.tag" 2>/dev/null))" }