Browse Source

file(GET_RUNTIME_DEPENDENCIES): Preserve casing for Windows PE binaries

For Windows PE files the `file(GET_RUNTIME_DEPENDENCIES)` command
converts the name of all  DLLs found during binary scanning to
lowercase in order to simplify the syntax requirements of its regex
filters; however, this has the side-effect of causing all DLL paths
returned via RESOLVED_DEPENDENCIES_VAR to be in lowercase, regardless
of their actual casing.

Instead, respect the original casing as closely as possible when
returning resolved dependencies after all filters have been
passed:

When evaluating a Windows PE format binary on a non-Windows host
the casing of dependencies recorded within the binary are
used. When the host is running Windows, the actual casing of the
dependencies on-disk are used instead.

Fixes: #23091
Christian Heimlich 2 years ago
parent
commit
fa45594407

+ 50 - 8
Source/cmBinUtilsWindowsPELinker.cxx

@@ -3,7 +3,10 @@
 
 #include "cmBinUtilsWindowsPELinker.h"
 
+#include <algorithm>
+#include <iterator>
 #include <sstream>
+#include <utility>
 #include <vector>
 
 #include <cm/memory>
@@ -16,6 +19,27 @@
 
 #ifdef _WIN32
 #  include <windows.h>
+
+#  include "cmsys/Encoding.hxx"
+#endif
+
+#ifdef _WIN32
+namespace {
+
+void ReplaceWithActualNameCasing(std::string& path)
+{
+  WIN32_FIND_DATAW findData;
+  HANDLE hFind = ::FindFirstFileW(
+    cmsys::Encoding::ToWindowsExtendedPath(path).c_str(), &findData);
+
+  if (hFind != INVALID_HANDLE_VALUE) {
+    auto onDiskName = cmsys::Encoding::ToNarrow(findData.cFileName);
+    ::FindClose(hFind);
+    path.replace(path.end() - onDiskName.size(), path.end(), onDiskName);
+  }
+}
+
+}
 #endif
 
 cmBinUtilsWindowsPELinker::cmBinUtilsWindowsPELinker(
@@ -60,29 +84,47 @@ bool cmBinUtilsWindowsPELinker::ScanDependencies(
   if (!this->Tool->GetFileInfo(file, needed)) {
     return false;
   }
-  for (auto& n : needed) {
-    n = cmSystemTools::LowerCase(n);
-  }
+
+  struct WinPEDependency
+  {
+    WinPEDependency(std::string o)
+      : Original(std::move(o))
+      , LowerCase(cmSystemTools::LowerCase(Original))
+    {
+    }
+    std::string const Original;
+    std::string const LowerCase;
+  };
+
+  std::vector<WinPEDependency> depends;
+  depends.reserve(needed.size());
+  std::move(needed.begin(), needed.end(), std::back_inserter(depends));
   std::string origin = cmSystemTools::GetFilenamePath(file);
 
-  for (auto const& lib : needed) {
-    if (!this->Archive->IsPreExcluded(lib)) {
+  for (auto const& lib : depends) {
+    if (!this->Archive->IsPreExcluded(lib.LowerCase)) {
       std::string path;
       bool resolved = false;
-      if (!this->ResolveDependency(lib, origin, path, resolved)) {
+      if (!this->ResolveDependency(lib.LowerCase, origin, path, resolved)) {
         return false;
       }
       if (resolved) {
         if (!this->Archive->IsPostExcluded(path)) {
+#ifdef _WIN32
+          ReplaceWithActualNameCasing(path);
+#else
+          path.replace(path.end() - lib.Original.size(), path.end(),
+                       lib.Original);
+#endif
           bool unique;
-          this->Archive->AddResolvedPath(lib, path, unique);
+          this->Archive->AddResolvedPath(lib.Original, path, unique);
           if (unique &&
               !this->ScanDependencies(path, cmStateEnums::SHARED_LIBRARY)) {
             return false;
           }
         }
       } else {
-        this->Archive->AddUnresolvedPath(lib);
+        this->Archive->AddUnresolvedPath(lib.Original);
       }
     }
   }

+ 1 - 0
Tests/RunCMake/CMakeLists.txt

@@ -784,6 +784,7 @@ if(DEFINED CMake_COMPILER_FORCES_NEW_DTAGS)
 endif()
 add_RunCMake_test(file-GET_RUNTIME_DEPENDENCIES
   -DCMake_INSTALL_NAME_TOOL_BUG=${CMake_INSTALL_NAME_TOOL_BUG}
+  -DCMAKE_C_COMPILER_ID=${CMAKE_C_COMPILER_ID}
   )
 
 add_RunCMake_test(CPackCommandLine)

+ 4 - 0
Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/RunCMakeTest.cmake

@@ -9,6 +9,10 @@ function(run_install_test case)
   run_cmake_command(${case}-build ${CMAKE_COMMAND} --build . --config Debug)
   # Check "all" components.
   set(CMAKE_INSTALL_PREFIX ${RunCMake_TEST_BINARY_DIR}/root-all)
+  set(maybe_stderr "${case}-all-stderr-${CMAKE_C_COMPILER_ID}.txt")
+  if(EXISTS "${RunCMake_SOURCE_DIR}/${maybe_stderr}")
+    set(RunCMake-stderr-file "${maybe_stderr}")
+  endif()
   run_cmake_command(${case}-all ${CMAKE_COMMAND} --install . --prefix ${CMAKE_INSTALL_PREFIX} --config Debug)
 endfunction()
 

+ 12 - 3
Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-all-check.cmake

@@ -1,20 +1,29 @@
+if(CMAKE_C_COMPILER_ID STREQUAL "Borland")
+  # Borland upper-cases dll names referenced in import libraries.
+  set(conflict_dll [[CONFLICT\.DLL]])
+  set(unresolved_dll [[UNRESOLVED\.DLL]])
+else()
+  set(conflict_dll [[conflict\.dll]])
+  set(unresolved_dll [[unresolved\.dll]])
+endif()
+
 set(_check
   [=[[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-build/root-all/bin/\.conflict/\.\./(lib)?libdir\.dll]=]
   [=[[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-build/root-all/bin/\.search/(lib)?search\.dll]=]
-  [=[[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-build/root-all/bin/(lib)?mixedcase\.dll]=]
+  [=[[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-build/root-all/bin/(lib)?MixedCase\.dll]=]
   [=[[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-build/root-all/bin/(lib)?testlib\.dll]=]
   )
 check_contents(deps/deps1.txt "^${_check}$")
 check_contents(deps/deps2.txt "^${_check}$")
 check_contents(deps/deps3.txt "^${_check}$")
 set(_check
-  [=[(lib)?unresolved\.dll]=]
+  "(lib)?${unresolved_dll}"
   )
 check_contents(deps/udeps1.txt "^${_check}$")
 check_contents(deps/udeps2.txt "^${_check}$")
 check_contents(deps/udeps3.txt "^${_check}$")
 set(_check
-  "^(lib)?conflict\\.dll:[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-build/root-all/bin/\\.conflict/(lib)?conflict\\.dll;[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-build/root-all/bin/(lib)?conflict\\.dll\n$"
+  "^(lib)?${conflict_dll}:[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-build/root-all/bin/\\.conflict/(lib)?conflict\\.dll;[^;]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-build/root-all/bin/(lib)?conflict\\.dll\n$"
   )
 check_contents(deps/cdeps1.txt "${_check}")
 check_contents(deps/cdeps2.txt "${_check}")

+ 7 - 0
Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-conflict-all-stderr-Borland.txt

@@ -0,0 +1,7 @@
+^CMake Error at cmake_install\.cmake:[0-9]+ \(file\):
+  file Multiple conflicting paths found for PATH\.DLL:
+
+    [^
+]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-conflict-build/root-all/lib/test1/path\.dll
+    [^
+]*/Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-conflict-build/root-all/lib/test2/path\.dll$

+ 4 - 0
Tests/RunCMake/file-GET_RUNTIME_DEPENDENCIES/windows-unresolved-all-stderr-Borland.txt

@@ -0,0 +1,4 @@
+^CMake Error at cmake_install\.cmake:[0-9]+ \(file\):
+  file Could not resolve runtime dependencies:
+
+    UNRESOLVED\.DLL$