Managing dependencies with qiBuild: an overview¶
Introduction¶
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:
- add the correct include directories when building
hello
- link
hello
with theworld
library, without mixing aworld
library compiled in release with anhello
executable compiled in debug (on windows at least) - make sure that the
hello
executable will find theworld
library, without messing up withPATH
,LD_LIBRARY_PATH
orDYLD_LIBRARY_PATH
- generate a
world-config.cmake
so that theworld
library is usable by standard CMake project when installed.
Overview of the process: the power of the SDK layout¶
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
- For those already familiar with CMake:
- We use
qi_create_lib
andqi_create_bin
instead ofadd_executable
andadd_library
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.
SDK and redistributable config files¶
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.
- The SDK file never has to call find_* functions: since we have just built the
library, we know where it is. The redistributable file however must call
find_library
, andfind_path
. - The SDK file uses absolute paths : we don’t care because we will never share this file with anyone. The redistributable file must only use relative paths to the root dir of the package.
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)
Calling qi_stage_lib¶
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:
- <PREFIX>_INCLUDE_DIRS
- 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)
- <PREFIX>_PATH_SUFFIXES
- only used in the redistributable file. The file will contain something like:
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.
- <PREFIX>_DEFINITIONS
- used by both config files. During the configuration of hello, we will simply call
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.
- <PREFIX>_DEPENDS
- used by both config files. If world depends on an thirdparty library (boost
for instance), we want to make sure that whenever we use
qi_use_lib(hello world)
, we also add the boost include directories.
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”.
- <PREFIX>_LIBRARIES
- used by both config files. In this case the SDK and the redistributable config file do not use the same code.
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 ...)
Calling qi_use_lib¶
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:
- We are using a
bar-config.cmake
that was generated by qibuild. - We are using the custom
bar-config.cmake
inqibuild/cmake/modules
. This can happen because the upstreamFindBar.cmake
does not exist or is not usable. (For instance, the upstreamFindGTest.cmake
setsGTEST_BOTH_LIBRARIES,
instead ofGTEST_LIBRARIES
...) - We are using upstream CMake
FindBar.cmake
.
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()