How to use Conan packages in your Waf project
Oct 23, 2023
For a short quickstart guide, check out this article instead
A Conan package is an archive of a compiled library or tool for a single architecture/platform. For example, a zlib package may have header files, a .so
file, and some configuration info. If you want to use that package in your project, you need to change your compiler/linker flags to e.g. find those headers and library files.
Conan's solution to this is called "generators" (not to be confused with Waf's "task generators"). A Conan generator's job is to generate the config/data/options that your build system needs in order to properly use a package. This is usually seamless, or even invisible to an end user.
Since there was no official Conan generator for waf, I wrote one here. That will give you access to the over 1,600 libraries/tools on Conan center in your waf projects.
Getting Started
Since this requires using my custom generator, you need to do some setup to install it.
There are [two ways] (https://docs.conan.io/2/reference/extensions/custom_generators.html) to use a custom generator, but here I'll show the "python_requires" method (check this article for an alternate method)
1 2 3 |
|
The conan export
command will copy the generator to your local Conan cache.
Sample Project
As a basic example, I'll show how to use the spdlog library to build a program with fancy logging features. Here's the full source code for the app we're about to build:
1 2 3 4 5 6 7 |
|
A good first step to using a library is to search Conan Center and see if there's a recipe for it. Since there is a recipe for spdlog, we now have to choose the version that we want to use. For our purposes, the latest version spdlog/1.12.0
will work.
Terminology note: A recipe is a script that Conan uses to create usable packages for a library.
Creating the conanfile
The conanfile is how we tell Conan which dependencies we want. There are two formats, but for this demo, we need to use conanfile.py
due to the way we plan to use the custom generator.
Note: It is possible to use conanfile.txt instead for this if you install the custom generator globally
1 2 3 4 5 6 7 8 9 10 11 12 |
|
I won't do a deep dive into Conanfiles here, just explain how we use the custom Waf generator we installed earlier.
The python_requires
attribute tells Conan that our conanfile depends on a package, and it should be built/downloaded before processing anything else. The package we're depending on is obviously the waf generator. This makes the module accessible to us in the conanfile, which we need to use in the generate ()
method (duh).
If you've used Conan before, then this should look similar to the modern CMakeDeps and CMakeToolchain generators. The only difference is that we need to load the Python modules from the self.python_requires
attribute, whereas with CMake you typically do a global import (since those are built-in to Conan).
That's all we need to do from the Conan side. When we call conan install
for our project, Conan will use the Waf generator to generate some Waf-specific file that we can use from our wscript.
Loading package info from Waf
This generator works by generating a waf tool which, when loaded, will populate your Waf environment with the correct information needed to compile/link with dependencies in your Conan graph.
The generated waf tool has everything you need, so there's no need to ship anything alongside the standard waf script.
1 2 3 4 5 6 7 |
|
In the configure
method, we load the tool (called "conan_deps") from the build folder, which is where we expect conan to write its output to when we call conan install
later. You don't need to have conan generate files in the build folder, but it's convenient since we can clean it with a simple waf distclean
, and don't need to add anything extra to our .gitignore
besides the usual.
This tool will do a couple of things to ensure we can access the libraries/tools in our Conan dependency graph. The main thing it will do is populate the current context's environment with the variables that we need to compile a C++ project with the spdlog package. These variables are namespaced as described in section 10.3.3 of the Waf book.
In other words, include flags like -I/conan/spdlog/include
will not be added to conf.env.INCLUDES
, but instead will be added to conf.env.INCLUDES_spdlog
. So if we want to use spdlog to compile some code, we need to add the usename "spdlog" to the use
attribute of the task generator.
Additionally, it will modify sys.path
, os.environ
, and conf.environ
to include environment variables from the Conan build environment. This is important in case we want to use tools as Conan packages (including waf tools/python scripts).
For example, flatbuffers is a serialization library that includes a custom schema language and a compiler for that language. Thanks to the environment manipulations performed by the tool, we can expect a call to conf.find_program("flatc")
to locate the flatc
binary inside of the Conan package, even if it's not installed on our system.
Using packages
Using a package in your Waf generators is very easy, just add the package you want to the use
attribute and include "conan" in your features
attribute:
1 2 3 4 5 6 7 |
|
The "conan" feature is necessary in order to expand the use
attribute with transient dependencies. For example, spdlog actually depends on a library called fmt. So if we only use the spdlog package, we'll get compile/link time errors.
While you could manually add "fmt" to the use
attribute, this is a bad idea since it makes our wscript reliant on a transient dependency, and makes us responsible for correctly representing the dependency graph... which is pointless because Conan did all of that already.
So the "conan" feature method is there to handle this case. As a rule of thumb: if a package is NOT listed in your conanfile's requirements, then it should NEVER be listed in a use
attribute. If you're getting compile/link errors, then it's either a bug with my generator, or some other problem.
Build and run it
Let's review the structure of our sample:
1 2 3 4 |
|
The first thing we need to do is execute conan install
(make sure to run these inside the sample_project folder):
1 2 3 |
|
Note: for the guide, you should ensure the output folder is the same as waf's 'out' folder
This will cause Conan to do the following things:
- build or download a package for the waf generator (the one we listed as a
python_requires
) - build or download all packages needed to satisfy our requirements (that's
spdlog
and its dependencyfmt
) - Execute the waf generator, producing the
conan_deps.py
waf tool in the build folder
Once that process completes, we can simply configure/build our waf app as usual:
Configure
1 2 3 4 5 6 7 8 9 |
|
During configure, you'll see a bunch of "Conan usename" lines being logged. These are the names that are available to you when adding to the use
attribute of your C++ task generators. Above, you can see that there's also an entry for fmt, which is a dependency of spdlog.
You'll also notice that there are some weird ones, like spdlog_libspdlog
and fmt__fmt
. These are actually Components.
In this example, the spdlog package defines a component called "spdlog::libspdlog", and the fmt package defines one called "fmt::_fmt", and my waf generator mangles those a bit for compatibility reasons. The linked article about components explains the what and why of Components, but generally, you can usually get away with just using the root package rather than having to worry about individual components (although it depends on the package). That's why we use "spdlog" in our example rather than "spdlog_libspdlog".
Build
1 2 3 4 5 6 7 8 |
|
Looking at the verbose output above, we can clearly see the extra flags added by the waf generator. Besides the obvious -I
and -L
flags, there are also these:
--std c++17
-O3
-DNDEBUG
-D_GLIBCXX_USE_CXX11_ABI=1
Those flags were added due to the Conan profile that I used to build it. Yours may be slightly different, but here's mine for reference:
1 2 3 4 5 6 7 8 |
|
Addtionally, the following definitions were added when compiling:
-DSPDLOG_FMT_EXTERNAL
-DSPDLOG_COMPILED_LIB
Those are just some config defs needed by spdlog, which are declared in its package.
Run
Our sample app statically links with spdlog and fmt because that's the default behavior for both of those packages. That means we can run the app by just executing it as you'd expect:
1 2 |
|
However, if we'd have dynamically linked with those dependencies, then we would need to either install the shared libraries to our operating system, or add them to the loader's search path.
Note: if you want to try this, add this attribute to your conanfile (e.g. under
requires = ...
) and then repeat all the steps above:default_options = { "*/*:shared": True }
Luckily, we don't have to worry about that because Conan install also generated some helpful scripts when we called conan install
. These are in the build folder:
conanbuild.sh
(or.bat
on windows)deactivate_conanbuild.sh
conanrun.sh
deactivate_conanrun.sh
These will activate and deactivate either the build or run environment. Among other things, it ensures that the correct dynamic libraries that live in our Conan cache will be found when executing our application.
An interesting usecase for this feature is to use Conan to obtain build tools. For example, you could build a CMake project even if you don't have CMake installed by using the Conan Center recipe for it (which can build CMake from source if needed):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
And that's it! You can see the full source for this sample project here.
The generator repo also has some other sample projects in the tests
folder which use some more advanced features of Conan, like using build tools in your wscripts and cross-compilation.
© Alejandro Ramallo 2024