소스 검색

install: Normalize DESTINATION paths

The file generated by install(EXPORT) computes _IMPORT_PREFIX
in a way that assumes a normalized path. If the DESTINATION
contains any ../ components, the computed _IMPORT_PREFIX
would be wrong. Force the DESTINATION path to be normalized,
subject to the new CMP0176 policy.

Also normalize all other DESTINATION paths for consistency,
except for INCLUDES DESTINATION, which is not strictly a
destination but rather a search path to add.

Fixes: #26252
Craig Scott 1 년 전
부모
커밋
6a1fac1450

+ 13 - 1
Help/command/install.rst

@@ -58,7 +58,7 @@ signatures that specify them.  The common options are:
   ``<dir>`` should be a relative path.  An absolute path is allowed,
   but not recommended.
 
-  When a relative path is given it is interpreted relative to the value
+  When a relative path is given, it is interpreted relative to the value
   of the :variable:`CMAKE_INSTALL_PREFIX` variable.
   The prefix can be relocated at install time using the ``DESTDIR``
   mechanism explained in the :variable:`CMAKE_INSTALL_PREFIX` variable
@@ -75,6 +75,11 @@ signatures that specify them.  The common options are:
   If an absolute path (with a leading slash or drive letter) is given
   it is used verbatim.
 
+  .. versionchanged:: 3.31
+    ``<dir>`` will be normalized according to the same
+    :ref:`normalization rules <Normalization>` as the
+    :command:`cmake_path` command.
+
 ``PERMISSIONS <permission>...``
   Specify permissions for installed files.  Valid permissions are
   ``OWNER_READ``, ``OWNER_WRITE``, ``OWNER_EXECUTE``, ``GROUP_READ``,
@@ -396,6 +401,12 @@ Signatures
     If a relative path is specified, it is treated as relative to the
     :genex:`$<INSTALL_PREFIX>`.
 
+    Unlike other ``DESTINATION`` arguments for the various ``install()``
+    subcommands, paths given after ``INCLUDES DESTINATION`` are used as
+    given.  They are not normalized, nor assumed to be normalized, although
+    it is recommended that they are given in normalized form (see
+    :ref:`Normalization`).
+
   ``RUNTIME_DEPENDENCY_SET <set-name>``
     .. versionadded:: 3.21
 
@@ -807,6 +818,7 @@ Signatures
   the generated file will be called ``<export-name>.cmake`` but the ``FILE``
   option may be used to specify a different name.  The value given to
   the ``FILE`` option must be a file name with the ``.cmake`` extension.
+
   If a ``CONFIGURATIONS`` option is given then the file will only be installed
   when one of the named configurations is installed.  Additionally, the
   generated import file will reference only the matching target

+ 1 - 0
Help/manual/cmake-policies.7.rst

@@ -57,6 +57,7 @@ Policies Introduced by CMake 3.31
 .. toctree::
    :maxdepth: 1
 
+   CMP0177: install() DESTINATION paths are normalized. </policy/CMP0177>
    CMP0176: execute_process() ENCODING is UTF-8 by default. </policy/CMP0176>
    CMP0175: add_custom_command() rejects invalid arguments. </policy/CMP0175>
    CMP0174: cmake_parse_arguments(PARSE_ARGV) defines a variable for an empty string after a single-value keyword. </policy/CMP0174>

+ 38 - 0
Help/policy/CMP0177.rst

