From c58ca242865118d65b5add11a256832d177afe6d Mon Sep 17 00:00:00 2001
From: Brad King <brad.king@kitware.com>
Date: Wed, 29 Apr 2009 13:13:29 -0400
Subject: ENH: Create file(COPY) command signature

The file(INSTALL) command has long been undocumented and used only to
implement install() scripts.  We now document it and provide a similar
file(COPY) signature which is useful in general-purpose scripts.  It
provides the capabilities of install(DIRECTORY) and install(FILES) but
operates immediately instead of contributing to install scripts.
---
 Source/cmFileCommand.cxx            | 122 +++++++++++++++++++++++++++++++-----
 Source/cmFileCommand.h              |  38 ++++++++++-
 Tests/StringFileTest/CMakeLists.txt |  11 +++-
 3 files changed, 155 insertions(+), 16 deletions(-)

diff --git a/Source/cmFileCommand.cxx b/Source/cmFileCommand.cxx
index c7348b7..cceea97 100644
--- a/Source/cmFileCommand.cxx
+++ b/Source/cmFileCommand.cxx
@@ -112,6 +112,10 @@ bool cmFileCommand
     {
     return this->HandleRemove(args, true);
     }
+  else if ( subCommand == "COPY" )
+    {
+    return this->HandleCopyCommand(args);
+    }
   else if ( subCommand == "INSTALL" )
     {
     return this->HandleInstallCommand(args);
@@ -903,9 +907,10 @@ cmFileCommand::HandleDifferentCommand(std::vector<std::string> const& args)
 // File installation helper class.
 struct cmFileCopier
 {
-  cmFileCopier(cmFileCommand* command):
+  cmFileCopier(cmFileCommand* command, const char* name = "COPY"):
     FileCommand(command),
     Makefile(command->GetMakefile()),
+    Name(name),
     Always(false),
     MatchlessFiles(true),
     FilePermissions(0),
@@ -913,7 +918,7 @@ struct cmFileCopier
     CurrentMatchRule(0),
     UseGivenPermissionsFile(false),
     UseGivenPermissionsDir(false),
-    UseSourcePermissions(false),
+    UseSourcePermissions(true),
     Doing(DoingNone)
     {
     }
@@ -923,6 +928,7 @@ protected:
 
   cmFileCommand* FileCommand;
   cmMakefile* Makefile;
+  const char* Name;
   bool Always;
   cmFileTimeComparison FileTimes;
 
@@ -984,7 +990,7 @@ protected:
     if(permissions && !cmSystemTools::SetPermissions(toFile, permissions))
       {
       cmOStringStream e;
-      e << "INSTALL cannot set permissions on \"" << toFile << "\"";
+      e << this->Name << " cannot set permissions on \"" << toFile << "\"";
       this->FileCommand->SetError(e.str().c_str());
       return false;
       }
@@ -1008,7 +1014,7 @@ protected:
     else
       {
       cmOStringStream e;
-      e << "INSTALL given invalid permission \"" << arg << "\".";
+      e << this->Name << " given invalid permission \"" << arg << "\".";
       this->FileCommand->SetError(e.str().c_str());
       return false;
       }
@@ -1035,7 +1041,7 @@ protected:
     {
     // The input file does not exist and installation is not optional.
     cmOStringStream e;
-    e << "INSTALL cannot find \"" << fromFile << "\".";
+    e << this->Name << " cannot find \"" << fromFile << "\".";
     this->FileCommand->SetError(e.str().c_str());
     return false;
     }
@@ -1055,6 +1061,7 @@ protected:
     DoingError,
     DoingDestination,
     DoingFiles,
+    DoingPattern,
     DoingRegex,
     DoingPermissionsFile,
     DoingPermissionsDir,
@@ -1067,14 +1074,14 @@ protected:
   void NotBeforeMatch(std::string const& arg)
     {
     cmOStringStream e;
-    e << "option " << arg << " may not appear before REGEX.";
+    e << "option " << arg << " may not appear before PATTERN or REGEX.";
     this->FileCommand->SetError(e.str().c_str());
     this->Doing = DoingError;
     }
   void NotAfterMatch(std::string const& arg)
     {
     cmOStringStream e;
-    e << "option " << arg << " may not appear after REGEX.";
+    e << "option " << arg << " may not appear after PATTERN or REGEX.";
     this->FileCommand->SetError(e.str().c_str());
     this->Doing = DoingError;
     }
@@ -1128,7 +1135,7 @@ bool cmFileCopier::Parse(std::vector<std::string> const& args)
   if(this->Destination.empty())
     {
     cmOStringStream e;
-    e << "INSTALL given no DESTINATION";
+    e << this->Name << " given no DESTINATION";
     this->FileCommand->SetError(e.str().c_str());
     return false;
     }
@@ -1162,6 +1169,10 @@ bool cmFileCopier::CheckKeyword(std::string const& arg)
       this->Doing = DoingDestination;
       }
     }
