CMake: how to change compiler for individual target
Asked Answered
B

5

34

I have embedded project using cross compiler. I would like to introduce Google test, compiled with native GCC compiler. Additionally build some unit test targets with CTC compiler.

Briefly:
I have 3 different targets and compile them with 3 different compilers. How to express it in CMakeLists.txt? I Tried SET_TARGET_PROPERTIES;
but it seems impossible to set CXX variable with this command!

Born answered 27/11, 2014 at 10:17 Comment(1)
The formally correct thing to do is to use ExternalProject or to detect the toolchain and configure only the relevant artifacts, using find_package and export to import things built by a different toolchain. CMake builds only support one compiler per language per configuration. Unless and until CMake's model changes, anything else is a brittle hack.Aztec
L
15

I just had the same issue right now, but the other answer didn't help me. I'm also cross-compiling, and I need some utility programs to be compiled with GCC, but my core code to be compiled with avr-gcc.

Basically, if you have a CMakeLists.txt, and you want all targets in this file to be compiled with another compiler, you can just set the variables by hand.

Define these macros somewhere:

macro(use_host_compiler)
  if (${CURRENT_COMPILER} STREQUAL "NATIVE")
    # Save current native flags
    set(NATIVE_C_FLAGS ${CMAKE_C_FLAGS} CACHE STRING "GCC flags for the native compiler." FORCE)

    # Change compiler
    set(CMAKE_SYSTEM_NAME ${CMAKE_HOST_SYSTEM_NAME})
    set(CMAKE_SYSTEM_PROCESSOR ${CMAKE_HOST_SYSTEM_PROCESSOR})
    set(CMAKE_C_COMPILER ${HOST_C_COMPILER})
    set(CMAKE_C_FLAGS ${HOST_C_FLAGS})
    set(CURRENT_COMPILER "HOST" CACHE STRING "Which compiler we are using." FORCE)
  endif()
endmacro()


macro(use_native_compiler)
  if (CMAKE_CROSSCOMPILING AND ${CURRENT_COMPILER} STREQUAL "HOST")
    # Save current host flags
    set(HOST_C_FLAGS ${CMAKE_C_FLAGS} CACHE STRING "GCC flags for the host compiler." FORCE)

    # Change compiler
    set(CMAKE_SYSTEM_NAME ${NATIVE_SYSTEM_NAME})
    set(CMAKE_SYSTEM_PROCESSOR ${NATIVE_SYSTEM_PROCESSOR})
    set(CMAKE_C_COMPILER ${NATIVE_C_COMPILER})
    set(CMAKE_C_FLAGS ${NATIVE_C_FLAGS})
    set(CURRENT_COMPILER "NATIVE" CACHE STRING "Which compiler we are using." FORCE)
  endif()
endmacro()

At the very beginning of your CMakeLists.txt script (or in a toolchain file), set the following variables according to what you need:

  • CURRENT_COMPILER
  • HOST_C_COMPILER
  • HOST_C_FLAGS
  • NATIVE_SYSTEM_NAME
  • NATIVE_C_COMPILER
  • NATIVE_C_FLAGS

The idea is that CMAKE_C_COMPILER (and company) is a variable like any other, so setting it inside a certain scope will only leave it changed within that scope.

Caveat: Changing CMAKE_C_COMPILER is a "hack" and isn't officially supported by CMake. That's because the CMake documentation on the compiler variable says "Once set, you can not change this variable". However, @ChrisB found out that, in practice and with the current versions of CMake (~3.25), this hack only works when all the targets in the same directory use the same compiler, at least when using CMake's Unix Makefiles generator. This means, if you want to have targets that use different compilers, these targets need to be in different subdirectories, using add_subdirectory. See @ChrisB's answer. I wouldn't be surprised if this hack works only with some CMake Generators and fails with others (say, works with Unix Makefiles and Ninja but not with Visual Studio 17 2022 or something like that).


Example usage:

# src/host/CMakeLists.txt
use_host_compiler()
add_executable(foo foo.c) # Compiled with your host (computer)'s compiler.

# src/native/CMakeLists.txt
use_native_compiler()
add_executable(bar bar.c) # Compiled with your native compiler (e.g. `avr-gcc`).

