UPDATE: I am no longer updating this post with the contents of my actual project since they have changed significantly (Unit testing is now done with doctest, code coverage is included, etc.). This post is still complete as it is and should suffice to serve as a starting point for new C++14 projects.
Recently, I have decided to create a C++14 oroject template that I plan on using for all of my C++ projects. The build system generator I use here is CMake. Here were my goals when I set this up:
- The project should compile with MSVC, GCC, and Clang.
- I can test the code on all platforms.
- I can install the targets on all platforms.
- I can generate Documentation on all platforms using Doxygen.
Here is my project structure:
project-name/
├── CMakeLists.txt
├── cmake
│ └── Modules
│ └── ParseAndAddCatchTests.cmake
├── doc
│ ├── CMakeLists.txt
│ ├── Doxyfile.in
│ └── main_page.md
├── include
│ └── project-abbr
│ ├── config.hpp <-- Contains project versioning
│ └── factorial.hpp
├── src
│ ├── CMakeLists.txt
│ ├── factorial.cpp
│ └── main.cpp
├── test
│ ├── CMakeLists.txt
│ ├── factorial_test.cpp
│ └── test_runner.cpp
└── third_party
└── catch
└── CMakeLists.txt
Here is my top level CMakeLists.txt
:
cmake_minimum_required(VERSION 3.1)
if (POLICY CMP0048)
cmake_policy(SET CMP0048 NEW)
endif (POLICY CMP0048)
project(Project-Name VERSION 0.1.0)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Options
option(BUILD_TESTS "Build test executable" OFF)
option(GEN_DOCS "Generate documentation" OFF)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/")
if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
message(STATUS "Setting build type to 'Release' as no build type was specified")
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the build type (Debug/Release)" FORCE)
endif (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2")
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /WX")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /O2")
endif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
include_directories(include)
add_subdirectory(src)
if (BUILD_TESTS)
add_subdirectory(third_party/catch)
include(CTest)
enable_testing()
add_subdirectory(test)
endif (BUILD_TESTS)
if (GEN_DOCS)
add_subdirectory(doc)
endif (GEN_DOCS)
# Install the project header files into the appropriate directory
# Other installs are in src/CMakeLists.txt
install(DIRECTORY include/ DESTINATION include)
The CMakeLists.txt
in src
:
add_executable(Project-Name main.cpp) # The main executable
add_library(Project-Name-lib hello_world.cpp factorial.cpp) # A library for tests
SET_TARGET_PROPERTIES(Project-Name-lib PROPERTIES PREFIX "") # Remove the lib prefix
target_link_libraries(Project-Name Project-Name-lib) # Link our sources to the executable
install(TARGETS Project-Name DESTINATION bin)
install(TARGETS Project-Name-lib
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
The CMakeLists.txt
in test
:
set(TEST_SOURCES factorial_test.cpp hello_world_test.cpp)
add_executable(test_runner test_runner.cpp ${TEST_SOURCES})
target_link_libraries(test_runner Project-Name-lib)
target_include_directories(test_runner PRIVATE ${CATCH_INCLUDE_DIR} ${COMMON_INCLUDES})
include(ParseAndAddCatchTests)
ParseAndAddCatchTests(test_runner)
The CMakeLists.txt
in third_party/catch
(Taken from official docs and slightly modified):
include(ExternalProject)
find_package(Git REQUIRED)
ExternalProject_Add(
catch
PREFIX ${CMAKE_BINARY_DIR}/catch
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
TIMEOUT 10
UPDATE_COMMAND ${GIT_EXECUTABLE} pull
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD ON
)
# Expose required variable (CATCH_INCLUDE_DIR) to parent scope
ExternalProject_Get_Property(catch source_dir)
set(CATCH_INCLUDE_DIR ${source_dir}/single_include CACHE INTERNAL "Path to include folder for Catch")
The CMakeLists.txt
in doc
:
find_package(Doxygen)
if (DOXYGEN_FOUND)
set(DOXYGEN_IN Doxyfile.in)
set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY)
add_custom_target(doc
COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
VERBATIM
)
else (DOXYGEN_FOUND)
message(FATAL_ERROR "Doxygen needs to be installed to generate documentation!")
endif (DOXYGEN_FOUND)
Here are the questions I want to be answered from this review:
- Am I following the best practices in terms of setting up my project?
- Is there any way my CMake syntax could be improved upon?
- Is my code suitable for Clang, GCC, and MSVC? (I have tested using Travis CI and Appveyor, but I just need to make sure)
- Am I installing all of my targets correctly (In the right directories)?
- How would I handle resource files in this setup? Suppose a file in
src
needed a file insrc/resources/
. Then where would this file be installed? Inbin
or somewhere else? (Full SO Question)
ParseAndAddCatchTests.cmake
left out intentionally? Also, do you want to support other c++ compilers, or just those three? \$\endgroup\$ParseAndAddCatchTests.cmake
. \$\endgroup\$