Search code examples
c++cmakecmake-custom-commandadd-custom-target

How can I use library in cmake-base project if sources and CMakeLists.txt file for this library I have to generate by external tool


I have to generate sources by external tool (I generate c++ classes from IDL-files for fastDDS messages), this tool also generate CMakeLists.txt file that allows me to compile generated files to <msgs_lib>.a file.

In my big superproject for one exec-target I wonna check existence of generated files and their building result. if <msgs_lib>.a does not exist I wonna call generation and building it.

I know about CMake commands add_custom_command and add_custom_target. But I can't use them in proper way, in some cases generation calls even if all files exists and they was built in proper order, in some cases nothing generates (and after cleaning nothing generates too), in some cases exec-target does not understand that it should check and call generation.

For example I'll write above one case for starting the discussion. I have simplify it a lot:

Project on github: https://github.com/gladijos/test_project

Project in one zip-file: mediaDrive-link

Or right here:

project directory tree:

.
├── CMakeLists.txt
├── deps
│   ├── gen_deps
│   └── gen_source
│       └── tb_msgs
│           ├── CMakeLists.txt
│           ├── tb_msg.cpp
│           └── tb_msg.h
├── scripts
│   └── build_msgs.sh
└── test_app
    ├── CMakeLists.txt
    └── main.cpp

root-CMakeLists.txt:

cmake_minimum_required(VERSION 3.0.2 FATAL_ERROR)

project(test_project LANGUAGES CXX VERSION 0.0.3)

set(TB_MSGS ${CMAKE_HOME_DIRECTORY}/deps/gen_deps/tb_msgs/build/libtb_msgs_lib.a)

add_custom_command(
  OUTPUT ${TB_MSGS}
  WORKING_DIR ${CMAKE_HOME_DIRECTORY}
  COMMAND ${CMAKE_HOME_DIRECTORY}/scripts/build_msgs.sh ${CMAKE_HOME_DIRECTORY}
)
 
add_custom_target(build_dds_msgs
# ALL 
DEPENDS ${TB_MSGS}
)

# apps
add_subdirectory(test_app)
add_dependencies(test_app build_dds_msgs)

tb_msgs CMakeLists.txt:

cmake_minimum_required(VERSION 3.5.1)

project("tb_lib")

set(CMAKE_CXX_STANDARD 11)

add_library(tb_msgs_lib 
    tb_msg.cpp
)

tb_msg.cpp:

#include "tb_msg.h"

MyClass::MyClass(){};

tb_msg.h:

#pragma once
class MyClass
{
public:
MyClass();
};

build_msgs.sh:

cp -rf $1/deps/gen_source/* $1/deps/gen_deps/;
cd $1/deps/gen_deps/tb_msgs;
mkdir -p  $1/deps/gen_deps/tb_msgs/build;
cd $1/deps/gen_deps/tb_msgs/build;
cmake .. ;
make;

test_app CMakeListst.txt:

cmake_minimum_required(VERSION 3.0.2 FATAL_ERROR)

project(test_app LANGUAGES CXX VERSION 0.0.1)

add_executable(${PROJECT_NAME} main.cpp)

target_link_libraries(${PROJECT_NAME}
  ${CMAKE_HOME_DIRECTORY}/deps/gen_deps/tb_msgs/build/libtb_msgs_lib.a
  )

test_app main.cpp:

#include "../deps/gen_deps/tb_msgs/tb_msg.h"
int main()
{
    MyClass myClass;
    return 0;
}

In this case I got call to scripts/build_msgs.sh every build , even if libtb_msgs_lib.a exists, and was built before test_app.

More one. In add_custom_command OUTPUT section I put .a, but in best case here should be all files in dir gen_source/* but i think it another problem and I can bit it after resolving main problem.


Solution

  • To solve your problem, I suggest not relying on a cmake script from an external tool, but instead managing the generation from IDL files and building the library. I assume that your project has a folder with IDL files, let's call it idl. In this folder also place a CMakeLists.txt file to generate and build the library.

    The structure of the project becomes as follows:

    .
    ├── CMakeLists.txt
    ├── idl
    │   ├── CMakeLists.txt
    │   ├── Bar.idl
    │   ├── Foo.idl
    │   └── MyClass.idl
    ├── scripts
    │   └── fastmsg.sh
    └── test_app
        ├── CMakeLists.txt
        └── main.cpp
    

    In this example, the files idl/Bar.idl, idl/Foo.idl and idl/MyClass.idl can be empty, only their presence is important.

    CMakeLists.txt

    cmake_minimum_required(VERSION 3.0)
    
    project(test_project LANGUAGES CXX VERSION 0.0.3)
    
    set(GENERATOR ${CMAKE_SOURCE_DIR}/scripts/fastmsg.sh)
    
    # apps
    add_subdirectory(idl)
    add_subdirectory(test_app)
    

    idl/CMakeLists.txt

    For each file in the IDL_LIST an external tool (GENERATOR from root CMakeLists.txt) is called to generate cpp and h files. The resulting list of cpp files is used to build the build_dds_msgs library.

    cmake_minimum_required(VERSION 3.0)
    
    set(IDL_LIST MyClass.idl Foo.idl Bar.idl)
    
    set(IDL_DIR ${CMAKE_CURRENT_SOURCE_DIR})
    set(CPP_DIR ${CMAKE_CURRENT_BINARY_DIR})
    
    foreach(IDL_FILE ${IDL_LIST})
      string(REPLACE ".idl" ".cpp" CPP_FILE ${IDL_FILE})
      string(REPLACE ".idl" ".h"   HDR_FILE ${IDL_FILE})
      list(APPEND CPP_LIST ${CPP_DIR}/${CPP_FILE})
      add_custom_command(
        OUTPUT ${CPP_DIR}/${CPP_FILE} ${CPP_DIR}/${HDR_FILE}
        COMMAND ${GENERATOR} ${IDL_DIR}/${IDL_FILE}
        WORKING_DIRECTORY ${CPP_DIR}
        DEPENDS  ${IDL_DIR}/${IDL_FILE}
        VERBATIM
      )
    endforeach()
     
    add_library(build_dds_msgs ${CPP_LIST})
    target_include_directories(build_dds_msgs PUBLIC ${CPP_DIR})
    
    # Add dependencies on an external instrument, if any.
    # target_include_directories(build_dds_msgs PRIVATE include_from_gen)
    # target_link_libraries(build_dds_msgs PRIVATE libs_from_gen)
    

    test_app/CMakeLists.txt & test_app/main.cpp

    The changes here are minimal, mostly stylistic.

    cmake_minimum_required(VERSION 3.0)
    
    project(test_app LANGUAGES CXX VERSION 0.0.1)
    
    add_executable(${PROJECT_NAME} main.cpp)
    target_link_libraries(${PROJECT_NAME} build_dds_msgs)
    
    #include "MyClass.h"
    
    int main()
    {
        MyClass myClass;
        return 0;
    }
    

    scripts/fastmsg.sh

    This is an emulator of a real generator. It takes a FileName.idl and creates FileName.h and FileName.cpp files with an empty FileName class.

    #!/bin/bash
    
    if [ -z "$1" ]
    then
      echo "Usage: `basename $0` filename"
      exit 1
    fi
    
    
    FILE=$(basename -- "$1")
    CLASS=${FILE%.*}
    
    cat > $CLASS.h <<EndHpp
    #pragma once
    
    class $CLASS
    {
    public:
        $CLASS();
    };
    EndHpp
    
    cat > $CLASS.cpp <<EndCpp
    #include "$CLASS.h"
    
    $CLASS::$CLASS(){};
    EndCpp
    
    exit 0