diff --git a/.gersemirc b/.gersemirc index c7e66ff1..7629acad 100644 --- a/.gersemirc +++ b/.gersemirc @@ -2,7 +2,7 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/master/gersemi/configuration.schema.json definitions: - - "CMake/ystdlib-cpp-helpers.cmake" + - "cmake/ystdlib-helpers.cmake" - "build/deps/Catch2/Catch2-src/extras/Catch.cmake" line_length: 100 list_expansion: "favour-expansion" diff --git a/CMake/ystdlib-cpp-helpers.cmake b/CMake/ystdlib-cpp-helpers.cmake deleted file mode 100644 index 0d780c5d..00000000 --- a/CMake/ystdlib-cpp-helpers.cmake +++ /dev/null @@ -1,162 +0,0 @@ -# @param {string[]} REQUIRED_ARGS The list of arguments to check. -# @param {string[]} ARG_KEYWORDS_MISSING_VALUES The list of arguments with missing values. -# @param {string} TARGET_NAME The target on which to perform the check. -function(require_argument_values REQUIRED_ARGS ARG_KEYWORDS_MISSING_VALUES TARGET_NAME) - foreach(arg IN LISTS REQUIRED_ARGS) - if("${arg}" IN_LIST ARG_KEYWORDS_MISSING_VALUES) - message(FATAL_ERROR "Missing values for argument '${arg}' in target '${TARGET_NAME}'.") - endif() - endforeach() -endfunction() - -# @param {string[]} SOURCE_LIST The list of source files to check. -# @param {bool} IS_HEADER_ONLY Returns TRUE if list only contains header files, FALSE otherwise. -# @param {string} NON_HEADER_FILE Returns the name of the first, if any, non-header file. -function(check_if_header_only SOURCE_LIST IS_HEADER_ONLY NON_HEADER_FILE) - set(_LOCAL_SOURCE_LIST "${${SOURCE_LIST}}") - foreach(src_file IN LISTS _LOCAL_SOURCE_LIST) - if(NOT ${src_file} MATCHES ".*\\.(h|hpp)") - set(${IS_HEADER_ONLY} FALSE PARENT_SCOPE) - set(${NON_HEADER_FILE} ${src_file} PARENT_SCOPE) - return() - endif() - endforeach() - set(${IS_HEADER_ONLY} TRUE PARENT_SCOPE) - set(${NON_HEADER_FILE} "" PARENT_SCOPE) -endfunction() - -# Adds a c++20 interface library in the subdirectory NAME with the target NAME and alias -# NAMESPACE::NAME. Libraries with multiple levels of namespace nesting are currently not supported. -# -# If `YSTDLIB_CPP_ENABLE_TESTS` is ON, builds the unit tests specific to the current library, and -# links this library against the unified unit test target for the entire `ystdlib-cpp` project. -# -# @param {string} NAME -# @param {string} NAMESPACE -# @param {string[]} PUBLIC_HEADERS -# @param {string[]} TESTS_SOURCES -# @param {string[]} [PRIVATE_SOURCES] -# @param {string[]} [PUBLIC_LINK_LIBRARIES] -# @param {string[]} [PRIVATE_LINK_LIBRARIES] -# @param {string[]} [TESTS_LINK_LIBRARIES] -# @param {string[]} [BUILD_INCLUDE_DIR="${PROJECT_SOURCE_DIR}/src"] The list of include paths for -# building the library and for external projects that builds `ystdlib-cpp` as a CMAKE subproject via -# the add_subdirectory() function. -function(cpp_library) - set(options "") - set(oneValueArgs - NAME - NAMESPACE - ) - set(multiValueArgs - PUBLIC_HEADERS - PRIVATE_SOURCES - PUBLIC_LINK_LIBRARIES - PRIVATE_LINK_LIBRARIES - TESTS_SOURCES - TESTS_LINK_LIBRARIES - BUILD_INCLUDE_DIR - ) - set(requireValueArgs - NAME - NAMESPACE - PUBLIC_HEADERS - TESTS_SOURCES - BUILD_INCLUDE_DIR - ) - cmake_parse_arguments(arg_cpp_lib "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - set(_ALIAS_TARGET_NAME "${arg_cpp_lib_NAMESPACE}::${arg_cpp_lib_NAME}") - - require_argument_values( - "${requireValueArgs}" - "${arg_cpp_lib_KEYWORDS_MISSING_VALUES}" - "${_ALIAS_TARGET_NAME}" - ) - - if(NOT DEFINED arg_cpp_lib_BUILD_INCLUDE_DIR) - set(arg_cpp_lib_BUILD_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/src") - endif() - - check_if_header_only(arg_cpp_lib_PUBLIC_HEADERS _IS_VALID_INTERFACE _INVALID_HEADER_FILE) - if(NOT _IS_VALID_INTERFACE) - message( - FATAL_ERROR - "Invalid interface header file ${_INVALID_HEADER_FILE} for ${_ALIAS_TARGET_NAME}." - ) - endif() - - check_if_header_only(arg_cpp_lib_PRIVATE_SOURCES _IS_INTERFACE_LIB _) - if(_IS_INTERFACE_LIB) - if(arg_cpp_lib_PRIVATE_LINK_LIBRARIES) - message( - FATAL_ERROR - "`PRIVATE_LINK_LIBRARIES` disabled for header-only library ${_ALIAS_TARGET_NAME}." - ) - endif() - add_library(${arg_cpp_lib_NAME} INTERFACE) - target_link_libraries(${arg_cpp_lib_NAME} INTERFACE ${arg_cpp_lib_PUBLIC_LINK_LIBRARIES}) - target_include_directories( - ${arg_cpp_lib_NAME} - INTERFACE - "$" - ) - target_compile_features(${arg_cpp_lib_NAME} INTERFACE cxx_std_20) - else() - # The library type is specified by `BUILD_SHARED_LIBS` if it is defined. Otherwise, the type - # defaults to static. - add_library(${arg_cpp_lib_NAME}) - target_sources( - ${arg_cpp_lib_NAME} - PRIVATE - ${arg_cpp_lib_PUBLIC_HEADERS} - ${arg_cpp_lib_PRIVATE_SOURCES} - ) - target_link_libraries( - ${arg_cpp_lib_NAME} - PUBLIC - ${arg_cpp_lib_PUBLIC_LINK_LIBRARIES} - PRIVATE - ${arg_cpp_lib_PRIVATE_LINK_LIBRARIES} - ) - target_include_directories( - ${arg_cpp_lib_NAME} - PUBLIC - "$" - ) - target_compile_features(${arg_cpp_lib_NAME} PUBLIC cxx_std_20) - endif() - - add_library(${_ALIAS_TARGET_NAME} ALIAS ${arg_cpp_lib_NAME}) - - if(YSTDLIB_CPP_ENABLE_TESTS) - # Build library-specific unit test target - set(_UNIT_TEST_TARGET "unit-test-${arg_cpp_lib_NAME}") - add_executable(${_UNIT_TEST_TARGET}) - target_sources(${_UNIT_TEST_TARGET} PRIVATE ${arg_cpp_lib_TESTS_SOURCES}) - target_link_libraries( - ${_UNIT_TEST_TARGET} - PRIVATE - Catch2::Catch2WithMain - ${_ALIAS_TARGET_NAME} - ${arg_cpp_lib_TESTS_LINK_LIBRARIES} - ) - target_compile_features(${_UNIT_TEST_TARGET} PRIVATE cxx_std_20) - set_property( - TARGET - ${_UNIT_TEST_TARGET} - PROPERTY - RUNTIME_OUTPUT_DIRECTORY - ${CMAKE_BINARY_DIR}/testbin - ) - - # Link against unified unit test - target_sources(${UNIFIED_UNIT_TEST_TARGET} PRIVATE ${arg_cpp_lib_TESTS_SOURCES}) - target_link_libraries( - ${UNIFIED_UNIT_TEST_TARGET} - PRIVATE - ${_ALIAS_TARGET_NAME} - ${arg_cpp_lib_TESTS_LINK_LIBRARIES} - ) - endif() -endfunction() diff --git a/CMakeLists.txt b/CMakeLists.txt index 689b164f..f368bfe5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,37 +1,36 @@ -cmake_minimum_required(VERSION 3.22.1) +cmake_minimum_required(VERSION 3.23) -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/CMake") -include(ystdlib-cpp-helpers) +project(ystdlib VERSION "0.1.0" LANGUAGES CXX) -project(YSTDLIB_CPP LANGUAGES CXX) - -set(YSTDLIB_CPP_VERSION "0.0.1" CACHE STRING "Project version.") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) +include(ystdlib-helpers) option(BUILD_SHARED_LIBS "Build using shared libraries." OFF) -option(YSTDLIB_CPP_BUILD_TESTING "Build the testing tree for ystdlib-cpp." ON) +option(ystdlib_BUILD_TESTING "Build the testing tree for ystdlib." ON) -# Require compiler versions that support the C++20 features necessary for compiling ystdlib-cpp +# Require compiler versions that support the C++20 features necessary for compiling ystdlib if("AppleClang" STREQUAL "${CMAKE_CXX_COMPILER_ID}") - set(YSTDLIB_CPP_CMAKE_CXX_COMPILER_MIN_VERSION "16") + set(ystdlib_CMAKE_CXX_COMPILER_MIN_VERSION "16") elseif("Clang" STREQUAL "${CMAKE_CXX_COMPILER_ID}") - set(YSTDLIB_CPP_CMAKE_CXX_COMPILER_MIN_VERSION "16") + set(ystdlib_CMAKE_CXX_COMPILER_MIN_VERSION "16") elseif("GNU" STREQUAL "${CMAKE_CXX_COMPILER_ID}") - set(YSTDLIB_CPP_CMAKE_CXX_COMPILER_MIN_VERSION "11") + set(ystdlib_CMAKE_CXX_COMPILER_MIN_VERSION "11") else() message( FATAL_ERROR "Unsupported compiler: ${CMAKE_CXX_COMPILER_ID}. Please use AppleClang, Clang, or GNU." ) endif() -if("${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS "${YSTDLIB_CPP_CMAKE_CXX_COMPILER_MIN_VERSION}") +if("${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS "${ystdlib_CMAKE_CXX_COMPILER_MIN_VERSION}") message( FATAL_ERROR "${CMAKE_CXX_COMPILER_ID} version ${CMAKE_CXX_COMPILER_VERSION} is too low. Must be at \ - least ${YSTDLIB_CPP_CMAKE_CXX_COMPILER_MIN_VERSION}." + least ${ystdlib_CMAKE_CXX_COMPILER_MIN_VERSION}." ) endif() -# Enable exporting compile commands set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE BOOL @@ -39,35 +38,39 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS FORCE ) -if(YSTDLIB_CPP_IS_TOP_LEVEL) +set(ystdlib_LIBRARIES + "containers" + "error_handling" + "io_interface" + "wrapped_facade_headers" + CACHE STRING + "Semicolon-separated list of libraries to be built." +) + +message(STATUS "Building the following libraries:") +foreach(LIB IN LISTS ystdlib_LIBRARIES) + message(STATUS " - ${LIB}") +endforeach() + +if(ystdlib_IS_TOP_LEVEL) # Include dependency settings if the project isn't being included as a subproject. # NOTE: We mark the file optional because if the user happens to have the dependencies # installed, this file is not necessary. - include("build/deps/cmake-settings/settings.cmake" OPTIONAL) + include("build/deps/cmake-settings/all.cmake" OPTIONAL) # If previously undefined, `BUILD_TESTING` will be set to ON. include(CTest) endif() -if(BUILD_TESTING AND YSTDLIB_CPP_BUILD_TESTING) - set(YSTDLIB_CPP_ENABLE_TESTS ON) +if(BUILD_TESTING AND ystdlib_BUILD_TESTING) + set(ystdlib_ENABLE_TESTS ON) endif() -find_package(Boost REQUIRED) -if(Boost_FOUND) - message(STATUS "Found Boost ${Boost_VERSION}.") -endif() - -if(YSTDLIB_CPP_ENABLE_TESTS) +if(ystdlib_ENABLE_TESTS) find_package(Catch2 3.8.0 REQUIRED) - if(Catch2_FOUND) - message(STATUS "Found Catch2 ${Catch2_VERSION}.") - else() - message(FATAL_ERROR "Could not find libraries for Catch2.") - endif() + message(STATUS "Found Catch2 ${Catch2_VERSION}.") include(Catch) - # Set up the unified unit test target set(UNIFIED_UNIT_TEST_TARGET "unit-test-all") add_executable(${UNIFIED_UNIT_TEST_TARGET}) target_link_libraries(${UNIFIED_UNIT_TEST_TARGET} PRIVATE Catch2::Catch2WithMain) @@ -82,4 +85,32 @@ if(YSTDLIB_CPP_ENABLE_TESTS) catch_discover_tests(${UNIFIED_UNIT_TEST_TARGET} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/testbin) endif() +# We require all libraries use the same minimum version of dependencies to avoid issues. +set(BOOST_MIN_VERSION "1.81.0") + +set(CONFIG_PATH_SUFFIX "cmake/ystdlib") +set(CONFIG_LIBS_PATH_SUFFIX "${CONFIG_PATH_SUFFIX}/libs") + +# Used by libraries and must come before add_subdirectory. +set(CONFIG_LIBS_DEST_DIR "${CMAKE_INSTALL_LIBDIR}/${CONFIG_LIBS_PATH_SUFFIX}") +set(CONFIG_LIBS_INPUT_DIR "${PROJECT_SOURCE_DIR}/${CONFIG_LIBS_PATH_SUFFIX}") + add_subdirectory(src/ystdlib) + +set(CONFIG_FILE_PREFIX "ystdlib-config") +set(CONFIG_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/${CONFIG_PATH_SUFFIX}") +set(CONFIG_OUTPUT_PATH "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG_FILE_PREFIX}.cmake") +set(CONFIG_VERSION_OUTPUT_PATH "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG_FILE_PREFIX}-version.cmake") + +configure_package_config_file( + "${CMAKE_CURRENT_LIST_DIR}/${CONFIG_PATH_SUFFIX}/${CONFIG_FILE_PREFIX}.cmake.in" + "${CONFIG_OUTPUT_PATH}" + INSTALL_DESTINATION "${CONFIG_INSTALL_DIR}" +) +write_basic_package_version_file("${CONFIG_VERSION_OUTPUT_PATH}" COMPATIBILITY SameMajorVersion) +install( + FILES + "${CONFIG_OUTPUT_PATH}" + "${CONFIG_VERSION_OUTPUT_PATH}" + DESTINATION "${CONFIG_INSTALL_DIR}" +) diff --git a/README.md b/README.md index fc01867c..67517b04 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,46 @@ -# ystdlib-cpp +# ystdlib + An open-source C++ library developed and used at YScope. # Usage -## Via CMake's add_subdirectory() -Clone `ystdlib-cpp` into your project. Then, in your project's `CMakeLists.txt`, add the following: +First, [build](#building) and [install](#installing) `ystdlib` onto your system. Then, in your +project's `CMakeLists.txt`, add the following: + ```cmake -# Set `YSTDLIB_CPP_BUILD_TESTING` to an accepted `FALSE` class value to skip building unit tests. -# option(YSTDLIB_CPP_BUILD_TESTING "" OFF) -add_subdirectory(/path/to/ystdlib-cpp EXCLUDE_FROM_ALL) -target_link_libraries( - ystdlib:: ystdlib:: ... ystdlib:: - # other libs... +find_package(ystdlib REQUIRED) +target_link_libraries( + [] + ystdlib:: + [ystdlib:: ... ystdlib::] ) ``` -Ensure that `ystdlib-cpp` is either within a subdirectory of the folder containing `CMakeLists.txt` -or at the same level. + +Where + +* `` is the name of your target. +* `` are the link options for your target. +* `lib_1`, `lib_2`, ..., `lib_N` are the names of the ystdlib libraries you wish to link with your + target. + +> [!NOTE] +> If `BUILD_TESTING` is `ON`, set `ystdlib_BUILD_TESTING` to `OFF` to skip building ystdlib's unit +> tests. + +> [!TIP] +> If ystdlib is not installed to a path that is searched by default, set `ystdlib_ROOT` to manually +> specify the location: +> +> ```cmake +> set(ystdlib_ROOT "") +> ``` # Contributing Follow the steps below to develop and contribute to the project. ## Requirements + +* CMake 3.23 or higher * Python 3.10 or higher * [Task] 3.40.0 or higher @@ -37,6 +57,12 @@ task deps:install-all ``` ## Building + +The library can be built directly using [CMake](#using-cmake) or indirectly using +[Task](#using-task). + +### Using Task + To build all targets: ```shell task build:all @@ -52,6 +78,37 @@ To build an executable containing a single library's unit tests: task build:unit-test- ``` +### Using CMake + +To build all libraries, run: + +```shell +cmake -S . -B ./build +cmake --build ./build +``` + +To build a subset of libraries, set the variable `ystdlib_LIBRARIES` to a semicolon-separated (`;`) +list of library names. The library names match their [directory name in src/](./src/ystdlib). For +example: + +```shell +cmake -S . -B ./build -Dystdlib_LIBRARIES="containers;io_interface" +cmake --build ./build +``` + +> [!NOTE] +> Internal dependencies of the libraries you choose will be automatically built, even if you don't +> explicitly specify them. In the example, specifying `io_interface` automatically adds +> `wrapped_facade_headers` to the build. + +## Installing + +After [building](#building), install with: + +```shell +cmake --install "./build" --prefix "$HOME/.local" +``` + ## Testing To build and run all unit tests: ```shell @@ -64,7 +121,7 @@ task test- ``` When generating a testing target, the CMake variable `BUILD_TESTING` is followed (unless overruled -by setting `YSTDLIB_CPP_BUILD_TESTING` to false). By default, if built as a top-level project, +by setting `ystdlib_BUILD_TESTING` to false). By default, if built as a top-level project, `BUILD_TESTING` is set to true and unit tests are built. ## Linting diff --git a/cmake/ystdlib-helpers.cmake b/cmake/ystdlib-helpers.cmake new file mode 100644 index 00000000..ca95d981 --- /dev/null +++ b/cmake/ystdlib-helpers.cmake @@ -0,0 +1,222 @@ +include(CMakePackageConfigHelpers) + +# Checks that each argument name in `REQUIRED_ARG_NAMES` is defined and non-empty. Assumes the +# arguments are stored in variables of the form `ARG_`. +# +# @param {string[]} REQUIRED_ARG_NAMES +macro(check_required_arguments_exist REQUIRED_ARG_NAMES) + set(_NAMES "${REQUIRED_ARG_NAMES}") + foreach(_NAME IN LISTS _NAMES) + if(NOT DEFINED ARG_${_NAME} OR ARG_${_NAME} STREQUAL "") + message(FATAL_ERROR "Missing or empty value for argument: '${_NAME}'") + endif() + endforeach() +endmacro() + +# Checks if `SOURCE_LIST` contains only header files. +# +# @param {string[]} SOURCE_LIST +# @param {bool} IS_HEADER_ONLY Returns whether the list contains only header files. +# @param {string} NON_HEADER_FILE Returns the name of the first, if any, non-header file. +function(check_if_header_only SOURCE_LIST IS_HEADER_ONLY NON_HEADER_FILE) + set(LOCAL_SOURCE_LIST "${${SOURCE_LIST}}") + foreach(SRC_FILE IN LISTS LOCAL_SOURCE_LIST) + if(NOT "${SRC_FILE}" MATCHES ".*\\.(h|hpp)") + set(${IS_HEADER_ONLY} FALSE PARENT_SCOPE) + set(${NON_HEADER_FILE} "${SRC_FILE}" PARENT_SCOPE) + return() + endif() + endforeach() + set(${IS_HEADER_ONLY} TRUE PARENT_SCOPE) + set(${NON_HEADER_FILE} "" PARENT_SCOPE) +endfunction() + +# Adds a C++20 library in the subdirectory NAME with the target NAME and alias NAMESPACE::NAME. +# Libraries with multiple levels of namespace nesting are currently not supported. +# +# @param {string} NAME +# @param {string} NAMESPACE +# @param {string[]} PUBLIC_HEADERS +# @param {string[]} [PRIVATE_SOURCES] +# @param {string[]} [PUBLIC_LINK_LIBRARIES] +# @param {string[]} [PRIVATE_LINK_LIBRARIES] +# @param {string[]} [BUILD_INCLUDE_DIRS="${PROJECT_SOURCE_DIR}/src"] The list of include paths for +# building the library and for external projects that use ystdlib via add_subdirectory(). +function(add_cpp_library) + set(SINGLE_VALUE_ARGS + NAME + NAMESPACE + ) + set(MULTI_VALUE_ARGS + PUBLIC_HEADERS + PRIVATE_SOURCES + PUBLIC_LINK_LIBRARIES + PRIVATE_LINK_LIBRARIES + BUILD_INCLUDE_DIRS + ) + set(REQUIRED_ARGS + NAME + NAMESPACE + PUBLIC_HEADERS + ) + cmake_parse_arguments(ARG "" "${SINGLE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN}) + check_required_arguments_exist("${REQUIRED_ARGS}") + + if(NOT DEFINED ARG_BUILD_INCLUDE_DIRS) + set(ARG_BUILD_INCLUDE_DIRS "${PROJECT_SOURCE_DIR}/src") + endif() + + set(ALIAS_TARGET_NAME "${ARG_NAMESPACE}::${ARG_NAME}") + + check_if_header_only(ARG_PUBLIC_HEADERS IS_VALID_INTERFACE INVALID_HEADER_FILE) + if(NOT IS_VALID_INTERFACE) + message( + FATAL_ERROR + "Invalid interface header file ${INVALID_HEADER_FILE} for ${ALIAS_TARGET_NAME}." + ) + endif() + + check_if_header_only(ARG_PRIVATE_SOURCES IS_INTERFACE_LIB _) + if(IS_INTERFACE_LIB) + if(ARG_PRIVATE_LINK_LIBRARIES) + message( + FATAL_ERROR + "`PRIVATE_LINK_LIBRARIES` disabled for header-only library ${ALIAS_TARGET_NAME}." + ) + endif() + add_library(${ARG_NAME} INTERFACE) + target_link_libraries(${ARG_NAME} INTERFACE ${ARG_PUBLIC_LINK_LIBRARIES}) + + target_compile_features(${ARG_NAME} INTERFACE cxx_std_20) + else() + add_library(${ARG_NAME}) + target_sources(${ARG_NAME} PRIVATE ${ARG_PRIVATE_SOURCES}) + target_link_libraries( + ${ARG_NAME} + PUBLIC + ${ARG_PUBLIC_LINK_LIBRARIES} + PRIVATE + ${ARG_PRIVATE_LINK_LIBRARIES} + ) + target_compile_features(${ARG_NAME} PUBLIC cxx_std_20) + endif() + + add_library(${ALIAS_TARGET_NAME} ALIAS ${ARG_NAME}) + + target_sources( + ${ARG_NAME} + PUBLIC + FILE_SET HEADERS + BASE_DIRS + "$" + "$" + FILES ${ARG_PUBLIC_HEADERS} + ) +endfunction() + +# Adds a C++ 20 test executable named `unit-test-NAME` that will be built with `SOURCES` and linked +# with `LINK_LIBRARIES`, in addition to Catch2. +# +# @param {string} NAME +# @param {string} NAMESPACE +# @param {string[]} SOURCES +# @param {string[]} [LINK_LIBRARIES] +# @param {string} [UNIFIED_TEST_TARGET] If set, `SOURCES` and `LINK_LIBRARIES` are also added to +# `UNIFIED_TEST_TARGET`. +function(add_catch2_tests) + set(SINGLE_VALUE_ARGS + NAME + NAMESPACE + UNIFIED_TEST_TARGET + ) + set(MULTI_VALUE_ARGS + SOURCES + LINK_LIBRARIES + ) + set(REQUIRED_ARGS + NAME + NAMESPACE + SOURCES + ) + cmake_parse_arguments(ARG "" "${SINGLE_VALUE_ARGS}" "${MULTI_VALUE_ARGS}" ${ARGN}) + check_required_arguments_exist("${REQUIRED_ARGS}") + + set(ALIAS_TARGET "${ARG_NAMESPACE}::${ARG_NAME}") + set(UNIT_TEST_TARGET "unit-test-${ARG_NAME}") + + add_executable(${UNIT_TEST_TARGET}) + target_sources(${UNIT_TEST_TARGET} PRIVATE ${ARG_SOURCES}) + target_link_libraries( + ${UNIT_TEST_TARGET} + PRIVATE + ${ALIAS_TARGET} + ${ARG_LINK_LIBRARIES} + Catch2::Catch2WithMain + ) + target_compile_features(${UNIT_TEST_TARGET} PRIVATE cxx_std_20) + set_property( + TARGET + ${UNIT_TEST_TARGET} + PROPERTY + RUNTIME_OUTPUT_DIRECTORY + ${CMAKE_BINARY_DIR}/testbin + ) + + if(ARG_UNIFIED_TEST_TARGET) + target_sources(${ARG_UNIFIED_TEST_TARGET} PRIVATE ${ARG_SOURCES}) + target_link_libraries( + ${ARG_UNIFIED_TEST_TARGET} + PRIVATE + ${ALIAS_TARGET} + ${ARG_LINK_LIBRARIES} + ) + endif() +endfunction() + +# Creates installation rules for library targets and generated configuration files. +# +# @param {string} NAME +# @param {string} NAMESPACE +# @param {string} CONFIG_DEST_DIR Destination to install the generated config file +# (`NAME-config.cmake`). +# @param {string} CONFIG_INPUT_DIR `configure_package_config_file` input file +# (`NAME-config.cmake.in`). +function(install_library) + set(SINGLE_VALUE_ARGS + NAME + NAMESPACE + CONFIG_DEST_DIR + CONFIG_INPUT_DIR + ) + set(REQUIRED_ARGS + NAME + NAMESPACE + CONFIG_DEST_DIR + CONFIG_INPUT_DIR + ) + cmake_parse_arguments(ARG "" "${SINGLE_VALUE_ARGS}" "" ${ARGN}) + check_required_arguments_exist("${REQUIRED_ARGS}") + + set(EXPORT_NAME "${ARG_NAME}-target") + install(TARGETS "${ARG_NAME}" EXPORT "${EXPORT_NAME}" LIBRARY ARCHIVE RUNTIME FILE_SET HEADERS) + set_target_properties( + "${ARG_NAME}" + PROPERTIES + OUTPUT_NAME + "${ARG_NAMESPACE}_${ARG_NAME}" + ) + install( + EXPORT "${EXPORT_NAME}" + DESTINATION ${ARG_CONFIG_DEST_DIR} + NAMESPACE "${ARG_NAMESPACE}::" + ) + + set(CONFIG_FILE_NAME "${ARG_NAME}-config.cmake") + set(CONFIG_FILE_OUTPUT_PATH "${CMAKE_CURRENT_BINARY_DIR}/${CONFIG_FILE_NAME}") + configure_package_config_file( + "${ARG_CONFIG_INPUT_DIR}/${CONFIG_FILE_NAME}.in" + "${CONFIG_FILE_OUTPUT_PATH}" + INSTALL_DESTINATION "${ARG_CONFIG_DEST_DIR}" + ) + install(FILES "${CONFIG_FILE_OUTPUT_PATH}" DESTINATION "${ARG_CONFIG_DEST_DIR}") +endfunction() diff --git a/cmake/ystdlib/libs/containers-config.cmake.in b/cmake/ystdlib/libs/containers-config.cmake.in new file mode 100644 index 00000000..280e9201 --- /dev/null +++ b/cmake/ystdlib/libs/containers-config.cmake.in @@ -0,0 +1,3 @@ +include_guard(GLOBAL) + +include("${CMAKE_CURRENT_LIST_DIR}/containers-target.cmake") diff --git a/cmake/ystdlib/libs/error_handling-config.cmake.in b/cmake/ystdlib/libs/error_handling-config.cmake.in new file mode 100644 index 00000000..358ec780 --- /dev/null +++ b/cmake/ystdlib/libs/error_handling-config.cmake.in @@ -0,0 +1,5 @@ +include_guard(GLOBAL) + +find_dependency(Boost @BOOST_FIND_PACKAGE_ARGS@) + +include("${CMAKE_CURRENT_LIST_DIR}/error_handling-target.cmake") diff --git a/cmake/ystdlib/libs/io_interface-config.cmake.in b/cmake/ystdlib/libs/io_interface-config.cmake.in new file mode 100644 index 00000000..b6f6d58a --- /dev/null +++ b/cmake/ystdlib/libs/io_interface-config.cmake.in @@ -0,0 +1,5 @@ +include_guard(GLOBAL) + +include("${CMAKE_CURRENT_LIST_DIR}/wrapped_facade_headers-config.cmake") + +include("${CMAKE_CURRENT_LIST_DIR}/io_interface-target.cmake") diff --git a/cmake/ystdlib/libs/wrapped_facade_headers-config.cmake.in b/cmake/ystdlib/libs/wrapped_facade_headers-config.cmake.in new file mode 100644 index 00000000..b176dacd --- /dev/null +++ b/cmake/ystdlib/libs/wrapped_facade_headers-config.cmake.in @@ -0,0 +1,3 @@ +include_guard(GLOBAL) + +include("${CMAKE_CURRENT_LIST_DIR}/wrapped_facade_headers-target.cmake") diff --git a/cmake/ystdlib/ystdlib-config.cmake.in b/cmake/ystdlib/ystdlib-config.cmake.in new file mode 100644 index 00000000..4d41775a --- /dev/null +++ b/cmake/ystdlib/ystdlib-config.cmake.in @@ -0,0 +1,12 @@ +include(CMakeFindDependencyMacro) + +@PACKAGE_INIT@ + +set(ystdlib_LIBRARIES_LIST "@ystdlib_LIBRARIES@") +foreach(LIB IN LISTS ystdlib_LIBRARIES_LIST) + if(NOT TARGET ystdlib::${LIB}) + include("${CMAKE_CURRENT_LIST_DIR}/libs/${LIB}-config.cmake") + endif() +endforeach() + +check_required_components(ystdlib) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 00000000..c22d06bd --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.23) + +project(ystdlib-examples) + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(default_build_type "Release") + message(STATUS "No build type specified. Setting to '${default_build_type}'.") + set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE STRING "Choose the type of build." FORCE) +endif() + +set(CMAKE_EXPORT_COMPILE_COMMANDS + ON + CACHE BOOL + "Enable/disable output of compile commands during generation." + FORCE +) + +if(ystdlib-examples_IS_TOP_LEVEL) + # Include dependency settings if the project isn't being included as a subproject. + # NOTE: We mark the file optional because if the user happens to have the dependencies + # installed, this file is not necessary. + include("${CMAKE_CURRENT_LIST_DIR}/../build/deps/cmake-settings/all.cmake" OPTIONAL) +endif() + +# Prefer the new BoostConfig.cmake package configuration if possible. +if(POLICY "CMP0167") + cmake_policy(SET "CMP0167" "NEW") +endif() +find_package(ystdlib REQUIRED) +message(STATUS "Found ystdlib ${ystdlib_VERSION}.") + +add_executable(linking-tests "${CMAKE_CURRENT_LIST_DIR}/src/linking-tests.cpp") + +target_compile_features(linking-tests PRIVATE cxx_std_20) + +target_link_libraries( + linking-tests + PRIVATE + ystdlib::containers + ystdlib::error_handling + ystdlib::io_interface + ystdlib::wrapped_facade_headers +) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..9b316990 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,28 @@ +# Examples + +This directory contains example programs that demonstrate how to use the ystdlib library. + +The example program `linking-tests` references all of ystdlib's library targets to ensure they can +be installed and linked correctly. + +## Requirements + +[Build](../README.md#building) and [install](../README.md#installing) ystdlib. The commands below +assume you've built and installed ystdlib to `./build/examples/ystdlib`. If you installed it to a +different location, adjust the paths accordingly. + +## Building + +```shell +cmake -S "./examples" -B "./build/examples" -Dystdlib_ROOT="./build/examples/ystdlib" + +cmake --build "./build/examples" +``` + +## Running + +```shell +./build/examples/linking-tests +``` + +On success, the exit code will be 0 with no output printed. diff --git a/examples/src/linking-tests.cpp b/examples/src/linking-tests.cpp new file mode 100644 index 00000000..22810387 --- /dev/null +++ b/examples/src/linking-tests.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +enum class BinaryErrorCodeEnum : uint8_t { + Success = 0, + Failure +}; + +using BinaryErrorCode = ystdlib::error_handling::ErrorCode; +using BinaryErrorCategory = ystdlib::error_handling::ErrorCategory; + +template <> +auto BinaryErrorCategory::name() const noexcept -> char const* { + return "Binary Error Code"; +} + +template <> +auto BinaryErrorCategory::message(BinaryErrorCodeEnum error_enum) const -> std::string { + switch (error_enum) { + case BinaryErrorCodeEnum::Success: + return std::string{"Success"}; + case BinaryErrorCodeEnum::Failure: + return std::string{"Failure"}; + default: + return std::string{"Unrecognized Error Code"}; + } +} + +YSTDLIB_ERROR_HANDLING_MARK_AS_ERROR_CODE_ENUM(BinaryErrorCodeEnum); + +namespace { +using ystdlib::io_interface::ErrorCode; + +class FailureReader : public ystdlib::io_interface::ReaderInterface { +public: + [[nodiscard]] auto read([[maybe_unused]] char* buf, [[maybe_unused]] size_t num_bytes_to_read, [[maybe_unused]] size_t& num_bytes_read) + -> ErrorCode override { + return ErrorCode::ErrorCode_Unsupported; + } + + [[nodiscard]] auto seek_from_begin([[maybe_unused]] size_t pos) -> ErrorCode override { + return ErrorCode::ErrorCode_Unsupported; + } + + [[nodiscard]] auto seek_from_current([[maybe_unused]] off_t offset) -> ErrorCode override { + return ErrorCode::ErrorCode_Unsupported; + } + + [[nodiscard]] auto get_pos([[maybe_unused]] size_t& pos) -> ErrorCode override { + return ErrorCode::ErrorCode_Unsupported; + } +}; + +auto test_containers() -> bool; +auto test_error_handling() -> bool; +auto test_io_interface() -> bool; +auto test_wrapped_facade_headers() -> bool; + +auto test_containers() -> bool { + try { + constexpr size_t cBufferSize{1024}; + ystdlib::containers::Array arr(cBufferSize); + for (size_t idx{0}; idx < cBufferSize; ++idx) { + arr.at(idx) = idx; + } + auto const& arr_const_ref = arr; + return std::ranges::equal(arr, arr_const_ref); + } catch (std::exception const&) { + return false; + } +} + +auto test_error_handling() -> bool { + BinaryErrorCode const success{BinaryErrorCodeEnum::Success}; + std::error_code const success_error_code{success}; + return success == success_error_code; +} + +auto test_io_interface() -> bool { + FailureReader reader{}; + return ErrorCode::ErrorCode_Unsupported == reader.seek_from_begin(0); +} + +auto test_wrapped_facade_headers() -> bool { + u_int const uint{1}; + u_long const ulong{2}; + quad_t const quadt{3}; + return 1 == uint && 2 == ulong && 3 == quadt; +} +} // namespace + +auto main() -> int { + if (false == test_containers()) { + std::cerr << "Error: containers test failed. Could not validate array.\n"; + return 1; + } + + if (false == test_error_handling()) { + std::cerr << "Error: error_handling test failed. Could not use BinaryErrorCode.\n"; + return 2; + } + + if (false == test_io_interface()) { + std::cerr << "Error: io_interface test failed. Could not use FailureReader.\n"; + return 3; + } + + if (false == test_wrapped_facade_headers()) { + std::cerr << "Error: wrapped_facade_headers test failed. Could not create sys/types.\n"; + return 4; + } + + return 0; +} diff --git a/src/ystdlib/CMakeLists.txt b/src/ystdlib/CMakeLists.txt index 94a7164c..2bd0c711 100644 --- a/src/ystdlib/CMakeLists.txt +++ b/src/ystdlib/CMakeLists.txt @@ -1,4 +1,7 @@ -add_subdirectory(containers) -add_subdirectory(error_handling) -add_subdirectory(io_interface) -add_subdirectory(wrapped_facade_headers) +if(NOT DEFINED ystdlib_LIBRARIES OR ystdlib_LIBRARIES STREQUAL "") + message(FATAL_ERROR "ystdlib_LIBRARIES must be defined and non-empty.") +endif() + +foreach(LIB IN LISTS ystdlib_LIBRARIES) + add_subdirectory("${LIB}") +endforeach() diff --git a/src/ystdlib/containers/CMakeLists.txt b/src/ystdlib/containers/CMakeLists.txt index 873a08aa..52748e67 100644 --- a/src/ystdlib/containers/CMakeLists.txt +++ b/src/ystdlib/containers/CMakeLists.txt @@ -1,8 +1,18 @@ -cpp_library( +add_cpp_library(NAME containers NAMESPACE ystdlib PUBLIC_HEADERS Array.hpp) + +if(ystdlib_ENABLE_TESTS) + add_catch2_tests( + NAME containers + NAMESPACE ystdlib + SOURCES + test/test_Array.cpp + UNIFIED_TEST_TARGET "${UNIFIED_UNIT_TEST_TARGET}" + ) +endif() + +install_library( NAME containers NAMESPACE ystdlib - PUBLIC_HEADERS - Array.hpp - TESTS_SOURCES - test/test_Array.cpp + CONFIG_DEST_DIR "${CONFIG_LIBS_DEST_DIR}" + CONFIG_INPUT_DIR "${CONFIG_LIBS_INPUT_DIR}" ) diff --git a/src/ystdlib/error_handling/CMakeLists.txt b/src/ystdlib/error_handling/CMakeLists.txt index 9841226a..d60f0bfc 100644 --- a/src/ystdlib/error_handling/CMakeLists.txt +++ b/src/ystdlib/error_handling/CMakeLists.txt @@ -1,4 +1,17 @@ -cpp_library( +# Prefer the new BoostConfig.cmake package configuration if possible. +if(POLICY "CMP0167") + cmake_policy(SET "CMP0167" "NEW") +endif() +set(BOOST_FIND_PACKAGE_ARGS + "${BOOST_MIN_VERSION}" + REQUIRED + COMPONENTS + headers +) +find_package(Boost ${BOOST_FIND_PACKAGE_ARGS}) +message(STATUS "Found Boost ${Boost_VERSION}.") + +add_cpp_library( NAME error_handling NAMESPACE ystdlib PUBLIC_HEADERS @@ -8,11 +21,24 @@ cpp_library( utils.hpp PUBLIC_LINK_LIBRARIES Boost::headers - TESTS_SOURCES - test/constants.hpp - test/test_ErrorCode.cpp - test/test_Result.cpp - test/test_TraceableException.cpp - test/types.cpp - test/types.hpp +) + +if(ystdlib_ENABLE_TESTS) + add_catch2_tests( + NAME error_handling + NAMESPACE ystdlib + SOURCES + test/test_ErrorCode.cpp + test/test_Result.cpp + test/test_TraceableException.cpp + test/types.cpp + UNIFIED_TEST_TARGET "${UNIFIED_UNIT_TEST_TARGET}" + ) +endif() + +install_library( + NAME error_handling + NAMESPACE ystdlib + CONFIG_DEST_DIR "${CONFIG_LIBS_DEST_DIR}" + CONFIG_INPUT_DIR "${CONFIG_LIBS_INPUT_DIR}" ) diff --git a/src/ystdlib/io_interface/CMakeLists.txt b/src/ystdlib/io_interface/CMakeLists.txt index 0f638a4e..1fc98e36 100644 --- a/src/ystdlib/io_interface/CMakeLists.txt +++ b/src/ystdlib/io_interface/CMakeLists.txt @@ -1,4 +1,11 @@ -cpp_library( +if(NOT "wrapped_facade_headers" IN_LIST ystdlib_LIBRARIES) + add_subdirectory( + "${CMAKE_CURRENT_LIST_DIR}/../wrapped_facade_headers" + "${CMAKE_CURRENT_BINARY_DIR}/implicit_dep/wrapped_facade_headers" + ) +endif() + +add_cpp_library( NAME io_interface NAMESPACE ystdlib PUBLIC_HEADERS @@ -9,7 +16,22 @@ cpp_library( ReaderInterface.cpp PUBLIC_LINK_LIBRARIES ystdlib::wrapped_facade_headers - TESTS_SOURCES - test/test_ReaderInterface.cpp - test/test_WriterInterface.cpp +) + +if(ystdlib_ENABLE_TESTS) + add_catch2_tests( + NAME io_interface + NAMESPACE ystdlib + SOURCES + test/test_ReaderInterface.cpp + test/test_WriterInterface.cpp + UNIFIED_TEST_TARGET "${UNIFIED_UNIT_TEST_TARGET}" + ) +endif() + +install_library( + NAME io_interface + NAMESPACE ystdlib + CONFIG_DEST_DIR "${CONFIG_LIBS_DEST_DIR}" + CONFIG_INPUT_DIR "${CONFIG_LIBS_INPUT_DIR}" ) diff --git a/src/ystdlib/wrapped_facade_headers/CMakeLists.txt b/src/ystdlib/wrapped_facade_headers/CMakeLists.txt index ece0a321..4d87e606 100644 --- a/src/ystdlib/wrapped_facade_headers/CMakeLists.txt +++ b/src/ystdlib/wrapped_facade_headers/CMakeLists.txt @@ -1,8 +1,18 @@ -cpp_library( +add_cpp_library(NAME wrapped_facade_headers NAMESPACE ystdlib PUBLIC_HEADERS sys/types.h) + +if(ystdlib_ENABLE_TESTS) + add_catch2_tests( + NAME wrapped_facade_headers + NAMESPACE ystdlib + SOURCES + test/test_sys_types.cpp + UNIFIED_TEST_TARGET "${UNIFIED_UNIT_TEST_TARGET}" + ) +endif() + +install_library( NAME wrapped_facade_headers NAMESPACE ystdlib - PUBLIC_HEADERS - sys/types.h - TESTS_SOURCES - test/test_sys_types.cpp + CONFIG_DEST_DIR "${CONFIG_LIBS_DEST_DIR}" + CONFIG_INPUT_DIR "${CONFIG_LIBS_INPUT_DIR}" ) diff --git a/taskfile.yaml b/taskfile.yaml index 98b3dad9..51d3ebc5 100644 --- a/taskfile.yaml +++ b/taskfile.yaml @@ -16,7 +16,7 @@ vars: # These should be kept in-sync with its usage in CMakeLists.txt G_DEPS_CMAKE_SETTINGS_DIR: "{{.G_DEPS_DIR}}/cmake-settings" - G_DEPS_CMAKE_SETTINGS_FILE: "{{.G_DEPS_CMAKE_SETTINGS_DIR}}/settings.cmake" + G_DEPS_CMAKE_SETTINGS_FILE: "{{.G_DEPS_CMAKE_SETTINGS_DIR}}/all.cmake" G_TEST_BIN_DIR: "{{.G_BUILD_DIR}}/testbin" G_TEST_TARGET_SUFFIXES: diff --git a/taskfiles/lint-cmake.yaml b/taskfiles/lint-cmake.yaml index 06cde880..f157315b 100644 --- a/taskfiles/lint-cmake.yaml +++ b/taskfiles/lint-cmake.yaml @@ -50,6 +50,6 @@ tasks: find . \ \( -path '**/build' -o -path '**/cmake_install.cmake' -o -path '**/CMakeFiles' \ -o -path '**/submodules' -o -path '**/tools' \) -prune -o \ - \( -iname "CMakeLists.txt" -o -iname "*.cmake" -o -iname "*.cmake.in" \) \ + \( -iname "CMakeLists.txt" -o -iname "*.cmake" \) \ -print0 | \ xargs -0 gersemi {{.FLAGS}}