From 3559635aaf399b2dfadfbc4016aa50602bc1a0be Mon Sep 17 00:00:00 2001 From: Matthias Meschede Date: Fri, 17 Jul 2026 16:32:15 +0200 Subject: [PATCH 1/4] hdf5: 1.14.6 upgrade and fix linking - bump 1.12.3 -> 1.14.6, update source URL template - patches/0001-fenv-fe-invalid-guard.patch: guard FE_INVALID use in H5Tinit_float.c; emscripten's defines only rounding modes, FE_INVALID is not declared. This patch is already upstream in hdf5 2.x and can probably be dropped once we update further https://github.com/HDFGroup/hdf5/blob/f8844a64c639f7ba33592a7948d4adacacb2d451/src/H5Tinit_float.c#L614-L617 - patches/0002-drop-soversion.patch: skip VERSION/SOVERSION properties under EMSCRIPTEN so libhdf5_hl.so records bare "libhdf5.so" as its NEEDED entry. Otherwise it records "libhdf5.so.310.5.1" while other packages link with -lhdf5 and record "libhdf5.so". The emscripten loader seems to dedupe by string identity (not realpath) and ends up loading libhdf5.so twice. One visible symptom of this was that the hdf5 metadata cache errors when calls cross between those two loaded instances, something which blocks simple file reading and also affects netcdf4 transitively. Note that this is a hot fix and a proper solution that also works for other libraries would be nicer to have. - variant.yaml: pin hdf5 to 1.14.6 - 1.14 auto-generates settings/ so the old workaround is dropped --- recipes/recipes_emscripten/hdf5/build.sh | 6 +--- .../patches/0001-fenv-fe-invalid-guard.patch | 19 +++++++++++++ .../hdf5/patches/0002-drop-soversion.patch | 28 +++++++++++++++++++ recipes/recipes_emscripten/hdf5/recipe.yaml | 19 ++++++++++--- variant.yaml | 2 +- 5 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 recipes/recipes_emscripten/hdf5/patches/0001-fenv-fe-invalid-guard.patch create mode 100644 recipes/recipes_emscripten/hdf5/patches/0002-drop-soversion.patch diff --git a/recipes/recipes_emscripten/hdf5/build.sh b/recipes/recipes_emscripten/hdf5/build.sh index f9c221b4f2f..ce1c3879936 100644 --- a/recipes/recipes_emscripten/hdf5/build.sh +++ b/recipes/recipes_emscripten/hdf5/build.sh @@ -3,8 +3,6 @@ cd build export LDFLAGS="-sNODERAWFS=1 -sUSE_ZLIB=1 -sFORCE_FILESYSTEM=1" -cp ${RECIPE_DIR}/settings/* ../src/ - emcmake cmake .. \ -DCMAKE_INSTALL_PREFIX=${PREFIX} \ -DH5_HAVE_GETPWUID=OFF \ @@ -21,13 +19,11 @@ emcmake cmake .. \ -DHDF5_BUILD_EXAMPLES=OFF \ -DHDF5_BUILD_TOOLS=OFF \ -DHDF5_BUILD_UTILS=OFF \ + -DHDF5_BUILD_HL_LIB=ON \ -DHDF5_ENABLE_Z_LIB_SUPPORT=ON \ -DHDF5_ENABLE_ROS3_VFD=OFF \ -DZLIB_INCLUDE_DIR=${PREFIX}/include \ -DZLIB_LIBRARY=${PREFIX}/lib/libz.a -cp ${RECIPE_DIR}/settings/* ../src/ -cp ${RECIPE_DIR}/settings/* src/ - emmake make -j 4 emmake make install diff --git a/recipes/recipes_emscripten/hdf5/patches/0001-fenv-fe-invalid-guard.patch b/recipes/recipes_emscripten/hdf5/patches/0001-fenv-fe-invalid-guard.patch new file mode 100644 index 00000000000..045fc082527 --- /dev/null +++ b/recipes/recipes_emscripten/hdf5/patches/0001-fenv-fe-invalid-guard.patch @@ -0,0 +1,19 @@ +Emscripten's defines only rounding modes and FE_ALL_EXCEPT=0; +FE_INVALID is not defined because WASM has no floating-point exception +flags. Guard the FE_INVALID clear so the build proceeds; under WASM there +are no FP exceptions to clear, so the guarded branch is a legitimate no-op. + +--- a/src/H5Tinit_float.c ++++ b/src/H5Tinit_float.c +@@ -608,9 +608,11 @@ + #endif + + done: ++#ifdef FE_INVALID + /* Clear any FE_INVALID exceptions from NaN handling */ + if (feclearexcept(FE_INVALID) != 0) + HSYS_GOTO_ERROR(H5E_DATATYPE, H5E_CANTSET, FAIL, "can't clear floating-point exceptions"); ++#endif + + /* Restore the original environment */ + if (feupdateenv(&saved_fenv) != 0) diff --git a/recipes/recipes_emscripten/hdf5/patches/0002-drop-soversion.patch b/recipes/recipes_emscripten/hdf5/patches/0002-drop-soversion.patch new file mode 100644 index 00000000000..7672f7442e3 --- /dev/null +++ b/recipes/recipes_emscripten/hdf5/patches/0002-drop-soversion.patch @@ -0,0 +1,28 @@ +Under emscripten the loader dedupes shared libraries by string identity of +their NEEDED entry, not by realpath. When libhdf5_hl links against libhdf5 +with SOVERSION set, it records NEEDED "libhdf5.so.310.5.1", while consumers +like h5py link with bare "-lhdf5" and record NEEDED "libhdf5.so". Both paths +resolve to the same file via symlinks, but the loader sees them as two +distinct libraries and creates two separate instances — each with its own +copy of libhdf5.so's file-static globals (H5CX_head_g etc.), which breaks +the metadata cache when calls cross between the instances. + +Skip VERSION/SOVERSION for emscripten builds so every consumer records the +bare "libhdf5.so" NEEDED entry and the loader deduplicates properly. + +--- a/config/cmake/HDF5Macros.cmake ++++ b/config/cmake/HDF5Macros.cmake +@@ -22,10 +22,10 @@ + else () + set (LIBHDF_VERSION ${HDF5_${libpackage}_PACKAGE_SOVERSION_MAJOR}) + endif () +- set_target_properties (${libtarget} PROPERTIES VERSION ${PACKAGE_SOVERSION}) ++ if (NOT EMSCRIPTEN) ++ set_target_properties (${libtarget} PROPERTIES VERSION ${PACKAGE_SOVERSION}) ++ endif () + if (WIN32) + set (${LIB_OUT_NAME} "${LIB_OUT_NAME}-${LIBHDF_VERSION}") +- else () ++ elseif (NOT EMSCRIPTEN) + set_target_properties (${libtarget} PROPERTIES SOVERSION ${LIBHDF_VERSION}) + endif () diff --git a/recipes/recipes_emscripten/hdf5/recipe.yaml b/recipes/recipes_emscripten/hdf5/recipe.yaml index 19c4ee756cf..a0075b9bff8 100644 --- a/recipes/recipes_emscripten/hdf5/recipe.yaml +++ b/recipes/recipes_emscripten/hdf5/recipe.yaml @@ -1,14 +1,17 @@ context: - version: 1.12.3 - maj_min_ver: 1.12 + version: 1.14.6 + maj_min_ver: 1.14 package: name: hdf5 version: ${{ version }} source: - url: https://support.hdfgroup.org/ftp/HDF5/releases/hdf5-${{ maj_min_ver }}/hdf5-${{version }}/src/hdf5-${{ version }}.tar.gz - sha256: c15adf34647918dd48150ea1bd9dffd3b32a3aec5298991d56048cc3d39b4f6f + url: https://support.hdfgroup.org/releases/hdf5/v${{ maj_min_ver | replace(".", "_") }}/v${{ version | replace(".", "_") }}/downloads/hdf5-${{ version }}.tar.gz + sha256: e4defbac30f50d64e1556374aa49e574417c9e72c6b1de7a4ff88c4b1bea6e9b + patches: + - patches/0001-fenv-fe-invalid-guard.patch + - patches/0002-drop-soversion.patch build: number: 0 @@ -21,6 +24,14 @@ requirements: host: - zlib +tests: +- package_contents: + files: + - include/hdf5.h + - include/hdf5_hl.h + - lib/libhdf5.so + - lib/libhdf5_hl.so + about: homepage: https://www.hdfgroup.org/solutions/hdf5/ license: BSD-3-Clause diff --git a/variant.yaml b/variant.yaml index 47a8b53c23a..acb6c5d4637 100644 --- a/variant.yaml +++ b/variant.yaml @@ -383,7 +383,7 @@ harfbuzz: hdf4: - 4.2 hdf5: - - 1.12.3 + - 1.14.6 icu: - '73' ipopt: From 98690a43989798a09be1deb9fb7709f591d67b4e Mon Sep 17 00:00:00 2001 From: Matthias Meschede Date: Fri, 17 Jul 2026 16:32:54 +0200 Subject: [PATCH 2/4] h5py: rebuild against hdf5 1.14.6 - HDF5_VERSION 1.12.3 -> 1.14.6 - drop the ctypes.CDLL preload prepended to __init__.py; the hdf5 SOVERSION fix makes the emscripten loader dedupe libhdf5.so properly, so the manual dlopen seems no longer needed. - add version check and a dimension-scale round-trip test that guards the HL path (H5DSset_scale + attach_scale + is_scale after reopen) --- recipes/recipes_emscripten/h5py/build.sh | 9 +-------- recipes/recipes_emscripten/h5py/recipe.yaml | 2 +- recipes/recipes_emscripten/h5py/test_h5py.py | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/recipes/recipes_emscripten/h5py/build.sh b/recipes/recipes_emscripten/h5py/build.sh index 7a23296ef28..a1baa60802e 100644 --- a/recipes/recipes_emscripten/h5py/build.sh +++ b/recipes/recipes_emscripten/h5py/build.sh @@ -1,11 +1,4 @@ -# add ad-hoc code to load the shared libs -tmp_file="$(mktemp)" -printf 'import ctypes;ctypes.CDLL("/lib/libhdf5.so");ctypes.CDLL("/lib/libhdf5_hl.so")\n' > "$tmp_file" -cat "h5py/__init__.py" >> "$tmp_file" -mv "$tmp_file" "h5py/__init__.py" - - # remove the emcc symlink in the $BUILD_PREFIX/bin rm $BUILD_PREFIX/bin/emcc @@ -30,7 +23,7 @@ export H5PY_DIRECT_VFD='0' export HDF5_MPI=OFF # Explitly set HDF5 version -export HDF5_VERSION=1.12.3 +export HDF5_VERSION=1.14.6 ${PYTHON} -m pip install . -vvv diff --git a/recipes/recipes_emscripten/h5py/recipe.yaml b/recipes/recipes_emscripten/h5py/recipe.yaml index 66945f613e1..ee535737eda 100644 --- a/recipes/recipes_emscripten/h5py/recipe.yaml +++ b/recipes/recipes_emscripten/h5py/recipe.yaml @@ -13,7 +13,7 @@ source: - patches/configure.patch build: - number: 1 + number: 2 requirements: build: diff --git a/recipes/recipes_emscripten/h5py/test_h5py.py b/recipes/recipes_emscripten/h5py/test_h5py.py index 410ddf9c660..03760fc2c1d 100644 --- a/recipes/recipes_emscripten/h5py/test_h5py.py +++ b/recipes/recipes_emscripten/h5py/test_h5py.py @@ -34,4 +34,20 @@ def test_usage(): assert dset2.name == "/subgroup/another_dataset" f = h5py.File("mytestfile.hdf5", "r") - assert sorted(list(f.keys())) == ["mydataset", "subgroup"] \ No newline at end of file + assert sorted(list(f.keys())) == ["mydataset", "subgroup"] + + +def test_dimscales(tmp_path): + """Exercises the HL dimscale reopen path across a File close/open cycle.""" + import h5py + + path = str(tmp_path / "dimscales.h5") + with h5py.File(path, "w") as f: + scale = f.create_dataset("x", data=[0.0, 1.0, 2.0, 3.0]) + scale.make_scale("x") + data = f.create_dataset("temp", data=[10.0, 20.0, 30.0, 40.0]) + data.dims[0].attach_scale(scale) + + with h5py.File(path, "r") as f: + assert h5py.h5ds.is_scale(f["x"].id) + assert f["temp"].dims[0][0].name == "/x" \ No newline at end of file From de5ecbd009ed99b95f519223e47bfe1220a404f3 Mon Sep 17 00:00:00 2001 From: Matthias Meschede Date: Fri, 17 Jul 2026 16:33:38 +0200 Subject: [PATCH 3/4] libnetcdf: skip DAOS container check under emscripten netcdf-c's isdaoscontainer() in libdispatch/dinfermodel.c uses listxattr/getxattr to sniff a path for DAOS marker attributes. Emscripten's declares these but the runtime does not implement them, so the built .so fails to load with "cannot resolve symbol listxattr". There is no --disable-daos configure option (the check is hard-coded), so patch the source to early-return NC_ENOTNC under __EMSCRIPTEN__. Also bump the variant.yaml pin to 4.10.1 to match the recipe version; the previous 4.9.3 pin was forcing consumers to resolve against upstream 4.9.3 which had been built against HDF5 1.12.3 headers and aborted at runtime against a 1.14.6 runtime. --- .../patches/skip-daos-emscripten.patch | 25 +++++++++++++++++++ .../recipes_emscripten/libnetcdf/recipe.yaml | 3 ++- variant.yaml | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 recipes/recipes_emscripten/libnetcdf/patches/skip-daos-emscripten.patch diff --git a/recipes/recipes_emscripten/libnetcdf/patches/skip-daos-emscripten.patch b/recipes/recipes_emscripten/libnetcdf/patches/skip-daos-emscripten.patch new file mode 100644 index 00000000000..34ebaaf31b6 --- /dev/null +++ b/recipes/recipes_emscripten/libnetcdf/patches/skip-daos-emscripten.patch @@ -0,0 +1,25 @@ +DAOS container detection uses listxattr/getxattr which are declared in +Emscripten's but not implemented at runtime — the built .so +fails to load with "cannot resolve symbol listxattr". DAOS makes no sense +in a WASM sandbox anyway. Short-circuit isdaoscontainer() under +__EMSCRIPTEN__ so the xattr code is never reached. + +--- a/libdispatch/dinfermodel.c ++++ b/libdispatch/dinfermodel.c +@@ -1594,6 +1594,10 @@ + static int + isdaoscontainer(const char* path) + { ++#ifdef __EMSCRIPTEN__ ++ (void)path; ++ return NC_ENOTNC; ++#else + int stat = NC_ENOTNC; /* default is that this is not a DAOS container */ + #ifndef _WIN32 + #ifdef USE_HDF5 +@@ -1669,6 +1673,7 @@ + #endif + errno = 0; /* reset */ + return stat; ++#endif /* __EMSCRIPTEN__ */ + } diff --git a/recipes/recipes_emscripten/libnetcdf/recipe.yaml b/recipes/recipes_emscripten/libnetcdf/recipe.yaml index e6e0460d7e5..c9fc8aac73f 100644 --- a/recipes/recipes_emscripten/libnetcdf/recipe.yaml +++ b/recipes/recipes_emscripten/libnetcdf/recipe.yaml @@ -12,9 +12,10 @@ source: patches: - patches/parallel-disable.patch - patches/hdf5-parallel-disable.patch + - patches/skip-daos-emscripten.patch build: - number: 0 + number: 1 files: exclude: diff --git a/variant.yaml b/variant.yaml index acb6c5d4637..dc78831bfce 100644 --- a/variant.yaml +++ b/variant.yaml @@ -449,7 +449,7 @@ libmatio: libmicrohttpd: - 0.9 libnetcdf: - - 4.9.3 + - 4.10.1 libopencv: - 4.5.5 libpcap: From f682d4f177b14cd868386a40aa99a2230d58014a Mon Sep 17 00:00:00 2001 From: Matthias Meschede Date: Fri, 17 Jul 2026 16:34:17 +0200 Subject: [PATCH 4/4] netcdf4: link libhdf5_hl for dimension-scale symbols netcdf-4 file access walks the HDF5 tree with H5DSis_scale to distinguish coordinate dims from data variables. Without -lhdf5_hl in the extension LDFLAGS the built _netCDF4.so has no libhdf5_hl.so in its NEEDED entries, so the loader never brings it in and calls to H5DSis_scale die with "Dynamic linking error: cannot resolve symbol H5DSis_scale" the moment xarray/netCDF4 opens a NETCDF4 file. Also replace the smoke test with two focused cases: a NETCDF3_CLASSIC round-trip and a NETCDF4 round-trip that exercises dim + variable + attribute together. --- .../recipes_emscripten/netcdf4/recipe.yaml | 4 +-- .../netcdf4/test_netcdf4.py | 36 ++++++++++++++++--- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/recipes/recipes_emscripten/netcdf4/recipe.yaml b/recipes/recipes_emscripten/netcdf4/recipe.yaml index 1820b3af27e..b76c233ac20 100644 --- a/recipes/recipes_emscripten/netcdf4/recipe.yaml +++ b/recipes/recipes_emscripten/netcdf4/recipe.yaml @@ -13,9 +13,9 @@ source: - patches/0001-Fix-unknown-arg-R-PREFIX-lib.patch build: - number: 2 + number: 3 script: - - export LDFLAGS="-L$PREFIX/lib -lhdf5 $LDFLAGS" + - export LDFLAGS="-L$PREFIX/lib -lhdf5 -lhdf5_hl $LDFLAGS" - ${PYTHON} -m pip install . -vvv files: diff --git a/recipes/recipes_emscripten/netcdf4/test_netcdf4.py b/recipes/recipes_emscripten/netcdf4/test_netcdf4.py index fd8c6ece350..c93103159e0 100644 --- a/recipes/recipes_emscripten/netcdf4/test_netcdf4.py +++ b/recipes/recipes_emscripten/netcdf4/test_netcdf4.py @@ -1,7 +1,33 @@ -def test_netcdf4(): - from netCDF4 import Dataset - rootgrp = Dataset("test.nc", "w", format="NETCDF4") - rootgrp.data_model == "NETCDF4" - rootgrp.close() +import numpy as np +import netCDF4 +from netCDF4 import Dataset +def test_versions(): + print("netCDF4:", netCDF4.__version__) + print("libnetcdf:", netCDF4.__netcdf4libversion__) + print("libhdf5:", netCDF4.__hdf5libversion__) + + +def test_netcdf3_write_read(tmp_path): + path = str(tmp_path / "c.nc") + with Dataset(path, "w", format="NETCDF3_CLASSIC") as ds: + ds.createDimension("x", 4) + v = ds.createVariable("temp", "f8", ("x",)) + v[:] = np.arange(4.0) + with Dataset(path, "r") as ds: + assert ds.variables["temp"].shape == (4,) + + +def test_netcdf4_roundtrip(tmp_path): + """End-to-end netcdf-4 round-trip: dim + variable + attribute.""" + path = str(tmp_path / "v.nc") + with Dataset(path, "w", format="NETCDF4") as ds: + ds.title = "hello" + ds.createDimension("x", 4) + v = ds.createVariable("temp", "f8", ("x",)) + v[:] = np.arange(4.0) + with Dataset(path, "r") as ds: + assert ds.title == "hello" + assert "x" in ds.dimensions + np.testing.assert_array_equal(ds.variables["temp"][:], np.arange(4.0))