NOTE: Another approach, which requires more work but which is guaranteed to work well no matter what, and is good practice for projects of all sizes, is to call CMake multiple times on the project, once for each compiler, for example using a toolchain file to specify the compiler and flags for each build. That doesn't necessarily mean duplicating the CMake project and scripts. You can just ensure that your CMake build builds only the targets relevant for the current compiler.

For example, if you need to build a code generator that runs on the host platform (e.g. x64) that will generate code for an embedded platform (like was the case for me back then), then first invoke CMake with the host toolchain file (e.g. the x64 one), making sure that only the code generator gets built. Then, once that CMake build completes, make another CMake build in a new build directory with the embedded platform toolchain file (e.g. the one that uses avr-gcc), importing the code generator of the host platform that you just built. You can then write something like a shell script that makes both builds for you, unless you prefer simply writing the commands for building both of them in your project's README.md.

Lakeshialakey answered 1/6, 2017 at 15:53 Comment(10)
I'm in the same boat where I need different compilers for different targets. I've tried your example, but cannot get this to work. The added executable always is built with clang instead of my alternative compiler. Any suggestions to make this work?Ceilidh
It's difficult to say when you don't have access to the code, but you can try printing out the variables' values with message() just before calling add_executable (or add_library or whatever you are using) to see if they are still set according to your needs at that moment. Also make sure you use macro() and not function(), since setting a variable within a function would leave it unchanged in the caller's scope, whereas a variable that is set within a macro() stays at that value in the caller scope.Lakeshialakey
Thanks for the reply. Yes, I did this in a macro not a function. And I already tried printing out the the variable values. Here's the output just before defining the executable -- Switching compiler to TEST... -- CMAKE_SYSTEM_NAME: TEST_SYSTEM -- CMAKE_SYSTEM_PROCESSOR: -- CMAKE_C_COMPILER: /home/me/scripts/clang_wrapper.py -- CMAKE_CXX_COMPILER: /home/me/scripts/clang_wrapper++.py -- CMAKE_C_FLAGS: -- CMAKE_CXX_FLAGS: -- CURRENT_COMPILER: TESTCeilidh
I'm not familiar with this clang_wrapper.py of yours. It's possible that clang_wrapper.py does not accept the same kind of arguments as gcc/g++, which would cause CMake to call it with gcc's arguments, like -o outputfile.o -c inputfile.c, which may be invalid arguments to clang_wrapper.py.Lakeshialakey
Our wrapper script was designed to handle clang/gcc arguments. There's no indication that that is failing. What it seems like is it just isn't possible to have N compilers defined for the same CMake configured build. I can't find any way to do this without multiple config passes, specifying the compiler and a different build dir for each pass. I've decided to turn the problem around, and build all of our targets with our wrapper, where for most targets it just calls through to clang, and in other cases it does the special stuff that we need to do. That works around the problem for me.Ceilidh
Which CMake version did you use successfully with this code?Craigie
@DanielJour It was the up-to-date version of two years ago. I might have read somewhere that you couldn't do that anymore. We used this CMake trick as part of this project: github.com/MISTLab/BittyBuzz/blob/… . If you look at the root CMakeLists.txt file under src/, it seems we required CMake version 3.3.2 .Lakeshialakey
I can also confirm that this does not (/no longer?) work. Tested on cmake 3.25, running on debian.Tarn
@Tarn It's possible that that technique (hack?) works only with CMake's Makefile generator, which is what I was using at the time. But it definitely used to work. I was on Ubuntu. I added a note at the top of the answer to explain this.Lakeshialakey
@adentinger: I think the confusion here comes from your example at the bottom: use_host_compiler() add_executable(...) use_native_compiler() add_executable(...) In the same file, as the example kind of implies, will not work, and presumably never did. The reason this works (also in your repo) is add_subdirectory, which I point out in my answer. Admittedly, you do mention at the top that this sets it for all targets in this file, so this answer is technically correct, while being a bit confusing (apparently not just to me :)).Tarn
V
7

There is no proper way to change compiler for individual target.

