Pārlūkot izejas kodu

cmFileCommand: Factor out cmFileCopier and cmFileInstaller

Split these classes out into their own sources.
Bryon Bean 6 gadi atpakaļ
vecāks
revīzija
e2e8f6b132

+ 4 - 0
Source/CMakeLists.txt

@@ -224,6 +224,10 @@ set(SRCS
   cmFileAPICodemodel.h
   cmFileAPICMakeFiles.cxx
   cmFileAPICMakeFiles.h
+  cmFileCopier.cxx
+  cmFileCopier.h
+  cmFileInstaller.cxx
+  cmFileInstaller.h
   cmFileLock.cxx
   cmFileLock.h
   cmFileLockPool.cxx

+ 2 - 1080
Source/cmFileCommand.cxx

@@ -3,7 +3,6 @@
 #include "cmFileCommand.h"
 
 #include "cm_kwiml.h"
-#include "cmsys/Directory.hxx"
 #include "cmsys/FStream.hxx"
 #include "cmsys/Glob.hxx"
 #include "cmsys/RegularExpression.hxx"
@@ -16,20 +15,18 @@
 #include <sstream>
 #include <stdio.h>
 #include <stdlib.h>
-#include <string.h>
 #include <utility>
 #include <vector>
 
 #include "cmAlgorithms.h"
 #include "cmCommandArgumentsHelper.h"
 #include "cmCryptoHash.h"
-#include "cmFSPermissions.h"
+#include "cmFileCopier.h"
+#include "cmFileInstaller.h"
 #include "cmFileLockPool.h"
-#include "cmFileTimeComparison.h"
 #include "cmGeneratorExpression.h"
 #include "cmGlobalGenerator.h"
 #include "cmHexFileConverter.h"
-#include "cmInstallType.h"
 #include "cmListFileCache.h"
 #include "cmMakefile.h"
 #include "cmMessageType.h"
@@ -56,8 +53,6 @@
 
 class cmSystemToolsFileTime;
 
-using namespace cmFSPermissions;
-
 #if defined(_WIN32)
 // libcurl doesn't support file:// urls for unicode filenames on Windows.
 // Convert string from UTF-8 to ACP if this is a file:// URL.
@@ -1058,1085 +1053,12 @@ bool cmFileCommand::HandleDifferentCommand(
   return true;
 }
 
-// File installation helper class.
-struct cmFileCopier
-{
-  cmFileCopier(cmFileCommand* command, const char* name = "COPY")
-    : FileCommand(command)
-    , Makefile(command->GetMakefile())
-    , Name(name)
-    , Always(false)
-    , MatchlessFiles(true)
-    , FilePermissions(0)
-    , DirPermissions(0)
-    , CurrentMatchRule(nullptr)
-    , UseGivenPermissionsFile(false)
-    , UseGivenPermissionsDir(false)
-    , UseSourcePermissions(true)
-    , Doing(DoingNone)
-  {
-  }
-  virtual ~cmFileCopier() = default;
-
-  bool Run(std::vector<std::string> const& args);
-
-protected:
-  cmFileCommand* FileCommand;
-  cmMakefile* Makefile;
-  const char* Name;
-  bool Always;
-  cmFileTimeComparison FileTimes;
-
-  // Whether to install a file not matching any expression.
-  bool MatchlessFiles;
-
-  // Permissions for files and directories installed by this object.
-  mode_t FilePermissions;
-  mode_t DirPermissions;
-
-  // Properties set by pattern and regex match rules.
-  struct MatchProperties
-  {
-    bool Exclude = false;
-    mode_t Permissions = 0;
-  };
-  struct MatchRule
-  {
-    cmsys::RegularExpression Regex;
-    MatchProperties Properties;
-    std::string RegexString;
-    MatchRule(std::string const& regex)
-      : Regex(regex)
-      , RegexString(regex)
-    {
-    }
-  };
-  std::vector<MatchRule> MatchRules;
-
-  // Get the properties from rules matching this input file.
-  MatchProperties CollectMatchProperties(const std::string& file)
-  {
-// Match rules are case-insensitive on some platforms.
-#if defined(_WIN32) || defined(__APPLE__) || defined(__CYGWIN__)
-    const std::string file_to_match = cmSystemTools::LowerCase(file);
-#else
-    const std::string& file_to_match = file;
-#endif
-
-    // Collect properties from all matching rules.
-    bool matched = false;
-    MatchProperties result;
-    for (MatchRule& mr : this->MatchRules) {
-      if (mr.Regex.find(file_to_match)) {
-        matched = true;
-        result.Exclude |= mr.Properties.Exclude;
-        result.Permissions |= mr.Properties.Permissions;
-      }
-    }
-    if (!matched && !this->MatchlessFiles) {
-      result.Exclude = !cmSystemTools::FileIsDirectory(file);
-    }
-    return result;
-  }
-
-  bool SetPermissions(const std::string& toFile, mode_t permissions)
-  {
-    if (permissions) {
-#ifdef WIN32
-      if (Makefile->IsOn("CMAKE_CROSSCOMPILING")) {
-        // Store the mode in an NTFS alternate stream.
-        std::string mode_t_adt_filename = toFile + ":cmake_mode_t";
-
-        // Writing to an NTFS alternate stream changes the modification
-        // time, so we need to save and restore its original value.
-        cmSystemToolsFileTime* file_time_orig = cmSystemTools::FileTimeNew();
-        cmSystemTools::FileTimeGet(toFile, file_time_orig);
-
-        cmsys::ofstream permissionStream(mode_t_adt_filename.c_str());
-
-        if (permissionStream) {
-          permissionStream << std::oct << permissions << std::endl;
-        }
-
-        permissionStream.close();
-
-        cmSystemTools::FileTimeSet(toFile, file_time_orig);
-
-        cmSystemTools::FileTimeDelete(file_time_orig);
-      }
-#endif
-
-      if (!cmSystemTools::SetPermissions(toFile, permissions)) {
-        std::ostringstream e;
-        e << this->Name << " cannot set permissions on \"" << toFile << "\"";
-        this->FileCommand->SetError(e.str());
-        return false;
-      }
-    }
-    return true;
-  }
-
-  // Translate an argument to a permissions bit.
-  bool CheckPermissions(std::string const& arg, mode_t& permissions)
-  {
-    if (!cmFSPermissions::stringToModeT(arg, permissions)) {
-      std::ostringstream e;
-      e << this->Name << " given invalid permission \"" << arg << "\".";
-      this->FileCommand->SetError(e.str());
-      return false;
-    }
-    return true;
-  }
-
-  bool InstallSymlink(const std::string& fromFile, const std::string& toFile);
-  bool InstallFile(const std::string& fromFile, const std::string& toFile,
-                   MatchProperties match_properties);
-  bool InstallDirectory(const std::string& source,
-                        const std::string& destination,
-                        MatchProperties match_properties);
-  virtual bool Install(const std::string& fromFile, const std::string& toFile);
-  virtual std::string const& ToName(std::string const& fromName)
-  {
-    return fromName;
-  }
-
-  enum Type
-  {
-    TypeFile,
-    TypeDir,
-    TypeLink
-  };
-  virtual void ReportCopy(const std::string&, Type, bool) {}
-  virtual bool ReportMissing(const std::string& fromFile)
-  {
-    // The input file does not exist and installation is not optional.
-    std::ostringstream e;
-    e << this->Name << " cannot find \"" << fromFile << "\".";
-    this->FileCommand->SetError(e.str());
-    return false;
-  }
-
-  MatchRule* CurrentMatchRule;
-  bool UseGivenPermissionsFile;
-  bool UseGivenPermissionsDir;
-  bool UseSourcePermissions;
-  std::string Destination;
-  std::string FilesFromDir;
-  std::vector<std::string> Files;
-  int Doing;
-
-  virtual bool Parse(std::vector<std::string> const& args);
-  enum
-  {
-    DoingNone,
-    DoingError,
-    DoingDestination,
-    DoingFilesFromDir,
-    DoingFiles,
-    DoingPattern,
-    DoingRegex,
-    DoingPermissionsFile,
-    DoingPermissionsDir,
-    DoingPermissionsMatch,
-    DoingLast1
-  };
-  virtual bool CheckKeyword(std::string const& arg);
-  virtual bool CheckValue(std::string const& arg);
-
-  void NotBeforeMatch(std::string const& arg)
-  {
-    std::ostringstream e;
-    e << "option " << arg << " may not appear before PATTERN or REGEX.";
-    this->FileCommand->SetError(e.str());
-    this->Doing = DoingError;
-  }
-  void NotAfterMatch(std::string const& arg)
-  {
-    std::ostringstream e;
-    e << "option " << arg << " may not appear after PATTERN or REGEX.";
-    this->FileCommand->SetError(e.str());
-    this->Doing = DoingError;
-  }
-  virtual void DefaultFilePermissions()
-  {
-    // Use read/write permissions.
-    this->FilePermissions = 0;
-    this->FilePermissions |= mode_owner_read;
-    this->FilePermissions |= mode_owner_write;
-    this->FilePermissions |= mode_group_read;
-    this->FilePermissions |= mode_world_read;
-  }
-  virtual void DefaultDirectoryPermissions()
-  {
-    // Use read/write/executable permissions.
-    this->DirPermissions = 0;
-    this->DirPermissions |= mode_owner_read;
-    this->DirPermissions |= mode_owner_write;
-    this->DirPermissions |= mode_owner_execute;
-    this->DirPermissions |= mode_group_read;
-    this->DirPermissions |= mode_group_execute;
-    this->DirPermissions |= mode_world_read;
-    this->DirPermissions |= mode_world_execute;
-  }
-
-  bool GetDefaultDirectoryPermissions(mode_t** mode)
-  {
-    // check if default dir creation permissions were set
-    const char* default_dir_install_permissions =
-      this->Makefile->GetDefinition(
-        "CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS");
-    if (default_dir_install_permissions && *default_dir_install_permissions) {
-      std::vector<std::string> items;
-      cmSystemTools::ExpandListArgument(default_dir_install_permissions,
-                                        items);
-      for (const auto& arg : items) {
-        if (!this->CheckPermissions(arg, **mode)) {
-          std::ostringstream e;
-          e << this->FileCommand->GetError()
-            << " Set with CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS "
-               "variable.";
-          this->FileCommand->SetError(e.str());
-          return false;
-        }
-      }
-    } else {
-      *mode = nullptr;
-    }
-
-    return true;
-  }
-};
-
-bool cmFileCopier::Parse(std::vector<std::string> const& args)
-{
-  this->Doing = DoingFiles;
-  for (unsigned int i = 1; i < args.size(); ++i) {
-    // Check this argument.
-    if (!this->CheckKeyword(args[i]) && !this->CheckValue(args[i])) {
-      std::ostringstream e;
-      e << "called with unknown argument \"" << args[i] << "\".";
-      this->FileCommand->SetError(e.str());
-      return false;
-    }
-
-    // Quit if an argument is invalid.
-    if (this->Doing == DoingError) {
-      return false;
-    }
-  }
-
-  // Require a destination.
-  if (this->Destination.empty()) {
-    std::ostringstream e;
-    e << this->Name << " given no DESTINATION";
-    this->FileCommand->SetError(e.str());
-    return false;
-  }
-
-  // If file permissions were not specified set default permissions.
-  if (!this->UseGivenPermissionsFile && !this->UseSourcePermissions) {
-    this->DefaultFilePermissions();
-  }
-
-  // If directory permissions were not specified set default permissions.
-  if (!this->UseGivenPermissionsDir && !this->UseSourcePermissions) {
-    this->DefaultDirectoryPermissions();
-  }
-
-  return true;
-}
-
-bool cmFileCopier::CheckKeyword(std::string const& arg)
-{
-  if (arg == "DESTINATION") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingDestination;
-    }
-  } else if (arg == "FILES_FROM_DIR") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingFilesFromDir;
-    }
-  } else if (arg == "PATTERN") {
-    this->Doing = DoingPattern;
-  } else if (arg == "REGEX") {
-    this->Doing = DoingRegex;
-  } else if (arg == "EXCLUDE") {
-    // Add this property to the current match rule.
-    if (this->CurrentMatchRule) {
-      this->CurrentMatchRule->Properties.Exclude = true;
-      this->Doing = DoingNone;
-    } else {
-      this->NotBeforeMatch(arg);
-    }
-  } else if (arg == "PERMISSIONS") {
-    if (this->CurrentMatchRule) {
-      this->Doing = DoingPermissionsMatch;
-    } else {
-      this->NotBeforeMatch(arg);
-    }
-  } else if (arg == "FILE_PERMISSIONS") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingPermissionsFile;
-      this->UseGivenPermissionsFile = true;
-    }
-  } else if (arg == "DIRECTORY_PERMISSIONS") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingPermissionsDir;
-      this->UseGivenPermissionsDir = true;
-    }
-  } else if (arg == "USE_SOURCE_PERMISSIONS") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingNone;
-      this->UseSourcePermissions = true;
-    }
-  } else if (arg == "NO_SOURCE_PERMISSIONS") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingNone;
-      this->UseSourcePermissions = false;
-    }
-  } else if (arg == "FILES_MATCHING") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingNone;
-      this->MatchlessFiles = false;
-    }
-  } else {
-    return false;
-  }
-  return true;
-}
-
-bool cmFileCopier::CheckValue(std::string const& arg)
-{
-  switch (this->Doing) {
-    case DoingFiles:
-      this->Files.push_back(arg);
-      break;
-    case DoingDestination:
-      if (arg.empty() || cmSystemTools::FileIsFullPath(arg)) {
-        this->Destination = arg;
-      } else {
-        this->Destination = this->Makefile->GetCurrentBinaryDirectory();
-        this->Destination += "/" + arg;
-      }
-      this->Doing = DoingNone;
-      break;
-    case DoingFilesFromDir:
-      if (cmSystemTools::FileIsFullPath(arg)) {
-        this->FilesFromDir = arg;
-      } else {
-        this->FilesFromDir = this->Makefile->GetCurrentSourceDirectory();
-        this->FilesFromDir += "/" + arg;
-      }
-      cmSystemTools::ConvertToUnixSlashes(this->FilesFromDir);
-      this->Doing = DoingNone;
-      break;
-    case DoingPattern: {
-      // Convert the pattern to a regular expression.  Require a
-      // leading slash and trailing end-of-string in the matched
-      // string to make sure the pattern matches only whole file
-      // names.
-      std::string regex = "/";
-      regex += cmsys::Glob::PatternToRegex(arg, false);
-      regex += "$";
-      this->MatchRules.emplace_back(regex);
-      this->CurrentMatchRule = &*(this->MatchRules.end() - 1);
-      if (this->CurrentMatchRule->Regex.is_valid()) {
-        this->Doing = DoingNone;
-      } else {
-        std::ostringstream e;
-        e << "could not compile PATTERN \"" << arg << "\".";
-        this->FileCommand->SetError(e.str());
-        this->Doing = DoingError;
-      }
-    } break;
-    case DoingRegex:
-      this->MatchRules.emplace_back(arg);
-      this->CurrentMatchRule = &*(this->MatchRules.end() - 1);
-      if (this->CurrentMatchRule->Regex.is_valid()) {
-        this->Doing = DoingNone;
-      } else {
-        std::ostringstream e;
-        e << "could not compile REGEX \"" << arg << "\".";
-        this->FileCommand->SetError(e.str());
-        this->Doing = DoingError;
-      }
-      break;
-    case DoingPermissionsFile:
-      if (!this->CheckPermissions(arg, this->FilePermissions)) {
-        this->Doing = DoingError;
-      }
-      break;
-    case DoingPermissionsDir:
-      if (!this->CheckPermissions(arg, this->DirPermissions)) {
-        this->Doing = DoingError;
-      }
-      break;
-    case DoingPermissionsMatch:
-      if (!this->CheckPermissions(
-            arg, this->CurrentMatchRule->Properties.Permissions)) {
-        this->Doing = DoingError;
-      }
-      break;
-    default:
-      return false;
-  }
-  return true;
-}
-
-bool cmFileCopier::Run(std::vector<std::string> const& args)
-{
-  if (!this->Parse(args)) {
-    return false;
-  }
-
-  for (std::string const& f : this->Files) {
-    std::string file;
-    if (!f.empty() && !cmSystemTools::FileIsFullPath(f)) {
-      if (!this->FilesFromDir.empty()) {
-        file = this->FilesFromDir;
-      } else {
-        file = this->Makefile->GetCurrentSourceDirectory();
-      }
-      file += "/";
-      file += f;
-    } else if (!this->FilesFromDir.empty()) {
-      this->FileCommand->SetError("option FILES_FROM_DIR requires all files "
-                                  "to be specified as relative paths.");
-      return false;
-    } else {
-      file = f;
-    }
-
-    // Split the input file into its directory and name components.
-    std::vector<std::string> fromPathComponents;
-    cmSystemTools::SplitPath(file, fromPathComponents);
-    std::string fromName = *(fromPathComponents.end() - 1);
-    std::string fromDir = cmSystemTools::JoinPath(
-      fromPathComponents.begin(), fromPathComponents.end() - 1);
-
-    // Compute the full path to the destination file.
-    std::string toFile = this->Destination;
-    if (!this->FilesFromDir.empty()) {
-      std::string dir = cmSystemTools::GetFilenamePath(f);
-      if (!dir.empty()) {
-        toFile += "/";
-        toFile += dir;
-      }
-    }
-    std::string const& toName = this->ToName(fromName);
-    if (!toName.empty()) {
-      toFile += "/";
-      toFile += toName;
-    }
-
-    // Construct the full path to the source file.  The file name may
-    // have been changed above.
-    std::string fromFile = fromDir;
-    if (!fromName.empty()) {
-      fromFile += "/";
-      fromFile += fromName;
-    }
-
-    if (!this->Install(fromFile, toFile)) {
-      return false;
-    }
-  }
-  return true;
-}
-
-bool cmFileCopier::Install(const std::string& fromFile,
-                           const std::string& toFile)
-{
-  if (fromFile.empty()) {
-    std::ostringstream e;
-    e << "INSTALL encountered an empty string input file name.";
-    this->FileCommand->SetError(e.str());
-    return false;
-  }
-
-  // Collect any properties matching this file name.
-  MatchProperties match_properties = this->CollectMatchProperties(fromFile);
-
-  // Skip the file if it is excluded.
-  if (match_properties.Exclude) {
-    return true;
-  }
-
-  if (cmSystemTools::SameFile(fromFile, toFile)) {
-    return true;
-  }
-  if (cmSystemTools::FileIsSymlink(fromFile)) {
-    return this->InstallSymlink(fromFile, toFile);
-  }
-  if (cmSystemTools::FileIsDirectory(fromFile)) {
-    return this->InstallDirectory(fromFile, toFile, match_properties);
-  }
-  if (cmSystemTools::FileExists(fromFile)) {
-    return this->InstallFile(fromFile, toFile, match_properties);
-  }
-  return this->ReportMissing(fromFile);
-}
-
-bool cmFileCopier::InstallSymlink(const std::string& fromFile,
-                                  const std::string& toFile)
-{
-  // Read the original symlink.
-  std::string symlinkTarget;
-  if (!cmSystemTools::ReadSymlink(fromFile, symlinkTarget)) {
-    std::ostringstream e;
-    e << this->Name << " cannot read symlink \"" << fromFile
-      << "\" to duplicate at \"" << toFile << "\".";
-    this->FileCommand->SetError(e.str());
-    return false;
-  }
-
-  // Compare the symlink value to that at the destination if not
-  // always installing.
-  bool copy = true;
-  if (!this->Always) {
-    std::string oldSymlinkTarget;
-    if (cmSystemTools::ReadSymlink(toFile, oldSymlinkTarget)) {
-      if (symlinkTarget == oldSymlinkTarget) {
-        copy = false;
-      }
-    }
-  }
-
-  // Inform the user about this file installation.
-  this->ReportCopy(toFile, TypeLink, copy);
-
-  if (copy) {
-    // Remove the destination file so we can always create the symlink.
-    cmSystemTools::RemoveFile(toFile);
-
-    // Create destination directory if it doesn't exist
-    cmSystemTools::MakeDirectory(cmSystemTools::GetFilenamePath(toFile));
-
-    // Create the symlink.
-    if (!cmSystemTools::CreateSymlink(symlinkTarget, toFile)) {
-      std::ostringstream e;
-      e << this->Name << " cannot duplicate symlink \"" << fromFile
-        << "\" at \"" << toFile << "\".";
-      this->FileCommand->SetError(e.str());
-      return false;
-    }
-  }
-
-  return true;
-}
-
-bool cmFileCopier::InstallFile(const std::string& fromFile,
-                               const std::string& toFile,
-                               MatchProperties match_properties)
-{
-  // Determine whether we will copy the file.
-  bool copy = true;
-  if (!this->Always) {
-    // If both files exist with the same time do not copy.
-    if (!this->FileTimes.FileTimesDiffer(fromFile, toFile)) {
-      copy = false;
-    }
-  }
-
-  // Inform the user about this file installation.
-  this->ReportCopy(toFile, TypeFile, copy);
-
-  // Copy the file.
-  if (copy && !cmSystemTools::CopyAFile(fromFile, toFile, true)) {
-    std::ostringstream e;
-    e << this->Name << " cannot copy file \"" << fromFile << "\" to \""
-      << toFile << "\".";
-    this->FileCommand->SetError(e.str());
-    return false;
-  }
-
-  // Set the file modification time of the destination file.
-  if (copy && !this->Always) {
-    // Add write permission so we can set the file time.
-    // Permissions are set unconditionally below anyway.
-    mode_t perm = 0;
-    if (cmSystemTools::GetPermissions(toFile, perm)) {
-      cmSystemTools::SetPermissions(toFile, perm | mode_owner_write);
-    }
-    if (!cmSystemTools::CopyFileTime(fromFile, toFile)) {
-      std::ostringstream e;
-      e << this->Name << " cannot set modification time on \"" << toFile
-        << "\"";
-      this->FileCommand->SetError(e.str());
-      return false;
-    }
-  }
-
-  // Set permissions of the destination file.
-  mode_t permissions =
-    (match_properties.Permissions ? match_properties.Permissions
-                                  : this->FilePermissions);
-  if (!permissions) {
-    // No permissions were explicitly provided but the user requested
-    // that the source file permissions be used.
-    cmSystemTools::GetPermissions(fromFile, permissions);
-  }
-  return this->SetPermissions(toFile, permissions);
-}
-
-bool cmFileCopier::InstallDirectory(const std::string& source,
-                                    const std::string& destination,
-                                    MatchProperties match_properties)
-{
-  // Inform the user about this directory installation.
-  this->ReportCopy(destination, TypeDir,
-                   !cmSystemTools::FileIsDirectory(destination));
-
-  // check if default dir creation permissions were set
-  mode_t default_dir_mode_v = 0;
-  mode_t* default_dir_mode = &default_dir_mode_v;
-  if (!this->GetDefaultDirectoryPermissions(&default_dir_mode)) {
-    return false;
-  }
-
-  // Make sure the destination directory exists.
-  if (!cmSystemTools::MakeDirectory(destination, default_dir_mode)) {
-    std::ostringstream e;
-    e << this->Name << " cannot make directory \"" << destination
-      << "\": " << cmSystemTools::GetLastSystemError();
-    this->FileCommand->SetError(e.str());
-    return false;
-  }
-
-  // Compute the requested permissions for the destination directory.
-  mode_t permissions =
-    (match_properties.Permissions ? match_properties.Permissions
-                                  : this->DirPermissions);
-  if (!permissions) {
-    // No permissions were explicitly provided but the user requested
-    // that the source directory permissions be used.
-    cmSystemTools::GetPermissions(source, permissions);
-  }
-
-  // Compute the set of permissions required on this directory to
-  // recursively install files and subdirectories safely.
-  mode_t required_permissions =
-    mode_owner_read | mode_owner_write | mode_owner_execute;
-
-  // If the required permissions are specified it is safe to set the
-  // final permissions now.  Otherwise we must add the required
-  // permissions temporarily during file installation.
-  mode_t permissions_before = 0;
-  mode_t permissions_after = 0;
-  if ((permissions & required_permissions) == required_permissions) {
-    permissions_before = permissions;
-  } else {
-    permissions_before = permissions | required_permissions;
-    permissions_after = permissions;
-  }
-
-  // Set the required permissions of the destination directory.
-  if (!this->SetPermissions(destination, permissions_before)) {
-    return false;
-  }
-
-  // Load the directory contents to traverse it recursively.
-  cmsys::Directory dir;
-  if (!source.empty()) {
-    dir.Load(source);
-  }
-  unsigned long numFiles = static_cast<unsigned long>(dir.GetNumberOfFiles());
-  for (unsigned long fileNum = 0; fileNum < numFiles; ++fileNum) {
-    if (!(strcmp(dir.GetFile(fileNum), ".") == 0 ||
-          strcmp(dir.GetFile(fileNum), "..") == 0)) {
-      std::string fromPath = source;
-      fromPath += "/";
-      fromPath += dir.GetFile(fileNum);
-      std::string toPath = destination;
-      toPath += "/";
-      toPath += dir.GetFile(fileNum);
-      if (!this->Install(fromPath, toPath)) {
-        return false;
-      }
-    }
-  }
-
-  // Set the requested permissions of the destination directory.
-  return this->SetPermissions(destination, permissions_after);
-}
-
 bool cmFileCommand::HandleCopyCommand(std::vector<std::string> const& args)
 {
   cmFileCopier copier(this);
   return copier.Run(args);
 }
 
