Skip to content

cmake: include DefaultCFlags before Check* / Find* modules - #7362

Open
michael-g-matthews wants to merge 1 commit into
libgit2:mainfrom
michael-g-matthews:fix/nsec-feature-support
Open

michael-g-matthews wants to merge 1 commit into
libgit2:mainfrom
michael-g-matthews:fix/nsec-feature-support

Conversation

@michael-g-matthews

Copy link
Copy Markdown

Fixes #7328.

Problem

When libgit2 is consumed as a CMake subproject (add_subdirectory or FetchContent), nanosecond stat timestamp detection can silently produce the wrong answer, and in some configurations breaks the build outright.

On a platform with glibc that fully supports the member st_mtim in struct stat, a consuming project may experience any of the following with the main branch (0551dfd):

  • CMake configuration and compilation succeed with the nanosecond feature support enabled as expected.
  • A CMake configuration warning which incorrectly disables nanosecond support
  CMake Warning at cmake/SelectNsec.cmake:15 (message):
    nanosecond timestamp precision was not detected
  • A CMake configuration fatal error when explicitly using -DUSE_NSEC=ON
  CMake Error at cmake/SelectNsec.cmake:18 (message):
    nanosecond support was requested but no platform support is available
  • CMake configuration and compilation succeed without nanosecond support when explicitly using -DUSE_NSEC=OFF.

And additionally on release tag v1.9.7 (49e408b):

  • A compilation failure
  src/util/unix/posix.h:35:3: error: #error GIT_USE_NSEC defined but unknown struct stat nanosecond type

The cause is whether some unrelated part of the dependent project includes one of CMake's Check*SourceCompiles / CheckStructHasMember modules before reaching libgit2's scope.

Why it happens

  • FindStatNsec.cmake detects the nanosecond fields with check_struct_has_member("struct stat" st_mtim ... LANGUAGE C), which internally runs try_compile().
  • Whether try_compile() honors CMAKE_C_STANDARD / CMAKE_C_EXTENSIONS is governed by policy CMP0067 (introduced in CMake 3.8).
  • CheckSourceCompiles.cmake, CheckStructHasMember.cmake, and similar are all guarded with include_guard(GLOBAL), so the function that performs the check is defined exactly once per CMake run by whichever include() happens first
    anywhere in the project tree.
  • CMake's docs state, "Commands created by the function() and macro() commands record policy settings when they are created and use the pre-record policies when they are invoked."
  • libgit2's top-level CMakeLists.txt declares cmake_minimum_required(VERSION 3.5.1), so when libgit2 is the first to
    include these modules, the check function is recorded with CMP0067 unset, and therefore considered OLD by definition, and try_compile() uses the compiler's default standard (gnu17), where glibc exposes st_mtim.
  • If the consuming project declares cmake_minimum_required(VERSION 3.8) or newer and includes a Check* module first, the function is recorded with CMP0067 set to NEW. check_struct_has_member then compiles its probe with -std=c90, the libgit2 default with CMAKE_C_STANDARD=90 and CMAKE_C_EXTENSIONS=OFF.
  • -std=c90 defines __STRICT_ANSI__, which suppresses glibc's implicit feature-test macros _GNU_SOURCE or _POSIX_C_SOURCE, which expose st_mtim. So HAVE_STRUCT_STAT_ST_MTIM is given the value false, and since
    the other probes also fail, libgit2 concludes the platform has no nanosecond stat support.
  • This is wrong for the libgit2 targets themselves because DefaultCFlags.cmake unconditionally adds -D_GNU_SOURCE to CMAKE_C_FLAGS, so st_mtim is always available when libgit2's sources are compiled. However, DefaultCFlags
    was included after FindStatNsec, so -D_GNU_SOURCE was not yet in CMAKE_C_FLAGS when the probes ran.
  • On maint/v1.9, GIT_USE_NSEC is defined by default while GIT_USE_STAT_MTIM, GIT_USE_STAT_MTIMESPEC, and GIT_USE_STAT_MTIME_NSEC are all undefined, so src/util/unix/posix.h hits its #error during compilation.

Fix

Move DefaultCFlags (and the two modules it depends on, AddCFlagIfSupported and EnableWarnings) ahead of the Check* / Find* modules, so -D_GNU_SOURCE is already part of CMAKE_C_FLAGS when the stat checks run. The checks then see the same struct stat definition that libgit2's targets compile against, and detection is correct regardless of the value of CMP0067.

 # Modules

 include(FeatureSummary)
+include(AddCFlagIfSupported)
+include(EnableWarnings)
+include(DefaultCFlags)
 include(CheckLibraryExists)
 include(CheckFunctionExists)
 include(CheckSymbolExists)
 include(CheckStructHasMember)
 include(CheckPrototypeDefinitionSafe)
