diff --git a/core/base/CMakeLists.txt b/core/base/CMakeLists.txt index f2f9518eb3495..419fb0fde3e53 100644 --- a/core/base/CMakeLists.txt +++ b/core/base/CMakeLists.txt @@ -211,15 +211,9 @@ if(ROOT_NEED_STDCXXFS) target_link_libraries(Core PRIVATE stdc++fs) endif() -# This code about the LIB_CORE_NAME define is important for TROOT::GetLibDir() -get_target_property(core_prefix Core PREFIX) -get_target_property(core_suffix Core SUFFIX) -get_target_property(core_soversion Core SOVERSION) -if(core_soversion) - set(full_core_filename "${core_prefix}Core${core_suffix}.${core_soversion}") -else() - set(full_core_filename "${core_prefix}Core${core_suffix}") -endif() +# This code about the LIB_CORE_NAME define is important for TROOT::GetSharedLibDir() +set(full_core_filename $) + target_compile_options(Core PRIVATE -DLIB_CORE_NAME=${full_core_filename}) if(PCRE2_FOUND) diff --git a/core/base/src/TROOT.cxx b/core/base/src/TROOT.cxx index 91a40777d13de..1a7eace514bde 100644 --- a/core/base/src/TROOT.cxx +++ b/core/base/src/TROOT.cxx @@ -3016,6 +3016,33 @@ const TString& TROOT::GetBinDir() { //////////////////////////////////////////////////////////////////////////////// /// Get the library directory in the installation. Static utility function. /// +/// By default, this is just an alias for TROOT::GetSharedLibDir(), which +/// returns the directory containing the ROOT shared libraries. +/// +/// On Windows, the behavior is different. In that case, this function doesn't +/// return the directory of the **shared libraries** (like `libCore.dll`), but +/// the **import libraries**, which are used at link time (like `libCore.lib`). + +const TString &TROOT::GetLibDir() +{ +#if defined(R__WIN32) + static bool initialized = false; + static TString rootlibdir; + if (initialized) + return rootlibdir; + + initialized = true; + rootlibdir = "lib"; + gSystem->PrependPathName(GetRootSys(), rootlibdir); + return rootlibdir; +#else + return TROOT::GetSharedLibDir(); +#endif +} + +//////////////////////////////////////////////////////////////////////////////// +/// Get the shared libraries directory in the installation. Static utility function. +/// /// This function inspects the libraries currently loaded in the process to /// locate the ROOT Core library. Once found, it extracts and returns the /// directory containing that library. If the ROOT Core library was not found, @@ -3024,9 +3051,9 @@ const TString& TROOT::GetBinDir() { /// The result is cached in a static variable so the lookup is only performed /// once per process, and the implementation is platform-specific. /// -/// \return The directory path (as a `TString`) containing the ROOT core library. +/// \return The directory path (as a `TString`) containing the ROOT shared libraries. -const TString &TROOT::GetLibDir() +const TString &TROOT::GetSharedLibDir() { static bool haveLooked = false; static TString rootlibdir; @@ -3056,9 +3083,72 @@ const TString &TROOT::GetLibDir() #elif defined(_WIN32) - // Or Windows, the original hardcoded path is kept for now. - rootlibdir = "lib"; - gSystem->PrependPathName(GetRootSys(), rootlibdir); + HMODULE modulesStack[1024]; + std::vector modulesHeap; + HMODULE *modules = modulesStack; + DWORD needed = 0; + + HANDLE process = GetCurrentProcess(); + + bool success = EnumProcessModules(process, modulesStack, sizeof(modulesStack), &needed); + + // It is recommended in the API documentation to check if the output array + // was too small, and if yes, call EnumProcessModules again with an array of + // the required size. To avoid heap allocations, we use a heap array only + // when the number of modules was too large for the original stack array. + // See: https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-enumprocessmodules#remarks + if (needed > sizeof(modulesStack)) { + modulesHeap.resize(needed / sizeof(HMODULE)); + success = EnumProcessModules(process, modulesHeap.data(), needed, &needed); + modules = modulesHeap.data(); + } + + if (success) { + const unsigned int count = needed / sizeof(HMODULE); + + for (unsigned int i = 0; i < count; ++i) { + wchar_t wpath[MAX_PATH]; + DWORD len = GetModuleFileNameW(modules[i], wpath, MAX_PATH); + if (!len) + continue; + + // According to the Windows API documentation, there are exceptions + // where a path can be longer than MAX_PATH: + // https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry + // In case that happens here, we print an error message. + if (len == MAX_PATH) { + // Convert UTF-16 path to UTF-8 for the error message + int utf8len = WideCharToMultiByte(CP_UTF8, 0, wpath, -1, nullptr, 0, nullptr, nullptr); + + std::string utf8path(utf8len - 1, '\0'); + WideCharToMultiByte(CP_UTF8, 0, wpath, -1, utf8path.data(), utf8len, nullptr, nullptr); + + utf8path += "... [TRUNCATED]"; + + ::Error("TROOT::GetSharedLibDir", + "Module path \"%s\" exceeded maximum path length of %u characters! " + "ROOT might not be able to resolve its resource directories.", + utf8path.c_str(), MAX_PATH); + + continue; + } + + fs::path p{wpath}; + if (p.filename() == TO_LITERAL(LIB_CORE_NAME)) { + + // Convert UTF-16 to UTF-8 explicitly + const std::wstring wdir = p.parent_path().wstring(); + + int utf8len = WideCharToMultiByte(CP_UTF8, 0, wdir.c_str(), -1, nullptr, 0, nullptr, nullptr); + + std::string utf8dir(utf8len - 1, '\0'); + WideCharToMultiByte(CP_UTF8, 0, wdir.c_str(), -1, utf8dir.data(), utf8len, nullptr, nullptr); + + rootlibdir = utf8dir.c_str(); + break; + } + } + } #else @@ -3069,7 +3159,26 @@ const TString &TROOT::GetLibDir() fs::path p = info->dlpi_name; if (p.filename() == TO_LITERAL(LIB_CORE_NAME)) { - libdir = p.parent_path().c_str(); + std::error_code ec; + + // Resolve symlinks: critical for environments like CMSSW, where the + // ROOT libraries are loaded via symlinks that point to the actual + // install directory + fs::path resolved = fs::canonical(p, ec); + if (ec) { + ::Error("TROOT", + "Failed to canonicalize detected ROOT shared library path:\n" + "%s\n" + "Error code: %d (%s)\n" + "Error category: %s\n" + "This is an unexpected internal error and ROOT might not work.\n" + "Please report this issue on GitHub: https://github.com/root-project/root/issues", + p.string().c_str(), ec.value(), ec.message().c_str(), ec.category().name()); + // Fall back to the loader path if canonicalization fails. The path + // will likely be wrong, but at least not garbage + resolved = p; + } + libdir = resolved.parent_path().c_str(); return 1; // stop iteration } return 0; // continue @@ -3082,17 +3191,6 @@ const TString &TROOT::GetLibDir() return rootlibdir; } -//////////////////////////////////////////////////////////////////////////////// -/// Get the shared libraries directory in the installation. Static utility function. - -const TString& TROOT::GetSharedLibDir() { -#if defined(R__WIN32) - return TROOT::GetBinDir(); -#else - return TROOT::GetLibDir(); -#endif -} - //////////////////////////////////////////////////////////////////////////////// /// Get the include directory in the installation. Static utility function. diff --git a/core/base/test/CMakeLists.txt b/core/base/test/CMakeLists.txt index 69f14b63026e7..e839d78255c9f 100644 --- a/core/base/test/CMakeLists.txt +++ b/core/base/test/CMakeLists.txt @@ -15,6 +15,12 @@ ROOT_ADD_GTEST(CoreBaseTests TROOTTests.cxx LIBRARIES RIO Core) +# For TROOTTests.cxx: +if(ROOT_NEED_STDCXXFS) + target_link_libraries(CoreBaseTests PRIVATE stdc++fs) +endif() +target_compile_options(CoreBaseTests PRIVATE -DEXPECTED_SHARED_LIBRARY_DIR="${localruntimedir}") + ROOT_ADD_GTEST(CoreErrorTests TErrorTests.cxx LIBRARIES Core) ROOT_ADD_GTEST(CoreSystemTests TSystemTests.cxx LIBRARIES Core) diff --git a/core/base/test/TROOTTests.cxx b/core/base/test/TROOTTests.cxx index 796c6c986961f..5bc454b9e164a 100644 --- a/core/base/test/TROOTTests.cxx +++ b/core/base/test/TROOTTests.cxx @@ -2,8 +2,10 @@ #include "TROOT.h" -#include +#include #include +#include +#include TEST(TROOT, Version) { @@ -21,4 +23,21 @@ TEST(TROOT, Version) EXPECT_EQ(refLength[tokCounter++], buf.size()); } EXPECT_EQ(3, tokCounter); -} \ No newline at end of file +} + +// TROOT::GetSharedLibDir() is fundamental to resolve all the directories +// relevant for ROOT, because it can be inferred without environment variables +// like ROOTSYS by locating libCore, which is loaded by definition when using +// ROOT. Therefore, GetSharedLibDir() serves as an anchor to resolve all other +// directories, using the correct relative paths for either the build or +// install tree. Given this fundamental role, we need to test that it works. +TEST(TROOT, GetSharedLibDir) +{ + namespace fs = std::filesystem; + + // Use std::filesystem for automatic path normalization. + fs::path libDir = gROOT->GetSharedLibDir().Data(); + fs::path libDirRef = EXPECTED_SHARED_LIBRARY_DIR; + + EXPECT_EQ(libDir, libDirRef); +} diff --git a/tutorials/machine_learning/TMVA_SOFIE_GNN_Application.C b/tutorials/machine_learning/TMVA_SOFIE_GNN_Application.C index dcf03fb14c3e6..31255ec63d548 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_GNN_Application.C +++ b/tutorials/machine_learning/TMVA_SOFIE_GNN_Application.C @@ -234,10 +234,3 @@ void TMVA_SOFIE_GNN_Application (bool verbose = false) c2->cd(3); o3->Draw(); } - -int main () { - - TMVA_SOFIE_GNN_Application(); -} - -