diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b7e9fea..922d5aac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,24 @@ ## OpenBLAS v0.3.34 (v0.3.34) -### 0.3.34.237.0 (2026-09~06) +### 0.3.34.237.1 (2026-10-09) + +- Build every manylinux and musllinux wheel without a runtime dependency on + libgfortran, and therefore without libquadmath, which is only ever pulled in + as a dependency of libgfortran itself. This drops ~1MB (compressed) from the + wheels that bundled both and, since libquadmath is LGPL-2.1-or-later with no + GCC Runtime Library Exception, removes an LGPL obligation along with it. The + two `x**n` entry points are taken as-is out of the toolchain's libgfortran.a + so results stay bit-for-bit identical; see tools/gfortran_compat.sh. macOS + is unchanged for now (ld64 has no -Wl,--exclude-libs), and Windows already + static-links libgfortran. A toolchain that cannot support it falls back to + linking libgfortran as before. += Update windows devstudio 2022 to 2026 + + +### 0.3.34.237.0 (2026-09-06) - pick up a fix for SVE indentification -### 0.3.34.106.0 (2026-08~08) +### 0.3.34.106.0 (2026-08-08) - pick up a fix for windows threading ### 0.3.34.0.0 (2026-07-17) diff --git a/ci-repair-wheel.sh b/ci-repair-wheel.sh index 6b2f4209..137e759c 100755 --- a/ci-repair-wheel.sh +++ b/ci-repair-wheel.sh @@ -22,13 +22,22 @@ else # Add an RPATH to libgfortran: # https://github.com/pypa/auditwheel/issues/451 # Use zipfile since the manylinux images do not have `zip` - python3 -c " + # + # Platforms built with NO_LIBGFORTRAN=1 bundle no libgfortran (and so no + # libquadmath, which is the only reason this fixup is needed at all), so + # there is nothing to repair there. Count what we extracted and skip the + # rest when the wheel is already clean. + n_gfortran=$(python3 -c " import re, sys, zipfile, pathlib whl = next(pathlib.Path(sys.argv[1]).glob('*.whl')) with zipfile.ZipFile(whl, 'a') as z: members = [m for m in z.namelist() if re.search(r'libgfortran', m)] z.extractall(members=members) - " "$1" +print(len(members)) + " "$1") + if [ "$n_gfortran" = "0" ]; then + echo "wheel bundles no libgfortran; skipping the auditwheel #451 rpath fixup" + else patchelf --force-rpath --set-rpath '$ORIGIN' */lib/libgfortran* python3 -c " import sys, zipfile, pathlib, glob @@ -46,6 +55,7 @@ with zipfile.ZipFile(whl, 'w', compression=zipfile.ZIP_DEFLATED) as z: for item, data in entries.items(): z.writestr(item, data) " "$1" + fi mkdir -p /output # copy libs/openblas*.tar.gz to dist/ cp libs/openblas*.tar.gz /output/ diff --git a/ci-test.sh b/ci-test.sh index 0cdc0c1d..b4621eed 100755 --- a/ci-test.sh +++ b/ci-test.sh @@ -30,3 +30,43 @@ fi $PYTHON -m pip install pkgconf $PYTHON -m pkgconf scipy-openblas --cflags + +# Check the Fortran-runtime story of the installed wheel. +# +# Note this asks the *library* what happened rather than asking +# want_no_libgfortran what we intended: build_gfortran_compat falls back to +# linking libgfortran on toolchains it cannot support (no static +# libgfortran.a on riscv64, for instance), and such a wheel legitimately +# still bundles the runtime. The build already asserts the clean case on the +# library it produces; repeating it here catches anything +# auditwheel/delocate might reintroduce while repairing the wheel. +source tools/gfortran_compat.sh + +if [ "${INTERFACE64}" != "1" ]; then + pkgdir=$($PYTHON -c "import scipy_openblas32 as m, pathlib; print(pathlib.Path(m.__file__).parent)") +else + pkgdir=$($PYTHON -c "import scipy_openblas64 as m, pathlib; print(pathlib.Path(m.__file__).parent)") +fi +echo "checking installed package $pkgdir" + +openblas_lib=$(ls "$pkgdir"/lib/libscipy_openblas*.so "$pkgdir"/lib/libscipy_openblas*.dylib 2>/dev/null | head -1 || true) +if [ -z "$openblas_lib" ]; then + echo "FAIL: no OpenBLAS library found under $pkgdir/lib" + exit 1 +fi + +if [ -n "$(fortran_runtime_deps "$openblas_lib")" ]; then + echo "NOTE: $(basename "$openblas_lib") links the Fortran runtime, so this" + echo " platform took the fallback path in build_gfortran_compat and the" + echo " wheel is expected to bundle it. Nothing to assert." +else + # The library needs no Fortran runtime, so the wheel must not ship one. + found=$(find "$pkgdir" \( -name 'libgfortran*' -o -name 'libquadmath*' \) -print) + if [ -n "$found" ]; then + echo "FAIL: $(basename "$openblas_lib") needs no Fortran runtime, yet the" + echo " wheel still bundles it:" + echo "$found" + exit 1 + fi + assert_no_fortran_runtime "$openblas_lib" +fi diff --git a/patches/0001-allow-building-without-libgfortran.patch b/patches/0001-allow-building-without-libgfortran.patch new file mode 100644 index 00000000..e3e9e2f1 --- /dev/null +++ b/patches/0001-allow-building-without-libgfortran.patch @@ -0,0 +1,68 @@ +diff --git a/Makefile.system b/Makefile.system +index e8e732e9c..90c814ebd 100644 +--- a/Makefile.system ++++ b/Makefile.system +@@ -1207,15 +1207,17 @@ FCOMMON_OPT += -Wall + FCOMMON_OPT += -frecursive + # work around ABI problem with passing single-character arguments + FCOMMON_OPT += -fno-optimize-sibling-calls +-#Don't include -lgfortran, when NO_LAPACK=1 or lsbcc ++#Don't include -lgfortran, when NO_LAPACK=1 or lsbcc or NO_LIBGFORTRAN=1 + ifneq ($(NOFORTRAN), 1) + ifneq ($(NOFORTRAN), 2) + ifneq ($(NO_LAPACK), 1) ++ifneq ($(NO_LIBGFORTRAN), 1) + EXTRALIB += -lgfortran + endif + endif + endif + endif ++endif + ifdef NO_BINARY_MODE + ifeq ($(ARCH), $(filter $(ARCH),mips64)) + ifdef BINARY64 +@@ -1917,6 +1919,31 @@ export CROSS + export CROSS_SUFFIX + export NOFORTRAN + export C_LAPACK ++# NO_LIBGFORTRAN=1 builds a library with no runtime dependency on the Fortran ++# runtime. LIBGFORTRAN_COMPAT must point at an archive supplying the few ++# libgfortran entry points the Fortran LAPACK sources reference; the link ++# fails loudly if it does not cover them all. libquadmath goes away along ++# with libgfortran, being only ever a dependency of libgfortran itself. ++# ++# The archive is kept out of EXTRALIB on purpose: EXTRALIB is what ++# Makefile.install writes into the installed .pc file, and a build-time ++# absolute path has no business being there. It goes into ++# LIBGFORTRAN_COMPAT_LINK, which the shared-library link rules add directly, ++# and into FEXTRALIB, which is what the macOS dylib rule uses. ++# ++# Kept here, at the end and at top level, so it sees the final value of ++# FEXTRALIB whatever the USE_OPENMP and compiler conditionals above did. ++ifeq ($(NO_LIBGFORTRAN), 1) ++FEXTRALIB := $(filter-out -lgfortran -lquadmath,$(FEXTRALIB)) ++ifneq ($(LIBGFORTRAN_COMPAT), ) ++LIBGFORTRAN_COMPAT_LINK = -Wl,--exclude-libs,$(notdir $(LIBGFORTRAN_COMPAT)) $(LIBGFORTRAN_COMPAT) ++FEXTRALIB += $(LIBGFORTRAN_COMPAT_LINK) ++endif ++endif ++ ++export LIBGFORTRAN_COMPAT ++export LIBGFORTRAN_COMPAT_LINK ++export NO_LIBGFORTRAN + export NO_FBLAS + export EXTRALIB + export CEXTRALIB +diff --git a/exports/Makefile b/exports/Makefile +index c4633143b..55f086297 100644 +--- a/exports/Makefile ++++ b/exports/Makefile +@@ -213,7 +213,7 @@ else + ifneq ($(C_COMPILER), LSB) + $(CC) $(CFLAGS) $(LDFLAGS) -shared -o ../$(LIBSONAME) \ + -Wl,--whole-archive $< -Wl,--no-whole-archive \ +- -Wl,-soname,$(INTERNALNAME) $(EXTRALIB) ++ -Wl,-soname,$(INTERNALNAME) $(EXTRALIB) $(LIBGFORTRAN_COMPAT_LINK) + $(CC) $(CFLAGS) $(LDFLAGS) -w -o linktest linktest.c ../$(LIBSONAME) $(FEXTRALIB) && echo OK. + else + #for LSB diff --git a/pyproject.toml b/pyproject.toml index 7538ef04..13381375 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "scipy-openblas64" # v0.3.34-237-g1ad90fb1 -version = "0.3.34.237.0" +version = "0.3.34.237.1" requires-python = ">=3.7" description = "Provides OpenBLAS for python packaging" readme = "README.md" diff --git a/tools/build_steps.sh b/tools/build_steps.sh index 9fc5b75b..84c8dac0 100644 --- a/tools/build_steps.sh +++ b/tools/build_steps.sh @@ -7,6 +7,38 @@ ROOT_DIR=$(dirname $(dirname "${BASH_SOURCE[0]}")) MB_PYTHON_VERSION=3.9 +source $(dirname "${BASH_SOURCE[0]}")/gfortran_compat.sh + +# Whether to build without a runtime dependency on libgfortran (and so +# without libquadmath, which is only ever a dependency of libgfortran itself). +# +# Enabled for every manylinux and musllinux target. The set of libgfortran +# entry points the Fortran LAPACK sources reference is identical on all of +# them -- _gfortran_concat_string and _gfortran_etime, plus _gfortran_pow_r*_i8 +# for INTERFACE64 -- because it comes from the LAPACK Fortran itself +# (`SIDE // TRANS`, `x**n`) rather than from anything architecture specific. +# If a toolchain cannot support it anyway, build_gfortran_compat returns +# non-zero and the build falls back to linking libgfortran as before. +# +# macOS is excluded for now: the link needs -Wl,--exclude-libs to keep the +# compat symbols out of the dylib's export table, and ld64 has no equivalent, +# so it would be a hard link failure rather than a fallback. See +# tools/gfortran_compat.sh. Windows never gets here -- it uses +# tools/build_steps_windows.sh, which already static-links libgfortran and +# asserts that libquadmath is not pulled in. +# +# Set NO_LIBGFORTRAN=0 in a CI matrix row to opt that row out, or +# NO_LIBGFORTRAN=1 to force it on. +function want_no_libgfortran { + # want_no_libgfortran (plat accepted but not currently needed -- + # the decision is per-OS, and callers already have it to hand) + [ -n "$NO_LIBGFORTRAN" ] && { [ "$NO_LIBGFORTRAN" = "1" ]; return $?; } + case "$(uname -s)" in + Linux) return 0 ;; + *) return 1 ;; + esac +} + function before_build { # install gfortran, objconv on macOS if [ "$(uname -s)" == "Darwin" ]; then @@ -169,6 +201,25 @@ function build_lib { ;; esac interface_flags="$interface_flags SYMBOLPREFIX=scipy_ LIBNAMEPREFIX=scipy_ FIXED_LIBNAME=1" + + # Drop the libgfortran/libquadmath runtime dependency where we can. If + # the toolchain cannot support it (no static libgfortran.a, or the + # objects we need are no longer self-contained) fall back to the old + # behaviour rather than failing the build -- see tools/gfortran_compat.sh. + local compat_lib="" + if want_no_libgfortran "$plat"; then + if compat_lib=$(build_gfortran_compat "$PWD/build/gfortran_compat" "$interface64"); then + # TIMER=NONE selects LAPACK's second_NONE.f/dsecnd_NONE.f, which + # removes the only reference to _gfortran_etime. SECOND/DSECND + # then return 0.0, as they already do in a C_LAPACK build. + interface_flags="$interface_flags NO_LIBGFORTRAN=1 LIBGFORTRAN_COMPAT=$compat_lib TIMER=NONE" + echo "building without a libgfortran runtime dependency" + else + compat_lib="" + echo "WARNING: cannot drop libgfortran on ${MB_ML_LIBC:-manylinux}-$plat, linking it as usual" + fi + fi + mkdir -p libs set -x git config --global --add safe.directory '*' @@ -218,6 +269,26 @@ function build_lib { rm $BUILD_PREFIX/lib/pkgconfig/scipy-openblas.pc.bak fi + if [ -n "$compat_lib" ]; then + # The shared library picked these up from the link line (see + # LIBGFORTRAN_COMPAT_LINK in the patch), but the static library is + # assembled from OpenBLAS's own objects only. Add them so + # that linking libscipy_openblas*.a does not need libgfortran either + # -- the .pc file no longer advertises -lgfortran in Libs.private. + # Done after the INTERFACE64 branch above, which overwrites the + # installed archive with the symbol-renamed one. The renaming does + # not touch _gfortran_* names, so order is safe either way. + local compat_dir=$(dirname "$compat_lib") + for static_lib in $BUILD_PREFIX/lib/libscipy_openblas*.a; do + # An unmatched glob arrives as a literal, and `ar crs` would + # happily create an archive at that bogus path. + [ -f "$static_lib" ] || continue + ar crs "$static_lib" "$compat_dir"/*.o + done + assert_no_fortran_runtime $BUILD_PREFIX/lib/libscipy_openblas*.so \ + $BUILD_PREFIX/lib/libscipy_openblas*.dylib + fi + local out_name="openblas.tar.gz" tar zcvf libs/$out_name \ $BUILD_PREFIX/include/*blas* \ diff --git a/tools/build_steps_win_arm64.bat b/tools/build_steps_win_arm64.bat index de6e5bdb..9edc1260 100755 --- a/tools/build_steps_win_arm64.bat +++ b/tools/build_steps_win_arm64.bat @@ -80,7 +80,10 @@ REM sed -e "s/^VERSION = .*/VERSION = ${version}/" -i.bak Makefile.rule for /f "tokens=3 delims= " %%v in ('findstr /b "version = " ..\pyproject.toml') do set version=%%v set version=%version:"=% -powershell -Command "(Get-Content OpenBLAS/Makefile.rule) -replace '^VERSION = .*', 'VERSION = %version%' | Set-Content Makefile.rule" +REM cwd is already OpenBLAS here, so the source is Makefile.rule, not +REM OpenBLAS/Makefile.rule -- that path never existed, Get-Content errored, +REM and the VERSION substitution silently never happened. +powershell -Command "(Get-Content Makefile.rule) -replace '^VERSION = .*', 'VERSION = %version%' | Set-Content Makefile.rule" REM Set suffixed-ILP64 flags if "%if_bits%"=="64" ( @@ -94,9 +97,47 @@ if exist build (rmdir /S /Q build || exit /b 1) mkdir build || exit /b 1 & cd build || exit /b 1 echo Setting up ARM64 Developer Command Prompt and running CMake... - -REM Initialize VS ARM64 environment -CALL "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsarm64.bat" + +REM Initialize VS ARM64 environment. +REM +REM Ask vswhere where Visual Studio is rather than hardcoding the version and +REM edition: the windows-11-arm image migrated from VS 2022 to VS 2026 in +REM September 2026 (actions/runner-images#14602), which broke the old +REM hardcoded 2022\Enterprise path. That failed silently -- CALL on a missing +REM batch file sets no errorlevel worth checking here -- so the vcvars +REM environment was simply never applied and the first visible symptom was +REM CMake reporting "CMAKE_MT-NOTFOUND" and "Detecting C compiler ABI info - +REM failed", because mt.exe and the MSVC/SDK paths were missing. Fail loudly +REM instead, so the next image change is obvious from the log. +set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +if not exist "%VSWHERE%" set "VSWHERE=%ProgramFiles%\Microsoft Visual Studio\Installer\vswhere.exe" +if not exist "%VSWHERE%" ( + echo Error: cannot find vswhere.exe, so cannot locate Visual Studio. + exit /b 1 +) +set "VSINSTALL=" +for /f "usebackq delims=" %%i in (`"%VSWHERE%" -latest -prerelease -products * -property installationPath`) do set "VSINSTALL=%%i" +if not defined VSINSTALL ( + echo Error: vswhere.exe reported no Visual Studio installation. + exit /b 1 +) +set "VCVARSALL=%VSINSTALL%\VC\Auxiliary\Build\vcvarsall.bat" +if not exist "%VCVARSALL%" ( + echo Error: no VC\Auxiliary\Build\vcvarsall.bat under "%VSINSTALL%". + exit /b 1 +) +echo Using Visual Studio at "%VSINSTALL%" +CALL "%VCVARSALL%" arm64 +if errorlevel 1 ( + echo Error: "%VCVARSALL%" arm64 failed. + exit /b 1 +) +where mt.exe >NUL 2>&1 +if errorlevel 1 ( + echo Error: mt.exe is not on PATH after vcvarsall; CMake will fail with + echo CMAKE_MT-NOTFOUND. The Windows SDK component may be missing. + exit /b 1 +) REM Prefer LLVM flang PATH=C:\Program Files\LLVM\bin;%PATH% diff --git a/tools/gfortran_compat.c b/tools/gfortran_compat.c new file mode 100644 index 00000000..1811a4e9 --- /dev/null +++ b/tools/gfortran_compat.c @@ -0,0 +1,71 @@ +/* + * Replacement for the one libgfortran runtime entry point that OpenBLAS's + * Fortran LAPACK references and that cannot be lifted straight out of + * libgfortran.a as a self-contained object. + * + * The other entry points LAPACK needs are handled without any hand-written + * code at all (see tools/gfortran_compat.sh): + * + * _gfortran_pow_r4_i8, _gfortran_pow_r8_i8 + * taken verbatim as compiled objects out of the toolchain's + * libgfortran.a; both are fully self-contained. + * _gfortran_etime + * not referenced at all once LAPACK is built with TIMER=NONE, which + * selects the in-tree second_NONE.f / dsecnd_NONE.f. + * + * Provenance of the code below: the semantics follow the `s_cat` helper of + * the f2c prologue already vendored in the OpenBLAS tree -- see the top of + * OpenBLAS/lapack-netlib/INSTALL/dsecnd_NONE.c -- distributed under LAPACK's + * BSD-3 terms, and what OpenBLAS's own C_LAPACK build already uses to + * evaluate Fortran CHARACTER concatenation. No GCC or libgfortran source + * was consulted. + * + * In OpenBLAS's LAPACK the only call shape is the two-character OPTS + * argument of ILAENV, e.g. + * + * NB = ILAENV( 1, 'DORMQR', SIDE // TRANS, M, N, K, -1 ) + * + * where the destination length is exactly len1 + len2, so neither the + * truncation nor the blank-padding branch is reachable from LAPACK. Both + * are implemented anyway so the function matches Fortran CHARACTER + * assignment semantics for any caller. + */ + +#include +#include + +/* + * gfortran passes CHARACTER lengths as a signed integer the width of a + * pointer (gcc >= 8; the GFORTRAN_8 ABI version node). + * + * Hidden visibility keeps the symbol out of the shared library's export + * table: it is an implementation detail of this build, not part of + * OpenBLAS's ABI, and must not interpose on a real libgfortran that happens + * to be loaded into the same process. A hidden definition still satisfies + * the references from the LAPACK objects linked alongside it. + */ +__attribute__((visibility("hidden"))) +void _gfortran_concat_string(ptrdiff_t destlen, char *dest, + ptrdiff_t len1, const char *s1, + ptrdiff_t len2, const char *s2); + +void _gfortran_concat_string(ptrdiff_t destlen, char *dest, + ptrdiff_t len1, const char *s1, + ptrdiff_t len2, const char *s2) +{ + ptrdiff_t nc; + + nc = (len1 < destlen) ? len1 : destlen; + memcpy(dest, s1, (size_t) nc); + dest += nc; + destlen -= nc; + + nc = (len2 < destlen) ? len2 : destlen; + memcpy(dest, s2, (size_t) nc); + dest += nc; + destlen -= nc; + + if (destlen > 0) { + memset(dest, ' ', (size_t) destlen); + } +} diff --git a/tools/gfortran_compat.sh b/tools/gfortran_compat.sh new file mode 100644 index 00000000..16c4051a --- /dev/null +++ b/tools/gfortran_compat.sh @@ -0,0 +1,235 @@ +#!bash +# +# Build a small archive that satisfies the handful of libgfortran runtime +# entry points OpenBLAS's Fortran LAPACK references, so that the resulting +# libscipy_openblas has no runtime dependency on libgfortran -- and therefore +# none on libquadmath either, which is only ever pulled in as a dependency of +# libgfortran itself. +# +# Why this is worth doing: +# * libquadmath is LGPL-2.1-or-later and, unlike libgfortran, carries no GCC +# Runtime Library Exception, so shipping it in these wheels brings an +# LGPL obligation along with it. +# * it drops roughly 1MB (compressed) from every wheel that currently +# bundles both libraries. +# * it removes the need for the libgfortran RPATH fixup in +# ci-repair-wheel.sh, which only exists so the bundled libgfortran can +# find the bundled libquadmath (auditwheel issue #451). +# +# The library needs exactly these symbols (verified with +# `nm -D --undefined-only` on the shipped wheels, on every Linux arch): +# +# _gfortran_concat_string always +# _gfortran_etime always +# _gfortran_pow_r4_i8 INTERFACE64 builds only +# _gfortran_pow_r8_i8 INTERFACE64 builds only +# +# The two pow entry points are taken verbatim as compiled objects out of the +# toolchain's own libgfortran.a. They are fully self-contained -- zero +# undefined symbols -- so nothing else gets dragged in, and because they are +# the real implementation the results stay bit-for-bit identical to a build +# that links libgfortran dynamically. That matters: the obvious-looking +# alternative of forcing the exponent to INTEGER(4) so gcc emits libgcc's +# __powidf2 instead is NOT equivalent, because __powidf2 evaluates +# 1/(x**|n|) and flushes subnormal results to zero where gfortran evaluates +# (1/x)**|n|. In dgeequb that turns a tiny scale factor into a zero divisor. +# +# _gfortran_etime disappears entirely when LAPACK is built with TIMER=NONE, +# which selects the in-tree second_NONE.f / dsecnd_NONE.f. The only +# behavioural consequence is that LAPACK's SECOND/DSECND return 0.0, which is +# already what OpenBLAS's own C_LAPACK build does. +# +# Note on the license notices: tools/build_prepare_wheel.sh appends a single +# tools/LICENSE_linux.txt to every Linux wheel, and the platforms not yet +# converted still bundle libgfortran and libquadmath. The notices for both +# therefore have to stay until every platform is converted -- listing a +# library that is no longer shipped is harmless, dropping one that still is +# would not be. +# +# _gfortran_concat_string is the one entry point that is not self-contained in +# libgfortran.a (it lives in string_intrinsics.o, which pulls in +# _gfortrani_runtime_error and from there error.o and async.o -- neither of +# which is position-independent, so they cannot go into a shared library). It +# is reimplemented in tools/gfortran_compat.c; see that file for provenance. + +# Linux only for now. The blocker on macOS is the link, not this script: +# keeping the compat symbols out of the dylib's export table needs +# -Wl,--exclude-libs, and ld64 has no equivalent (the nearest is -load_hidden +# / -hidden-l). Without it the two _gfortran_pow_* symbols would be exported +# from libscipy_openblas.dylib and could interpose on a real libgfortran in +# the same process. Enabling macOS also means checking that the gfortran +# tarball before_build downloads ships a static libgfortran.a at all, and +# using llvm-nm/-dynamiclib spellings below. Refused explicitly rather than +# left to fail confusingly at link time. +# +# Locate the toolchain's static libgfortran. Note that on the manylinux +# images the compiler is a gcc-toolset under /opt/rh, so searching /usr will +# not find it -- always ask the compiler. +function find_libgfortran_a { + local fc="${FC:-gfortran}" + local path + path=$("$fc" -print-file-name=libgfortran.a 2>/dev/null) + # -print-file-name echoes the bare name back when the file is not found + case "$path" in + /*) [ -f "$path" ] && echo "$path" && return 0 ;; + esac + return 1 +} + +# build_gfortran_compat +# +# On success, creates /libgfortran_compat.a and echoes its path. +# Returns non-zero if this toolchain cannot support the approach, so the +# caller can fall back to linking libgfortran normally. +function build_gfortran_compat { + local workdir=$1 + local interface64=${2:-0} + local repo_root + repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) + + if [ "$(uname -s)" != "Linux" ]; then + echo "gfortran_compat: only supported on Linux so far (see above)" >&2 + return 1 + fi + + local archive + if ! archive=$(find_libgfortran_a); then + echo "gfortran_compat: no static libgfortran.a for ${FC:-gfortran}" >&2 + return 1 + fi + echo "gfortran_compat: using $archive" >&2 + + rm -rf "$workdir" + mkdir -p "$workdir" + local here=$PWD + cd "$workdir" || return 1 + + # Only the INTERFACE64 build needs the pow entry points: they come from + # x**n where -fdefault-integer-8 has made n an INTEGER(8). With the + # default 4-byte integer gcc expands x**n inline instead. + local wanted="" + if [ "$interface64" = "1" ]; then + wanted="_gfortran_pow_r4_i8 _gfortran_pow_r8_i8" + fi + + local sym member + for sym in $wanted; do + # `|| true`: grep exits 1 when the member is absent, and under + # `set -e` that would abort the build instead of returning non-zero + # here and letting the caller fall back to linking libgfortran. + member=$(nm -A --defined-only "$archive" 2>/dev/null \ + | grep " $sym\$" | head -1 | cut -d: -f2 || true) + if [ -z "$member" ]; then + echo "gfortran_compat: $sym not found in $archive" >&2 + cd "$here"; return 1 + fi + if ! ar x "$archive" "$member" 2>/dev/null; then + echo "gfortran_compat: cannot extract $member" >&2 + cd "$here"; return 1 + fi + # Refuse anything that is not self-contained: a member with undefined + # symbols would drag in libgfortran internals (and possibly non-PIC + # objects), which is exactly what this approach avoids. + # + # Linker-synthesized symbols do not count. On ppc64le every object + # that touches the TOC references .TOC., which ld provides itself -- + # treating that as a dependency rejected a member that is in fact + # perfectly self-contained. The shared-link check below remains the + # real gate: anything genuinely unresolvable fails there instead. + local undef + undef=$(nm --undefined-only "$member" | awk '{print $2}' \ + | grep -vxF -e '.TOC.' -e '_GLOBAL_OFFSET_TABLE_' \ + | tr '\n' ' ' || true) + if [ -n "$undef" ]; then + echo "gfortran_compat: $member is not self-contained: $undef" >&2 + cd "$here"; return 1 + fi + echo "gfortran_compat: took $member for $sym" >&2 + done + + # The hand-written part. + if ! ${CC:-gcc} ${CFLAGS} -O2 -fPIC -fvisibility=hidden \ + -c "$repo_root/tools/gfortran_compat.c" -o gfortran_compat.o; then + echo "gfortran_compat: failed to compile gfortran_compat.c" >&2 + cd "$here"; return 1 + fi + + # Prove the objects can actually go into a shared library before the + # OpenBLAS link tries it. This is the check that rules out cherry-picking + # libgfortran's error.o/async.o, which carry TLS initial-exec relocations. + if ! ${CC:-gcc} -shared -o pictest.so ./*.o 2>pictest.err; then + echo "gfortran_compat: objects are not usable in a shared library:" >&2 + sed 's/^/ /' pictest.err >&2 + cd "$here"; return 1 + fi + rm -f pictest.so pictest.err + + rm -f libgfortran_compat.a + ar crs libgfortran_compat.a ./*.o || { cd "$here"; return 1; } + cd "$here" + echo "$workdir/libgfortran_compat.a" +} + +# fortran_runtime_deps +# +# Echo the libgfortran/libquadmath entries in a library's dependency list, or +# nothing if there are none. Used both by the assertion below and by +# ci-test.sh, which needs to know what the build actually produced rather than +# what it intended: on a toolchain that cannot support the compat archive +# build_gfortran_compat falls back, and the wheel then legitimately still +# bundles the runtime. +function fortran_runtime_deps { + local lib=$1 + if [ "$(uname -s)" == "Darwin" ]; then + otool -L "$lib" | tail -n +2 | awk '{print $1}' \ + | grep -i 'gfortran\|quadmath' || true + else + readelf -d "$lib" | sed -n 's/.*(NEEDED).*\[\(.*\)\]/\1/p' \ + | grep -i 'gfortran\|quadmath' || true + fi +} + +# assert_no_fortran_runtime ... +# +# Fail the build if a built library still depends on the Fortran runtime. +# This is the tripwire for the whole approach: if a future OpenBLAS or LAPACK +# release starts referencing a libgfortran entry point we do not provide, the +# link fails outright; if something re-adds -lgfortran to the link line, this +# catches it. +function assert_no_fortran_runtime { + local lib status=0 checked=0 + for lib in "$@"; do + # Callers pass globs covering both .so and .dylib; an unmatched glob + # arrives here as a literal string, so skip anything that is not a + # file rather than treating it as a failure. + [ -f "$lib" ] || continue + checked=$((checked + 1)) + local undef bad + if [ "$(uname -s)" == "Darwin" ]; then + undef=$(nm -u "$lib" 2>/dev/null | grep -o '_gfortran_[A-Za-z0-9_]*' | sort -u || true) + else + undef=$(nm -D --undefined-only "$lib" 2>/dev/null \ + | grep -o '_gfortran_[A-Za-z0-9_]*' | sort -u || true) + fi + bad=$(fortran_runtime_deps "$lib") + if [ -n "$bad" ]; then + echo "FAIL: $(basename "$lib") still links the Fortran runtime:" >&2 + echo "$bad" | sed 's/^/ /' >&2 + status=1 + fi + if [ -n "$undef" ]; then + echo "FAIL: $(basename "$lib") has unresolved libgfortran symbols:" >&2 + echo "$undef" | sed 's/^/ /' >&2 + status=1 + continue + fi + [ -z "$bad" ] && echo "OK: $(basename "$lib") has no Fortran runtime dependency" + done + # Refuse to pass silently when the globs matched nothing at all -- that + # would make this tripwire useless the day a path or name changes. + if [ "$checked" = "0" ]; then + echo "FAIL: assert_no_fortran_runtime found no library to check in: $*" >&2 + return 1 + fi + return $status +}