-include(AddCFlagIfSupported)
 include(FindPkgLibraries)
 include(FindThreads)
 include(FindStatNsec)
 include(Findfutimens)
 include(GNUInstallDirs)
 include(IdeSplitSources)
-include(EnableWarnings)
-include(DefaultCFlags)
 include(ExperimentalFeatures)

DefaultCFlags.cmake only reads CMAKE_SYSTEM_NAME, BUILD_SHARED_LIBS, MSVC, MINGW, reproducible-build options, and libgit2_VERSION_*, all of which are already set by this point, so it is safe to run earlier. It does not depend on any result produced by the Check*/ Find* modules.

This does not attempt to change libgit2's default C standard or to stop the Check* modules from being policy-sensitive. Rather, it ensures the necessary C flag for the struct stat checks (-D_GNU_SOURCE) is present before they run.

Note: maint/v1.9 additionally has a check result variable name mismatch FindStatNsec.cmake sets HAVE_STRUCT_STAT_MTIME_NSEC while src/CMakeLists.txt checks the value of HAVE_STRUCT_STAT_ST_MTIME_NSEC so GIT_USE_STAT_MTIME_NSEC is never defined on any platform. main no longer has this (the logic moved to SelectNsec.cmake). It is not addressed in this PR as it no longer exists on the main branch. I mention it because it should be addressed in any potential backport of this fix.

Verification

Reproduction project

A minimal CMake parent project that links libgit2 as a subproject (add_subdirectory). It contains a single toggle that decides whether a Check* module is included before libgit2 scope:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.8 FATAL_ERROR)
project(parent LANGUAGES C)

option(CHECK_FIRST "Do a C compilation check before adding libgit2" OFF)

if(CHECK_FIRST)
    include(CheckCSourceCompiles)
    check_c_source_compiles([[
        int main(void) { return 0; }
    ]] ARBITRARY_C_CODE_COMPILES)
endif()

add_subdirectory(deps/libgit2)

add_executable(libgit2_dependent src/main.c)
target_link_libraries(libgit2_dependent PRIVATE libgit2package)
target_include_directories(libgit2_dependent PRIVATE ${libgit2_BINARY_DIR}/include)
set_target_properties(libgit2_dependent PROPERTIES OUTPUT_NAME libgit2-dependent)
/* src/main.c */
#include <git2.h>
#include <stdio.h>

int main(void) {
    int n = git_libgit2_init();
    printf("libgit2 initialized %d time%s.\n", n, (n > 1) ? "s" : "");
    return 0;
}

-DCHECK_FIRST maps directly onto the effective CMP0067 value seen by the guarded check functions:

CHECK_FIRST Who includes Check* first CMP0067 recorded on the check function
OFF libgit2, under its cmake_minimum_required(VERSION 3.5.1) unset -> OLD
ON the parent project, under cmake_minimum_required(VERSION 3.8) NEW

Environment

  • Ubuntu 24.04.4 LTS, Linux 6.8.0-138-generic, glibc 2.39
  • CMake 4.4.2, Ninja 1.11.1, GCC 13.3.0/Clang 18.1.3

Commands

# CMP0067 USE_NSEC cmake command-line options
1 OLD default (unset) -DCHECK_FIRST=OFF
2 NEW default (unset) -DCHECK_FIRST=ON
3 OLD "true" -DCHECK_FIRST=OFF -DUSE_NSEC:STRING="true"
4 NEW "true" -DCHECK_FIRST=ON -DUSE_NSEC:STRING="true"
5 OLD "false" -DCHECK_FIRST=OFF -DUSE_NSEC:STRING="false"
6 NEW "false" -DCHECK_FIRST=ON -DUSE_NSEC:STRING="false"

main (0551dfd4ad989b6a3d5683c0d4cf326c6efef929)

# CMP0067 USE_NSEC HAVE_STRUCT_STAT_ST_MTIM check Configuration result Compilation result Nanosecond support
1 OLD default Success Nanosecond support, using mtim compiles Enabled
2 NEW default Failed CMake Warning ... nanosecond timestamp precision was not detected, feature listed as disabled compiles Disabled
3 OLD "true" Success Nanosecond support, using mtim compiles Enabled
4 NEW "true" Failed CMake Error at cmake/SelectNsec.cmake:18 ... nanosecond support was requested but no platform support is available - -
5 OLD "false" Success Nanosecond support disabled compiles Disabled (as requested)
6 NEW "false" Failed Nanosecond support disabled compiles Disabled (as requested)

Rows 2 and 4 show the bug: the platform supports st_mtim, but a change in
Check* function behavior, outside libgit2's control, disables the feature with
a warning (row 2) or fails to configure (row 4).

main after this PR (d59f04354d846a7305520d22fd2eb60d75d84612)