-struct cmFileInstaller : public cmFileCopier
-{
-  cmFileInstaller(cmFileCommand* command)
-    : cmFileCopier(command, "INSTALL")
-    , InstallType(cmInstallType_FILES)
-    , Optional(false)
-    , MessageAlways(false)
-    , MessageLazy(false)
-    , MessageNever(false)
-    , DestDirLength(0)
-  {
-    // Installation does not use source permissions by default.
-    this->UseSourcePermissions = false;
-    // Check whether to copy files always or only if they have changed.
-    std::string install_always;
-    if (cmSystemTools::GetEnv("CMAKE_INSTALL_ALWAYS", install_always)) {
-      this->Always = cmSystemTools::IsOn(install_always);
-    }
-    // Get the current manifest.
-    this->Manifest =
-      this->Makefile->GetSafeDefinition("CMAKE_INSTALL_MANIFEST_FILES");
-  }
-  ~cmFileInstaller() override
-  {
-    // Save the updated install manifest.
-    this->Makefile->AddDefinition("CMAKE_INSTALL_MANIFEST_FILES",
-                                  this->Manifest.c_str());
-  }
-
-protected:
-  cmInstallType InstallType;
-  bool Optional;
-  bool MessageAlways;
-  bool MessageLazy;
-  bool MessageNever;
-  int DestDirLength;
-  std::string Rename;
-
-  std::string Manifest;
-  void ManifestAppend(std::string const& file)
-  {
-    if (!this->Manifest.empty()) {
-      this->Manifest += ";";
-    }
-    this->Manifest += file.substr(this->DestDirLength);
-  }
-
-  std::string const& ToName(std::string const& fromName) override
-  {
-    return this->Rename.empty() ? fromName : this->Rename;
-  }
-
-  void ReportCopy(const std::string& toFile, Type type, bool copy) override
-  {
-    if (!this->MessageNever && (copy || !this->MessageLazy)) {
-      std::string message = (copy ? "Installing: " : "Up-to-date: ");
-      message += toFile;
-      this->Makefile->DisplayStatus(message, -1);
-    }
-    if (type != TypeDir) {
-      // Add the file to the manifest.
-      this->ManifestAppend(toFile);
-    }
-  }
-  bool ReportMissing(const std::string& fromFile) override
-  {
-    return (this->Optional || this->cmFileCopier::ReportMissing(fromFile));
-  }
-  bool Install(const std::string& fromFile, const std::string& toFile) override
-  {
-    // Support installing from empty source to make a directory.
-    if (this->InstallType == cmInstallType_DIRECTORY && fromFile.empty()) {
-      return this->InstallDirectory(fromFile, toFile, MatchProperties());
-    }
-    return this->cmFileCopier::Install(fromFile, toFile);
-  }
-
-  bool Parse(std::vector<std::string> const& args) override;
-  enum
-  {
-    DoingType = DoingLast1,
-    DoingRename,
-    DoingLast2
-  };
-  bool CheckKeyword(std::string const& arg) override;
-  bool CheckValue(std::string const& arg) override;
-  void DefaultFilePermissions() override
-  {
-    this->cmFileCopier::DefaultFilePermissions();
-    // Add execute permissions based on the target type.
-    switch (this->InstallType) {
-      case cmInstallType_SHARED_LIBRARY:
-      case cmInstallType_MODULE_LIBRARY:
-        if (this->Makefile->IsOn("CMAKE_INSTALL_SO_NO_EXE")) {
-          break;
-        }
-        CM_FALLTHROUGH;
-      case cmInstallType_EXECUTABLE:
-      case cmInstallType_PROGRAMS:
-        this->FilePermissions |= mode_owner_execute;
-        this->FilePermissions |= mode_group_execute;
-        this->FilePermissions |= mode_world_execute;
-        break;
-      default:
-        break;
-    }
-  }
-  bool GetTargetTypeFromString(const std::string& stype);
-  bool HandleInstallDestination();
-};
-
-bool cmFileInstaller::Parse(std::vector<std::string> const& args)
-{
-  if (!this->cmFileCopier::Parse(args)) {
-    return false;
-  }
-
-  if (!this->Rename.empty()) {
-    if (!this->FilesFromDir.empty()) {
-      this->FileCommand->SetError("INSTALL option RENAME may not be "
-                                  "combined with FILES_FROM_DIR.");
-      return false;
-    }
-    if (this->InstallType != cmInstallType_FILES &&
-        this->InstallType != cmInstallType_PROGRAMS) {
-      this->FileCommand->SetError("INSTALL option RENAME may be used "
-                                  "only with FILES or PROGRAMS.");
-      return false;
-    }
-    if (this->Files.size() > 1) {
-      this->FileCommand->SetError("INSTALL option RENAME may be used "
-                                  "only with one file.");
-      return false;
-    }
-  }
-
-  if (!this->HandleInstallDestination()) {
-    return false;
-  }
-
-  if (((this->MessageAlways ? 1 : 0) + (this->MessageLazy ? 1 : 0) +
-       (this->MessageNever ? 1 : 0)) > 1) {
-    this->FileCommand->SetError("INSTALL options MESSAGE_ALWAYS, "
-                                "MESSAGE_LAZY, and MESSAGE_NEVER "
-                                "are mutually exclusive.");
-    return false;
-  }
-
-  return true;
-}
-
-bool cmFileInstaller::CheckKeyword(std::string const& arg)
-{
-  if (arg == "TYPE") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingType;
-    }
-  } else if (arg == "FILES") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingFiles;
-    }
-  } else if (arg == "RENAME") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingRename;
-    }
-  } else if (arg == "OPTIONAL") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingNone;
-      this->Optional = true;
-    }
-  } else if (arg == "MESSAGE_ALWAYS") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingNone;
-      this->MessageAlways = true;
-    }
-  } else if (arg == "MESSAGE_LAZY") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingNone;
-      this->MessageLazy = true;
-    }
-  } else if (arg == "MESSAGE_NEVER") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      this->Doing = DoingNone;
-      this->MessageNever = true;
-    }
-  } else if (arg == "PERMISSIONS") {
-    if (this->CurrentMatchRule) {
-      this->Doing = DoingPermissionsMatch;
-    } else {
-      // file(INSTALL) aliases PERMISSIONS to FILE_PERMISSIONS
-      this->Doing = DoingPermissionsFile;
-      this->UseGivenPermissionsFile = true;
-    }
-  } else if (arg == "DIR_PERMISSIONS") {
-    if (this->CurrentMatchRule) {
-      this->NotAfterMatch(arg);
-    } else {
-      // file(INSTALL) aliases DIR_PERMISSIONS to DIRECTORY_PERMISSIONS
-      this->Doing = DoingPermissionsDir;
-      this->UseGivenPermissionsDir = true;
-    }
-  } else if (arg == "COMPONENTS" || arg == "CONFIGURATIONS" ||
-             arg == "PROPERTIES") {
-    std::ostringstream e;
-    e << "INSTALL called with old-style " << arg << " argument.  "
-      << "This script was generated with an older version of CMake.  "
-      << "Re-run this cmake version on your build tree.";
-    this->FileCommand->SetError(e.str());
-    this->Doing = DoingError;
-  } else {
-    return this->cmFileCopier::CheckKeyword(arg);
-  }
-  return true;
-}
-
-bool cmFileInstaller::CheckValue(std::string const& arg)
-{
-  switch (this->Doing) {
-    case DoingType:
-      if (!this->GetTargetTypeFromString(arg)) {
-        this->Doing = DoingError;
-      }
-      break;
-    case DoingRename:
-      this->Rename = arg;
-      break;
-    default:
-      return this->cmFileCopier::CheckValue(arg);
-  }
-  return true;
-}
-
-bool cmFileInstaller::GetTargetTypeFromString(const std::string& stype)
-{
-  if (stype == "EXECUTABLE") {
-    this->InstallType = cmInstallType_EXECUTABLE;
-  } else if (stype == "FILE") {
-    this->InstallType = cmInstallType_FILES;
-  } else if (stype == "PROGRAM") {
-    this->InstallType = cmInstallType_PROGRAMS;
-  } else if (stype == "STATIC_LIBRARY") {
-    this->InstallType = cmInstallType_STATIC_LIBRARY;
-  } else if (stype == "SHARED_LIBRARY") {
-    this->InstallType = cmInstallType_SHARED_LIBRARY;
-  } else if (stype == "MODULE") {
-    this->InstallType = cmInstallType_MODULE_LIBRARY;
-  } else if (stype == "DIRECTORY") {
-    this->InstallType = cmInstallType_DIRECTORY;
-  } else {
-    std::ostringstream e;
-    e << "Option TYPE given unknown value \"" << stype << "\".";
-    this->FileCommand->SetError(e.str());
-    return false;
-  }
-  return true;
-}
-
-bool cmFileInstaller::HandleInstallDestination()
-{
-  std::string& destination = this->Destination;
-
-  // allow for / to be a valid destination
-  if (destination.size() < 2 && destination != "/") {
-    this->FileCommand->SetError("called with inappropriate arguments. "
-                                "No DESTINATION provided or .");
-    return false;
-  }
-
-  std::string sdestdir;
-  if (cmSystemTools::GetEnv("DESTDIR", sdestdir) && !sdestdir.empty()) {
-    cmSystemTools::ConvertToUnixSlashes(sdestdir);
-    char ch1 = destination[0];
-    char ch2 = destination[1];
-    char ch3 = 0;
-    if (destination.size() > 2) {
-      ch3 = destination[2];
-    }
-    int skip = 0;
-    if (ch1 != '/') {
-      int relative = 0;
-      if (((ch1 >= 'a' && ch1 <= 'z') || (ch1 >= 'A' && ch1 <= 'Z')) &&
-          ch2 == ':') {
-        // Assume windows
-        // let's do some destdir magic:
-        skip = 2;
-        if (ch3 != '/') {
-          relative = 1;
-        }
-      } else {
-        relative = 1;
-      }
-      if (relative) {
-        // This is relative path on unix or windows. Since we are doing
-        // destdir, this case does not make sense.
-        this->FileCommand->SetError(
-          "called with relative DESTINATION. This "
-          "does not make sense when using DESTDIR. Specify "
-          "absolute path or remove DESTDIR environment variable.");
-        return false;
-      }
-    } else {
-      if (ch2 == '/') {
-        // looks like a network path.
-        std::string message =
-          "called with network path DESTINATION. This "
-          "does not make sense when using DESTDIR. Specify local "
-          "absolute path or remove DESTDIR environment variable."
-          "\nDESTINATION=\n";
-        message += destination;
-        this->FileCommand->SetError(message);
-        return false;
-      }
-    }
-    destination = sdestdir + (destination.c_str() + skip);
-    this->DestDirLength = int(sdestdir.size());
-  }
-
-  // check if default dir creation permissions were set
-  mode_t default_dir_mode_v = 0;
-  mode_t* default_dir_mode = &default_dir_mode_v;
-  if (!this->GetDefaultDirectoryPermissions(&default_dir_mode)) {
-    return false;
-  }
-
-  if (this->InstallType != cmInstallType_DIRECTORY) {
-    if (!cmSystemTools::FileExists(destination)) {
-      if (!cmSystemTools::MakeDirectory(destination, default_dir_mode)) {
-        std::string errstring = "cannot create directory: " + destination +
-          ". Maybe need administrative privileges.";
-        this->FileCommand->SetError(errstring);
-        return false;
-      }
-    }
-    if (!cmSystemTools::FileIsDirectory(destination)) {
-      std::string errstring =
-        "INSTALL destination: " + destination + " is not a directory.";
-      this->FileCommand->SetError(errstring);
-      return false;
-    }
-  }
-  return true;
-}
-
 bool cmFileCommand::HandleRPathChangeCommand(
   std::vector<std::string> const& args)
 {

+ 660 - 0
Source/cmFileCopier.cxx

@@ -0,0 +1,660 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmFileCopier.h"
+
+#include "cmFSPermissions.h"
+#include "cmFileCommand.h"
+#include "cmMakefile.h"
+#include "cmSystemTools.h"
+#include "cmsys/Directory.hxx"
+#include "cmsys/Glob.hxx"
+
+#ifdef _WIN32
+#  include "cmsys/FStream.hxx"
+#endif
+
+#include <sstream>
+#include <string.h>
+
+using namespace cmFSPermissions;
+
+cmFileCopier::cmFileCopier(cmFileCommand* command, const char* name)
+  : FileCommand(command)
+  , Makefile(command->GetMakefile())
+  , Name(name)
+  , Always(false)
+  , MatchlessFiles(true)
+  , FilePermissions(0)
+  , DirPermissions(0)
+  , CurrentMatchRule(nullptr)
+  , UseGivenPermissionsFile(false)
+  , UseGivenPermissionsDir(false)
+  , UseSourcePermissions(true)
+  , Doing(DoingNone)
+{
+}
+
+cmFileCopier::~cmFileCopier() = default;
+
+cmFileCopier::MatchProperties cmFileCopier::CollectMatchProperties(
+  const std::string& file)
+{
+  // Match rules are case-insensitive on some platforms.
+#if defined(_WIN32) || defined(__APPLE__) || defined(__CYGWIN__)
+  const std::string file_to_match = cmSystemTools::LowerCase(file);
+#else
+  const std::string& file_to_match = file;
+#endif
+
+  // Collect properties from all matching rules.
+  bool matched = false;
+  MatchProperties result;
+  for (MatchRule& mr : this->MatchRules) {
+    if (mr.Regex.find(file_to_match)) {
+      matched = true;
+      result.Exclude |= mr.Properties.Exclude;
+      result.Permissions |= mr.Properties.Permissions;
+    }
+  }
+  if (!matched && !this->MatchlessFiles) {
+    result.Exclude = !cmSystemTools::FileIsDirectory(file);
+  }
+  return result;
+}
+
+bool cmFileCopier::SetPermissions(const std::string& toFile,
+                                  mode_t permissions)
+{
+  if (permissions) {
+#ifdef WIN32
+    if (Makefile->IsOn("CMAKE_CROSSCOMPILING")) {
+      // Store the mode in an NTFS alternate stream.
+      std::string mode_t_adt_filename = toFile + ":cmake_mode_t";
+
+      // Writing to an NTFS alternate stream changes the modification
+      // time, so we need to save and restore its original value.
+      cmSystemToolsFileTime* file_time_orig = cmSystemTools::FileTimeNew();
+      cmSystemTools::FileTimeGet(toFile, file_time_orig);
+
+      cmsys::ofstream permissionStream(mode_t_adt_filename.c_str());
+
+      if (permissionStream) {
+        permissionStream << std::oct << permissions << std::endl;
+      }
+
+      permissionStream.close();
+
+      cmSystemTools::FileTimeSet(toFile, file_time_orig);
+
+      cmSystemTools::FileTimeDelete(file_time_orig);
+    }
+#endif
+
+    if (!cmSystemTools::SetPermissions(toFile, permissions)) {
+      std::ostringstream e;
+      e << this->Name << " cannot set permissions on \"" << toFile << "\"";
+      this->FileCommand->SetError(e.str());
+      return false;
+    }
+  }
+  return true;
+}
+
+// Translate an argument to a permissions bit.
+bool cmFileCopier::CheckPermissions(std::string const& arg,
+                                    mode_t& permissions)
+{
+  if (!cmFSPermissions::stringToModeT(arg, permissions)) {
+    std::ostringstream e;
+    e << this->Name << " given invalid permission \"" << arg << "\".";
+    this->FileCommand->SetError(e.str());
+    return false;
+  }
+  return true;
+}
+
+std::string const& cmFileCopier::ToName(std::string const& fromName)
+{
+  return fromName;
+}
+
+bool cmFileCopier::ReportMissing(const std::string& fromFile)
+{
+  // The input file does not exist and installation is not optional.
+  std::ostringstream e;
+  e << this->Name << " cannot find \"" << fromFile << "\".";
+  this->FileCommand->SetError(e.str());
+  return false;
+}
+
+void cmFileCopier::NotBeforeMatch(std::string const& arg)
+{
+  std::ostringstream e;
+  e << "option " << arg << " may not appear before PATTERN or REGEX.";
+  this->FileCommand->SetError(e.str());
+  this->Doing = DoingError;
+}
+
+void cmFileCopier::NotAfterMatch(std::string const& arg)
+{
+  std::ostringstream e;
+  e << "option " << arg << " may not appear after PATTERN or REGEX.";
+  this->FileCommand->SetError(e.str());
+  this->Doing = DoingError;
+}
+
+void cmFileCopier::DefaultFilePermissions()
+{
+  // Use read/write permissions.
+  this->FilePermissions = 0;
+  this->FilePermissions |= mode_owner_read;
+  this->FilePermissions |= mode_owner_write;
+  this->FilePermissions |= mode_group_read;
+  this->FilePermissions |= mode_world_read;
+}
+
+void cmFileCopier::DefaultDirectoryPermissions()
+{
+  // Use read/write/executable permissions.
+  this->DirPermissions = 0;
+  this->DirPermissions |= mode_owner_read;
+  this->DirPermissions |= mode_owner_write;
+  this->DirPermissions |= mode_owner_execute;
+  this->DirPermissions |= mode_group_read;
+  this->DirPermissions |= mode_group_execute;
+  this->DirPermissions |= mode_world_read;
+  this->DirPermissions |= mode_world_execute;
+}
+
+bool cmFileCopier::GetDefaultDirectoryPermissions(mode_t** mode)
+{
+  // check if default dir creation permissions were set
+  const char* default_dir_install_permissions = this->Makefile->GetDefinition(
+    "CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS");
+  if (default_dir_install_permissions && *default_dir_install_permissions) {
+    std::vector<std::string> items;
+    cmSystemTools::ExpandListArgument(default_dir_install_permissions, items);
+    for (const auto& arg : items) {
+      if (!this->CheckPermissions(arg, **mode)) {
+        std::ostringstream e;
+        e << this->FileCommand->GetError()
+          << " Set with CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS "
+             "variable.";
+        this->FileCommand->SetError(e.str());
+        return false;
+      }
+    }
+  } else {
+    *mode = nullptr;
+  }
+
+  return true;
+}
+
+bool cmFileCopier::Parse(std::vector<std::string> const& args)
+{
+  this->Doing = DoingFiles;
+  for (unsigned int i = 1; i < args.size(); ++i) {
+    // Check this argument.
+    if (!this->CheckKeyword(args[i]) && !this->CheckValue(args[i])) {
+      std::ostringstream e;
+      e << "called with unknown argument \"" << args[i] << "\".";
+      this->FileCommand->SetError(e.str());
+      return false;
+    }
+
+    // Quit if an argument is invalid.
+    if (this->Doing == DoingError) {
+      return false;
+    }
+  }
+
+  // Require a destination.
+  if (this->Destination.empty()) {
+    std::ostringstream e;
+    e << this->Name << " given no DESTINATION";
+    this->FileCommand->SetError(e.str());
+    return false;
+  }
+
+  // If file permissions were not specified set default permissions.
+  if (!this->UseGivenPermissionsFile && !this->UseSourcePermissions) {
+    this->DefaultFilePermissions();
+  }
+
+  // If directory permissions were not specified set default permissions.
+  if (!this->UseGivenPermissionsDir && !this->UseSourcePermissions) {
+    this->DefaultDirectoryPermissions();
+  }
+
+  return true;
+}
+
+bool cmFileCopier::CheckKeyword(std::string const& arg)
+{
+  if (arg == "DESTINATION") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingDestination;
+    }
+  } else if (arg == "FILES_FROM_DIR") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingFilesFromDir;
+    }
+  } else if (arg == "PATTERN") {
+    this->Doing = DoingPattern;
+  } else if (arg == "REGEX") {
+    this->Doing = DoingRegex;
+  } else if (arg == "EXCLUDE") {
+    // Add this property to the current match rule.
+    if (this->CurrentMatchRule) {
+      this->CurrentMatchRule->Properties.Exclude = true;
+      this->Doing = DoingNone;
+    } else {
+      this->NotBeforeMatch(arg);
+    }
+  } else if (arg == "PERMISSIONS") {
+    if (this->CurrentMatchRule) {
+      this->Doing = DoingPermissionsMatch;
+    } else {
+      this->NotBeforeMatch(arg);
+    }
+  } else if (arg == "FILE_PERMISSIONS") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingPermissionsFile;
+      this->UseGivenPermissionsFile = true;
+    }
+  } else if (arg == "DIRECTORY_PERMISSIONS") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingPermissionsDir;
+      this->UseGivenPermissionsDir = true;
+    }
+  } else if (arg == "USE_SOURCE_PERMISSIONS") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingNone;
+      this->UseSourcePermissions = true;
+    }
+  } else if (arg == "NO_SOURCE_PERMISSIONS") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingNone;
+      this->UseSourcePermissions = false;
+    }
+  } else if (arg == "FILES_MATCHING") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingNone;
+      this->MatchlessFiles = false;
+    }
+  } else {
+    return false;
+  }
+  return true;
+}
+
+bool cmFileCopier::CheckValue(std::string const& arg)
+{
+  switch (this->Doing) {
+    case DoingFiles:
+      this->Files.push_back(arg);
+      break;
+    case DoingDestination:
+      if (arg.empty() || cmSystemTools::FileIsFullPath(arg)) {
+        this->Destination = arg;
+      } else {
+        this->Destination = this->Makefile->GetCurrentBinaryDirectory();
+        this->Destination += "/" + arg;
+      }
+      this->Doing = DoingNone;
+      break;
+    case DoingFilesFromDir:
+      if (cmSystemTools::FileIsFullPath(arg)) {
+        this->FilesFromDir = arg;
+      } else {
+        this->FilesFromDir = this->Makefile->GetCurrentSourceDirectory();
+        this->FilesFromDir += "/" + arg;
+      }
+      cmSystemTools::ConvertToUnixSlashes(this->FilesFromDir);
+      this->Doing = DoingNone;
+      break;
+    case DoingPattern: {
+      // Convert the pattern to a regular expression.  Require a
+      // leading slash and trailing end-of-string in the matched
+      // string to make sure the pattern matches only whole file
+      // names.
+      std::string regex = "/";
+      regex += cmsys::Glob::PatternToRegex(arg, false);
+      regex += "$";
+      this->MatchRules.emplace_back(regex);
+      this->CurrentMatchRule = &*(this->MatchRules.end() - 1);
+      if (this->CurrentMatchRule->Regex.is_valid()) {
+        this->Doing = DoingNone;
+      } else {
+        std::ostringstream e;
+        e << "could not compile PATTERN \"" << arg << "\".";
+        this->FileCommand->SetError(e.str());
+        this->Doing = DoingError;
+      }
+    } break;
+    case DoingRegex:
+      this->MatchRules.emplace_back(arg);
+      this->CurrentMatchRule = &*(this->MatchRules.end() - 1);
+      if (this->CurrentMatchRule->Regex.is_valid()) {
+        this->Doing = DoingNone;
+      } else {
+        std::ostringstream e;
+        e << "could not compile REGEX \"" << arg << "\".";
+        this->FileCommand->SetError(e.str());
+        this->Doing = DoingError;
+      }
+      break;
+    case DoingPermissionsFile:
+      if (!this->CheckPermissions(arg, this->FilePermissions)) {
+        this->Doing = DoingError;
+      }
+      break;
+    case DoingPermissionsDir:
+      if (!this->CheckPermissions(arg, this->DirPermissions)) {
+        this->Doing = DoingError;
+      }
+      break;
+    case DoingPermissionsMatch:
+      if (!this->CheckPermissions(
+            arg, this->CurrentMatchRule->Properties.Permissions)) {
+        this->Doing = DoingError;
+      }
+      break;
+    default:
+      return false;
+  }
+  return true;
+}
+
+bool cmFileCopier::Run(std::vector<std::string> const& args)
+{
+  if (!this->Parse(args)) {
+    return false;
+  }
+
+  for (std::string const& f : this->Files) {
+    std::string file;
+    if (!f.empty() && !cmSystemTools::FileIsFullPath(f)) {
+      if (!this->FilesFromDir.empty()) {
+        file = this->FilesFromDir;
+      } else {
+        file = this->Makefile->GetCurrentSourceDirectory();
+      }
+      file += "/";
+      file += f;
+    } else if (!this->FilesFromDir.empty()) {
+      this->FileCommand->SetError("option FILES_FROM_DIR requires all files "
+                                  "to be specified as relative paths.");
+      return false;
+    } else {
+      file = f;
+    }
+
+    // Split the input file into its directory and name components.
+    std::vector<std::string> fromPathComponents;
+    cmSystemTools::SplitPath(file, fromPathComponents);
+    std::string fromName = *(fromPathComponents.end() - 1);
+    std::string fromDir = cmSystemTools::JoinPath(
+      fromPathComponents.begin(), fromPathComponents.end() - 1);
+
+    // Compute the full path to the destination file.
+    std::string toFile = this->Destination;
+    if (!this->FilesFromDir.empty()) {
+      std::string dir = cmSystemTools::GetFilenamePath(f);
+      if (!dir.empty()) {
+        toFile += "/";
+        toFile += dir;
+      }
+    }
+    std::string const& toName = this->ToName(fromName);
+    if (!toName.empty()) {
+      toFile += "/";
+      toFile += toName;
+    }
+
+    // Construct the full path to the source file.  The file name may
+    // have been changed above.
+    std::string fromFile = fromDir;
+    if (!fromName.empty()) {
+      fromFile += "/";
+      fromFile += fromName;
+    }
+
+    if (!this->Install(fromFile, toFile)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+bool cmFileCopier::Install(const std::string& fromFile,
+                           const std::string& toFile)
+{
+  if (fromFile.empty()) {
+    std::ostringstream e;
+    e << "INSTALL encountered an empty string input file name.";
+    this->FileCommand->SetError(e.str());
+    return false;
+  }
+
+  // Collect any properties matching this file name.
+  MatchProperties match_properties = this->CollectMatchProperties(fromFile);
+
+  // Skip the file if it is excluded.
+  if (match_properties.Exclude) {
+    return true;
+  }
+
+  if (cmSystemTools::SameFile(fromFile, toFile)) {
+    return true;
+  }
+  if (cmSystemTools::FileIsSymlink(fromFile)) {
+    return this->InstallSymlink(fromFile, toFile);
+  }
+  if (cmSystemTools::FileIsDirectory(fromFile)) {
+    return this->InstallDirectory(fromFile, toFile, match_properties);
+  }
+  if (cmSystemTools::FileExists(fromFile)) {
+    return this->InstallFile(fromFile, toFile, match_properties);
+  }
+  return this->ReportMissing(fromFile);
+}
+
+bool cmFileCopier::InstallSymlink(const std::string& fromFile,
+                                  const std::string& toFile)
+{
+  // Read the original symlink.
+  std::string symlinkTarget;
+  if (!cmSystemTools::ReadSymlink(fromFile, symlinkTarget)) {
+    std::ostringstream e;
+    e << this->Name << " cannot read symlink \"" << fromFile
+      << "\" to duplicate at \"" << toFile << "\".";
+    this->FileCommand->SetError(e.str());
+    return false;
+  }
+
+  // Compare the symlink value to that at the destination if not
+  // always installing.
+  bool copy = true;
+  if (!this->Always) {
+    std::string oldSymlinkTarget;
+    if (cmSystemTools::ReadSymlink(toFile, oldSymlinkTarget)) {
+      if (symlinkTarget == oldSymlinkTarget) {
+        copy = false;
+      }
+    }
+  }
+
+  // Inform the user about this file installation.
+  this->ReportCopy(toFile, TypeLink, copy);
+
+  if (copy) {
+    // Remove the destination file so we can always create the symlink.
+    cmSystemTools::RemoveFile(toFile);
+
+    // Create destination directory if it doesn't exist
+    cmSystemTools::MakeDirectory(cmSystemTools::GetFilenamePath(toFile));
+
+    // Create the symlink.
+    if (!cmSystemTools::CreateSymlink(symlinkTarget, toFile)) {
+      std::ostringstream e;
+      e << this->Name << " cannot duplicate symlink \"" << fromFile
+        << "\" at \"" << toFile << "\".";
+      this->FileCommand->SetError(e.str());
+      return false;
+    }
+  }
+
+  return true;
+}
+
+bool cmFileCopier::InstallFile(const std::string& fromFile,
+                               const std::string& toFile,
+                               MatchProperties match_properties)
+{
+  // Determine whether we will copy the file.
+  bool copy = true;
+  if (!this->Always) {
+    // If both files exist with the same time do not copy.
+    if (!this->FileTimes.FileTimesDiffer(fromFile, toFile)) {
+      copy = false;
+    }
+  }
+
+  // Inform the user about this file installation.
+  this->ReportCopy(toFile, TypeFile, copy);
+
+  // Copy the file.
+  if (copy && !cmSystemTools::CopyAFile(fromFile, toFile, true)) {
+    std::ostringstream e;
+    e << this->Name << " cannot copy file \"" << fromFile << "\" to \""
+      << toFile << "\".";
+    this->FileCommand->SetError(e.str());
+    return false;
+  }
+
+  // Set the file modification time of the destination file.
+  if (copy && !this->Always) {
+    // Add write permission so we can set the file time.
+    // Permissions are set unconditionally below anyway.
+    mode_t perm = 0;
+    if (cmSystemTools::GetPermissions(toFile, perm)) {
+      cmSystemTools::SetPermissions(toFile, perm | mode_owner_write);
+    }
+    if (!cmSystemTools::CopyFileTime(fromFile, toFile)) {
+      std::ostringstream e;
+      e << this->Name << " cannot set modification time on \"" << toFile
+        << "\"";
+      this->FileCommand->SetError(e.str());
+      return false;
+    }
+  }
+
+  // Set permissions of the destination file.
+  mode_t permissions =
+    (match_properties.Permissions ? match_properties.Permissions
+                                  : this->FilePermissions);
+  if (!permissions) {
+    // No permissions were explicitly provided but the user requested
+    // that the source file permissions be used.
+    cmSystemTools::GetPermissions(fromFile, permissions);
+  }
+  return this->SetPermissions(toFile, permissions);
+}
+
+bool cmFileCopier::InstallDirectory(const std::string& source,
+                                    const std::string& destination,
+                                    MatchProperties match_properties)
+{
+  // Inform the user about this directory installation.
+  this->ReportCopy(destination, TypeDir,
+                   !cmSystemTools::FileIsDirectory(destination));
+
+  // check if default dir creation permissions were set
+  mode_t default_dir_mode_v = 0;
+  mode_t* default_dir_mode = &default_dir_mode_v;
+  if (!this->GetDefaultDirectoryPermissions(&default_dir_mode)) {
+    return false;
+  }
+
+  // Make sure the destination directory exists.
+  if (!cmSystemTools::MakeDirectory(destination, default_dir_mode)) {
+    std::ostringstream e;
+    e << this->Name << " cannot make directory \"" << destination
+      << "\": " << cmSystemTools::GetLastSystemError();
+    this->FileCommand->SetError(e.str());
+    return false;
+  }
+
+  // Compute the requested permissions for the destination directory.
+  mode_t permissions =
+    (match_properties.Permissions ? match_properties.Permissions
+                                  : this->DirPermissions);
+  if (!permissions) {
+    // No permissions were explicitly provided but the user requested
+    // that the source directory permissions be used.
+    cmSystemTools::GetPermissions(source, permissions);
+  }
+
+  // Compute the set of permissions required on this directory to
+  // recursively install files and subdirectories safely.
+  mode_t required_permissions =
+    mode_owner_read | mode_owner_write | mode_owner_execute;
+
+  // If the required permissions are specified it is safe to set the
+  // final permissions now.  Otherwise we must add the required
+  // permissions temporarily during file installation.
+  mode_t permissions_before = 0;
+  mode_t permissions_after = 0;
+  if ((permissions & required_permissions) == required_permissions) {
+    permissions_before = permissions;
+  } else {
+    permissions_before = permissions | required_permissions;
+    permissions_after = permissions;
+  }
+
+  // Set the required permissions of the destination directory.
+  if (!this->SetPermissions(destination, permissions_before)) {
+    return false;
+  }
+
+  // Load the directory contents to traverse it recursively.
+  cmsys::Directory dir;
+  if (!source.empty()) {
+    dir.Load(source);
+  }
+  unsigned long numFiles = static_cast<unsigned long>(dir.GetNumberOfFiles());
+  for (unsigned long fileNum = 0; fileNum < numFiles; ++fileNum) {
+    if (!(strcmp(dir.GetFile(fileNum), ".") == 0 ||
+          strcmp(dir.GetFile(fileNum), "..") == 0)) {
+      std::string fromPath = source;
+      fromPath += "/";
+      fromPath += dir.GetFile(fileNum);
+      std::string toPath = destination;
+      toPath += "/";
+      toPath += dir.GetFile(fileNum);
+      if (!this->Install(fromPath, toPath)) {
+        return false;
+      }
+    }
+  }
+
+  // Set the requested permissions of the destination directory.
+  return this->SetPermissions(destination, permissions_after);
+}

+ 120 - 0
Source/cmFileCopier.h

@@ -0,0 +1,120 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#ifndef cmFileCopier_h
+#define cmFileCopier_h
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include "cmFileTimeComparison.h"
+#include "cm_sys_stat.h"
+#include "cmsys/RegularExpression.hxx"
+
+#include <string>
+#include <vector>
+
+class cmFileCommand;
+class cmMakefile;
+
+// File installation helper class.
+struct cmFileCopier
+{
+  cmFileCopier(cmFileCommand* command, const char* name = "COPY");
+  virtual ~cmFileCopier();
+
+  bool Run(std::vector<std::string> const& args);
+
+protected:
+  cmFileCommand* FileCommand;
+  cmMakefile* Makefile;
+  const char* Name;
+  bool Always;
+  cmFileTimeComparison FileTimes;
+
+  // Whether to install a file not matching any expression.
+  bool MatchlessFiles;
+
+  // Permissions for files and directories installed by this object.
+  mode_t FilePermissions;
+  mode_t DirPermissions;
+
+  // Properties set by pattern and regex match rules.
+  struct MatchProperties
+  {
+    bool Exclude = false;
+    mode_t Permissions = 0;
+  };
+  struct MatchRule
+  {
+    cmsys::RegularExpression Regex;
+    MatchProperties Properties;
+    std::string RegexString;
+    MatchRule(std::string const& regex)
+      : Regex(regex)
+      , RegexString(regex)
+    {
+    }
+  };
+  std::vector<MatchRule> MatchRules;
+
+  // Get the properties from rules matching this input file.
+  MatchProperties CollectMatchProperties(const std::string& file);
+
+  bool SetPermissions(const std::string& toFile, mode_t permissions);
+
+  // Translate an argument to a permissions bit.
+  bool CheckPermissions(std::string const& arg, mode_t& permissions);
+
+  bool InstallSymlink(const std::string& fromFile, const std::string& toFile);
+  bool InstallFile(const std::string& fromFile, const std::string& toFile,
+                   MatchProperties match_properties);
+  bool InstallDirectory(const std::string& source,
+                        const std::string& destination,
+                        MatchProperties match_properties);
+  virtual bool Install(const std::string& fromFile, const std::string& toFile);
+  virtual std::string const& ToName(std::string const& fromName);
+
+  enum Type
+  {
+    TypeFile,
+    TypeDir,
+    TypeLink
+  };
+  virtual void ReportCopy(const std::string&, Type, bool) {}
+  virtual bool ReportMissing(const std::string& fromFile);
+
+  MatchRule* CurrentMatchRule;
+  bool UseGivenPermissionsFile;
+  bool UseGivenPermissionsDir;
+  bool UseSourcePermissions;
+  std::string Destination;
+  std::string FilesFromDir;
+  std::vector<std::string> Files;
+  int Doing;
+
+  virtual bool Parse(std::vector<std::string> const& args);
+  enum
+  {
+    DoingNone,
+    DoingError,
+    DoingDestination,
+    DoingFilesFromDir,
+    DoingFiles,
+    DoingPattern,
+    DoingRegex,
+    DoingPermissionsFile,
+    DoingPermissionsDir,
+    DoingPermissionsMatch,
+    DoingLast1
+  };
+  virtual bool CheckKeyword(std::string const& arg);
+  virtual bool CheckValue(std::string const& arg);
+
+  void NotBeforeMatch(std::string const& arg);
+  void NotAfterMatch(std::string const& arg);
+  virtual void DefaultFilePermissions();
+  virtual void DefaultDirectoryPermissions();
+
+  bool GetDefaultDirectoryPermissions(mode_t** mode);
+};
+
+#endif

+ 350 - 0
Source/cmFileInstaller.cxx

@@ -0,0 +1,350 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmFileInstaller.h"
+
+#include "cmFSPermissions.h"
+#include "cmFileCommand.h"
+#include "cmMakefile.h"
+#include "cmSystemTools.h"
+
+#include "cm_sys_stat.h"
+
+#include <sstream>
+
+using namespace cmFSPermissions;
+
+cmFileInstaller::cmFileInstaller(cmFileCommand* command)
+  : cmFileCopier(command, "INSTALL")
+  , InstallType(cmInstallType_FILES)
+  , Optional(false)
+  , MessageAlways(false)
+  , MessageLazy(false)
+  , MessageNever(false)
+  , DestDirLength(0)
+{
+  // Installation does not use source permissions by default.
+  this->UseSourcePermissions = false;
+  // Check whether to copy files always or only if they have changed.
+  std::string install_always;
+  if (cmSystemTools::GetEnv("CMAKE_INSTALL_ALWAYS", install_always)) {
+    this->Always = cmSystemTools::IsOn(install_always);
+  }
+  // Get the current manifest.
+  this->Manifest =
+    this->Makefile->GetSafeDefinition("CMAKE_INSTALL_MANIFEST_FILES");
+}
+cmFileInstaller::~cmFileInstaller()
+{
+  // Save the updated install manifest.
+  this->Makefile->AddDefinition("CMAKE_INSTALL_MANIFEST_FILES",
+                                this->Manifest.c_str());
+}
+
+void cmFileInstaller::ManifestAppend(std::string const& file)
+{
+  if (!this->Manifest.empty()) {
+    this->Manifest += ";";
+  }
+  this->Manifest += file.substr(this->DestDirLength);
+}
+
+std::string const& cmFileInstaller::ToName(std::string const& fromName)
+{
+  return this->Rename.empty() ? fromName : this->Rename;
+}
+
+void cmFileInstaller::ReportCopy(const std::string& toFile, Type type,
+                                 bool copy)
+{
+  if (!this->MessageNever && (copy || !this->MessageLazy)) {
+    std::string message = (copy ? "Installing: " : "Up-to-date: ");
+    message += toFile;
+    this->Makefile->DisplayStatus(message, -1);
+  }
+  if (type != TypeDir) {
+    // Add the file to the manifest.
+    this->ManifestAppend(toFile);
+  }
+}
+bool cmFileInstaller::ReportMissing(const std::string& fromFile)
+{
+  return (this->Optional || this->cmFileCopier::ReportMissing(fromFile));
+}
+bool cmFileInstaller::Install(const std::string& fromFile,
+                              const std::string& toFile)
+{
+  // Support installing from empty source to make a directory.
+  if (this->InstallType == cmInstallType_DIRECTORY && fromFile.empty()) {
+    return this->InstallDirectory(fromFile, toFile, MatchProperties());
+  }
+  return this->cmFileCopier::Install(fromFile, toFile);
+}
+
+void cmFileInstaller::DefaultFilePermissions()
+{
+  this->cmFileCopier::DefaultFilePermissions();
+  // Add execute permissions based on the target type.
+  switch (this->InstallType) {
+    case cmInstallType_SHARED_LIBRARY:
+    case cmInstallType_MODULE_LIBRARY:
+      if (this->Makefile->IsOn("CMAKE_INSTALL_SO_NO_EXE")) {
+        break;
+      }
+      CM_FALLTHROUGH;
+    case cmInstallType_EXECUTABLE:
+    case cmInstallType_PROGRAMS:
+      this->FilePermissions |= mode_owner_execute;
+      this->FilePermissions |= mode_group_execute;
+      this->FilePermissions |= mode_world_execute;
+      break;
+    default:
+      break;
+  }
+}
+
+bool cmFileInstaller::Parse(std::vector<std::string> const& args)
+{
+  if (!this->cmFileCopier::Parse(args)) {
+    return false;
+  }
+
+  if (!this->Rename.empty()) {
+    if (!this->FilesFromDir.empty()) {
+      this->FileCommand->SetError("INSTALL option RENAME may not be "
+                                  "combined with FILES_FROM_DIR.");
+      return false;
+    }
+    if (this->InstallType != cmInstallType_FILES &&
+        this->InstallType != cmInstallType_PROGRAMS) {
+      this->FileCommand->SetError("INSTALL option RENAME may be used "
+                                  "only with FILES or PROGRAMS.");
+      return false;
+    }
+    if (this->Files.size() > 1) {
+      this->FileCommand->SetError("INSTALL option RENAME may be used "
+                                  "only with one file.");
+      return false;
+    }
+  }
+
+  if (!this->HandleInstallDestination()) {
+    return false;
+  }
+
+  if (((this->MessageAlways ? 1 : 0) + (this->MessageLazy ? 1 : 0) +
+       (this->MessageNever ? 1 : 0)) > 1) {
+    this->FileCommand->SetError("INSTALL options MESSAGE_ALWAYS, "
+                                "MESSAGE_LAZY, and MESSAGE_NEVER "
+                                "are mutually exclusive.");
+    return false;
+  }
+
+  return true;
+}
+
+bool cmFileInstaller::CheckKeyword(std::string const& arg)
+{
+  if (arg == "TYPE") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingType;
+    }
+  } else if (arg == "FILES") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingFiles;
+    }
+  } else if (arg == "RENAME") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingRename;
+    }
+  } else if (arg == "OPTIONAL") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingNone;
+      this->Optional = true;
+    }
+  } else if (arg == "MESSAGE_ALWAYS") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingNone;
+      this->MessageAlways = true;
+    }
+  } else if (arg == "MESSAGE_LAZY") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingNone;
+      this->MessageLazy = true;
+    }
+  } else if (arg == "MESSAGE_NEVER") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      this->Doing = DoingNone;
+      this->MessageNever = true;
+    }
+  } else if (arg == "PERMISSIONS") {
+    if (this->CurrentMatchRule) {
+      this->Doing = DoingPermissionsMatch;
+    } else {
+      // file(INSTALL) aliases PERMISSIONS to FILE_PERMISSIONS
+      this->Doing = DoingPermissionsFile;
+      this->UseGivenPermissionsFile = true;
+    }
+  } else if (arg == "DIR_PERMISSIONS") {
+    if (this->CurrentMatchRule) {
+      this->NotAfterMatch(arg);
+    } else {
+      // file(INSTALL) aliases DIR_PERMISSIONS to DIRECTORY_PERMISSIONS
+      this->Doing = DoingPermissionsDir;
+      this->UseGivenPermissionsDir = true;
+    }
+  } else if (arg == "COMPONENTS" || arg == "CONFIGURATIONS" ||
+             arg == "PROPERTIES") {
+    std::ostringstream e;
+    e << "INSTALL called with old-style " << arg << " argument.  "
+      << "This script was generated with an older version of CMake.  "
+      << "Re-run this cmake version on your build tree.";
+    this->FileCommand->SetError(e.str());
+    this->Doing = DoingError;
+  } else {
+    return this->cmFileCopier::CheckKeyword(arg);
+  }
+  return true;
+}
+
+bool cmFileInstaller::CheckValue(std::string const& arg)
+{
+  switch (this->Doing) {
+    case DoingType:
+      if (!this->GetTargetTypeFromString(arg)) {
+        this->Doing = DoingError;
+      }
+      break;
+    case DoingRename:
+      this->Rename = arg;
+      break;
+    default:
+      return this->cmFileCopier::CheckValue(arg);
+  }
+  return true;
+}
+
+bool cmFileInstaller::GetTargetTypeFromString(const std::string& stype)
+{
+  if (stype == "EXECUTABLE") {
+    this->InstallType = cmInstallType_EXECUTABLE;
+  } else if (stype == "FILE") {
+    this->InstallType = cmInstallType_FILES;
+  } else if (stype == "PROGRAM") {
+    this->InstallType = cmInstallType_PROGRAMS;
+  } else if (stype == "STATIC_LIBRARY") {
+    this->InstallType = cmInstallType_STATIC_LIBRARY;
+  } else if (stype == "SHARED_LIBRARY") {
+    this->InstallType = cmInstallType_SHARED_LIBRARY;
+  } else if (stype == "MODULE") {
+    this->InstallType = cmInstallType_MODULE_LIBRARY;
+  } else if (stype == "DIRECTORY") {
+    this->InstallType = cmInstallType_DIRECTORY;
+  } else {
+    std::ostringstream e;
+    e << "Option TYPE given unknown value \"" << stype << "\".";
+    this->FileCommand->SetError(e.str());
+    return false;
+  }
+  return true;
+}
+
+bool cmFileInstaller::HandleInstallDestination()
+{
+  std::string& destination = this->Destination;
+
+  // allow for / to be a valid destination
+  if (destination.size() < 2 && destination != "/") {
+    this->FileCommand->SetError("called with inappropriate arguments. "
+                                "No DESTINATION provided or .");
+    return false;
+  }
+
+  std::string sdestdir;
+  if (cmSystemTools::GetEnv("DESTDIR", sdestdir) && !sdestdir.empty()) {
+    cmSystemTools::ConvertToUnixSlashes(sdestdir);
+    char ch1 = destination[0];
+    char ch2 = destination[1];
+    char ch3 = 0;
+    if (destination.size() > 2) {
+      ch3 = destination[2];
+    }
+    int skip = 0;
+    if (ch1 != '/') {
+      int relative = 0;
+      if (((ch1 >= 'a' && ch1 <= 'z') || (ch1 >= 'A' && ch1 <= 'Z')) &&
+          ch2 == ':') {
+        // Assume windows
+        // let's do some destdir magic:
+        skip = 2;
+        if (ch3 != '/') {
+          relative = 1;
+        }
+      } else {
+        relative = 1;
+      }
+      if (relative) {
+        // This is relative path on unix or windows. Since we are doing
+        // destdir, this case does not make sense.
+        this->FileCommand->SetError(
+          "called with relative DESTINATION. This "
+          "does not make sense when using DESTDIR. Specify "
+          "absolute path or remove DESTDIR environment variable.");
+        return false;
+      }
+    } else {
+      if (ch2 == '/') {
+        // looks like a network path.
+        std::string message =
+          "called with network path DESTINATION. This "
+          "does not make sense when using DESTDIR. Specify local "
+          "absolute path or remove DESTDIR environment variable."
+          "\nDESTINATION=\n";
+        message += destination;
+        this->FileCommand->SetError(message);
+        return false;
+      }
+    }
+    destination = sdestdir + (destination.c_str() + skip);
+    this->DestDirLength = int(sdestdir.size());
+  }
+
+  // check if default dir creation permissions were set
+  mode_t default_dir_mode_v = 0;
+  mode_t* default_dir_mode = &default_dir_mode_v;
+  if (!this->GetDefaultDirectoryPermissions(&default_dir_mode)) {
+    return false;
+  }
+
+  if (this->InstallType != cmInstallType_DIRECTORY) {
+    if (!cmSystemTools::FileExists(destination)) {
+      if (!cmSystemTools::MakeDirectory(destination, default_dir_mode)) {
+        std::string errstring = "cannot create directory: " + destination +
+          ". Maybe need administrative privileges.";
+        this->FileCommand->SetError(errstring);
+        return false;
+      }
+    }
+    if (!cmSystemTools::FileIsDirectory(destination)) {
+      std::string errstring =
+        "INSTALL destination: " + destination + " is not a directory.";
+      this->FileCommand->SetError(errstring);
+      return false;
+    }
+  }
+  return true;
+}

