Skip to content

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

Description

@michael-g-matthews

When attempting to use libgit2 as a dependency without installing it to the environment, I ran into an issue where libgit2 failed to compile:

/…/deps/libgit2/src/util/unix/posix.h:35:3: error: #error GIT_USE_NSEC defined but unknown struct stat nanosecond type
   35 | # error GIT_USE_NSEC defined but unknown struct stat nanosecond type

This is ultimately caused by some of CMake's nuances around policy CMP0067 and whether some unrelated CMake includes any of the Check*SourceCompiles.cmake modules before FindStatNsec.cmake during build configuration.

My environment is:

  • Ubuntu 24.04.4 LTS (Linux 6.8.0-136-generic), glibc
  • CMake 4.4.0
  • Ninja 1.11.1
  • GCC 13.3.0 and Clang 18.1.3 (I tried both as part of my sanity checking)

This was happening on tag v1.9.6 (26055f5), but is still present on main (2e307f0), although instead of a compilation error, there is an incorrect CMake warning, and it is not possible to turn on the nanosecond feature.

Here is a minimal example:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.11) # This will occur with any CMake 3.8+, but 3.11 is when FetchContent was added
project(foo LANGUAGES CXX)

option(CHECK_FIRST "Toggle this ON to include CheckCXXSourceCompiles, which breaks libgit2" OFF)
if(CHECK_FIRST)
  include(CheckCXXSourceCompiles)
  check_cxx_source_compiles("int main(){return 0;}" ARBITRARY_CXX_CODE_COMPILES)
endif()

include(FetchContent)
FetchContent_Declare(libgit2
  GIT_REPOSITORY https://github.com/libgit2/libgit2.git
  GIT_TAG v1.9.6)
FetchContent_MakeAvailable(libgit2)
$ cmake -S . -B build-ok  -DCHECK_FIRST=OFF
...
-- Performing Test HAVE_STRUCT_STAT_ST_MTIM
-- Performing Test HAVE_STRUCT_STAT_ST_MTIM - Success
...
$ cmake --build build-ok        # succeeds

$ cmake -S . -B build-bad -DCHECK_FIRST=ON
...
-- Performing Test ARBITRARY_CXX_CODE_COMPILES
-- Performing Test ARBITRARY_CXX_CODE_COMPILES - Success
-- Performing Test HAVE_STRUCT_STAT_ST_MTIM
-- Performing Test HAVE_STRUCT_STAT_ST_MTIM - Failed
-- Performing Test HAVE_STRUCT_STAT_ST_MTIMESPEC
-- Performing Test HAVE_STRUCT_STAT_ST_MTIMESPEC - Failed
-- Performing Test HAVE_STRUCT_STAT_MTIME_NSEC
-- Performing Test HAVE_STRUCT_STAT_MTIME_NSEC - Failed
...
$ cmake --build build-bad
...
error: #error GIT_USE_NSEC defined but unknown struct stat nanosecond type

The only difference between the two configurations is whether an unrelated include(CheckCXXSourceCompiles) runs before FetchContent_MakeAvailable(libgit2). This also will happen if libgit2 is instead added via git submodules and add_subdirectory.

Why does this happen?

During the checks in FindStatNsec.cmake, the compiler options are different than those that are used when compiling libgit2.

  • FindStatNsec.cmake calls check_struct_has_member("struct stat" st_mtim ... LANGUAGE C) to detect whether nanosecond-precision fields are present in the struct stat's definition. Internally this calls try_compile(... SOURCE_FROM_VAR ...).
  • Whether try_compile honors the definitions of CMAKE_C_STANDARD and CMAKE_C_EXTENSIONS is set by policy CMP0067, which is introduced in CMake 3.8.
  • The modules Check*SourceCompiles and CheckStructHasMember (and other similar modules) are guarded with include_guard(GLOBAL), which means the CMake function that performs the check is defined exactly once per build configuration. The definition is set by whichever include() happens first in the project tree.
  • CMake's cmake_policy documentation says "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". And the modules depend on and include CheckSourceCompiles.cmake. So whichever scope is active when whichever module is first included dictates the policy behavior for all of them.
  • When one of the modules is included with CMP0067=NEW, check_struct_has_member compiles the test programs with -std=c90 (CMAKE_C_STANDARD=90 and CMAKE_C_EXTENSIONS=OFF), rather than using the compiler's default standard (gnu17) when no standard is explicitly defined.
  • -std=c90 defines __STRICT_ANSI__, which suppresses glibc's default/implicit enabling of feature-test macros like _GNU_SOURCE or _POSIX_C_SOURCE (the ones that expose st_mtim). Unless something explicitly defines them, they will not be available when using -std=c90.
  • The libgit2 targets do get -D_GNU_SOURCE added to CMAKE_C_FLAGS in DefaultCFlags.cmake. So, st_mtim is defined even with -std=c90. However, this is included after FindStatNsec.cmake. So the targets will always have st_mtim, but the check may incorrectly report that it doesn't, based on CMP0067's value when check_struct_has_member is first defined.
  • On v1.9.6, when HAVE_STRUCT_STAT_ST_MTIM is incorrectly OFF and the other two tests always fail, GIT_USE_STAT_MTIM, GIT_USE_STAT_MTIMESPEC, and GIT_USE_STAT_MTIME_NSEC are all not defined and GIT_USE_NSEC is defined by default, which leads to the #error in src/util/unix/posix.h.
  • On main, when HAVE_STRUCT_STAT_ST_MTIM is incorrectly OFF and the other two tests always fail, USE_NSEC is set to OFF, displays a warning message, and sets GIT_NSEC to 0 (which keeps GIT_NSEC undefined in git2_features.h). The code compiles, but always without nanosecond functionality, in the default configuration. If -DUSE_NSEC=ON is added, a fatal error is displayed instead.

Separately, I also noticed on v1.9.6, there is a variable name-mismatch for HAVE_STRUCT_STAT_MTIME_NSEC. In src/CMakeLists.txt, there is a check for HAVE_STRUCT_STAT_ST_MTIME_NSEC instead, so GIT_USE_STAT_MTIME_NSEC is never defined on any platform.

I'd be happy to put together a PR to fix this in main. Could a backport fix to maint/v1.9 also be considered?

And thank you for the fantastic library!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions