c++cmakebuild-dependencies

The cmake quandary


I am working on a C++ project. It is not much complicated so far, yet depends on a bunch of "popular" libraries (nlohmann/json, ToruNiina/toml11 just to name a few). All of them have some CMakeLists.txt and from my not-that-experienced point of view, I consider them well structured.

Now of course I can compile the libraries one by one, or include a "copy" into my project repo, but I want to be better than that. After researching about available build tools, I have decided to use cmake to build and manage a C++ project. The promise was to get a stable, widely supported tool that will help to simplify & unify the build process. Moreover, from the project nature I have no privilege to impose any requirements on the target machine; I need to pack everything for the deployment.

I have spent several days reading, watching and testing out various cmake tutorials, handbooks and manuals. I have to admit, I quickly started to feel that a tool that is supposed to clarify development process keeps introducing new obscurities contrary to its purpose. Originally, I credited this to my lack of experience, yet...

I read articles about why not to bundle dependencies, only to be followed by methods of doing so. I have found recommendation to use one way A over B, C over B and later A over C. It took me a while to figure out the differences between 2.8 and 3.0, the obscurity of target_link_libraries, setting cxx version and/or compiler warning flags and so on.

My point is that even after an exhausting expedition into the seas of cmake, I am still not sure about some elementary questions:

How is cmake meant to be used?

What is a standard, what is a courtesy, and what is none of those?

How can I tell that something is a feature, an archaic backwards compatibility, or both?

Now I will illustrate this on my project. I only need something like this

cmake_minimum_required(VERSION 3.14)
project(CaseCore CXX)

add_executable(myBinary list/of/cpp/sources.cpp)
target_link_libraries(myBinary PUBLIC someExternalLibs likeForExample nlohmann_json::nlohmann_json oqs)

The only problem is with the libraries (there is no space for other problems anyway). I want to build them with the project and dont want to make a local copy (not to drag ton of unrelated files all along). First, I created forks of library repos in order to have a reliable source and be able to merge newer versions into my fork.

Now the decision was whether to use git submodule or some other scheme, I've read submodule doesnt perform that well and also preferred the whole thing to be managed by cmake alone. I started with ExternalProject_Add but later I found about FetchContent which helped me to add the external dependencies easily into my cmake list

FetchContent_Declare(nlohmann
    GIT_REPOSITORY https://github.com/my-reliable-fork-of/json
    GIT_TAG v3.7.3
)
message(STATUS  "Fetching Json...this may take a while")
FetchContent_MakeAvailable(nlohmann)

Seems and works well and minimal. However, I always have to search the library itself in order to find/guess which targets to link to my executable target? There seems to be close to zero convention and unless the respective CMakeLists.txt is simple enough to read it, I tend to guess the target names until i find it.

Recently I wanted to link liboqs from here and the abovementioned scenario did not really help; for some reason, I can link oqs, #include "oqs/oqs.h", yet the dynamic library is not created and execution terminates. I am pretty sure I can resolve the problem after another span of time spent googling and playing around with various cmake variables. Yet this is not the way that I expected cmake to help me manage my project; it is actually quite the opposite.

Just to be clear, I turned down other methods including

add_subdirectory from local repo copy (git submodule)
ExternalProject_Add from local repo copy (git submodule)
ExternalProject_Add from online repo
find_package

as they seemed to be much more obscure/old-style etc (eventhough despite hours of researching, they all still seem as pretty much as just many ways to do the same thing to me)

Now that I have

Am I doing something wrong, or is it really what working with cmake should look like?

Do I really have to "reverse-engineer" other people's CMakeLists in order to use a library?

Under these circumstances, how can I convince my coworkers to use similar work process?

and finally

How can I adjust my work in order to ease these difficulities for others?

I love C++ the more I use it. Yet I spend a tremendous amount of my productive time on solving dependencies...and I do not want to make this guy even more angry.


Solution

  • How is cmake meant to be used?

    The typical cmake usage matches the old autotools usage:

    $ cmake /path/to/src #replaces /path/to/src/configure
    $ make
    $ make install
    

    Some targets changed (e.g., make check vs make test), and cmake doesn't provide all the same standard targets (e.g., make distclean), but the usage I have above is what most developers will do (and since cmake re-runs itself, it's really just the second step most of the time).

    If your CMakeLists.txt doesn't support this workflow, you should have a very good reason. Most tooling will assume a workflow like this, so you're severely limiting yourself.

    What is a standard, what is a courtesy, and what is none of those?

    Outside of the above, cmake is pretty much the wild west. Things are becoming more standardized thanks to better documentation and training, but it's far from perfect.

    A well-behaved cmake project should export its targets (lots of questions and answers on Stack Overflow about this) and propagate flags and dependencies. This makes it far easier for dependent projects to consume exported targets, and luckily it's easy to do.

    How can I tell that something is a feature, an archaic backwards compatibility, or both?

    There's nothing I'm aware of that makes these distinctions. In general, newer methods leverage the target_* functions instead of the global ones (e.g., target_include_directories vs include_directories). The target_* functions are also used to propagate flags, include directories, compiler features, and dependent libraries like I mentioned above.

    Am I doing something wrong, or is it really what working with cmake should look like?

    You're talking about managing external dependencies, and I'm going to skip this to avoid getting into opinions. The short version is that C and C++ dependencies are hard, and there's many competing ways of managing them in a project. They each have pros and cons, but most are still designed for the authors' use cases. You'll have to figure out what use cases you need, and choose tools and workflows based on that.

    Do I really have to "reverse-engineer" other people's CMakeLists in order to use a library?

    A well-behaved cmake project will export its targets properly, even if they use different dependency management than you do. If they don't, send the project a pull request (exporting isn't hard, and it's good to learn how) or just file bugs against them, especially if they're already using cmake as a build system.

    Under these circumstances, how can I convince my coworkers to use similar work process?

    It depends on your coworkers, and mileage will vary. I've dealt with coworkers who want to embrace best practices and support flexibility, and I've dealt with coworkers who are content only doing enough to solve the problems we're facing right now.