file(GET_RUNTIME_DEPENDENCIES): Fix resolution of repeated ELF dependencies

When a library file name is encountered multiple times, reuse the result
from the first time.  This more closely matches the behavior of the
dynamic loader on Linux.

Fixes: #24621
This commit is contained in:
Aliaksandr Averchanka
2024-08-15 10:01:02 +03:00
parent b575a8fc8c
commit 4d4e008e69
8 changed files with 148 additions and 36 deletions

View File

@@ -1169,6 +1169,14 @@ Handling Runtime Binaries
5. Otherwise, the dependency is unresolved.
.. versionchanged:: 3.31
Resolution of each encountered library file name occurs at most once
while processing a given root ELF file (executable or shared object).
If a library file name is encountered again in the dependency tree,
the original resolution is assumed. This behavior more closely matches
the dynamic loader's behavior on Linux.
On Windows platforms, library resolution works as follows:
1. DLL dependency names are converted to lowercase for matching filters.

View File

@@ -0,0 +1,5 @@
elf-lib-deps-resolve
--------------------
* The :command:`file(GET_RUNTIME_DEPENDENCIES)` command was updated
to more closely match the dynamic loader's behavior on Linux.

View File

@@ -3,7 +3,10 @@
#include "cmBinUtilsLinuxELFLinker.h"
#include <queue>
#include <sstream>
#include <unordered_set>
#include <utility>
#include <cm/memory>
#include <cm/string_view>
@@ -89,8 +92,6 @@ bool cmBinUtilsLinuxELFLinker::Prepare()
bool cmBinUtilsLinuxELFLinker::ScanDependencies(
std::string const& file, cmStateEnums::TargetType /* unused */)
{
std::vector<std::string> parentRpaths;
cmELF elf(file.c_str());
if (!elf) {
return false;
@@ -106,40 +107,53 @@ bool cmBinUtilsLinuxELFLinker::ScanDependencies(
}
}
return this->ScanDependencies(file, parentRpaths);
return this->ScanDependencies(file);
}
bool cmBinUtilsLinuxELFLinker::ScanDependencies(
std::string const& file, std::vector<std::string> const& parentRpaths)
bool cmBinUtilsLinuxELFLinker::ScanDependencies(std::string const& mainFile)
{
std::string origin = cmSystemTools::GetFilenamePath(file);
std::vector<std::string> needed;
std::vector<std::string> rpaths;
std::vector<std::string> runpaths;
if (!this->Tool->GetFileInfo(file, needed, rpaths, runpaths)) {
return false;
}
for (auto& runpath : runpaths) {
runpath = ReplaceOrigin(runpath, origin);
}
for (auto& rpath : rpaths) {
rpath = ReplaceOrigin(rpath, origin);
}
std::unordered_set<std::string> resolvedDependencies;
std::queue<std::pair<std::string, std::vector<std::string>>> queueToResolve;
queueToResolve.push(std::make_pair(mainFile, std::vector<std::string>{}));
std::vector<std::string> searchPaths;
if (!runpaths.empty()) {
searchPaths = runpaths;
} else {
searchPaths = rpaths;
searchPaths.insert(searchPaths.end(), parentRpaths.begin(),
parentRpaths.end());
}
while (!queueToResolve.empty()) {
std::string file = std::move(queueToResolve.front().first);
std::vector<std::string> parentRpaths =
std::move(queueToResolve.front().second);
queueToResolve.pop();
searchPaths.insert(searchPaths.end(), this->LDConfigPaths.begin(),
this->LDConfigPaths.end());
std::string origin = cmSystemTools::GetFilenamePath(file);
std::vector<std::string> needed;
std::vector<std::string> rpaths;
std::vector<std::string> runpaths;
if (!this->Tool->GetFileInfo(file, needed, rpaths, runpaths)) {
return false;
}
for (auto& runpath : runpaths) {
runpath = ReplaceOrigin(runpath, origin);
}
for (auto& rpath : rpaths) {
rpath = ReplaceOrigin(rpath, origin);
}
std::vector<std::string> searchPaths;
if (!runpaths.empty()) {
searchPaths = runpaths;
} else {
searchPaths = rpaths;
searchPaths.insert(searchPaths.end(), parentRpaths.begin(),
parentRpaths.end());
}
searchPaths.insert(searchPaths.end(), this->LDConfigPaths.begin(),
this->LDConfigPaths.end());
for (auto const& dep : needed) {
if (resolvedDependencies.count(dep) != 0 ||
this->Archive->IsPreExcluded(dep)) {
continue;
}
for (auto const& dep : needed) {
if (!this->Archive->IsPreExcluded(dep)) {
std::string path;
bool resolved = false;
if (dep.find('/') != std::string::npos) {
@@ -150,6 +164,7 @@ bool cmBinUtilsLinuxELFLinker::ScanDependencies(
return false;
}
if (resolved) {
resolvedDependencies.emplace(dep);
if (!this->Archive->IsPostExcluded(path)) {
bool unique;
this->Archive->AddResolvedPath(dep, path, unique);
@@ -157,9 +172,8 @@ bool cmBinUtilsLinuxELFLinker::ScanDependencies(
std::vector<std::string> combinedParentRpaths = parentRpaths;
combinedParentRpaths.insert(combinedParentRpaths.end(),
rpaths.begin(), rpaths.end());
if (!this->ScanDependencies(path, combinedParentRpaths)) {
return false;
}
queueToResolve.push(std::make_pair(path, combinedParentRpaths));
}
}
} else {

View File

@@ -34,8 +34,7 @@ private:
std::vector<std::string> LDConfigPaths;
std::uint16_t Machine = 0;
bool ScanDependencies(std::string const& file,
std::vector<std::string> const& parentRpaths);
bool ScanDependencies(std::string const& mainFile);
bool ResolveDependency(std::string const& name,
std::vector<std::string> const& searchPaths,

View File

@@ -72,6 +72,7 @@ elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
run_install_test(linux-unresolved)
run_install_test(linux-conflict)
run_install_test(linux-notfile)
run_install_test(linux-indirect-dependencies)
run_cmake(project)
run_cmake(badargs1)
run_cmake(badargs2)

View File

@@ -1,4 +1,5 @@
set(_check
[[[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/linux-build/root-all/lib/conflict/libconflict\.so]]
[[[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/linux-build/root-all/lib/libtest_rpath\.so]]
[[[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/linux-build/root-all/lib/libtest_runpath\.so]]
[[[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/linux-build/root-all/lib/rpath/librpath\.so]]
@@ -19,7 +20,7 @@ check_contents(deps/udeps1.txt "^${_check}$")
check_contents(deps/udeps2.txt "^${_check}$")
check_contents(deps/udeps3.txt "^${_check}$")
set(_check
"^libconflict\\.so:[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/linux-build/root-all/lib/conflict/libconflict\\.so;[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/linux-build/root-all/lib/conflict2/libconflict\\.so\n$"
"^$"
)
check_contents(deps/cdeps1.txt "${_check}")
check_contents(deps/cdeps2.txt "${_check}")

View File

@@ -0,0 +1 @@
Resolved dependencies: /

View File

@@ -0,0 +1,83 @@
enable_language(C)
cmake_policy(SET CMP0095 NEW)
file(WRITE "${CMAKE_BINARY_DIR}/A.c" "void libA(void) {}\n")
file(WRITE "${CMAKE_BINARY_DIR}/C.c" "void libC(void) {}\n")
file(WRITE "${CMAKE_BINARY_DIR}/BUseAC.c" [[
extern void libA(void);
extern void libC(void);
void libB(void)
{
libA();
libC();
}
]])
file(WRITE "${CMAKE_BINARY_DIR}/mainABC.c" [[
extern void libA(void);
extern void libB(void);
extern void libC(void);
int main(void)
{
libA();
libB();
libC();
return 0;
}
]])
set(lib_dirExe "${CMAKE_BINARY_DIR}/Exe")
set(lib_dirA "${CMAKE_BINARY_DIR}/libA")
set(lib_dirB "${CMAKE_BINARY_DIR}/libB")
set(lib_dirC "${CMAKE_BINARY_DIR}/libC")
file(MAKE_DIRECTORY ${lib_dirExe})
file(MAKE_DIRECTORY ${lib_dirA})
file(MAKE_DIRECTORY ${lib_dirB})
file(MAKE_DIRECTORY ${lib_dirC})
add_library(A SHARED "${CMAKE_BINARY_DIR}/A.c")
set_property(TARGET A PROPERTY LIBRARY_OUTPUT_DIRECTORY ${lib_dirA})
add_library(C SHARED "${CMAKE_BINARY_DIR}/C.c")
set_property(TARGET C PROPERTY LIBRARY_OUTPUT_DIRECTORY ${lib_dirC})
# We doesn't need to set A as a dependency of B, because we don't need `RUNPATH` value set for B
add_library(B SHARED "${CMAKE_BINARY_DIR}/BUseAC.c")
target_link_libraries(B PRIVATE A C)
set_property(TARGET B PROPERTY LIBRARY_OUTPUT_DIRECTORY ${lib_dirB})
# We MUST have empty `RUNPATH` in A & B
set_target_properties(A B C PROPERTIES
BUILD_WITH_INSTALL_RPATH 1
)
# The executable is really workable without `RUNPATH` in B
add_executable(exe "${CMAKE_BINARY_DIR}/mainABC.c")
target_link_libraries(exe A B C)
set_property(TARGET exe PROPERTY RUNTIME_OUTPUT_DIRECTORY ${lib_dirExe})
# We MUST have `RUNPATH` in exe, not `RPATH`
# Test will pass if we have `RPATH`, because of the inheritance
target_link_options(exe PRIVATE -Wl,--enable-new-dtags)
install(CODE [[
# Work with non-installed binary, because of the RUNPATH values
set(exeFile "$<TARGET_FILE:exe>")
# Check executable is can be successfully finished
execute_process(
COMMAND "${exeFile}"
COMMAND_ERROR_IS_FATAL ANY
)
# Check dependencies resolved
file(GET_RUNTIME_DEPENDENCIES
RESOLVED_DEPENDENCIES_VAR RESOLVED
PRE_INCLUDE_REGEXES "^lib[ABC]\\.so$"
PRE_EXCLUDE_REGEXES ".*"
EXECUTABLES
"${exeFile}"
)
message(STATUS "Resolved dependencies: ${RESOLVED}")
]])