Building CMake projects¶
For this overview, we will assume we have:
- A
foo-sdk
toolchain containing a packages, namedbar
- A worktree containing two projects,
the
world
project, and ahello
project, which depends onworld
andbar
. - A build profile named
my-profile
configured in the worktree.
This overview guides you through all what happens from the moment you run:
$ qibuild configure --worktree=/path/to/worktree \
--config foo-sdk \
--profile my-profile \
--release \
-DWITH_SPAM=ON \
hello
to every cmake code that is generated, and what CMake flags are passed.
(We chose a complex example here on purpose to show all the various cases that need to be handled)
Command line parsing - first pass¶
This is done by qisys.script.root_command_main
from
bin/qibuild
script.
We look for every module in qibuild.actions
, and find the configure.py
module.
Then, we create a argparse.ArgumentParser
parser, and run qibuild.configure.configure_parser
on it.
We parse the command line arguments using this parser, and we now have a argparse.NameSpace
object we can pass to qibuild.configure.do
.
# in qibuild.actions.configure
def configure_parser(parser):
# Calls functions in ``qibuild.parsers`` to add all the options
# this command recognize
def do(args):
# In this point we have a argparse.Namespace object with the "raw"
# result of the arguments parsing
args.worktree = "/path/to/worktree"
args.build_type = "Release"
args.config = "foo-sdk"
args.projects = ["hello"]
args.profiles = ["my-profile"]
This “raw” command parsing already took care of simple tasks, like
making sure the --debug
or --release
arguments are converted
to a proper CMAKE_BUILD_TYPE
.
Command line parsing - second pass¶
The goal is to get a correctly initialized CMakeBuilder
object
This is done in just a single line:
# in qibuild.actions.configure
def do(args):
cmake_builder = qibuild.parsers.get_cmake_builder(args)
The get_
functions in qibuild.parsers
are here to factorize code
that must be called in every action that uses a BuildWorkTree.
The get_cmake_builder
action looks like
# in qibuild.parsers
def get_cmake_builder(args):
""" Get a CMakeBuilder object from the command line
"""
build_worktree = get_build_worktree(args)
# dep solving will be made later by the CMakeBuilder
build_projects = get_build_projects(build_worktree, args, solve_deps=False)
cmake_builder = qibuild.cmake_builder.CMakeBuilder(build_worktree, build_projects)
cmake_builder.dep_types = get_dep_types(args)
return cmake_builder
Here’s what those functions do:
get_build_worktree¶
A new WorkTree object is initialized using the path given in args.worktree, or by exploring parent directories until a
.qi
directory is found if--worktree
is not given At this point, every path registered in the worktree can be found inworktree.projects
A new BuildWorkTree is initialized. A list of
BuildProject
objects is built from every project inworktree.projects
, by inspecting the variousqiproject.xml
and looking for<qibuild>
tags. Note that at this momentbuild_project.build_depends
,build_project.run_depends
, andbuild_project.test_depends
are sets of names because no dependency resolution has been done yet.A new CMakeBuildConfig object is initialized, using the
.qi/qibuild.xml
file to read the default config that should be used. If the user has an incorrect default config specified in the.qi/qibuild.xml
file, an error is raised immediately.Then, the
build_config
object is configured using theargs
object and theqibuild.xml
configuration files.First, the
-c
argument is checked to see if it matches a known toolchain. If not, an error is raised.Then, the configuration specific settings and the default settings in
~/.config/qi/qibuild.xml
are read.For instance, if the user specified
-c foo-sdk
on the command line there is a<cmake gererator="Ninja">
tag in the<config name="foo-sdk">
section of~/.config/qi/qibuild.xml
,build_config.cmake_generator
is set toNinja
andbuild_config.toolchain_name
tofoo-sdk
Lastly, the options coming from the command line are applied to the
build_config
object.This is done after reading the config files, so that settings can be overwritten. Thus the user can for instance specify
--cmake-generator="Unix Makefiles"
to overwrite the default CMake generator configured in~/.config/qi/qibuild.xml
Lastly, the
build_config
is applied to theBuildWorkTree
:worktree.build_config = build_config
.
Note: the code later looks like:
# in BuildProject
def configure(self, **kwargs)
cmake_args = self.cmake_args
build_directory = self.build_directory
But actually, cmake_args
and build_directory
are both properties.
This means that the build dir will always match the latest build settings, and that the list of CMake args in the BuildProject will always be up to date.
# in CMakeBuildConfig
@property
def cmake_args(self):
# Transform all the "high level" settings into a list of
# CMake arguments
>>> build_config.cmake_generator == "Ninja"
>>> build_config.cmake_args
["-G", "Ninja"]
>>> build_config.toolchain_name = "foo-sdk"
>>> build_config.cmake_args
["-DCMAKE_TOOLCHAIN_FILE=/path/to/foo-sdk/toolchain.cmake"]
The build config also manages the environment variables, so that
you can for instance set a suitable PATH
when using mingw
on windows without to mess with the registry base.
# in BuildProject
@property
def build_directory(self):
# Create a sensible build dir, using
# self.build_worktree.build_config
>>> build_config.build_type = "Release"
>>> hello_project.build_directory = "/path/to/hello/build-release"
>>> build_config.profiles = ["my-profile"]
>>> hello_project.build_directory = "/path/to/hello/build-my-profile"
get_build_projects¶
The goal here is to get a list of BuildProject
objects to build.
If no build project named is specified, the parent directories are explored until a
qiproject.xml
containing a<qibuild>
tag is found.If no such project is registered in the
BuildWorkTree
yet, it will be automatically added to the worktree cache.If the user specified some projects in the command line, a matching
build_project
is searched in thebuild_worktree
for every project name specified on the command line. If no build project is found, an error is raised.
Note that at this point, no dependency solving has been done yet.
Meaning that the projects
list only contains the hello project
get_dep_types¶
Here, get_dep_types
is used to converting the --runtime
,
--build-deps-only
, --single
arguments into a list of build types:
- default:
["build", "runtime"]
--runtime
:["build", "runtime"]
-s, --single
:[]
--build-deps-only
:["build"]
Finally, CMakeBuilder.dep_types
is set
In our examples, no argument was specified at all, so the build and the
runtime dependencies are going to be used.
Configuring the project and its dependencies¶
Here’s what the code looks like:
# in qibuild.cmake_builder
class CMakeBuilder:
def __init__(self, build_worktree, projects):
self.build_worktree = build_worktree
self.projects = projects
self.deps_solver = BuildDepsSolver(self)
def configure(self, **kwargs):
self.bootstrap_projects()
projects = self.deps_solver.get_dep_projects(self.projects, self.dep_types)
for project in projects:
project.configure(**kwargs)
Note that the CMakeBuilder
contains a BuildDepsSolver
to delegates
all the dependencies solving.
For instance, configuring hello
, by default should call configure()
on
the world
project, unless -s
was specified.
Also, since hello
has a runtime dependency on the bar
package,
qibuild install --runtime hello /tmp/hl
should install both hello
and bar
to /tmp/hl
Also note that CMakeBuilder
delegates the actual call to cmake
to
the build project itself
Generating the dependencies.cmake¶
For the CMake
call to work, a dependencies.cmake
must be written
in the build directory
This is done by cmake_builder.bootstrap_projects
Here it is important that the dependencies.cmake
always contains the list of every
build dependencies, even if -s
is used.
Calling CMake¶
Here deps_solver
uses self.dep_types
, so that when
qibuild configure -s hello
, is used,
world.configure()
is not called.
Installing¶
When installing a project, the deps_solver
is again used to get a
list of packages to install.
- Then either:
- the whole contents of the packages are installed (the “-config.cmake” files, the headers, the static and shared libraries, etc.)
- if
solving_type
was set toruntime
, only the runtime parts of the packages (shared libraries) will be installed.
Building projects outside a qiBuild action¶
This could be part of a continuous integration script, for instance:
worktree = qisys.worktree.WorkTree(worktree_root)
build_worktree = BuildWorkTree(worktree)
build_config = build_worktree.build_config
Note
Here the build_config has already been initialized from the various config files, and default values, but you can still use:
build_config.set_active_config("mytoolchain")
build_config.build_type = "Release"
project = build_worktree.get_build_project(name)
cmake_builder = CMakeBuilder(build_worktree, [projet])
# Configure and build the build and runtime deps of the
# project:
cmake_builder.configure()
cmake_builder.build()
If you then need to install the runtime parts only (to make a redistributable package for instance)
cmake_builder.dep_types = ["runtime"]
cmake_builder.install(destdir="package")