@@ -0,0 +1,38 @@
+CMP0177
+-------
+
+.. versionadded:: 3.31
+
+:command:`install` ``DESTINATION`` paths are normalized.
+
+The :command:`install` command has a number of different forms, and most of
+them take a ``DESTINATION`` keyword, some in more than one place.
+CMake 3.30 and earlier used the value given after the ``DESTINATION`` keyword
+as provided with no transformations.  The :command:`install(EXPORT)` form
+assumes the path contains no ``..`` or ``.`` path components when computing
+a path relative to the ``DESTINATION``, and if the project provided a path
+that violated that assumption, the computed path would be incorrect.
+
+CMake 3.31 normalizes all ``DESTINATION`` values given in any form of the
+:command:`install` command, except for the ``INCLUDES DESTINATION`` of the
+:command:`install(TARGETS)` form.  The normalization performed is the same
+as for the :command:`cmake_path` command (see :ref:`Normalization`).
+
+The ``OLD`` behavior of this policy performs no translation on the
+``DESTINATION`` values of any :command:`install` command.  They are used
+exactly as provided.  If a destination path contains ``..`` or ``.`` path
+components, :command:`install(EXPORT)` will use the same wrong paths as
+CMake 3.30 and earlier.
+
+The ``NEW`` behavior will normalize all ``DESTINATION`` values except for
+``INCLUDES DESTINATION``.  If a destination path contains a generator
+expression, it will be wrapped in a ``$<PATH:CMAKE_PATH,NORMALIZE,...>``
+generator expression.
+
+This policy was introduced in CMake version 3.31.
+It may be set by :command:`cmake_policy` or :command:`cmake_minimum_required`.
+If it is not set, CMake will warn if it detects a path that would be different
+if normalized, and uses ``OLD`` behavior.  If a destination path contains a
+generator expression, no such warning will be issued regardless of the value.
+
+.. include:: DEPRECATED.txt

+ 6 - 0
Help/release/dev/normalize-install-destination-paths.rst

@@ -0,0 +1,6 @@
+normalize-install-destination-paths
+-----------------------------------
+
+* All ``DESTINATION`` arguments in :command:`install` commands
+  are now :ref:`normalized <Normalization>`, with the exception
+  of ``INCLUDES DESTINATION`` arguments in the ``TARGETS`` form.

+ 81 - 33
Source/cmInstallCommand.cxx

@@ -20,6 +20,7 @@
 
 #include "cmArgumentParser.h"
 #include "cmArgumentParserTypes.h"
+#include "cmCMakePath.h"
 #include "cmExecutionStatus.h"
 #include "cmExperimental.h"
 #include "cmExportSet.h"
@@ -453,7 +454,8 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
     runtimeDependenciesArgVector;
   std::string runtimeDependencySetArg;
   std::vector<std::string> unknownArgs;
-  cmInstallCommandArguments genericArgs(helper.DefaultComponentName);
+  cmInstallCommandArguments genericArgs(helper.DefaultComponentName,
+                                        *helper.Makefile);
   genericArgs.Bind("TARGETS"_s, targetList);
   genericArgs.Bind("EXPORT"_s, exports);
   genericArgs.Bind("RUNTIME_DEPENDENCIES"_s, runtimeDependenciesArgVector);
@@ -467,19 +469,31 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
                                          &unknownArgs)
     : RuntimeDependenciesArgs();
 
-  cmInstallCommandArguments archiveArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments libraryArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments runtimeArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments objectArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments frameworkArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments bundleArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments privateHeaderArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments publicHeaderArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments resourceArgs(helper.DefaultComponentName);
+  cmInstallCommandArguments archiveArgs(helper.DefaultComponentName,
+                                        *helper.Makefile);
+  cmInstallCommandArguments libraryArgs(helper.DefaultComponentName,
+                                        *helper.Makefile);
+  cmInstallCommandArguments runtimeArgs(helper.DefaultComponentName,
+                                        *helper.Makefile);
+  cmInstallCommandArguments objectArgs(helper.DefaultComponentName,
+                                       *helper.Makefile);
+  cmInstallCommandArguments frameworkArgs(helper.DefaultComponentName,
+                                          *helper.Makefile);
+  cmInstallCommandArguments bundleArgs(helper.DefaultComponentName,
+                                       *helper.Makefile);
+  cmInstallCommandArguments privateHeaderArgs(helper.DefaultComponentName,
+                                              *helper.Makefile);
+  cmInstallCommandArguments publicHeaderArgs(helper.DefaultComponentName,
+                                             *helper.Makefile);
+  cmInstallCommandArguments resourceArgs(helper.DefaultComponentName,
+                                         *helper.Makefile);
   cmInstallCommandIncludesArgument includesArgs;
   std::vector<cmInstallCommandFileSetArguments> fileSetArgs(
-    argVectors.FileSets.size(), { helper.DefaultComponentName });
-  cmInstallCommandArguments cxxModuleBmiArgs(helper.DefaultComponentName);
+    argVectors.FileSets.size(),
+    { cmInstallCommandFileSetArguments(helper.DefaultComponentName,
+                                       *helper.Makefile) });
+  cmInstallCommandArguments cxxModuleBmiArgs(helper.DefaultComponentName,
+                                             *helper.Makefile);
 
   // now parse the args for specific parts of the target (e.g. LIBRARY,
   // RUNTIME, ARCHIVE etc.
