10
\$\begingroup\$

I started working on a C++ project using Google Test framework for unit testing and header-only CImg library for internal operations. I want to use CMake build system to compile it.

$ tree
.
├── build
├── cmake
│   └── includes.cmake
├── CMakeLists.txt
├── include
│   └── soccl
│       └── soccl.hpp
└── src
    ├── soccl
    │   ├── CMakeLists.txt
    │   └── soccl.cpp
    └── tests
        ├── CMakeLists.txt
        ├── main.cpp
        └── soccl.cpp

7 directories, 8 files

Source:

# .gitignore
*.o
*.so

build

# CMakeLists.txt
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(SoftwareOccluder VERSION 0.1 LANGUAGES CXX)

set(CMAKE_BUILD_TYPE Release)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14 -Os -Wall -pedantic")
set(CMAKE_EXE_LINKER_FLAGS "-s ${CIMG_FLAGS}")

set(EXTERNAL_INSTALL_LOCATION ${CMAKE_BINARY_DIR}/external)

include_directories(include)
include(cmake/includes.cmake)

ADD_SUBDIRECTORY(src/soccl)
ADD_SUBDIRECTORY(src/tests)

# cmake/includes.cmake
include(ExternalProject)

ExternalProject_Add(
    cimg
    GIT_REPOSITORY https://github.com/dtschump/CImg.git
    SOURCE_DIR "cimg"
    CONFIGURE_COMMAND ""
    BUILD_COMMAND ""
    INSTALL_COMMAND ""
)

include_directories(${CMAKE_CURRENT_BINARY_DIR}/cimg)

SET(CIMG_FLAGS "-lm -lpthread")

/* include/soccl/soccl.hpp */
#ifndef SOCCL_HPP
#define SOCCL_HPP

#define cimg_display 0
#include <CImg.h>

namespace soccl {

struct RGB {
    int r, g, b;
};

class Soccl {
public:
    Soccl();

    RGB get_pixel(int x, int y);
    void set_pixel(int x, int y, RGB rgb);
private:
    cimg_library::CImg<unsigned char> img_;
};

} // namespace soccl

#endif

# src/soccl/CMakeLists.txt
file(GLOB SOURCES "*.cpp")
add_library(soccl SHARED ${SOURCES})
install(TARGETS soccl DESTINATION /usr/lib)

/* src/soccl/soccl.cpp */
#include "soccl/soccl.hpp"

namespace soccl {

Soccl::Soccl() : img_(320, 240, 1, 3) {
    img_.fill(0);
}

RGB Soccl::get_pixel(int x, int y) {
    RGB rgb;
    rgb.r = static_cast<int>(img_(x, y, 0, 0));
    rgb.g = static_cast<int>(img_(x, y, 0, 1));
    rgb.b = static_cast<int>(img_(x, y, 0, 2));
    return rgb; 
}

void Soccl::set_pixel(int x, int y, RGB rgb) {
    img_(x, y, 0, 0) = rgb.r;
    img_(x, y, 0, 1) = rgb.g;
    img_(x, y, 0, 2) = rgb.b;
}

} // namespace soccl

# src/tests/CMakeLists.txt
include(ExternalProject)

ExternalProject_Add(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest
    CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${EXTERNAL_INSTALL_LOCATION}
)
include_directories(${EXTERNAL_INSTALL_LOCATION}/include)
link_directories(${EXTERNAL_INSTALL_LOCATION}/lib)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
file(GLOB TESTS "*.cpp")
add_executable(tests ${TESTS})
target_link_libraries(tests soccl gtest gmock gtest_main pthread)

/* src/tests/main.cpp */
#include "gmock/gmock.h"

int main(int argc, char** argv) {
    testing::InitGoogleMock(&argc, argv);
    return RUN_ALL_TESTS();
}

/* src/tests/soccl.cpp */
#include "soccl/soccl.hpp"

#include "gmock/gmock.h"

class SocclTest: public testing::Test {
public:
    soccl::Soccl soccl;
};

TEST_F(SocclTest, TestPixelGetter) {
    soccl::RGB rgb = soccl.get_pixel(10, 20);
    ASSERT_EQ(rgb.r, 0);
    ASSERT_EQ(rgb.g, 0);
    ASSERT_EQ(rgb.b, 0);
}

TEST_F(SocclTest, TestPixelSetter) {
    soccl::RGB rgb{12, 21, 44};
    soccl.set_pixel(43, 23, rgb);

    ASSERT_EQ(rgb.r, 12);
    ASSERT_EQ(rgb.g, 21);
    ASSERT_EQ(rgb.b, 44);
}

Compilation:

$ cd build
$ cmake ..
$ make

Running tests (compiled as a binary file in build dir):

