GROMACS currently uses source validation scripts and extra macros in various build system tools to reconcile documentation organization and visibility, and internal header usage in terms of declared API levels.
The heuristics are not particularly easy to resolve during development and are not enforced at the most useful times.
This is a proposal to use a combination of repository layout and basic CMake functionality to improve the clarity, maintainability, and rigor of API / header scoping. As a result, we will need less custom infrastructure (source tree checker scripts).
Side benefits include decoupling from Doxygen in several ways that should make it easier to update our Doxygen version requirements in the future.
Proposal: Define API level and dependency policies via CMake¶
Module dependencies are explicit in the CMakeLists.txt. Dependence on an INTERFACE target allows resolution of headers in the installed API. Dependence on the PUBLIC target allows includes to resolve headers for the build-tree / library-internal module public API.
This necessarily means that the sources and headers of all API levels cannot stay mixed together the way they are. The ability to
#include "gromacs/moduleA_installed_header.h" should not convey the ability to
#include "gromacs/moduleB_detail.h" at build time.
moduleB_detail.h would only be available as
moduleB_detail.h to sources in the
moduleB_public.h would only be available to
gromacs/moduleB_public.h by depending on moduleA PUBLIC in CMakeLists.txt.
src/api/cpp/include/gmxapi demonstrate how this can work. However, this confuses the current
include sorter script, and the headers are accidentally exposed because all of the modules in
src/ in their include path. The five headers in
src/ could easily be moved to a subdirectory.
Module API level:¶
Option 1: API level is determined at the scope of a module. A module's public interface is either in the installed headers / public API or the module is available in the build-tree only. Doxygen visibility directives are not necessary, because the modules in the two APIs are curated.
Option 1.1: The installed API is provided by one (or a few) module(s) explicitly as a distinct API and C++ namespace.
Option 2: Each module may have installed headers and library-internal headers. Two completely separate sets of API documentation are generated, or at least C++ namespace should help alert library API doc viewers / module maintainers when a symbol is actually part of the installed API.
Documentation for API levels becomes scoped by file and location. The list(s) of installed headers (necessary for the CMake target INSTALL command) is the complete list of files for Doxygen to process for the public/installed API. The library-internal / modules API can be documented largely as before, but should link to the public API documentation for entities that are no longer in scope.
Distinguish between API documentation scope and API documentation verbosity.¶
- It may be appropriate or unavoidable in some cases for public API headers to include notes, details, or references beyond the scope of the public API (such as a developer note explaining the need for a forward reference from the library API, or a necessary detail that is explicitly excluded from the compatibility guarantee of the public API specification). Such details can be hidden (e.g. @\internal) to be excluded from the standard public API documentation, but revealed in developer builds of the public API documentation. Ref #3402.
- The library-internal API may similarly have standard documentation for normative use cases, plus internal implementation details or other more verbose documentation revealed by a more verbose documentation build target. Public API documentation accessible from the library-internal API documentation should have multiple visual reminders of the API distinction.
- When browsing the library documentation, it should be obvious to the developer of new library code whether a dependency they are considering is in the public API or library-internal API, and easy to identify module details that are not part of the library-internal API. Doxygen doesn't have great mechanisms for identifying which content was rendered conditionally (
\internal), so it may be appropriate to use a clearly distinct documentation scope for module-internal docs, so that the scope of the library-internal API is not obscured when browsing non-public module details.
The easiest way to make these distinctions is to use distinct Doxygen groups for different API levels. In other words, if we want to continue to define "modules" such that a module expresses in multiple documented API levels, documentation clarity and maintainability can be improved by using multiple Doxygen groups per module instead of one group per module.
- Should the public API documentation link to library API documentation or provide separate local docs for e.g. forward references or types / functionality that is opaque at that API level?
Is there a preference for whether library or module interface headers are necessarily nested in single subdirectory trees versus separated into parallel directory trees? E.g.
A handful of "convenience headers" in
src/gromacs aggregate "include"s of module headers, providing multiple ways to access the same API without a clear indication of what is normative. If we want module APIs to be expressed in
gromacs/module.h headers, or to distinguish between
gromacs/module/feature.h, we should clarify the meaning of these paths. Is there a compelling reason not to just get rid of these headers?
Is more necessary to decouple from specific Doxygen versions (constraints on Doxygen version?)
Other checks to reimplement¶
gmxpre.h go away?¶
Check for a precompiler define instead of
#include "gmxpre.h", or replace "gmxpre.h" with a preprocessor define provided by CMake?
sort includes with clang-format instead of includesorter.py¶
Use Doxygen directly instead of script acting on Doxygen XML for dependency graphs.¶
Document doxygen groups and directories in module headers instead of misc.cpp and directories.cpp¶
Consider freezing API and implementation versions.¶
Use monotonically versioned implementation namespaces (for (a) supporting multiple API versions such as file format revisions, and (b) clarifying API versions at compile time without worrying about API compatibility guarantees or versioning API. Very limited use may be allowed for (c) feature development or API migration across multiple commits, with tracking and approval of the release manager)