+ 55 - 0
Source/cmFileInstaller.h

@@ -0,0 +1,55 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#ifndef cmFileInstaller_h
+#define cmFileInstaller_h
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include "cmFileCopier.h"
+
+#include "cmInstallType.h"
+
+#include <string>
+#include <vector>
+
+class cmFileCommand;
+
+struct cmFileInstaller : public cmFileCopier
+{
+  cmFileInstaller(cmFileCommand* command);
+  ~cmFileInstaller() override;
+
+protected:
+  cmInstallType InstallType;
+  bool Optional;
+  bool MessageAlways;
+  bool MessageLazy;
+  bool MessageNever;
+  int DestDirLength;
+  std::string Rename;
+
+  std::string Manifest;
+  void ManifestAppend(std::string const& file);
+
+  std::string const& ToName(std::string const& fromName) override;
+
+  void ReportCopy(const std::string& toFile, Type type, bool copy) override;
+  bool ReportMissing(const std::string& fromFile) override;
+  bool Install(const std::string& fromFile,
+               const std::string& toFile) override;
+
+  bool Parse(std::vector<std::string> const& args) override;
+  enum
+  {
+    DoingType = DoingLast1,
+    DoingRename,
+    DoingLast2
+  };
+  bool CheckKeyword(std::string const& arg) override;
+  bool CheckValue(std::string const& arg) override;
+  void DefaultFilePermissions() override;
+  bool GetTargetTypeFromString(const std::string& stype);
+  bool HandleInstallDestination();
+};
+
+#endif

+ 2 - 0
bootstrap

@@ -302,6 +302,8 @@ CMAKE_CXX_SOURCES="\
   cmExprParserHelper \
   cmExternalMakefileProjectGenerator \
   cmFileCommand \
+  cmFileCopier \
+  cmFileInstaller \
   cmFileTimeComparison \
   cmFindBase \
   cmFindCommon \