$ ./tests 
[==========] Running 2 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 2 tests from SocclTest
[ RUN      ] SocclTest.TestPixelGetter
[       OK ] SocclTest.TestPixelGetter (0 ms)
[ RUN      ] SocclTest.TestPixelSetter
[       OK ] SocclTest.TestPixelSetter (0 ms)
[----------] 2 tests from SocclTest (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test case ran. (0 ms total)
[  PASSED  ] 2 tests.
\$\endgroup\$
3
  • \$\begingroup\$ How is it working out for you so far, running as intended? Any trouble you encounter you'd want to improve? \$\endgroup\$
    – Mast
    Sep 25, 2017 at 19:13
  • 1
    \$\begingroup\$ Everything seems to work as intended. I'm mostly interested if I follow CMake best practices correctly, since I don't have much experience with it and presented C++ code is rather simple and straightforward. \$\endgroup\$
    – Nykakin
    Sep 25, 2017 at 19:22
  • \$\begingroup\$ You've gotta ask yourself one question: "Why would I use CMake on the first place?". With C/C++ pluralism there are numerous fancy ways of organizing and building projects. And CMake is rather sophisticated tool that can deal with many of such ways or even be extended to deal with even more fancier scenarios. But the thing is that with CMake overall code organization and project building complexity only gets higher. Actually I would even say that with CMake it instantly jumps on epic height. While all sorts of best practices suggest to keep things simple. \$\endgroup\$ Sep 30, 2017 at 21:55

1 Answer 1

8
\$\begingroup\$

Since you have asked for CMake best practices let me concentrate on that part. Here are my recommendations:

  • Keep it cross-platform. Avoid compiler/platform specific settings.
  • Reorder your project to use one sub-folder with one CMakeLists.txt for each target.
  • Don't use ExternalProject_Add() if your project is on Git. Use Git Submodules instead.
  • For external projects that don't come with CMake support use "Imported Targets".
  • Don't use file(GLOB ...) to collect source files. CMake won't find newly added sources automatically otherwise.
  • Add header files also to your target's source file list. It will otherwise not show up in generated IDE projects.
  • Use target_compile_features() command to specify a C++ feature or a standard your project needs.
  • Don't specify full paths as install(... DESTINATION ...) to allow the user of your library to specify another CMAKE_INSTALL_PREFIX.
  • Allow the user to specify his/her own CMAKE_BUILD_TYPE. Don't put it in your CMakeList.txt file.

I've uploaded my modified version to: https://github.com/FloriansGit/SoftwareOccluder

Note: Since the Git repository has submodules you need to add --recursive to your clone command like:

git clone --recursive https://github.com/FloriansGit/SoftwareOccluder.git

.gitmodules

[submodule "CImg"]
    path = CImg
    url = https://github.com/dtschump/CImg
[submodule "googletest"]
    path = googletest
    url = https://github.com/google/googletest

CMakeLists.txt

cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

project(SoftwareOccluder VERSION 0.1 LANGUAGES CXX)

add_library(CImg INTERFACE IMPORTED GLOBAL)
set_property(
    TARGET CImg 
    APPEND 
    PROPERTY
        INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/CImg"
)

# Prevent overriding the parent project's compiler/linker
# settings on Windows
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
add_subdirectory("googletest")

add_subdirectory("soccl")

soccl/CMakeLists.txt

set(
    SOURCES 
        "src/soccl.cpp"
        "include/soccl.hpp"
)

add_library(soccl ${SOURCES})
target_include_directories(soccl PUBLIC "include")
target_compile_features(soccl PUBLIC "cxx_std_14")
target_link_libraries(soccl CImg)

install(
    TARGETS soccl 
    ARCHIVE DESTINATION lib
    LIBRARY DESTINATION lib
    RUNTIME DESTINATION bin
)

add_subdirectory("test")

soccl/test/CMakeLists.txt

set(
    TESTS 
        "src/main.cpp"
        "src/soccl_test.cpp"
)
add_executable(tests ${TESTS})
target_link_libraries(tests soccl gtest gmock)

References

\$\endgroup\$
2
  • \$\begingroup\$ I've got two errors on cmake ..: The source directory /home/testuser/SoftwareOccluder/googletest does not contain a CMakeLists.txt file. and target_compile_features specified unknown feature "cxx_std_14" for target "soccl". \$\endgroup\$
    – Nykakin
    Dec 5, 2017 at 22:42
  • \$\begingroup\$ @Nykakin Since the Git repository has submodules you need to add --recursive to your clone command. Sorry forgot to mention this. That will fix the empty googletest directory. I've updated my answer accordingly. For the "unknown feature cxx_std_14" which compiler do you use? You can check what CMake does see as valid/supported features for your compiler with something like message("CMAKE_CXX_COMPILE_FEATURES = ${CMAKE_CXX_COMPILE_FEATURES}"). \$\endgroup\$
    – Florian
    Dec 6, 2017 at 8:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.