cmake: replicate boost-build's "build everything in Jamfile" behaviour?
Asked Answered
F

1

2

I am converting a boost-build build system to cmake.

One of the features of boost-build is that you can specify a path to a Jamfile (the equivalent of a CMakeLists.txt file) and all the targets specified therein will be built.

For example, with the following project structure:

root
|
+--- foo
|    |
|    +--- test
|
+--- bar
|    |
|    +--- test
|
+--- app

If you enter the following command:

$ b2 foo

The Jamfile under root/foo will be executed, resulting in the foo library being built, and the test tests being built and run

boost-build example

Here is a simple build configuration using boost-build:

Jamroot:

using gcc ;

project proj : requirements
    <link>static
    <include>.
;

build-project foo ;

foo/Jamfile:

lib foo : [ glob *.cpp ] ;

build-project test ;

foo/test/Jamfile:

import testing ;

unit-test foo-tests
:   [ glob *.cpp ]
    ..//foo
;

You will notice that within foo's Jamfile there is a directive build-project test

This means that if I type b2 foo then everything in lib/Jamfile will be executed, resulting in foo and foo/test being built.

Also, within the Jamroot there is a directive build-project foo

This means that if I just type b2 then everything in Jamroot will be executed, resulting in foo and foo/test being built.

It is thus easy to build the whole project and get all sources and all tests built.

It is also easy to build just a subdirectory and get only it's sources and tests build.

It is this behaviour I'm trying to replicate.

cmake example

root/CMakeLists.txt:

cmake_minimum_required(VERSION 3.2.2)
project(proj CXX)

add_subdirectory(foo)

foo/CMakeLists.txt:

file(GLOB src "*.cpp")

add_library(foo STATIC ${src})

add_subdirectory(test)

foo/test/CMakeLists.txt:

file(GLOB src "*.cpp")

add_executable(foo_test ${src})

add_test(foo_test foo_test foo)

# run tests if foo_test.passed is missing or outdated
add_custom_command(
    OUTPUT  foo_test.passed
    COMMAND foo_test
    COMMAND ${CMAKE_COMMAND} -E touch foo_test.passed
    DEPENDS foo_test
    )

# make tests run as part of ALL target
add_custom_target(run_foo_test
    ALL
    DEPENDS foo_test.passed)

The above CMakeLists.txt structure allows me to make and have both foo and foo_test built.

However, if I specify make foo, only foo will be built, but foo_test won't be, and the tests won't be run.

Question:

  • How can I have everything within foo/CMakeLists.txt built when I type make foo?
  • Alternately, how can I cause target foo_test.passed to be built as part of updating target foo AND build as part of the ALL target?
Fabrizio answered 18/4, 2016 at 18:8 Comment(0)
F
1

Here is an implementation which achieves the requirements.

It's a bit convoluted as it requires several phony targets and dependency chaining.

Step 1:

  • create a phony target which all other targets for a "module" will be collected under
  • add the phony target to ALL so that it is built as part of the global build process

foo/CMakeLists.txt:

# create a phony target which all 'foo' related items will be added to
add_custom_target(foo
    ALL
    )

Step 2:

  • add every target within the "module" as a dependency to this phony target

libfoo:

  • Note here I have renamed foo to libfoo as we've now used foo for our earlier phony target.
  • (This will in fact result in the generated library being called liblibfoo.a, which is a bit ugly)

foo/CMakeLists.txt:

add_library(libfoo STATIC ${src})

# add libfoo as a dependency of foo, so 'make foo' will build libfoo
add_dependencies(foo 
    libfoo)

foo_test:

Making the tests run automatically as part of the build is a bit convoluted.

You have to:

  • create the test executable
  • add a custom_command which runs the tests and generates a sentinel file (foo_test.passed) if they pass
  • add a custom_target (foo_test.run) which depends on the sentinel (foo_test.passed)
  • add a dependency between foo and foo_test.run

foo_test/CMakeLists.txt:

add_executable       (foo_test main.cpp)
target_link_libraries(foo_test libfoo ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY})

# create foo_test.passed command which generates the sentinel file when the tests pass
add_custom_command(
    OUTPUT  foo_test.passed
    COMMAND foo_test
    COMMAND ${CMAKE_COMMAND} -E touch foo_test.passed
    DEPENDS foo_test
    )

# create foo_test.run target which depends on foo_test.passed
add_custom_target(foo_test.run
    DEPENDS foo_test.passed
    )

# add foo_test.run as a dependency of foo, so 'make foo' will build foo_test.run
add_dependencies(foo
    foo_test.run
    )

As such, target foo has libfoo and foo_test.run as dependencies.

As a result, both make and make foo build libfoo and build and run foo_test (via foo_test.run and foo_test.passed)

Perceived clunkiness:

  • The foo -> foo_test.run -> foo_test.passed -> foo_test dependency chain.

  • In boost-build you can name the library foo without causing a clash between the foo phony target and the foo library (and b2 foo builds both the foo library and its tests)

However, it works, and in the absence of a more elegant solution, will give me what I want.

Fabrizio answered 18/4, 2016 at 19:37 Comment(1)
Well, first of all, usage of globbing in CMake to collect source files is discouraged by the authors, see "Note:". Since CMake generates a statical set of make rules for each given source file, creating a new .cpp file without touching CMakeLists.txt won't include it into the build process. Likewise, removal of a file from filesystem won't exclude it from the build process. BTW automake has a similar [mis]feature, and they specifically stated why this is a bad practice. Maybe it's inevitably in a two stage build system.Nikolos

© 2022 - 2024 — McMap. All rights reserved.