+  else if(arg == "PATTERN")
+    {
+    this->Doing = DoingPattern;
+    }
   else if(arg == "REGEX")
     {
     this->Doing = DoingRegex;
@@ -1179,6 +1190,41 @@ bool cmFileCopier::CheckKeyword(std::string const& arg)
       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)
@@ -1191,6 +1237,18 @@ bool cmFileCopier::CheckKeyword(std::string const& arg)
       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)
@@ -1239,6 +1297,30 @@ bool cmFileCopier::CheckValue(std::string const& arg)
         }
       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.push_back(MatchRule(regex));
+      this->CurrentMatchRule = &*(this->MatchRules.end()-1);
+      if(this->CurrentMatchRule->Regex.is_valid())
+        {
+        this->Doing = DoingNone;
+        }
+      else
+        {
+        cmOStringStream e;
+        e << "could not compile PATTERN \"" << arg << "\".";
+        this->FileCommand->SetError(e.str().c_str());
+        this->Doing = DoingError;
+        }
+      }
+      break;
     case DoingRegex:
       this->MatchRules.push_back(MatchRule(arg));
       this->CurrentMatchRule = &*(this->MatchRules.end()-1);
@@ -1370,7 +1452,7 @@ bool cmFileCopier::InstallSymlink(const char* fromFile, const char* toFile)
   if(!cmSystemTools::ReadSymlink(fromFile, symlinkTarget))
     {
     cmOStringStream e;
-    e << "INSTALL cannot read symlink \"" << fromFile
+    e << this->Name << " cannot read symlink \"" << fromFile
       << "\" to duplicate at \"" << toFile << "\".";
     this->FileCommand->SetError(e.str().c_str());
     return false;
@@ -1403,7 +1485,7 @@ bool cmFileCopier::InstallSymlink(const char* fromFile, const char* toFile)
     if(!cmSystemTools::CreateSymlink(symlinkTarget.c_str(), toFile))
       {
       cmOStringStream e;
-      e << "INSTALL cannot duplicate symlink \"" << fromFile
+      e << this->Name <<  " cannot duplicate symlink \"" << fromFile
         << "\" at \"" << toFile << "\".";
       this->FileCommand->SetError(e.str().c_str());
       return false;
@@ -1435,7 +1517,7 @@ bool cmFileCopier::InstallFile(const char* fromFile, const char* toFile,
   if(copy && !cmSystemTools::CopyAFile(fromFile, toFile, true, false))
     {
     cmOStringStream e;
-    e << "INSTALL cannot copy file \"" << fromFile
+    e << this->Name << " cannot copy file \"" << fromFile
       << "\" to \"" << toFile << "\".";
     this->FileCommand->SetError(e.str().c_str());
     return false;
@@ -1447,7 +1529,8 @@ bool cmFileCopier::InstallFile(const char* fromFile, const char* toFile,
     if (!cmSystemTools::CopyFileTime(fromFile, toFile))
       {
       cmOStringStream e;
-      e << "INSTALL cannot set modification time on \"" << toFile << "\"";
+      e << this->Name << " cannot set modification time on \""
+        << toFile << "\"";
       this->FileCommand->SetError(e.str().c_str());
       return false;
       }
@@ -1477,7 +1560,7 @@ bool cmFileCopier::InstallDirectory(const char* source,
   if(!cmSystemTools::MakeDirectory(destination))
     {
     cmOStringStream e;
-    e << "INSTALL cannot make directory \"" << destination << "\": "
+    e << this->Name << " cannot make directory \"" << destination << "\": "
       << cmSystemTools::GetLastSystemError();
     this->FileCommand->SetError(e.str().c_str());
     return false;
@@ -1549,14 +1632,23 @@ bool cmFileCopier::InstallDirectory(const char* source,
 }
 
 //----------------------------------------------------------------------------
+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),
+    cmFileCopier(command, "INSTALL"),
     InstallType(cmTarget::INSTALL_FILES),
     Optional(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.
     this->Always =
       cmSystemTools::IsOn(cmSystemTools::GetEnv("CMAKE_INSTALL_ALWAYS"));
@@ -1736,6 +1828,7 @@ bool cmFileInstaller::CheckKeyword(std::string const& arg)
       }
     else
       {
+      // file(INSTALL) aliases PERMISSIONS to FILE_PERMISSIONS
       this->Doing = DoingPermissionsFile;
       this->UseGivenPermissionsFile = true;
       }
@@ -1748,6 +1841,7 @@ bool cmFileInstaller::CheckKeyword(std::string const& arg)
       }
     else
       {
+      // file(INSTALL) aliases DIR_PERMISSIONS to DIRECTORY_PERMISSIONS
       this->Doing = DoingPermissionsDir;
       this->UseGivenPermissionsDir = true;
       }
diff --git a/Source/cmFileCommand.h b/Source/cmFileCommand.h
index 04bb661..cadb1a9 100644
--- a/Source/cmFileCommand.h
+++ b/Source/cmFileCommand.h
@@ -157,7 +157,42 @@ public:
       "and the second element is a string value for the error. A 0 "
       "numeric error means no error in the operation. "
       "If TIMEOUT time is specified, the operation will "
-      "timeout after time seconds, time can be specified as a float.\n";
+      "timeout after time seconds, time can be specified as a float."
+      "\n"
+      "The file() command also provides COPY and INSTALL signatures:\n"
+      "  file(<COPY|INSTALL> files... DESTINATION <dir>\n"
+      "       [FILE_PERMISSIONS permissions...]\n"
+      "       [DIRECTORY_PERMISSIONS permissions...]\n"
+      "       [NO_SOURCE_PERMISSIONS] [USE_SOURCE_PERMISSIONS]\n"
+      "       [FILES_MATCHING]\n"
+      "       [[PATTERN <pattern> | REGEX <regex>]\n"
+      "        [EXCLUDE] [PERMISSIONS permissions...]] [...])\n"
+      "The COPY signature copies files, directories, and symlinks to a "
+      "destination folder.  "
+      "Relative input paths are evaluated with respect to the current "
+      "source directory, and a relative destination is evaluated with "
+      "respect to the current build directory.  "
+      "Copying preserves input file timestamps, and optimizes out a file "
+      "if it exists at the destination with the same timestamp.  "
+      "Copying preserves input permissions unless explicit permissions or "
+      "NO_SOURCE_PERMISSIONS are given (default is USE_SOURCE_PERMISSIONS).  "
+      "See the install(DIRECTORY) command for documentation of permissions, "
+      "PATTERN, REGEX, and EXCLUDE options.  "
+      "\n"
+      "The INSTALL signature differs slightly from COPY: "
+      "it prints status messages, and NO_SOURCE_PERMISSIONS is default.  "
+      "Installation scripts generated by the install() command use this "
+      "signature (with some undocumented options for internal use)."
+      // Undocumented INSTALL options:
+      //  - RENAME <name>
+      //  - OPTIONAL
+      //  - FILES keyword to re-enter files... list
+      //  - PERMISSIONS before REGEX is alias for FILE_PERMISSIONS
+      //  - DIR_PERMISSIONS is alias for DIRECTORY_PERMISSIONS
+      //  - TYPE <FILE|DIRECTORY|EXECUTABLE|PROGRAM|
+      //          STATIC_LIBRARY|SHARED_LIBRARY|MODULE>
+      //  - COMPONENTS, CONFIGURATIONS, PROPERTIES (ignored for compat)
+      ;
     }
 
   cmTypeMacro(cmFileCommand, cmCommand);
@@ -179,6 +214,7 @@ protected:
   bool HandleRPathRemoveCommand(std::vector<std::string> const& args);
   bool HandleDifferentCommand(std::vector<std::string> const& args);
 
+  bool HandleCopyCommand(std::vector<std::string> const& args);
   bool HandleInstallCommand(std::vector<std::string> const& args);
   bool HandleDownloadCommand(std::vector<std::string> const& args);
 };
diff --git a/Tests/StringFileTest/CMakeLists.txt b/Tests/StringFileTest/CMakeLists.txt
index b50d203..398243c 100644
--- a/Tests/StringFileTest/CMakeLists.txt
+++ b/Tests/StringFileTest/CMakeLists.txt
@@ -188,10 +188,19 @@ FILE(WRITE "${CMAKE_CURRENT_BINARY_DIR}/OutputFile.h-tmp" "${outfile}")
 FILE(RENAME "${CMAKE_CURRENT_BINARY_DIR}/OutputFile.h-tmp"
             "${CMAKE_CURRENT_BINARY_DIR}/OutputFile.h")
 
+# Test file copy with relative paths
+FILE(COPY .
+  DESTINATION src
+  FILE_PERMISSIONS OWNER_READ OWNER_WRITE
+  DIRECTORY_PERMISSIONS OWNER_READ OWNER_EXECUTE # test no OWNER_WRITE
+  FILES_MATCHING PATTERN *.cxx # Only copy the main source file
+  REGEX /src$ EXCLUDE # Block recursion for in-source build
+  )
+
 # Test file glob
 FILE(GLOB_RECURSE src_files "${CMAKE_CURRENT_SOURCE_DIR}/*")
 MESSAGE("Files in ${CMAKE_CURRENT_SOURCE_DIR} are ${src_files}")
-SET(expr "${CMAKE_CURRENT_SOURCE_DIR}/[sS][!a-su-zA-Z0-9][^a-qs-zA-Z0-9]ing?ile*.cxx")
+SET(expr "${CMAKE_CURRENT_BINARY_DIR}/src/[sS][!a-su-zA-Z0-9][^a-qs-zA-Z0-9]ing?ile*.cxx")
 MESSAGE("Glob expression is [${expr}].")
 FILE(GLOB src_files "${expr}")
 MESSAGE("Globbed files [${src_files}].")
-- 
cgit v0.12