According to cmake manual "Once set, you can not change this variable". This is about CMAKE_<LANG>_COMPILER.

The solution suggested by AnthonyD973 does not seem to work, which is sad of course. The ability to use several compilers in a project without custom_command things is very useful.

Villus answered 14/8, 2019 at 14:40 Comment(4)
IMO this should not be posted as answer but as comment.Tegantegmen
I haven't tried to reproduce, but perhaps a CMake update broke this if it no longer works. It definitely used to work ; we built a project that would cross-compile some of the programs with the code I posted in my answer.Lakeshialakey
Which CMake version were you using where the code from the other answer didn't work?Craigie
I have 3.13.4 on my Ubuntu 19.04.Villus
T
5

The compiler can be set through the CMAKE_C_COMPILER variable (CMAKE_CXX_COMPILER if you're using c++).

CMake only checks the value of this variable once, after the full build script has finished. This means that if you set the variable multiple times, only the last value that was set actually matters. (Note: according to the docs, setting this variable multiple times is not officially supported).

If we want to use multiple compilers, we need to introduce multiple separate scopes (namespaces) using add_subdirectory. We can then set the CMAKE_C_COMPILER variable to different values in each of these scopes. Here's a simple example project layout:

project
├── foo
│   └── CMakeLists.txt
├── bar
│   └── CMakeLists.txt
├── CMakeLists.txt
└── main.c
# ./project/CMakeLists.txt:
cmake_minimum_required(VERSION 3.0)
project(demo)
add_subdirectory(./foo)
add_subdirectory(./bar)
# ./project/foo/CMakeLists.txt:
set(CMAKE_C_COMPILER clang)
add_executable(foo ../main.c)
# ./project/bar/CMakeLists.txt:
set(CMAKE_C_COMPILER gcc)
add_executable(bar ../main.c)

Variables set in the parent CMakeLists.txt before the call to add_subdirectory, will be inherited by the subscopes.

Ideally, these scopes somewhat correspond to your project's actual directory layout, so you don't need to add a bunch of dummy folders.

Tarn answered 18/3, 2023 at 17:27 Comment(6)
This looks exactly the same as my answer. Or does your use of subdirectories somehow change something?Lakeshialakey
@Lakeshialakey subdirectories is scoping the state of variables, so you don't need to wrap state change in macrosVarela
@adentinger: I updated the answer to clarify why we need the subdirectories. Is it more clear now or do you think it needs further improvement?Tarn
@Tarn Thanks! I've pointed to your answer from mine. One last thing actually. Since the documentation for CMAKE_C_COMPILER says "Once set, you can not change this variable", where/how did you know to also say: CMake only checks the value of this variable once, after the full build script has finished?Lakeshialakey
@adentinger: What I describe is the behavior observed in practice. I have even seen projects were this happens (Hyrum's Law strikes again). The documentation is simply wrong here. (Unless ''you can not" is supposed to be read as "you shall not").Tarn
@Tarn Got it; makes sense. I think, like you said, that this should be read as "you shouldn't; it's not officially supported" but seems to work in practice, for now and on some CMake generators anyway. Our answers are therefore complementary; the end of mine explains an alternate strategy to fixing this problem.Lakeshialakey
A
1

One solution (that I haven't tried yet) is to use

set_target_properties(your_target CXX_COMPILER_LAUNCHER foo_wrapper)

Then make foo_wrapper a script that just drops the first argument (which will be the default compiler, e.g. c++) and then calls the compiler you want.

There's also CXX_LINKER_LAUNCHER and the same for C_....

Atharvaveda answered 2/2, 2023 at 13:33 Comment(0)
C
-9

CMake is a make file generator. It generates a file that you can then use to build. If you want to more than one target platform, you need to run CMake multiple times with different generators.

So what you want to do is not possible in CMake, but with CMake: You can create a shell script that invokes CMake multiple times.

Cb answered 27/11, 2014 at 16:42 Comment(1)
I tend to agree with this, but you could create custom_target which invokes cmake with some other toolchain file, build and install it to some intermediate stage dir, export-import executables... Although probably more difficult then external script, this may look nicer from outside.Velamen

© 2022 - 2025 — McMap. All rights reserved.