Building CMake projects¶
For this overview, we will assume we have:
- A
foo-sdktoolchain containing a packages, namedbar - A worktree containing two projects,
the
worldproject, and ahelloproject, which depends onworldandbar. - A build profile named
my-profileconfigured 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
.qidirectory is found if--worktreeis not given At this point, every path registered in the worktree can be found inworktree.projectsA new BuildWorkTree is initialized. A list of
BuildProjectobjects is built from every project inworktree.projects, by inspecting the variousqiproject.xmland looking for<qibuild>tags. Note that at this momentbuild_project.build_depends,build_project.run_depends, andbuild_project.test_dependsare sets of names because no dependency resolution has been done yet.A new CMakeBuildConfig object is initialized, using the
.qi/qibuild.xmlfile to read the default config that should be used. If the user has an incorrect default config specified in the.qi/qibuild.xmlfile, an error is raised immediately.Then, the
build_configobject is configured using theargsobject and theqibuild.xmlconfiguration files.First, the
-cargument 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.xmlare read.For instance, if the user specified
-c foo-sdkon 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_generatoris set toNinjaandbuild_config.toolchain_nametofoo-sdkLastly, the options coming from the command line are applied to the
build_configobject.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.xmlLastly, the
build_configis 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.xmlcontaining a<qibuild>tag is found.If no such project is registered in the
BuildWorkTreeyet, it will be automatically added to the worktree cache.If the user specified some projects in the command line, a matching
build_projectis searched in thebuild_worktreefor 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_typewas 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")