7
\$\begingroup\$

I wasn't quite sure where to go with this, and this seemed to be the best place I could think of so I thought why not. I threw together a build system for C++ projects today using CMake and I wanted your opinions on it, on things I could do better, or change to better comply with best practices.

Here's my directory structure:

.
├── bin (Compiled executables)
│   └── lib (Executables from third-party libraries)
├── build (The CMake build folder)
│   ├── CMakeCache.txt
│   ├── CMakeFiles
│   ├── cmake_install.cmake
│   ├── inc
│   ├── lib
│   ├── Makefile
│   └── test
├── CMakeLists.txt
├── inc (Objects used in programming)
│   └── CMakeLists.txt
|   └── fake_object (An example)
|       └── fake_object.h
|       └── fake_object.cc
├── lib (External/third-party libraries)
│   ├── CMakeLists.txt
│   └── gmock (Unit testing)
├── main.cc
├── res (External resources such as images, sounds, fonts)
└── test (tests, lol)
    └── CMakeLists.txt

I decided to take a modular approach to my C++ structure, so each object I build for my programs has its own directory within the inc folder.

Here is my main CMakeLists.txt:

########################################################################
#                              Basic info                              #
########################################################################

cmake_minimum_required(VERSION 2.8)
project(skeleton)

########################################################################
#                              Variables                               #
########################################################################

# My vars
set(test_prefix ${CMAKE_SOURCE_DIR}/test)
set(GMOCK_DIR ${CMAKE_SOURCE_DIR}/lib/gmock)
set(GMOCK_TEST_LIBS ${CMAKE_SOURCE_DIR}/bin/lib)

# System vars
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)
set(CMAKE_CXX_FLAGS -lpthread)

# Play around boiiiiiis
set(CMAKE_VERBOSE_MAKEFILE on)
set(NO_MAIN true)
set(classes) # All objects go here

########################################################################
#                                 Main                                 #
########################################################################

##############
#  Includes  #
##############

include_directories(inc)
include_directories(res)

include_directories(${GMOCK_DIR}/include)
include_directories(${GMOCK_DIR}/gtest/include)

###########
#  Logic  #
###########

if(NOT ${NO_MAIN})
    add_executable(${CMAKE_PROJECT_NAME} main.cc)
    target_link_libraries(${CMAKE_PROJECT_NAME} ${classes})
endif(NOT ${NO_MAIN})

########################################################################
#                         Where do we go now?                          #
########################################################################

add_subdirectory(inc)
add_subdirectory(lib)
add_subdirectory(test)

The CMakeLists.txt in the test directory:

########################################################################
#                         Find GMock/Test Libs                         #
########################################################################

find_library(GMOCK_LIBRARY
        NAMES gmock
        libgmock
        libgmock.a
        PATHS ${GMOCK_TEST_LIBS})

find_library(GMOCK_LIBRARY_MAIN
        NAMES gmock_main
        libgmock_main
        libgmock_main.a
        PATHS ${GMOCK_TEST_LIBS})

find_library(GTEST_LIBRARY
        NAMES gtest
        libgtest
        libgtest.a
        PATHS ${GMOCK_TEST_LIBS})

find_library(GTEST_LIBRARY_MAIN
        NAMES gtest_main
        libgtest_main
        libgtest.a
        PATHS ${GMOCK_TEST_LIBS})

########################################################################
#                            Compile tests                             #
########################################################################


#########################
#  Grab all test cases  #
#########################

set(test_sources)

foreach(TEST ${classes})
    list(APPEND test_sources "${TEST}_test.cc")
endforeach(TEST)

#######################
#  Create executable  #
#######################

if (DEFINED ${test_sources})
    add_executable(run_tests ${test_sources})

    target_link_libraries(run_tests ${GMOCK_LIBRARY} ${GMOCK_LIBRARY_MAIN})
    target_link_libraries(run_tests ${GTEST_LIBRARY} ${GTEST_LIBRARY_MAIN})
    target_link_libraries(run_tests ${classes})
endif(DEFINED ${test_sources})

The CMakeLists.txt in the inc directory:

########################################################################
#                      Make all subdirs libraries                      #
########################################################################

foreach(INC ${classes})
    add_library(${INC} ${INC}/${INC}.cc)
endforeach(INC)

I'm also going to implement a doc directory for documentation (probably using Doxygen), but I've done so before and I know it's relatively easy to implement.

\$\endgroup\$
3
\$\begingroup\$

This generally looks pretty good, but I have a few suggestions that might allow you to make your code and build system a little better.

Replace inc with src

The source code for your projects is apparently going into inc. I would expect source code in src and header-only includes perhaps in inc. I'd suggest changing from inc to src to make it clear that it's all source code and not just include files in there.

Move your main.cc with the rest of the source code

There is no one place for source code with this system, which is unfortunate. For something like ctags which can recursively build references to parts of your code, it's convenient for all of the code to be in a particular place, or at least at the top of a hierarchical directory tree. This might be the top of a hierarchy, except that the build directory is also under it, so source files copied there would have to be explicitly excluded. For my projects, I find it easier to put the source in a single subdirectory named src.

Reconsider "one directory per object"

If your projects are anything like mine, I think you'll find that there is very little to be gained in having each object get its own subdirectory. I usually find it easier to put all of the source together in a single directory. Only major subsystems, rather than individual objects, get a subdirectory.

Add a source package target

Sharing with others via source repository is good, but sometimes it's handy to just create a tarball. For that, I'd recommend using CPack to create it. Here's what I use:

set(CPACK_PACKAGE_VERSION_MAJOR "0")
set(CPACK_PACKAGE_VERSION_MINOR "1")
set(CPACK_PACKAGE_VERSION_PATCH "1")
SET(CPACK_SOURCE_GENERATOR "TBZ2")
SET(CPACK_SOURCE_PACKAGE_FILE_NAME "${CPACK_PACKAGE_FILE_NAME}_Source.tar.gz")
SET(CPACK_SOURCE_IGNORE_FILES "/build/;/*.swp;.git*")
set(CPACK_SOURCE_PACKAGE_FILE_NAME
  "${CMAKE_PROJECT_NAME}-${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}")
include (CPack)

One significant use of this is that once you have a "standard tree" set up, you can create the source tarball and then whenever you start a new project, simply untar the scaffolding into place and then go in and edit just the few things that would need to be project specific.

Add a doc subdirectory

As you've mentioned, generating documentation via Doxygen is often handy. I typically have a doc subdirectory off the top level into which the source for documentation would go. That is, if man or user manual is generated, I put the sources in there. I also tend to put programmer notes in there if they aren't already part of the Doxygen output. Then the generated documents that result from all of that get put into the corresponding doc within the build tree.

Add a top-level README file

I usually put in a README file at the very top. One obvious thing to put into it would be exactly the directory tree structure that you put into your question, perhaps with a bit more explanation about what goes where and simple instructions for how to build the project.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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