Getting CMake to build out of source without wrapping scripts

Solution 1:

CMake 3.13 or newer supports the command line options -S and -B to specify source and binary directory, respectively.

cmake -S . -B build -G "MSYS Makefiles"

This will look for the CMakeLists.txt in the current folder and create a build folder (if it does not yet exist) in it.

For older versions of CMake, you can use the undocumented CMake options -H and -B to specify the source and binary directory upon invoking cmake:

cmake -H. -Bbuild -G "MSYS Makefiles"

Note that there must not be a space character between the option and the directory path.

Solution 2:

A solution that I found recently is to combine the out-of-source build concept with a Makefile wrapper.

In my top-level CMakeLists.txt file, I include the following to prevent in-source builds:

if ( ${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR} )
    message( FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt." )
endif()

Then, I create a top-level Makefile, and include the following:

# -----------------------------------------------------------------------------
# CMake project wrapper Makefile ----------------------------------------------
# -----------------------------------------------------------------------------

SHELL := /bin/bash
RM    := rm -rf
MKDIR := mkdir -p

all: ./build/Makefile
    @ $(MAKE) -C build

./build/Makefile:
    @  ($(MKDIR) build > /dev/null)
    @  (cd build > /dev/null 2>&1 && cmake ..)

distclean:
    @  ($(MKDIR) build > /dev/null)
    @  (cd build > /dev/null 2>&1 && cmake .. > /dev/null 2>&1)
    @- $(MAKE) --silent -C build clean || true
    @- $(RM) ./build/Makefile
    @- $(RM) ./build/src
    @- $(RM) ./build/test
    @- $(RM) ./build/CMake*
    @- $(RM) ./build/cmake.*
    @- $(RM) ./build/*.cmake
    @- $(RM) ./build/*.txt

ifeq ($(findstring distclean,$(MAKECMDGOALS)),)
    $(MAKECMDGOALS): ./build/Makefile
    @ $(MAKE) -C build $(MAKECMDGOALS)
endif

The default target all is called by typing make, and invokes the target ./build/Makefile.

The first thing the target ./build/Makefile does is to create the build directory using $(MKDIR), which is a variable for mkdir -p. The directory build is where we will perform our out-of-source build. We provide the argument -p to ensure that mkdir does not scream at us for trying to create a directory that may already exist.

The second thing the target ./build/Makefile does is to change directories to the build directory and invoke cmake.

Back to the all target, we invoke $(MAKE) -C build, where $(MAKE) is a Makefile variable automatically generated for make. make -C changes the directory before doing anything. Therefore, using $(MAKE) -C build is equivalent to doing cd build; make.

To summarize, calling this Makefile wrapper with make all or make is equivalent to doing:

mkdir build
cd build
cmake ..
make 

The target distclean invokes cmake .., then make -C build clean, and finally, removes all contents from the build directory. I believe this is exactly what you requested in your question.

The last piece of the Makefile evaluates if the user-provided target is or is not distclean. If not, it will change directories to build before invoking it. This is very powerful because the user can type, for example, make clean, and the Makefile will transform that into an equivalent of cd build; make clean.

In conclusion, this Makefile wrapper, in combination with a mandatory out-of-source build CMake configuration, make it so that the user never has to interact with the command cmake. This solution also provides an elegant method to remove all CMake output files from the build directory.

P.S. In the Makefile, we use the prefix @ to suppress the output from a shell command, and the prefix @- to ignore errors from a shell command. When using rm as part of the distclean target, the command will return an error if the files do not exist (they may have been deleted already using the command line with rm -rf build, or they were never generated in the first place). This return error will force our Makefile to exit. We use the prefix @- to prevent that. It is acceptable if a file was removed already; we want our Makefile to keep going and remove the rest.

Another thing to note: This Makefile may not work if you use a variable number of CMake variables to build your project, for example, cmake .. -DSOMEBUILDSUSETHIS:STRING="foo" -DSOMEOTHERBUILDSUSETHISTOO:STRING="bar". This Makefile assumes you invoke CMake in a consistent way, either by typing cmake .. or by providing cmake a consistent number of arguments (that you can include in your Makefile).

Finally, credit where credit is due. This Makefile wrapper was adapted from the Makefile provided by the C++ Application Project Template.

This answer was originally posted here. I thought it applied to your situation as well.

Solution 3:

Based on the previous answers, I wrote the following module that you can include to enforce an out-of-source build.

set(DEFAULT_OUT_OF_SOURCE_FOLDER "cmake_output")

if (${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})
    message(WARNING "In-source builds not allowed. CMake will now be run with arguments:
        cmake -H. -B${DEFAULT_OUT_OF_SOURCE_FOLDER}
")

    # Run CMake with out of source flag
    execute_process(
            COMMAND ${CMAKE_COMMAND} -H. -B${DEFAULT_OUT_OF_SOURCE_FOLDER}
            WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})

    # Cause fatal error to stop the script from further execution
    message(FATAL_ERROR "CMake has been ran to create an out of source build.
This error prevents CMake from running an in-source build.")
endif ()

This works, however I already noticed two downsides:

  • When the user is lazy and simply runs cmake ., they will always see a FATAL_ERROR. I could not find another way to prevent CMake from doing any other operations and exit early.
  • Any command line arguments passed to the original call to cmake will not be passed to the "out-of-source build call".

Suggestions to improve this module are welcome.