@@ -499,7 +513,8 @@ bool HandleTargetsMode(std::vector<std::string> const& args,
     // cmArgumentParser<void>::Bind() binds to a specific address, but the
     // objects in the vector can move around. So we parse in an object with a
     // fixed address and then copy the data into the vector.
-    cmInstallCommandFileSetArguments fileSetArg(helper.DefaultComponentName);
+    cmInstallCommandFileSetArguments fileSetArg(helper.DefaultComponentName,
+                                                *helper.Makefile);
     fileSetArg.Parse(argVectors.FileSets[i], &unknownArgs);
     fileSetArgs[i] = std::move(fileSetArg);
   }
@@ -1310,16 +1325,21 @@ bool HandleImportedRuntimeArtifactsMode(std::vector<std::string> const& args,
   ArgumentParser::MaybeEmpty<std::vector<std::string>> targetList;
   std::string runtimeDependencySetArg;
   std::vector<std::string> unknownArgs;
-  cmInstallCommandArguments genericArgs(helper.DefaultComponentName);
+  cmInstallCommandArguments genericArgs(helper.DefaultComponentName,
+                                        *helper.Makefile);
   genericArgs.Bind("IMPORTED_RUNTIME_ARTIFACTS"_s, targetList)
     .Bind("RUNTIME_DEPENDENCY_SET"_s, runtimeDependencySetArg);
   genericArgs.Parse(genericArgVector, &unknownArgs);
   bool success = genericArgs.Finalize();
 
-  cmInstallCommandArguments libraryArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments runtimeArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments frameworkArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments bundleArgs(helper.DefaultComponentName);
+  cmInstallCommandArguments libraryArgs(helper.DefaultComponentName,
+                                        *helper.Makefile);
+  cmInstallCommandArguments runtimeArgs(helper.DefaultComponentName,
+                                        *helper.Makefile);
+  cmInstallCommandArguments frameworkArgs(helper.DefaultComponentName,
+                                          *helper.Makefile);
+  cmInstallCommandArguments bundleArgs(helper.DefaultComponentName,
+                                       *helper.Makefile);
 
   // now parse the args for specific parts of the target (e.g. LIBRARY,
   // RUNTIME etc.
@@ -1547,7 +1567,7 @@ bool HandleFilesMode(std::vector<std::string> const& args,
 
   // This is the FILES mode.
   bool programs = (args[0] == "PROGRAMS");
-  cmInstallCommandArguments ica(helper.DefaultComponentName);
+  cmInstallCommandArguments ica(helper.DefaultComponentName, *helper.Makefile);
   ArgumentParser::MaybeEmpty<std::vector<std::string>> files;
   ica.Bind(programs ? "PROGRAMS"_s : "FILES"_s, files);
   std::vector<std::string> unknownArgs;
@@ -1681,7 +1701,7 @@ bool HandleDirectoryMode(std::vector<std::string> const& args,
   bool exclude_from_all = false;
   bool message_never = false;
   std::vector<std::string> dirs;
-  const std::string* destination = nullptr;
+  cm::optional<std::string> destination;
   std::string permissions_file;
   std::string permissions_dir;
   std::vector<std::string> configurations;
@@ -1839,7 +1859,33 @@ bool HandleDirectoryMode(std::vector<std::string> const& args,
     } else if (doing == DoingConfigurations) {
       configurations.push_back(args[i]);
     } else if (doing == DoingDestination) {
-      destination = &args[i];
+      // A trailing slash is meaningful for this form, but normalization
+      // preserves it if present
+      switch (status.GetMakefile().GetPolicyStatus(cmPolicies::CMP0177)) {
+        case cmPolicies::NEW:
+          destination = cmCMakePath(args[i]).Normal().String();
+          break;
+        case cmPolicies::WARN:
+          // We can't be certain if a warning is appropriate if there are any
+          // generator expressions
+          if (cmGeneratorExpression::Find(args[i]) == cm::string_view::npos &&
+              args[i] != cmCMakePath(args[i]).Normal().String()) {
+            status.GetMakefile().IssueMessage(
+              MessageType::AUTHOR_WARNING,
+              cmPolicies::GetPolicyWarning(cmPolicies::CMP0177));
+          }
+          CM_FALLTHROUGH;
+        case cmPolicies::OLD:
+          destination = args[i];
+          break;
+        case cmPolicies::REQUIRED_ALWAYS:
+        case cmPolicies::REQUIRED_IF_USED:
+          // We should never get here, only OLD, WARN, and NEW are used
+          status.GetMakefile().IssueMessage(
+            MessageType::FATAL_ERROR,
+            cmPolicies::GetRequiredPolicyError(cmPolicies::CMP0177));
+          return false;
+      }
       doing = DoingNone;
     } else if (doing == DoingType) {
       if (allowedTypes.count(args[i]) == 0) {
@@ -1909,7 +1955,7 @@ bool HandleDirectoryMode(std::vector<std::string> const& args,
   }
 
   // Support installing an empty directory.
-  if (dirs.empty() && destination) {
+  if (dirs.empty() && destination.has_value()) {
     dirs.emplace_back();
   }
 
@@ -1917,15 +1963,13 @@ bool HandleDirectoryMode(std::vector<std::string> const& args,
   if (dirs.empty()) {
     return true;
   }
-  std::string destinationStr;
-  if (!destination) {
+  if (!destination.has_value()) {
     if (type.empty()) {
       // A destination is required.
       status.SetError(cmStrCat(args[0], " given no DESTINATION!"));
       return false;
     }
-    destinationStr = helper.GetDestinationForType(nullptr, type);
-    destination = &destinationStr;
+    destination = helper.GetDestinationForType(nullptr, type);
   } else if (!type.empty()) {
     status.SetError(cmStrCat(args[0],
                              " given both TYPE and DESTINATION "
@@ -1957,7 +2001,7 @@ bool HandleExportAndroidMKMode(std::vector<std::string> const& args,
   Helper helper(status);
 
   // This is the EXPORT mode.
-  cmInstallCommandArguments ica(helper.DefaultComponentName);
+  cmInstallCommandArguments ica(helper.DefaultComponentName, *helper.Makefile);
 
   std::string exp;
   std::string name_space;
@@ -2050,7 +2094,7 @@ bool HandleExportMode(std::vector<std::string> const& args,
   Helper helper(status);
 
   // This is the EXPORT mode.
-  cmInstallCommandArguments ica(helper.DefaultComponentName);
+  cmInstallCommandArguments ica(helper.DefaultComponentName, *helper.Makefile);
 
   std::string exp;
   std::string name_space;
@@ -2176,7 +2220,7 @@ bool HandlePackageInfoMode(std::vector<std::string> const& args,
   Helper helper(status);
 
   // This is the PACKAGE_INFO mode.
-  cmInstallCommandArguments ica(helper.DefaultComponentName);
+  cmInstallCommandArguments ica(helper.DefaultComponentName, *helper.Makefile);
 
   ArgumentParser::NonEmpty<std::string> pkg;
   ArgumentParser::NonEmpty<std::string> appendix;
@@ -2335,14 +2379,18 @@ bool HandleRuntimeDependencySetMode(std::vector<std::string> const& args,
   // These generic args also contain the runtime dependency set
   std::string runtimeDependencySetArg;
   std::vector<std::string> runtimeDependencyArgVector;
-  cmInstallCommandArguments genericArgs(helper.DefaultComponentName);
+  cmInstallCommandArguments genericArgs(helper.DefaultComponentName,
+                                        *helper.Makefile);
   genericArgs.Bind("RUNTIME_DEPENDENCY_SET"_s, runtimeDependencySetArg);
   genericArgs.Parse(genericArgVector, &runtimeDependencyArgVector);
   bool success = genericArgs.Finalize();
 
-  cmInstallCommandArguments libraryArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments runtimeArgs(helper.DefaultComponentName);
-  cmInstallCommandArguments frameworkArgs(helper.DefaultComponentName);
+  cmInstallCommandArguments libraryArgs(helper.DefaultComponentName,
+                                        *helper.Makefile);
+  cmInstallCommandArguments runtimeArgs(helper.DefaultComponentName,
+                                        *helper.Makefile);
+  cmInstallCommandArguments frameworkArgs(helper.DefaultComponentName,
+                                          *helper.Makefile);
 
   // Now also parse the file(GET_RUNTIME_DEPENDENCY) args
   std::vector<std::string> unknownArgs;

+ 55 - 4
Source/cmInstallCommandArguments.cxx

@@ -3,11 +3,19 @@
 #include "cmInstallCommandArguments.h"
 
 #include <algorithm>
+#include <functional>
 #include <utility>
 
+#include <cm/string_view>
 #include <cmext/string_view>
 
+#include "cmCMakePath.h"
+#include "cmGeneratorExpression.h"
+#include "cmMakefile.h"
+#include "cmMessageType.h"
+#include "cmPolicies.h"
 #include "cmRange.h"
+#include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
 
 // Table of valid permissions.
@@ -20,10 +28,53 @@ const char* cmInstallCommandArguments::PermissionsTable[] = {
 const std::string cmInstallCommandArguments::EmptyString;
 
 cmInstallCommandArguments::cmInstallCommandArguments(
-  std::string defaultComponent)
+  std::string defaultComponent, cmMakefile& makefile)
   : DefaultComponentName(std::move(defaultComponent))
 {
-  this->Bind("DESTINATION"_s, this->Destination);
+  std::function<ArgumentParser::Continue(cm::string_view)> normalizeDest;
+
+  switch (makefile.GetPolicyStatus(cmPolicies::CMP0177)) {
+    case cmPolicies::OLD:
+      normalizeDest = [this](cm::string_view arg) -> ArgumentParser::Continue {
+        this->Destination = std::string(arg.begin(), arg.end());
+        return ArgumentParser::Continue::Yes;
+      };
+      break;
+    case cmPolicies::WARN:
+      normalizeDest =
+        [this, &makefile](cm::string_view arg) -> ArgumentParser::Continue {
+        this->Destination = std::string(arg.begin(), arg.end());
+        // We can't be certain if a warning is appropriate if there are any
+        // generator expressions
+        if (cmGeneratorExpression::Find(arg) == cm::string_view::npos &&
+            arg != cmCMakePath(arg).Normal().String()) {
+          makefile.IssueMessage(
+            MessageType::AUTHOR_WARNING,
+            cmPolicies::GetPolicyWarning(cmPolicies::CMP0177));
+        }
+        return ArgumentParser::Continue::Yes;
+      };
+      break;
+    case cmPolicies::NEW:
+      normalizeDest = [this](cm::string_view arg) -> ArgumentParser::Continue {
+        if (cmGeneratorExpression::Find(arg) == cm::string_view::npos) {
+          this->Destination = cmCMakePath(arg).Normal().String();
+        } else {
+          this->Destination =
+            cmStrCat("$<PATH:CMAKE_PATH,NORMALIZE,", arg, '>');
+        }
+        return ArgumentParser::Continue::Yes;
+      };
+      break;
+    case cmPolicies::REQUIRED_ALWAYS:
+    case cmPolicies::REQUIRED_IF_USED:
+      // We should never get here, only OLD, WARN, and NEW are used
+      makefile.IssueMessage(
+        MessageType::FATAL_ERROR,
+        cmPolicies::GetRequiredPolicyError(cmPolicies::CMP0177));
+  }
+
+  this->Bind("DESTINATION"_s, normalizeDest);
   this->Bind("COMPONENT"_s, this->Component);
   this->Bind("NAMELINK_COMPONENT"_s, this->NamelinkComponent);
   this->Bind("EXCLUDE_FROM_ALL"_s, this->ExcludeFromAll);
@@ -227,8 +278,8 @@ void cmInstallCommandIncludesArgument::Parse(
 }
 
 cmInstallCommandFileSetArguments::cmInstallCommandFileSetArguments(
-  std::string defaultComponent)
-  : cmInstallCommandArguments(std::move(defaultComponent))
+  std::string defaultComponent, cmMakefile& makefile)
+  : cmInstallCommandArguments(std::move(defaultComponent), makefile)
 {
   this->Bind("FILE_SET"_s, this->FileSet);
 }

+ 6 - 2
Source/cmInstallCommandArguments.h

@@ -10,10 +10,13 @@
 #include "cmArgumentParser.h"
 #include "cmArgumentParserTypes.h"
 
+class cmMakefile;
+
 class cmInstallCommandArguments : public cmArgumentParser<void>
 {
 public:
-  cmInstallCommandArguments(std::string defaultComponent);
+  cmInstallCommandArguments(std::string defaultComponent,
+                            cmMakefile& makefile);
   void SetGenericArguments(cmInstallCommandArguments* args)
   {
     this->GenericArguments = args;
@@ -78,7 +81,8 @@ private:
 class cmInstallCommandFileSetArguments : public cmInstallCommandArguments
 {
 public:
-  cmInstallCommandFileSetArguments(std::string defaultComponent);
+  cmInstallCommandFileSetArguments(std::string defaultComponent,
+                                   cmMakefile& makefile);
 
   void Parse(std::vector<std::string> args,
              std::vector<std::string>* unconsumedArgs);

+ 3 - 1
Source/cmPolicies.h

@@ -541,7 +541,9 @@ class cmMakefile;
   SELECT(POLICY, CMP0175, "add_custom_command() rejects invalid arguments.",  \
          3, 31, 0, cmPolicies::WARN)                                          \
   SELECT(POLICY, CMP0176, "execute_process() ENCODING is UTF-8 by default.",  \
-         3, 31, 0, cmPolicies::WARN)
+         3, 31, 0, cmPolicies::WARN)                                          \
+  SELECT(POLICY, CMP0177, "install() DESTINATION paths are normalized.", 3,   \
+         31, 0, cmPolicies::WARN)
 
 #define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)
 #define CM_FOR_EACH_POLICY_ID(POLICY)                                         \

+ 36 - 0
Tests/RunCMake/install/CMP0177-NEW-all-check.cmake

@@ -0,0 +1,36 @@
+set(installBase ${RunCMake_TEST_BINARY_DIR}/root-all)
+
+foreach(i RANGE 1 5)
+  set(subdir shouldNotRemain${i})
+  if(IS_DIRECTORY ${installBase}/${subdir})
+    set(RunCMake_TEST_FAILED "Check failed.")
+    string(APPEND RunCMake_TEST_FAILURE_MESSAGE
+      "\nUnexpectedly created install path that should have disappeared with "
+      "normalization:\n"
+      "  ${installBase}/${subdir}"
+    )
+  endif()
+endforeach()
+
+file(GLOB perConfigFiles ${installBase}/lib/cmake/pkg/pkg-config-*.cmake)
+foreach(file IN LISTS perConfigFiles ITEMS ${installBase}/lib/cmake/pkg/pkg-config.cmake)
+  file(STRINGS ${file} matches REGEX shouldNotRemain)
+  if(NOT matches STREQUAL "")
+    set(RunCMake_TEST_FAILED "Check failed.")
+    string(APPEND RunCMake_TEST_FAILURE_MESSAGE
+      "\nNon-normalized path found in ${file}:"
+    )
+    foreach(match IN LISTS matches)
+      string(APPEND RunCMake_TEST_FAILURE_MESSAGE "\n  ${match}")
+    endforeach()
+  endif()
+endforeach()
+
+if(NOT EXISTS "${installBase}/dirs/dir/empty.txt")
+  set(RunCMake_TEST_FAILED "Check failed.")
+  string(APPEND RunCMake_TEST_FAILURE_MESSAGE
+    "\nNon-normalized DIRECTORY destination not handled correctly. "
+    "Expected to find the following file, but it was missing:\n"
+    "  ${installBase}/dirs/dir/empty.txt"
+  )
+endif()

+ 2 - 0
Tests/RunCMake/install/CMP0177-NEW-verify.cmake

@@ -0,0 +1,2 @@
+enable_language(C)
+find_package(pkg REQUIRED)

+ 2 - 0
Tests/RunCMake/install/CMP0177-NEW.cmake

@@ -0,0 +1,2 @@
+cmake_policy(SET CMP0177 NEW)
+include(${CMAKE_CURRENT_LIST_DIR}/CMP0177.cmake)

+ 1 - 0
Tests/RunCMake/install/CMP0177-OLD-verify-result.txt

@@ -0,0 +1 @@
+1

+ 13 - 0
Tests/RunCMake/install/CMP0177-OLD-verify-stderr.txt

@@ -0,0 +1,13 @@
+CMake Error at [^
+]*CMP0177-OLD-build/root-all/lib/cmake/pkg/pkg-config\.cmake:[0-9]+ \(message\):
+  The imported target "foo1" references the file
++     ".*/shouldNotRemain1/\.\./lib/(libfoo1\.a|foo1\.l(ib)?)"
++  but this file does not exist\.  Possible reasons include:
++  \* The file was deleted, renamed, or moved to another location\.
++  \* An install or uninstall procedure did not complete successfully\.
++  \* The installation package was faulty and contained
++     ".*/Tests/RunCMake/install/CMP0177-OLD-build/root-all/lib/cmake/pkg/pkg-config\.cmake"
++  but not all the files it references\.
++Call Stack \(most recent call first\):
+  CMP0177-OLD-verify\.cmake:2 \(find_package\)
+  CMakeLists\.txt:3 \(include\)

+ 2 - 0
Tests/RunCMake/install/CMP0177-OLD-verify.cmake

@@ -0,0 +1,2 @@
+enable_language(C)
+find_package(pkg REQUIRED)

+ 2 - 0
Tests/RunCMake/install/CMP0177-OLD.cmake

@@ -0,0 +1,2 @@
+cmake_policy(SET CMP0177 OLD)
+include(${CMAKE_CURRENT_LIST_DIR}/CMP0177.cmake)

+ 35 - 0
Tests/RunCMake/install/CMP0177-WARN-stderr.txt

@@ -0,0 +1,35 @@
+CMake Warning \(dev\) at CMP0177\.cmake:[0-9]+ \(install\):
+  Policy CMP0177 is not set: install\(\) DESTINATION paths are normalized\.  Run
+  "cmake --help-policy CMP0177" for policy details\.  Use the cmake_policy
+  command to set the policy and suppress this warning\.
+Call Stack \(most recent call first\):
+  CMP0177-WARN\.cmake:1 \(include\)
+  CMakeLists\.txt:3 \(include\)
+This warning is for project developers\.  Use -Wno-dev to suppress it\.
+
+CMake Warning \(dev\) at CMP0177\.cmake:[0-9]+ \(install\):
+  Policy CMP0177 is not set: install\(\) DESTINATION paths are normalized\.  Run
+  "cmake --help-policy CMP0177" for policy details\.  Use the cmake_policy
+  command to set the policy and suppress this warning\.
+Call Stack \(most recent call first\):
+  CMP0177-WARN\.cmake:1 \(include\)
+  CMakeLists\.txt:3 \(include\)
+This warning is for project developers\.  Use -Wno-dev to suppress it\.
+
+CMake Warning \(dev\) at CMP0177\.cmake:[0-9]+ \(install\):
+  Policy CMP0177 is not set: install\(\) DESTINATION paths are normalized\.  Run
+  "cmake --help-policy CMP0177" for policy details\.  Use the cmake_policy
+  command to set the policy and suppress this warning\.
+Call Stack \(most recent call first\):
+  CMP0177-WARN\.cmake:1 \(include\)
+  CMakeLists\.txt:3 \(include\)
+This warning is for project developers\.  Use -Wno-dev to suppress it\.
+
+CMake Warning \(dev\) at CMP0177\.cmake:[0-9]+ \(install\):
+  Policy CMP0177 is not set: install\(\) DESTINATION paths are normalized\.  Run
+  "cmake --help-policy CMP0177" for policy details\.  Use the cmake_policy
+  command to set the policy and suppress this warning\.
+Call Stack \(most recent call first\):
+  CMP0177-WARN\.cmake:1 \(include\)
+  CMakeLists\.txt:3 \(include\)
+This warning is for project developers\.  Use -Wno-dev to suppress it\.

+ 1 - 0
Tests/RunCMake/install/CMP0177-WARN-verify-result.txt

@@ -0,0 +1 @@
+1

+ 13 - 0
Tests/RunCMake/install/CMP0177-WARN-verify-stderr.txt

@@ -0,0 +1,13 @@
+CMake Error at [^
+]*CMP0177-WARN-build/root-all/lib/cmake/pkg/pkg-config\.cmake:[0-9]+ \(message\):
+  The imported target "foo1" references the file
++     ".*/shouldNotRemain1/\.\./lib/(libfoo1\.a|foo1\.l(ib)?)"
++  but this file does not exist\.  Possible reasons include:
++  \* The file was deleted, renamed, or moved to another location\.
++  \* An install or uninstall procedure did not complete successfully\.
++  \* The installation package was faulty and contained
++     ".*/Tests/RunCMake/install/CMP0177-WARN-build/root-all/lib/cmake/pkg/pkg-config\.cmake"
++  but not all the files it references\.
++Call Stack \(most recent call first\):
+  CMP0177-WARN-verify\.cmake:2 \(find_package\)
+  CMakeLists\.txt:3 \(include\)

+ 2 - 0
Tests/RunCMake/install/CMP0177-WARN-verify.cmake

@@ -0,0 +1,2 @@
+enable_language(C)
+find_package(pkg REQUIRED)

+ 1 - 0
Tests/RunCMake/install/CMP0177-WARN.cmake

@@ -0,0 +1 @@
+include(${CMAKE_CURRENT_LIST_DIR}/CMP0177.cmake)

+ 29 - 0
Tests/RunCMake/install/CMP0177.cmake

@@ -0,0 +1,29 @@
+enable_language(C)
+
+add_library(foo1 STATIC obj1.c)
+add_library(foo2 STATIC obj2.c)
+
+set_target_properties(foo2 PROPERTIES HIDDEN_DOUBLE_DOT "..")
+
+# All the shouldNotRemainX path components below should be normalized out when
+# CMP0177 is set to NEW, and retained for OLD and WARN.
+
+install(TARGETS foo1
+  EXPORT pkg
+  ARCHIVE DESTINATION shouldNotRemain1/../lib
+)
+install(TARGETS foo2
+  EXPORT pkg
+  ARCHIVE DESTINATION shouldNotRemain2/$<TARGET_PROPERTY:foo2,HIDDEN_DOUBLE_DOT>/lib
+)
+install(EXPORT pkg
+  DESTINATION shouldNotRemain3/deeper/../.././lib/cmake/pkg
+  FILE pkg-config.cmake
+)
+install(FILES obj1.c
+  DESTINATION shouldNotRemain4/anotherSubdir/../../files
+)
+install(DIRECTORY dir
+  # Trailing slash here is significant
+  DESTINATION shouldNotRemain5/../dirs/more/../
+)

+ 6 - 0
Tests/RunCMake/install/RunCMakeTest.cmake

@@ -94,6 +94,12 @@ run_cmake(CMP0062-WARN)
 run_cmake(CMP0087-OLD)
 run_cmake(CMP0087-NEW)
 run_cmake(CMP0087-WARN)
+foreach(policy IN ITEMS NEW OLD WARN)
+  run_install_test(CMP0177-${policy})
+  run_cmake_with_options(CMP0177-${policy}-verify
+    -DCMAKE_PREFIX_PATH=${RunCMake_BINARY_DIR}/CMP0177-${policy}-build/root-all
+  )
+endforeach()
 run_cmake(TARGETS-ImportedGlobal)
 run_cmake(TARGETS-NAMELINK_COMPONENT-bad-all)
 run_cmake(TARGETS-NAMELINK_COMPONENT-bad-exc)