Here we will use the same project layout we used in the Managing dependencies between projects tutorial.
Make sure you have followed it before going further.
A quick reminder of what we want.
We have a project called world, which contains a dynamic library also called world. So we have a libworld.so on linux, and libworld.dylib on mac, and a world.dll on windows.
Then we have a project called hello, which contains a executable also called hello, which uses code from the world library.
Using the following two lines of cmake
qi_stage_lib(world)
qi_use_lib(hello world)
we want to:
If you have a look at hello/qibuild.manifest, you will see the following lines
[project "hello"]
depends = world
Each time you run qibuild, it looks for a .qi to guess your current worktree.
After this, the worktree is parsed to find qibuild.manifest files.
Here, there are two qibuild.manifest files, so qibuild can find the two projects: hello and world.
The relevant lines of the CMakeLists.txt are:.
In world/CMakeLists
qi_create_lib(world SRC world/world.h world/world.cpp)
qi_stage_lib(world)
In hello/CMakeLists.txt
qi_create_bin(hello "main.cpp")
qi_use_lib(hello world)
Note
We never have to call find_package or include_directories, or target_link_libraries.
This first part is the job is done by the qi_create_bin and qi_create_lib functions.
Those are just wrappers for add_executable and add_library.
They just set a few properties (like the RUNTIME_OUTPUT_LOCATION for instance).
There are other properties that are used so that the executable can find the dynamic libraries it depends on at runtime, more on this later.
This way, we always generate binaries and libraries in the SDK directory. The build/sdk contains only the results of the compilation that are necessary to be used by other projects.
Also, the executables are created in build/sdk/bin, and the libraries in build/sdk/lib, so that we stick to the FHS convention inside the build/sdk directory.
On Windows, the binaries compiled in debug contain _d in their names, so you can share the same build directory, and the same Visual Studio solution for several build configurations, without the risk of a mix of binaries compiled in release and binaries compiled in debug.
This is done by something like
# in qibuild/general
set(QI_SDK_DIR ${CMAKE_BINARY_DIR}/sdk)
# in internal/layout:
qi_persistent_set(QI_SDK_BIN "bin")
qi_persistent_set(QI_SDK_LIB "lib")
# then, in target.cmake
set_target_properties(${name}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${QI_SDK_DIR}/${QI_SDK_BIN}
RUNTIME_OUTPUT_DIRECTORY_RELEASE ${QI_SDK_DIR}/${QI_SDK_BIN}
RUNTIME_OUTPUT_DIRECTORY_DEBUG ${QI_SDK_DIR}/${QI_SDK_BIN}
ARCHIVE_OUTPUT_DIRECTORY ${QI_SDK_DIR}/${QI_SDK_LIB}
LIBRARY_OUTPUT_DIRECTORY ${QI_SDK_DIR}/${QI_SDK_LIB}
)
if(WIN32)
set_target_properties("${name}" PROPERTIES DEBUG_POSTFIX "_d")
endif()
The call to qi_stage_lib causes a world-config.cmake to be generated in world/build/sdk/cmake/
When using qibuild configure hello, a dependencies.cmake files is generated in hello/build/dependencies.cmake
(this file is automatically included by the qibuild.cmake file at the root of the hello project)
This file contains a call to
list(INSERT CMAKE_FIND_ROOT_PATH 0 "QI_WORK_TREE/world/build/sdk")
So when qi_use_lib(hello world) is called, we only have run
find_package(world)
Since the variable CMAKE_FIND_ROOT_PATH is correctly set, CMake can find the world-config.cmake file in the build dir of world.
Since everything under build/sdk follows the standard FHS conventions, finding the library in sdk/lib is also works.
Note
you can see qibuild as a way to automatically follow the cmake conventions See the CMake wiki for more information
In fact we have two different world-config files.
The first one is installed. It is supposed to be used with a world pre-compiled package, from an other machine than the one used to compile world. We call it the redistributable config file.
The second one is generated in build/sdk/share/cmake/world/world-config.cmake so that CMake will find it if CMAKE_FIND_ROOT_PATH is set to build/sdk. We call it the SDK config file.
There are several differences between the redistributable config file and the SDK config file.
This is how we can set ROOT_DIR to world-prefix from world-config.cmake
We now we have a layout looking like:
world-prefix
|__ share
| |__ cmake
| |__ world
| |__ world-config.cmake
|__ include
| |__ world
| |__ world.h
|__ lib
|__ libworld.so
So we generate the following code to set ROOT_DIR
get_filename_component(_cur_dir ${CMAKE_CURRENT_LIST_FILE} PATH)
set(_root_dir "${_cur_dir}/../../../")
get_filename_component(ROOT_DIR ${_root_dir} ABSOLUTE)
The complete signature to qi_stage_lib is in fact:
qi_stage_lib(prefix
INCLUDE_DIRS ...
PATH_SUFFIXES ...
DEFINITIONS ...
DEPENDS ...
)
When flags are missing, we will guess them.
Note that prefix is always the name of a cmake target, i.e the first argument of something like qi_create_lib. There is an error message if you try to use qi_stage_lib on something that is not a target.
Let’s go through the variables one by one:
only used in the sdk file. During the configuration of hello, we will simply call include_directories(WORLD_INCLUDE_DIRS)
If not given, this can be guessed using the “directory properties”, like so:
get_directory_property(_inc_dirs INCLUDE_DIRECTORIES)
set(WORLD_INCLUDE_DIRS
"${ROOT_DIR}/include"
"${ROOT_DIR}/include/${WORLD_PATH_SUFFIXES}")
A few words about what this variable is for.
Let’s assume a client of the world library wants to use #include<world.h>, but world.h is installed in world-prefix/include/world/world.h
Other people, on the other hand, want to use #include<world/world.h>.
The standard CMake way to deal with this is to call
find_path(WORLD_INCLUDE_DIR world.h PATH_SUFFIXES world)
find_path(WORLD_INCLUDE_DIR world/world.h)
(hence the name of the variable)
This will never be guessed, because it’s too specific.
set_target_properties(hello
PROPERTIES
COMPILE_DEFINITIONS "${WORLD_DEFINITIONS}"
)
This will never been guessed. We could have done something like:
get_target_property(_world_defs world COMPILE_DEFINITIONS)
But most of the time you don’t have to propagate the compile flags everywhere.
Unless the world headers have been very carefully written, (using private pointer implementations, forward declarations and the like), there’s a great chance we will also need the boost headers when compiling hello, that’s why we always propagate the dependencies by default.
This is guessed using the previous call to qi_use_lib. In our example, after using qi_use_lib(world boost), WORLD_DEPENDS contains “boost”.
In the SDK file, we use something like:
get_target_property(_world_location world LOCATION)
set(WORLD_LIBRARIES_world_location})
In the redistributable file, we use:
find_library(world ...)
set(WORLD_LIBRARIES ...)
So what happens when using a qi_use_lib?
When using qi_use_lib(foo bar), we will always call
find_package(bar)
But we have several cases here:
To do this, we have to search for the -config.cmake files generated by qiBuild, then look for upstream Find-\*.cmake
See also
The relevant lines of code are:
find_package(${_pkg} NO_MODULE QUIET)
find_package(${_pkg} REQUIRED)
Note
You can NOT specify optional dependencies when using qi_use_lib.
That’s because it’s hard to know from CMake whether the foo-config.cmake file was not found or the foo-config.cmake was found, the FOO_INCLUDE_DIRS was found, but not the FOO_LIBRARIES). If you really want to have optional dependencies, you can do this this way:
find_package(FOO QUIET)
if(FOO_FOUND)
add_definitions(-DWITH_FOO)
qi_use_lib(bar FOO)
endif()