# CMP0067 USE_NSEC HAVE_STRUCT_STAT_ST_MTIM check Configuration result Compilation result Nanosecond runtime support
1 OLD default Success Nanosecond support, using mtim compiles Enabled
2 NEW default Success Nanosecond support, using mtim compiles Enabled
3 OLD "true" Success Nanosecond support, using mtim compiles Enabled
4 NEW "true" Success Nanosecond support, using mtim compiles Enabled
5 OLD "false" Success Nanosecond support disabled compiles Disabled (as requested)
6 NEW "false" Success Nanosecond support disabled compiles Disabled (as requested)

git2_features.h now contains #define GIT_NSEC 1 / #define GIT_NSEC_MTIM 1
in rows 1–4 regardless of CMP0067. USE_NSEC:STRING="false" still cleanly
disables the feature.


Backport to maint/v1.9

The commit cherry-picks onto maint/v1.9 (49e408b, v1.9.7) cleanly.

maint/v1.9(v1.9.7, 49e408b3208bc3093757a1c2db938d3590f3f412)

# CMP0067 USE_NSEC HAVE_STRUCT_STAT_ST_MTIM check Configuration result Compilation result Nanosecond runtime support
1 OLD default Success nanoseconds, support nanosecond precision file mtimes and ctimes compiles Enabled
2 NEW default Failed feature listed as enabled (nanoseconds, support nanosecond precision file mtimes and ctimes) Fails: posix.h:35:3: error: #error GIT_USE_NSEC defined but unknown struct stat nanosecond type -
3 OLD "true" Success nanoseconds, support nanosecond precision file mtimes and ctimes compiles Enabled
4 NEW "true" Failed feature listed as enabled (nanoseconds, support nanosecond precision file mtimes and ctimes) Fails: posix.h:35:3: error: #error GIT_USE_NSEC defined but unknown struct stat nanosecond type -
5 OLD "false" Success Nanosecond support disabled compiles Disabled (as requested)
6 NEW "false" Failed Nanosecond support disabled compiles Disabled (as requested)

maint/v1.9 after backport (8713217c6fc007c22d33cdeab63bc2089a857a2b)

# CMP0067 USE_NSEC HAVE_STRUCT_STAT_ST_MTIM check Configuration result Build Runtime nsec
1 OLD default Success nanoseconds, support nanosecond precision file mtimes and ctimes compiles Enabled (GIT_USE_NSEC + GIT_USE_STAT_MTIM)
2 NEW default Success nanoseconds, support nanosecond precision file mtimes and ctimes compiles Enabled
3 OLD "true" Success nanoseconds, support nanosecond precision file mtimes and ctimes compiles Enabled
4 NEW "true" Success nanoseconds, support nanosecond precision file mtimes and ctimes compiles Enabled
5 OLD "false" Success Nanosecond support disabled compiles Disabled (as requested)
6 NEW "false" Success Nanosecond support disabled compiles Disabled (as requested)

After the backport, all six configurations compile, the NEW rows match the OLD rows, and the two previously broken build failures (rows 2 and 4) are resolved. USE_NSEC:STRING="false" still disables the feature as requested.

If a backport is desired, should I open another PR targeting that branch, which also includes the fix to the HAVE_STRUCT_STAT_MTIME_NSEC, HAVE_STRUCT_STAT_ST_MTIME_NSEC mismatch?

When libgit2 is a CMake dependency for another project, the behavior of
check_source_compiles is not guaranteed. Functions defined in any
Check*SourceCompiles-related module follow the value of CMP0067 at that
module's first include() in a CMake configuration. If the consuming
project, either directly or indirectly, includes these modules before
reaching us, there's no guarantee CMP0067 is set to OLD.

When CMP0067 is NEW, check_struct_has_member (used in
FindStatNsec.cmake) compiles its test program honoring CMAKE_C_STANDARD
and CMAKE_C_EXTENSIONS. By default they are 90 and OFF, respectively,
which adds the -std=c90 compiler option. For glibc, -std=c90 defines
__STRICT_ANSI__, which suppresses the feature macros that provide
st_mtim in struct stat, causing HAVE_STRUCT_STAT_ST_MTIM to report
failure.

libgit2 targets, however, compile with a definition of struct stat that
does have st_mtim. DefaultCFlags.cmake unconditionally adds
-D_GNU_SOURCE to CMAKE_C_FLAGS, which provides st_mtim even compiled
with -std=c90. But DefaultCFlags.cmake was included after FindStatNsec,
so -D_GNU_SOURCE was not yet present when the checks ran.

With the USE_NSEC option left at its default, this disabled nanosecond
timestamp support with a warning, even though the platform supports it.
With -DUSE_NSEC=ON, CMake instead failed to configure outright.

Move DefaultCFlags and its dependencies (AddCFlagIfSupported and
EnableWarnings) ahead of the Check*/Find* modules, so the tests result in
the expected behavior, and therefore the desired features are enabled,
regardless of CMP0067.

Fixes libgit2#7328

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consuming libgit2 as a dependency with CMake's add_subdirectory or FetchContent can silently corrupt results from FindStatNsec

1 participant