HDFGroup / HDFGroup/hdf5

Proposal: modernize the installed CMake package around stable namespaced targets

Open
#6,580 1 comment 4 reactions 1 assignee Claimed by @mattjala View on GitHub
Component - Build
Dominant language
C
Stars
988
Forks
355
Avg merge
4d 2h
Merged PRs (30d)
12

Description

I would like to propose modernizing HDF5's installed CMake package so that downstream projects can consume HDF5 entirely through stable, namespaced imported targets.

At present, the Config package exports implementation-specific targets such as:

```cmake
hdf5-shared
hdf5-static
hdf5_hl-shared
hdf5_hl-static
hdf5_fortran-shared
hdf5_fortran-static
hdf5_hl_fortran-shared
hdf5_hl_fortran-static
```

This exposes both HDF5's internal target naming and the selected linkage type to downstream projects. Consumers must know which variants were built and often construct target or variable names from `STATIC` or `SHARED`.

Instead, the public CMake API should provide stable targets such as:

```cmake
hdf5::hdf5
hdf5::hdf5_cpp
hdf5::hdf5_fortran
hdf5::hdf5_hl
hdf5::hdf5_hl_cpp
hdf5::hdf5_hl_fortran
```

The names of these targets should describe the API being consumed, not whether the underlying library happens to be static or shared.

This would also align HDF5's own Config package with the target interface already provided by CMake's `FindHDF5` module.

## Motivation

The normal downstream usage should be as simple as:

```cmake
find_package(hdf5 CONFIG REQUIRED COMPONENTS C)
target_link_libraries(my_application PRIVATE hdf5::hdf5)
```

For Fortran and the high-level API:

```cmake
find_package(hdf5 CONFIG REQUIRED COMPONENTS Fortran HL)
target_link_libraries(
my_application
PRIVATE
hdf5::hdf5_hl_fortran
)
```

The same downstream code should work regardless of whether the selected HDF5 installation provides a static library, a shared library, or both.

Consumers should not need to write code such as:

```cmake
set(LIB_TYPE STATIC)
string(TOLOWER "${LIB_TYPE}" SEARCH_TYPE)

find_package(HDF5 CONFIG COMPONENTS C "${SEARCH_TYPE}")

target_link_libraries(
my_application
PRIVATE
"${HDF5_C_${LIB_TYPE}_LIBRARY}"
)
```

Nor should they need to select between:

```cmake
hdf5-static
hdf5-shared
```

in every `target_link_libraries()` call.

Static versus shared linkage is a package-selection concern. It should not be encoded in the public logical target name.

## Proposed public target API

The installed package should provide the following targets when the corresponding components are available:

| Target | Meaning |
| ----------------------- | ----------------------------------- |
| `hdf5::hdf5` | HDF5 C library |
| `hdf5::hdf5_cpp` | HDF5 C++ bindings |
| `hdf5::hdf5_fortran` | HDF5 Fortran bindings |
| `hdf5::hdf5_hl` | High-level C library |
| `hdf5::hdf5_hl_cpp` | High-level C++ bindings |
| `hdf5::hdf5_hl_fortran` | High-level Fortran bindings |
| `hdf5::h5diff` | `h5diff` executable, when installed |

It may also be useful to provide:

```cmake
HDF5::HDF5
```

as an aggregate target containing all bindings requested by the current `find_package()` call, matching the interface provided by CMake's `FindHDF5`.

The target relationships should express the actual HDF5 API dependency graph. Conceptually:

```text
hdf5::hdf5_cpp
-> hdf5::hdf5

hdf5::hdf5_fortran
-> hdf5::hdf5

hdf5::hdf5_hl
-> hdf5::hdf5

hdf5::hdf5_hl_cpp
-> hdf5::hdf5_cpp
-> hdf5::hdf5_hl

hdf5::hdf5_hl_fortran
-> hdf5::hdf5_fortran
-> hdf5::hdf5_hl
```

Targets such as `hdf5_f90cstub` and `hdf5_hl_f90cstub` appear to be implementation details. They may still need to exist in an export set, especially for static linking, but downstream users should not need to know or link them explicitly.

## Selecting static or shared linkage

The static or shared implementation should be selected while resolving the package, without changing the public target names.

The existing components could remain available:

```cmake
find_package(HDF5 CONFIG REQUIRED COMPONENTS C shared)
target_link_libraries(my_application PRIVATE hdf5::hdf5)
```

and:

```cmake
find_package(HDF5 CONFIG REQUIRED COMPONENTS C static)
target_link_libraries(my_application PRIVATE hdf5::hdf5)
```

The `static` and `shared` components should be mutually exclusive.

Supporting `HDF5_USE_STATIC_LIBRARIES` would also make the Config package consistent with `FindHDF5`:

```cmake
set(HDF5_USE_STATIC_LIBRARIES ON)
find_package(HDF5 CONFIG REQUIRED COMPONENTS C)
target_link_libraries(my_application PRIVATE hdf5::hdf5)
```

When neither linkage type is explicitly selected, the package should use a deterministic and documented default. For example:

1. select shared libraries when available;
2. otherwise select static libraries.

Only one implementation should be exposed through the canonical targets during one package resolution. Conflicting attempts to select both implementations should produce a clear diagnostic.

This proposal does not necessarily require immediately removing the existing implementation-specific targets. They could remain temporarily as compatibility or advanced targets. They should, however, no longer be the primary documented downstream interface.

## Dependencies should also be target-based

The exported HDF5 targets should express all usage requirements through imported targets.

Examples include:

```cmake
Threads::Threads
MPI::MPI_C
MPI::MPI_CXX
MPI::MPI_Fortran
ZLIB::ZLIB
libaec::aec
libaec::sz
```

The exported interfaces should not contain:

* absolute paths to dependency libraries from the HDF5 build machine;
* values expanded from variables such as `MPI_C_LIBRARIES`;
* plain `-l...` flags when an imported dependency target is available;
* include directories copied from the build environment;
* compiler-specific MPI wrapper output.

For example, an installed target should contain something equivalent to:

```cmake
INTERFACE_LINK_LIBRARIES
"MPI::MPI_Fortran;Threads::Threads"
```

rather than:

```cmake
INTERFACE_LINK_LIBRARIES
"/opt/software/mpich/lib/libmpifort.so;
/opt/software/mpich/lib/libmpi.so"
```

The latter makes the installed HDF5 package dependent on the exact filesystem layout used to build HDF5 and prevents reliable relocation or packaging.

When a dependency does not provide its own Config package, HDF5 may use or ship a Find module that creates an imported target. The exported HDF5 interface should nevertheless remain target-based.

## Resolve only dependencies required by selected targets

`hdf5-config.cmake` should call `find_dependency()` only for dependencies required by the selected HDF5 variant and requested components.

In particular:

* a static HDF5 target must propagate all libraries needed to link it successfully;
* a shared HDF5 target should not require development packages for dependencies that are private to the already-linked shared library;
* MPI language components should be resolved according to the HDF5 bindings being consumed;
* dependencies of unrequested HDF5 components should not be resolved;
* loading a C-only HDF5 target should not unnecessarily resolve Fortran dependencies;
* loading a shared-only package should not unnecessarily resolve dependencies used exclusively by static archives.

Issues such as #6279 and #6347 demonstrate that this distinction is currently difficult to maintain when dependency discovery is handled globally in `hdf5-config.cmake`.

Making the selected target graph the source of truth would make the intended behavior clearer:

* the selected static target carries its complete transitive link interface;
* the selected shared target carries only the usage requirements that consumers actually need;
* the Config file resolves exactly the imported targets referenced by that interface.

## Avoid Config-package side effects

As part of this modernization, the installed package should avoid modifying the consuming project more than necessary.

In particular, a package configuration file should preferably not:

```cmake
set(
CMAKE_MODULE_PATH
${CMAKE_MODULE_PATH}
"${CMAKE_CURRENT_LIST_DIR}/Modules"
)
```

without restoring or scoping the modification.

If bundled Find modules are needed, changes to `CMAKE_MODULE_PATH` should be local to dependency resolution.

The package should also avoid enabling languages on behalf of the consuming project:

```cmake
enable_language(C)
```

A consumer requesting a language-specific HDF5 binding should enable the corresponding language in its own `project()` or `enable_language()` call. If a required language has not been enabled, the package should give a clear diagnostic rather than modifying the project globally.

## Consistency between Config mode and FindHDF5

