Browse Source

Merge topic 'normalize-install-destination-paths'

6a1fac1450 install: Normalize DESTINATION paths
2184fcfb00 Tests: Configure RunCMake.install cases with correct build type
5a8a6dfe81 cmGeneratorExpression: Change Find() parameter type to cm::string_view
d810374b3d install(PACKAGE_INFO): Remove outdated TODO comment
d13ed01d54 Tests: Remove unused files from RunCMake.install

Acked-by: Kitware Robot <[email protected]>
Merge-request: !9800
Brad King 1 year ago
parent
commit
fdceee50e6

+ 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
 
@@ -815,6 +826,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.

+ 6 - 5
Source/cmGeneratorExpression.cxx

@@ -375,14 +375,15 @@ std::string cmGeneratorExpression::Preprocess(const std::string& input,
   return std::string();
 }
 
-std::string::size_type cmGeneratorExpression::Find(const std::string& input)
+cm::string_view::size_type cmGeneratorExpression::Find(
+  const cm::string_view& input)
 {
-  const std::string::size_type openpos = input.find("$<");
-  if (openpos != std::string::npos &&
-      input.find('>', openpos) != std::string::npos) {
+  const cm::string_view::size_type openpos = input.find("$<");
+  if (openpos != cm::string_view::npos &&
+      input.find('>', openpos) != cm::string_view::npos) {
     return openpos;
   }
-  return std::string::npos;
+  return cm::string_view::npos;
 }
 
 bool cmGeneratorExpression::IsValidTargetName(const std::string& input)

+ 1 - 1
Source/cmGeneratorExpression.h

@@ -66,7 +66,7 @@ public:
   static void Split(const std::string& input,
                     std::vector<std::string>& output);
 
-  static std::string::size_type Find(const std::string& input);
+  static cm::string_view::size_type Find(const cm::string_view& input);
 
   static bool IsValidTargetName(const std::string& input);
 

+ 81 - 34
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"
@@ -455,7 +456,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);
@@ -469,19 +471,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.
@@ -501,7 +515,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);
   }
@@ -1312,16 +1327,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.
@@ -1549,7 +1569,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;
@@ -1683,7 +1703,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;
@@ -1841,7 +1861,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) {
@@ -1911,7 +1957,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();
   }
 
@@ -1919,15 +1965,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 "
@@ -1959,7 +2003,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;
@@ -2052,7 +2096,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;
@@ -2178,7 +2222,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;
@@ -2191,7 +2235,6 @@ bool HandlePackageInfoMode(std::vector<std::string> const& args,
   ArgumentParser::NonEmpty<std::vector<std::string>> defaultConfigs;
   ArgumentParser::NonEmpty<std::string> cxxModulesDirectory;
 
-  // TODO: Support DESTINATION.
   ica.Bind("PACKAGE_INFO"_s, pkg);
   ica.Bind("EXPORT"_s, exportName);
   ica.Bind("APPENDIX"_s, appendix);
@@ -2338,14 +2381,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/../
+)

+ 0 - 5
Tests/RunCMake/install/EXPORT-TargetTwice-pkg1.txt

@@ -1,5 +0,0 @@
-.+
-set_target_properties\(pkg1::foo PROPERTIES
-.+INTERFACE_INCLUDE_DIRECTORIES "\${_IMPORT_PREFIX}/pkg1/inc"
-\)
-.+

+ 0 - 5
Tests/RunCMake/install/EXPORT-TargetTwice-pkg2.txt

@@ -1,5 +0,0 @@
-.+
-set_target_properties\(pkg2::foo PROPERTIES
-.+INTERFACE_INCLUDE_DIRECTORIES "\${_IMPORT_PREFIX}/pkg2/inc"
-\)
-.+

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

@@ -7,7 +7,9 @@ function(run_install_test case)
   set(RunCMake_TEST_NO_CLEAN 1)
   file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
   file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
+  set(RunCMake_TEST_RAW_ARGS -DCMAKE_BUILD_TYPE:STRING=Debug)
   run_cmake(${case})
+  unset(RunCMake_TEST_RAW_ARGS)
   set(RunCMake_TEST_OUTPUT_MERGE 1)
   run_cmake_command(${case}-build ${CMAKE_COMMAND} --build . --config Debug)
   unset(RunCMake_TEST_OUTPUT_MERGE)
@@ -92,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)