Improve CMake test discovery

This commit is contained in:
Mario Fetka
2026-06-03 09:52:09 +00:00
parent 77be5b0815
commit 3d940f24bb
2 changed files with 132 additions and 131 deletions

View File

@@ -15,31 +15,40 @@ include(CheckCSourceCompiles)
include(CheckIncludeFile)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
include(CTest)
option(LIBOWFAT_BUILD_STATIC "Build static libowfat library" OFF)
option(LIBOWFAT_BUILD_STATIC "Build static libowfat/nwowfat library" OFF)
option(LIBOWFAT_BUILD_SHARED "Build shared libowfat library" ON)
set(LIBOWFAT_LIBRARY_PREFIX "" CACHE STRING "Optional library basename prefix. Empty builds libowfat; nw builds libnwowfat")
set(LIBOWFAT_LIBRARY_PREFIX "" CACHE STRING "Optional library basename prefix, e.g. nw builds libnwowfat")
option(LIBOWFAT_INSTALL "Install libraries and headers" ON)
option(LIBOWFAT_GLIBC_COMPAT "Enable glibc-compatible feature-test defines" ON)
option(LIBOWFAT_BUILD_UNIT_TESTS "Build and register the UNITTEST sources used by GNUmakefile check" OFF)
if(LIBOWFAT_LIBRARY_PREFIX MATCHES "[^A-Za-z0-9_+-]")
message(FATAL_ERROR "LIBOWFAT_LIBRARY_PREFIX must be a simple library basename prefix, for example '' or 'nw'")
if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
include(CTest)
set(_libowfat_testing_default ${BUILD_TESTING})
else()
set(_libowfat_testing_default OFF)
endif()
option(LIBOWFAT_BUILD_TEST_PROGRAMS "Build the programs in the test directory" ${_libowfat_testing_default})
option(LIBOWFAT_RUN_SELF_TESTS "Register non-interactive tests with CTest" ${_libowfat_testing_default})
if(LIBOWFAT_RUN_SELF_TESTS)
enable_testing()
endif()
set(LIBOWFAT_OUTPUT_NAME "${LIBOWFAT_LIBRARY_PREFIX}owfat")
if(NOT LIBOWFAT_BUILD_STATIC AND NOT LIBOWFAT_BUILD_SHARED)
message(FATAL_ERROR "At least one of LIBOWFAT_BUILD_STATIC or LIBOWFAT_BUILD_SHARED must be ON")
endif()
# Keep this CMake build close to the existing GNUmakefile, but make the checks
# out-of-tree friendly and avoid dietlibc-specific defaults. The feature
# checks use _GNU_SOURCE on Linux so glibc extension declarations stay visible.
# out-of-tree friendly and avoid dietlibc-specific defaults. glibc builds get
# _GNU_SOURCE so Linux/glibc extension declarations stay visible.
set(LIBOWFAT_FEATURE_DEFINES _REENTRANT)
if(LIBOWFAT_GLIBC_COMPAT AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
list(APPEND LIBOWFAT_FEATURE_DEFINES _GNU_SOURCE)
check_c_source_compiles("#ifndef _GNU_SOURCE\n#error _GNU_SOURCE is not predefined\n#endif\nint main(void) { return 0; }" LIBOWFAT_GNU_SOURCE_PREDEFINED)
if(NOT LIBOWFAT_GNU_SOURCE_PREDEFINED)
list(APPEND LIBOWFAT_FEATURE_DEFINES _GNU_SOURCE)
endif()
endif()
set(LIBOWFAT_OUTPUT_NAME "${LIBOWFAT_LIBRARY_PREFIX}owfat")
set(CMAKE_REQUIRED_DEFINITIONS "")
foreach(_def IN LISTS LIBOWFAT_FEATURE_DEFINES)
@@ -73,11 +82,16 @@ set(_generated_dir "${CMAKE_CURRENT_BINARY_DIR}/generated")
set(_generated_libowfat_dir "${_generated_dir}/libowfat")
file(MAKE_DIRECTORY "${_generated_libowfat_dir}")
function(_write_generated_header name content)
file(WRITE "${_generated_dir}/${name}" "${content}")
file(WRITE "${_generated_libowfat_dir}/${name}" "${content}")
endfunction()
function(_write_feature_header name content_if_true)
if(${ARGV2})
file(WRITE "${_generated_dir}/${name}" "${content_if_true}\n")
_write_generated_header("${name}" "${content_if_true}\n")
else()
file(WRITE "${_generated_dir}/${name}" "")
_write_generated_header("${name}" "")
endif()
endfunction()
@@ -86,15 +100,15 @@ _write_feature_header(havescope.h "#define LIBC_HAS_SCOPE_ID" LIBC_HAS_SCOPE_ID)
_write_feature_header(haven2i.h "#define HAVE_N2I" HAVE_N2I)
_write_feature_header(haveinline.h "#define inline" NOT_HAVE_INLINE)
if(HAVE_INLINE)
file(WRITE "${_generated_dir}/haveinline.h" "")
_write_generated_header(haveinline.h "")
endif()
_write_feature_header(havekqueue.h "#define HAVE_KQUEUE" HAVE_KQUEUE)
_write_feature_header(havebsdsf.h "#define HAVE_BSDSENDFILE" HAVE_BSDSENDFILE)
_write_feature_header(havesendfile.h "#define HAVE_SENDFILE" HAVE_SENDFILE)
if(HAVE_EPOLL)
file(WRITE "${_generated_dir}/haveepoll.h" "#define HAVE_EPOLL 1\n")
_write_generated_header(haveepoll.h "#define HAVE_EPOLL 1\n")
else()
file(WRITE "${_generated_dir}/haveepoll.h" "")
_write_generated_header(haveepoll.h "")
endif()
_write_feature_header(havedevpoll.h "#define HAVE_DEVPOLL" HAVE_DEVPOLL)
_write_feature_header(havesigio.h "#define HAVE_SIGIO" HAVE_SIGIO)
@@ -103,37 +117,39 @@ _write_feature_header(havepread.h "#define HAVE_PREAD" HAVE_PREAD)
_write_feature_header(haveaccept4.h "#define HAVE_ACCEPT4" HAVE_ACCEPT4)
if(HAVE_SOCKLEN_T)
file(WRITE "${_generated_dir}/havesl.h" "")
_write_generated_header(havesl.h "")
else()
file(WRITE "${_generated_dir}/havesl.h" "typedef int socklen_t;\n")
_write_generated_header(havesl.h "typedef int socklen_t;\n")
endif()
file(WRITE "${_generated_dir}/havealloca.h" "#include <stdlib.h>\n")
set(_havealloca_content "#include <stdlib.h>\n")
if(HAVE_ALLOCA_H)
file(APPEND "${_generated_dir}/havealloca.h" "#include <alloca.h>\n")
string(APPEND _havealloca_content "#include <alloca.h>\n")
endif()
if(HAVE_MALLOC_H)
file(APPEND "${_generated_dir}/havealloca.h" "#include <malloc.h>\n")
string(APPEND _havealloca_content "#include <malloc.h>\n")
endif()
_write_generated_header(havealloca.h "${_havealloca_content}")
if(HAVE_POLL)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/iopause.h2" "${_generated_dir}/iopause.h" COPYONLY)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/iopause.h2" "${_generated_libowfat_dir}/iopause.h" COPYONLY)
else()
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/iopause.h1" "${_generated_dir}/iopause.h" COPYONLY)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/iopause.h1" "${_generated_libowfat_dir}/iopause.h" COPYONLY)
endif()
if(HAVE_SYS_SELECT_H)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/select.h2" "${_generated_dir}/select.h" COPYONLY)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/select.h2" "${_generated_libowfat_dir}/select.h" COPYONLY)
else()
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/select.h1" "${_generated_dir}/select.h" COPYONLY)
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/select.h1" "${_generated_libowfat_dir}/select.h" COPYONLY)
endif()
add_executable(libowfat_ent EXCLUDE_FROM_ALL "${CMAKE_CURRENT_SOURCE_DIR}/ent.c")
target_include_directories(libowfat_ent PRIVATE "${_generated_dir}" "${CMAKE_CURRENT_SOURCE_DIR}")
target_compile_definitions(libowfat_ent PRIVATE _REENTRANT)
add_custom_command(
OUTPUT
"${_generated_dir}/entities.h"
"${_generated_libowfat_dir}/entities.h"
OUTPUT "${_generated_dir}/entities.h" "${_generated_libowfat_dir}/entities.h"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/entities.json" "${_generated_dir}/entities.json"
COMMAND libowfat_ent
@@ -143,16 +159,7 @@ add_custom_command(
DEPENDS libowfat_ent "${CMAKE_CURRENT_SOURCE_DIR}/entities.json"
COMMENT "Generating entities.h"
VERBATIM)
add_custom_target(libowfat_entities DEPENDS
"${_generated_dir}/entities.h"
"${_generated_libowfat_dir}/entities.h")
set(LIBOWFAT_GENERATED_HEADERS
haveaccept4.h havealloca.h havebsdsf.h havedevpoll.h haveepoll.h
haveinline.h haveip6.h havekqueue.h haven2i.h havepread.h
havescope.h havesendfile.h havesigio.h havesl.h haveuint128.h
iopause.h select.h entities.h
)
add_custom_target(libowfat_entities DEPENDS "${_generated_dir}/entities.h" "${_generated_libowfat_dir}/entities.h")
set(LIBOWFAT_PUBLIC_HEADERS
buffer.h byte.h fmt.h ip4.h ip6.h mmap.h scan.h socket.h str.h stralloc.h
@@ -170,12 +177,6 @@ foreach(_hdr IN LISTS LIBOWFAT_PUBLIC_HEADERS)
endif()
endforeach()
foreach(_hdr IN LISTS LIBOWFAT_GENERATED_HEADERS)
if(EXISTS "${_generated_dir}/${_hdr}")
configure_file("${_generated_dir}/${_hdr}" "${_generated_libowfat_dir}/${_hdr}" COPYONLY)
endif()
endforeach()
file(GLOB LIBOWFAT_SOURCES CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/array/*.c"
"${CMAKE_CURRENT_SOURCE_DIR}/buffer/*.c"
@@ -211,6 +212,7 @@ target_include_directories(libowfat_objects
PUBLIC
"$<BUILD_INTERFACE:${_generated_dir}>"
"$<BUILD_INTERFACE:${_generated_libowfat_dir}>"
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/libowfat>")
if(LIBOWFAT_BUILD_SHARED)
@@ -243,6 +245,7 @@ foreach(_target IN LISTS _libowfat_targets)
PUBLIC
"$<BUILD_INTERFACE:${_generated_dir}>"
"$<BUILD_INTERFACE:${_generated_libowfat_dir}>"
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/libowfat>")
endforeach()
@@ -253,47 +256,42 @@ elseif(LIBOWFAT_BUILD_STATIC)
add_library(libowfat::libowfat ALIAS libowfat_static)
endif()
if(BUILD_TESTING)
set(_libowfat_test_target libowfat_shared)
if(NOT LIBOWFAT_BUILD_SHARED)
set(_libowfat_test_target libowfat_static)
endif()
add_executable(t "${CMAKE_CURRENT_SOURCE_DIR}/t.c")
target_link_libraries(t PRIVATE ${_libowfat_test_target})
target_include_directories(t PRIVATE "${_generated_dir}" "${_generated_libowfat_dir}" "${CMAKE_CURRENT_SOURCE_DIR}")
target_compile_definitions(t PRIVATE _REENTRANT)
find_package(Threads)
if(Threads_FOUND)
target_link_libraries(t PRIVATE Threads::Threads)
endif()
add_test(NAME libowfat_smoke COMMAND t)
if(LIBOWFAT_RUN_SELF_TESTS)
add_executable(libowfat_smoke_t "${CMAKE_CURRENT_SOURCE_DIR}/t.c")
target_link_libraries(libowfat_smoke_t PRIVATE libowfat::libowfat)
add_test(NAME libowfat_smoke_t COMMAND libowfat_smoke_t)
set_tests_properties(libowfat_smoke_t PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
endif()
if(LIBOWFAT_BUILD_UNIT_TESTS)
file(GLOB_RECURSE LIBOWFAT_UNITTEST_SOURCES CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/*/*.c")
set(_libowfat_unittest_index 0)
foreach(_test_src IN LISTS LIBOWFAT_UNITTEST_SOURCES)
file(READ "${_test_src}" _test_content)
if(_test_content MATCHES "UNITTEST")
get_filename_component(_test_dir "${_test_src}" DIRECTORY)
get_filename_component(_test_dir_name "${_test_dir}" NAME)
get_filename_component(_test_name "${_test_src}" NAME_WE)
set(_test_target "libowfat_unittest_${_test_dir_name}_${_test_name}")
add_executable(${_test_target} "${_test_src}")
target_compile_definitions(${_test_target} PRIVATE UNITTEST _REENTRANT)
target_include_directories(${_test_target} PRIVATE
"${_generated_dir}"
"${_generated_libowfat_dir}"
"${CMAKE_CURRENT_SOURCE_DIR}")
add_dependencies(${_test_target} libowfat_entities)
list(APPEND _libowfat_unittest_targets ${_test_target})
add_test(NAME "unittest/${_test_dir_name}/${_test_name}" COMMAND ${_test_target})
set_tests_properties("unittest/${_test_dir_name}/${_test_name}" PROPERTIES LABELS libowfat-unit)
math(EXPR _libowfat_unittest_index "${_libowfat_unittest_index} + 1")
if(LIBOWFAT_BUILD_TEST_PROGRAMS)
file(GLOB LIBOWFAT_TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/test/*.c")
foreach(_test_src IN LISTS LIBOWFAT_TEST_SOURCES)
get_filename_component(_test_name "${_test_src}" NAME_WE)
string(MAKE_C_IDENTIFIER "libowfat_test_${_test_name}" _test_target)
add_executable(${_test_target} "${_test_src}")
target_link_libraries(${_test_target} PRIVATE libowfat::libowfat)
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${_test_target} PRIVATE
-Wno-error=implicit-int
-Wno-error=incompatible-pointer-types
-Wno-error=implicit-function-declaration)
endif()
endforeach()
if(LIBOWFAT_RUN_SELF_TESTS)
set(_libowfat_noarg_tests
array buffer_1 buffer_tosa byte_copy cas fmt fmt_httpdate fmt_human fmt_ip6
fmt_iso8691 fmt_long fmt_longlong fmt_strm_alloca json marshal
mult netstring protobuf range scan scan_long stralloc_chomp textcode uint utf8
)
foreach(_test_name IN LISTS _libowfat_noarg_tests)
string(MAKE_C_IDENTIFIER "libowfat_test_${_test_name}" _test_target)
if(TARGET ${_test_target})
add_test(NAME ${_test_target} COMMAND ${_test_target})
set_tests_properties(${_test_target} PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
endif()
endforeach()
add_custom_target(libowfat-unit-tests DEPENDS ${_libowfat_unittest_targets})
endif()
endif()

111
README.md
View File

@@ -1,94 +1,97 @@
# libowfat
This tree keeps the original GNUmakefile build unchanged and adds a standalone
CMake build for projects that want to consume libowfat directly or as a CMake
subdirectory.
This tree keeps the original GNUmakefile build untouched and adds an optional
CMake build for projects that want to consume libowfat directly with CMake.
## CMake build
Out-of-tree builds are supported and recommended:
Out-of-tree builds are supported:
```sh
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure
```
The CMake build reads the library version from the first line of `CHANGES`.
For example, with the current `CHANGES` this produces version `0.35`.
By default the CMake build produces the normal shared library name:
By default CMake builds the shared library. Static and shared builds can be
selected independently:
```text
libowfat.so
```
A prefix can be added to the library basename. For example, this builds
`libnwowfat.so` and, when static builds are enabled, `libnwowfat.a`:
```sh
cmake -S . -B build \
-DLIBOWFAT_BUILD_SHARED=ON \
-DLIBOWFAT_BUILD_STATIC=ON
cmake -S . -B build-nw \
-DLIBOWFAT_LIBRARY_PREFIX=nw \
-DLIBOWFAT_BUILD_STATIC=ON \
-DLIBOWFAT_BUILD_SHARED=ON
cmake --build build-nw
```
The installed library basename can be prefixed without changing the headers or
CMake package name. The default prefix is empty, so the output is `libowfat`.
For a NetWare-flavoured name, pass `nw` and the output becomes `libnwowfat`:
The library version is read from the first line of `CHANGES`.
```sh
cmake -S . -B build -DLIBOWFAT_LIBRARY_PREFIX=nw
```
## Installation
## Install
Installation uses CMake `GNUInstallDirs`, so the library directory follows the
target platform and install prefix (`lib`, `lib64`, etc.). Headers are installed
below `include/libowfat`.
CMake installation uses `GNUInstallDirs`, so the library directory follows the
platform and the configured prefix, for example `lib` or `lib64`:
```sh
cmake --install build --prefix /usr
```
Useful install options:
Headers are installed under:
```sh
cmake -S . -B build \
-DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_INSTALL_LIBDIR=lib64
```text
${prefix}/include/libowfat
```
## Generated compatibility headers
Generated compatibility headers such as `haveip6.h`, `havesendfile.h`,
`haveaccept4.h`, `havesl.h`, `iopause.h`, `select.h`, and `entities.h` are
installed there too. This lets consumers include either `libowfat/foo.h` or,
for legacy code that relies on the target include path, plain `foo.h`.
The CMake build generates the same style of compatibility headers as the
GNUmakefile build, but places them in the build tree instead of the source tree.
Both the build-tree target and the installed package export include paths for:
## Using from another CMake project
- `<build>/generated`
- `<build>/generated/libowfat`
- `<prefix>/include`
- `<prefix>/include/libowfat`
As an installed package:
This lets normal includes such as `<libowfat/buffer.h>` work, and also keeps
internal or compatibility includes such as `"haveip6.h"`, `"havesendfile.h"`,
`"iopause.h"`, `"select.h"`, and `"entities.h"` available when the project is
used through `add_subdirectory()` or `find_package(libowfat CONFIG REQUIRED)`.
```cmake
find_package(libowfat CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE libowfat::libowfat)
```
As a subdirectory or submodule:
```cmake
add_subdirectory(extern/libowfat)
target_link_libraries(myapp PRIVATE libowfat::libowfat)
```
The exported build-tree target includes both the source include directory and
the generated header directories, so generated `have*.h` headers are available
without copying files into the source tree.
## Tests
The GNUmakefile `all` target builds the root `t` smoke-test program; CMake does
the same when `BUILD_TESTING` is enabled, and registers it with CTest:
When libowfat is built as the top-level CMake project, `BUILD_TESTING=ON`
registers `t.c` as a smoke test and builds the programs under `test/`:
```sh
cmake -S . -B build -DBUILD_TESTING=ON
cmake --build build
ctest --test-dir build --output-on-failure
```
The `UNITTEST` sources used by the GNUmakefile `check` target can also be built
and registered with CTest:
Only the non-interactive tests are registered with CTest. Network/server/demo
programs and tests that need command-line input are compiled but not run by
CTest.
When libowfat is used through `add_subdirectory()`, tests are off by default so
that a parent project does not unexpectedly build the whole test tree. They can
be enabled explicitly:
```sh
cmake -S . -B build -DLIBOWFAT_BUILD_UNIT_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failure -L libowfat-unit
-DLIBOWFAT_BUILD_TEST_PROGRAMS=ON
-DLIBOWFAT_RUN_SELF_TESTS=ON
```
## glibc compatibility
The CMake build uses the system C compiler and glibc-compatible feature checks
instead of dietlibc-specific assumptions. On Linux the configure checks use
`_GNU_SOURCE` so glibc extension declarations are visible; the library itself
still relies on the existing source-level feature defines where individual
files need them.