Hi, I have inherited a system on short notice in the midst of being cmakeified. For $reasons not a lot of explanation came with it, so I’m left to puzzle it out and finish it.
My first issue is this: we have an embedded toolchain being vendorized with FetchContent. OK, but how do I specify that toolchain when it doesn’t exist at configure time (or at least at the start of configure time? Previously I’ve only used CMake with an already installed toolchain that I can specify on the cmake command line.
The toolchain only needs to exist at the point something wants to use it. That will most likely be the first
project() call, which tests the compilers for the enabled languages (C and CXX by default if no languages are explicitly given in the
project() command). If you are retrieving the toolchain via FetchContent before the first
project() call, it should work okay.
I’m assuming you are also using a toolchain file? Is that toolchain file also downloaded via FetchContent or does it exist already before you run CMake for the first time? Both can work, it should follow the same rules as for the toolchain in the above. As long as it exists at the point something needs it (the first
project() call), it should be fine. The toolchain file should point to the toolchain at its downloaded location. That does mean you will need to know where FetchContent will put it, but that is documented and predictable.
If you need further clarification, then perhaps if you can post your toolchain file and the contents of your
CMakeLists.txt file up to the first
project() call, that may make it easier to provide more guidance.
It sounds like maybe the real question should be “what is best practice to do …”, since I can change anything I need to. I’ll describe what I want in more detail, but first let me answer the questions.
- Ah hah. No, project() is called right after cmake_minimum_required() in the top level CMakeLIsts.txt file. It sounds like the add_subdirectory(build) call, where the cmake file with the FetchContent calls, should come before the project() call?
While I’m at it, I should probably rename that directory because build/ implies something different in the cmake community.
- I take it that is the answer to how you specify the toolchain on the command line, set a variable to a toolchain file rather than just to the compiler name. Makes a lot of sense.
It’s confusing because this project was using bazel, so there are toolchain files laying around for bazel. I didn’t find any for CMake, but the vendorized toolchain (esp-idf) looks like it has one. That’s presumably correct, but it won’t exist until after the FetchContent call. Is it best practice to just copy their toolchain file into our repo, or is there a way to set it after FetchContent? If the latter, then do we just create a CMake variable to identify what build we want to do?
What I really want to do is be able to build code for multiple platforms (to start with, esp32 and soon ARM, plus preferably on OSX and Linux so we can test as much as possible locally and with Jenkins without flashing the boards.
It seems best to vendorize the toolchains because I can pin them to a specific version and make the builds as reproducible as possible. But if the overall approach I’m describing is misguided, tell me the better way. It’s in my lap, so I can change whatever is needed.
I’ve seen in the CMake tests where
project() is used as
project( test NONE ) and then the language is enabled via
enable_language( C ). I suppose downloading and installing a toolchain between the two could also work.
I use FetchContent in my CMake toolchain file to download the SDK. The only drawback is that CMake searches for ninja after including the toolchain file but FetchContent needs it. Specifying the ninja tool manually solved it.
It’s hard to recommend a “best practice” for this, since it depends on what your goals are. There are different approaches, such as that mentioned by @hsattler, but also others.
Toolchain files get called any time a language is enabled which wasn’t enabled before. Most of the time, this is just the first
project() call, but it can come later via explicit
enable_language() calls or another
project() call which adds more languages. The toolchain file also gets read for
try_compile() builds, which are separate sub-builds isolated from your main build. You probably don’t want the
try_compile() calls downloading their own separate copy of fetched contents, so you’d need to consider how to prevent that if you took the approach mentioned by @hsattler.
You can put toolchain files in a remote repo and have FetchContent download that remote repo before the first
project() call. The user can then select one of the downloaded toolchain files if they want to by passing the path to the toolchain file where it will be after download. This requires that you know where the toolchain file you want will be located, but if your remote repo has a flat structure and only has toolchains, it can be fairly straightforward. Overriding the default download location for the repo can also make it simpler. Here’s a slightly simplified example from the
FetchContent section of my book (Professional CMake: A Practical Guide):
You can then invoke
cmake like so:
cmake -DCMAKE_TOOLCHAIN_FILE=toolchains/beta_cxx.cmake ...
Note how we didn’t specify the full absolute path to the toolchain file. CMake will look in both the source directory and build directory when given a relative path for the toolchain file. The
FetchContent_Declare() call overrides the
SOURCE_DIR to place the downloaded toolchains in a
toolchains subdirectory below the top of the build tree, so our relative path will find the toolchain files with the simple relative path shown. You would typically only want to do this for situations where you know the subdirectory name you choose will never clash with other parts of the project which may try to do the same thing (think about hierarchical projects), such as in a company environment where the set of projects that may be involved in a build is well known and under your control.
An advantage of putting your toolchain files in a dedicated repo like this is that the user is still free to choose whether they want to actually use the supplied toolchains or not. If they want to try out a different toolchain file, they can specify that on their
cmake command line instead of one of the toolchain files from the downloaded repo. They are still free to have their own toolchain file refer to things inside the downloaded toolchains repo. This might be handy when experimenting with a new or updated toolchain file.