LDFLAGS -L flags take out rpath flag and breaks transitive linking
It looks like LDFLAGS
environment variable pointing to the library install directory can inhibit -Wl,-rpath
flag from being generated for the linking step -- which can break transitive dependencies.
In short (in the code example described later) the following command is generated without LDFLAGS
:
/usr/bin/c++ -O3 -DNDEBUG CMakeFiles/baz.dir/baz.cc.o -o baz -Wl,-rpath,tmp-install/lib: tmp-install/lib/libbar.so
With LDFLAGS
pointing to the same directory (tmp-install/lib
), the following command is generated instead:
/usr/bin/c++ -O3 -DNDEBUG -Ltmp-install/lib CMakeFiles/baz.dir/baz.cc.o -o baz tmp-install/lib/libbar.so
So, essentially -L
flag came from LDFLAGS
and cmake silently took out -Wl,-rpath
flag. The result is a failed link step:
/usr/lib/gcc/x86_64-pc-linux-gnu/14/../../../../x86_64-pc-linux-gnu/bin/ld: warning: libfoo.so, needed by tmp-install/lib/libbar.so, not found (try using -rpath or -rpath-link)
/usr/lib/gcc/x86_64-pc-linux-gnu/14/../../../../x86_64-pc-linux-gnu/bin/ld: tmp-install/lib/libbar.so: undefined reference to `foo()'
This seems quite unexpected and I'm not sure what is the point of this behavior (if it indeed works as designed). The tested version is CMake v3.29.2 and v3.30.0.
Detailed steps to reproduce:
-
We are going to create libfoo.so, libbar.so (depends on libfoo.so) and baz application (depends on libbar.so).
-
libfoo:
mkdir foo
cat <<'END' >foo/foo.h
#pragma once
int foo();
END
cat <<'END' >foo/foo.cc
#include "foo.h"
int foo() { return 42; }
END
cat <<'END' >foo/CMakeLists.txt
cmake_minimum_required(VERSION 3.29)
project(Foo LANGUAGES CXX)
add_library(foo SHARED foo.cc)
target_include_directories(foo PRIVATE "${CMAKE_SOURCE_DIR}")
target_include_directories(foo INTERFACE $<INSTALL_INTERFACE:include>)
install(FILES foo.h DESTINATION "include")
install(TARGETS foo DESTINATION "lib" EXPORT FooTargets)
install(EXPORT FooTargets FILE "FooConfig.cmake" NAMESPACE "Foo::" DESTINATION "lib/cmake/Foo")
END
- libbar:
mkdir bar
cat <<'END' >bar/bar.h
#pragma once
int bar();
END
cat <<'END' >bar/bar.cc
#include "foo.h"
#include "bar.h"
int bar() { return foo(); }
END
cat <<'END' >bar/CMakeLists.txt
cmake_minimum_required(VERSION 3.29)
project(Bar LANGUAGES CXX)
find_package(Foo REQUIRED NO_MODULE NO_CMAKE_FIND_ROOT_PATH NO_DEFAULT_PATH PATHS "${CMAKE_INSTALL_PREFIX}")
add_library(bar SHARED bar.cc)
target_include_directories(bar PRIVATE "${CMAKE_SOURCE_DIR}")
target_include_directories(bar INTERFACE $<INSTALL_INTERFACE:include>)
target_link_libraries(bar PRIVATE Foo::foo)
install(FILES bar.h DESTINATION "include")
install(TARGETS bar DESTINATION "lib" EXPORT BarTargets)
install(EXPORT BarTargets FILE "BarConfig.cmake" NAMESPACE "Bar::" DESTINATION "lib/cmake/Bar")
END
- baz application
mkdir baz
cat <<'END' >baz/baz.cc
#include "bar.h"
int main() { return bar(); }
END
cat <<'END' >baz/CMakeLists.txt
cmake_minimum_required(VERSION 3.29)
project(Baz LANGUAGES CXX)
find_package(Bar REQUIRED NO_MODULE NO_CMAKE_FIND_ROOT_PATH NO_DEFAULT_PATH PATHS "${CMAKE_INSTALL_PREFIX}")
add_executable(baz baz.cc)
target_link_libraries(baz PRIVATE Bar::bar)
install(TARGETS baz DESTINATION "bin")
END
- The only thing left to do is to build everything:
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=tmp-install -S foo -B tmp-build/foo
cmake --build tmp-build/foo --target install -- VERBOSE=1
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=tmp-install -S bar -B tmp-build/bar
cmake --build tmp-build/bar --target install -- VERBOSE=1
env LDFLAGS="-L$PWD/tmp-install/lib" \
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=tmp-install -S baz -B tmp-build/baz
cmake --build tmp-build/baz --target install -- VERBOSE=1
- With LDFLAGS the linking of
baz
application is expected to fail, while succeeding without LDFLAGS.