Assigning framework directories in INTERFACE_INCLUDE_DIRECTORIES is problematic
Hi,
This is an issue that Qt encounters, but I've distilled it into a small project that I'll attach at the end to demonstrate the problem. First a description though.
We allow projects to include Qt headers in 2 ways:
#include <Gui> // regular style
#include <Gui/Gui> // framework style
To achieve that for macOS framework bundles, we assign 2 values to INTERFACE_INCLUDE_DIRECTORIES
of an IMPORTED SHARED FRAMEWORK
target.
set_target_properties(Gui
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/lib/Gui.framework;${_IMPORT_PREFIX}/lib/Gui.framework/Headers"
When CMake generates the compilation rules, it is framework-aware, and transforms the entries to command line flags as follows:
${_IMPORT_PREFIX}/lib/Gui.framework -> -iframework ${_IMPORT_PREFIX}/lib
${_IMPORT_PREFIX}/lib/Gui.framework/Headers -> -isystem ${_IMPORT_PREFIX}/lib/Gui.framework/Headers
So far so good.
Unfortunately this can cause issues because the INTERFACE_INCLUDE_DIRECTORIES
values are passed verbatim to other third-party tools (e.g. via file(GENERATE CONTENT "$<TARGET_PROPERTY:Gui,INTERFACE_INCLUDE_DIRECTORIES>")
.
One of those cases is AUTOMOC
-> moc
, but it can affect other tools that use Qt as well.
AUTOMOC
takes the INTERFACE_INCLUDE_DIRECTORIES
values and passes them to moc
, prepending -I
to each value, so the passed args will be
-I lib/Gui.framework
-I lib/Gui.framework/Headers
Note in the example above the C++
file has #include <Gui>
When moc
runs its preprocessor, it will consider -I lib/Gui.framework
+ #include <Gui>
,
it will thus find lib/Gui.framework/Gui
which is not actually a header file, but the shared library,
which it will then include, and skip including the real lib/Gui.framework/Headers/Gui
header, which is the crux of the problem.
Now, one could say that there are a bunch of mitigations that can be done:
- don't name your framework headers exactly the same as the framework name, without file extensions
- change
moc
to filter out include paths like-Ilib/Gui.framework
- change
AUTOMOC
to filter out include paths-Ilib/Gui.framework
- change the order of the include paths so
Headers
comes first
We can't do 1). We can do the rest.
What we can't do, is expect other projects using CMake and Qt to expect to filter / preprocess such include paths when they are used in their own 3rd party tools.
This issue would not be a problem if we could remove the ${_IMPORT_PREFIX}/lib/Gui.framework
value from INTERFACE_INCLUDE_DIRECTORIES
, and instead rely on #23336 (closed) / target_link_libraries
to correctly add framework header search paths.
To be honest, I can't really come up with a clean solution that would work with older CMake versions, apart from the already mentioned mitigations.
It seems an unfortunate consequence of CMake relying on ${_IMPORT_PREFIX}/lib/Gui.framework
-style values in INTERFACE_INCLUDE_DIRECTORIES
for framework includes to work.
But if #23336 (closed) were to be fixed, we could at least switch to using that for future CMake versions.
And while the above example is a Qt-specific case, generally -I ./SomeFw.framework
is not a valid include path, and any project using frameworks could run into this when they read values from INTERFACE_INCLUDE_DIRECTORIES
. It just so happens that most tooling ignores invalid include paths (either silently or with warnings).
Project:
cmake_minimum_required(VERSION 3.16)
project(proj LANGUAGES CXX)
# Create public header and implementation
set(header "${CMAKE_BINARY_DIR}/Gui.h")
file(WRITE "${header}" "int foo();")
file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/Gui.cpp" CONTENT "int foo() { return 0; }")
# Create forwarding header without a file extension and place it in the framework bundle
set(forward_header "${CMAKE_BINARY_DIR}/Gui.framework/Headers/Gui")
add_custom_command(OUTPUT "${forward_header}"
COMMAND echo "#include \"Gui.h\"" > "${forward_header}"
VERBATIM
)
# Create framework and ensure both headers are placed in the framework bundle under Headers
add_library(Gui SHARED ${CMAKE_BINARY_DIR}/Gui.cpp "${header}" "${forward_header}")
set_target_properties(Gui PROPERTIES
PUBLIC_HEADER "${header}"
FRAMEWORK TRUE
)
# Propagate regular-style include paths and framework-style include paths
target_include_directories(Gui
INTERFACE
"${CMAKE_BINARY_DIR}/Gui.framework"
"${CMAKE_BINARY_DIR}/Gui.framework/Headers"
)
# Create app that uses the headers
file(GENERATE OUTPUT "main.cpp" CONTENT "
#include <Gui> // regular-style
#include <Gui/Gui> // framework-style
int main(int, char**) {
foo();
return 0;
}
")
add_executable(app ${CMAKE_BINARY_DIR}/main.cpp)
add_dependencies(app Gui)
target_link_libraries(app PRIVATE Gui)
# Print non-processed include paths. Note the troublesome ./Gui.framework include path
file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/include_paths.rsp"
CONTENT "-I$<JOIN:$<TARGET_PROPERTY:Gui,INTERFACE_INCLUDE_DIRECTORIES>,\n-I>\n")
add_custom_target(print_include_paths ALL
COMMENT "Print include paths"
COMMAND cat "${CMAKE_BINARY_DIR}/include_paths.rsp"
VERBATIM)