CMake's `FindHDF5` already documents and provides targets such as:

```cmake
hdf5::hdf5
hdf5::hdf5_cpp
hdf5::hdf5_fortran
hdf5::hdf5_hl
hdf5::hdf5_hl_cpp
hdf5::hdf5_hl_fortran
HDF5::HDF5
```

HDF5's own Config package should better expose the same public target interface.

This would allow downstream projects to use:

```cmake
find_package(HDF5 REQUIRED COMPONENTS Fortran HL)
target_link_libraries(
my_application
PRIVATE
hdf5::hdf5_hl_fortran
)
```

without needing to know whether the package was resolved through:

* CMake's `FindHDF5` module;
* HDF5's own `hdf5-config.cmake`;
* an HDF5 installation providing shared libraries;
* an HDF5 installation providing static libraries.

At present, the module-mode and Config-mode interfaces differ enough that downstream projects often need special handling for each path.

## Consistency between build-tree and installed usage

The same public target names should ideally be available when HDF5 is:

* built as a subproject with `add_subdirectory()`;
* consumed from its build tree;
* consumed from an installation through `find_package()`.

For example, all three cases should allow:

```cmake
target_link_libraries(my_application PRIVATE hdf5::hdf5)
```

Internal build targets may retain names such as `hdf5-shared` or `hdf5-static`, but aliases or exported names should provide the stable public API.

## Possible implementation direction

The exact implementation is open for discussion, but one possible approach would be:

1. retain the existing internal static and shared targets;
2. install static and shared variants in separate export sets;
3. assign stable exported names with `EXPORT_NAME`;
4. apply the `hdf5::` namespace with `install(EXPORT ... NAMESPACE hdf5::)`;
5. have `hdf5-config.cmake` include only the export set selected by the package options or components;
6. provide the same names as aliases in the HDF5 build tree.

Conceptually, the selected implementation would be exported as:

```cmake
hdf5::hdf5
```

rather than as:

```cmake
hdf5-static
```

or:

```cmake
hdf5-shared
```

The implementation details are less important than establishing a stable public contract for downstream consumers.

## Backward compatibility

A staged transition could preserve compatibility:

1. introduce the namespaced targets as the preferred API;
2. update HDF5's downstream CMake documentation and examples to use them;
3. retain the current variables and implementation-specific targets temporarily;
4. mark the old target interface as deprecated;
5. remove or stop documenting it in a future major release.

Existing variables such as:

```cmake
HDF5_C_STATIC_LIBRARY
HDF5_C_SHARED_LIBRARY
HDF5_Fortran_STATIC_LIBRARY
HDF5_Fortran_SHARED_LIBRARY
```

may remain available for compatibility, but modern examples should use imported targets exclusively.

## Suggested tests

The installed package tests should cover at least:

* shared-only builds;
* static-only builds;
* installations containing both static and shared libraries;
* explicit static selection;
* explicit shared selection;
* the default selection behavior;
* C, C++, Fortran and HL components;
* serial and parallel HDF5;
* external Threads, MPI, Zlib and libaec installations;
* installation relocation;
* package staging through `DESTDIR`;
* checking that exported files contain no build-prefix dependency paths;
* linking a shared HDF5 consumer without static-only dependency development files;
* linking a static HDF5 consumer using only the HDF5 imported target;
* equivalent target names for build-tree and installed consumption;
* equivalent target names between Config mode and `FindHDF5`.

A useful relocation test would inspect the installed files and reject references to the original build prefix:

```cmake
/opt/software/mpich/lib/libmpi.so
```

or any corresponding build-time path.

## Expected outcome

After this change, the normal downstream interface would be:

```cmake
find_package(HDF5 CONFIG REQUIRED COMPONENTS C Fortran HL)

target_link_libraries(
my_application
PRIVATE
hdf5::hdf5
hdf5::hdf5_hl_fortran
)
```

The consumer would not need to:

* distinguish `hdf5-static` from `hdf5-shared`;
* manually add HDF5 include directories;
* manually propagate MPI, Threads or compression libraries;
* inspect HDF5 library variables;
* account for different target APIs in module mode and Config mode;
* know about HDF5's internal stub targets.

This would make HDF5's Config package easier to consume, more relocatable, and more consistent with modern target-based CMake package design.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.