From d2e1f2b4d6d87c171464b5dc41b00b609c90bf26 Mon Sep 17 00:00:00 2001
From: Brad King <brad.king@kitware.com>
Date: Tue, 11 Aug 2009 09:54:56 -0400
Subject: Introduce "generator expressions" to add_test()

This introduces a new syntax called "generator expressions" to the test
COMMAND option of the add_test(NAME) command mode.  These expressions
have a syntax like $<TARGET_FILE:mytarget> and are evaluated during
build system generation.  This syntax allows per-configuration target
output files to be referenced in test commands and arguments.
---
 Source/CMakeLists.txt            |   2 +
 Source/cmAddTestCommand.h        |  23 +++++
 Source/cmGeneratorExpression.cxx | 196 +++++++++++++++++++++++++++++++++++++++
 Source/cmGeneratorExpression.h   |  55 +++++++++++
 Source/cmTestGenerator.cxx       |   9 +-
 bootstrap                        |   1 +
 6 files changed, 284 insertions(+), 2 deletions(-)
 create mode 100644 Source/cmGeneratorExpression.cxx
 create mode 100644 Source/cmGeneratorExpression.h

diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index 6474cb0..01d3976 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -159,6 +159,8 @@ SET(SRCS
   cmFileTimeComparison.cxx
   cmFileTimeComparison.h
   cmGeneratedFileStream.cxx
+  cmGeneratorExpression.cxx
+  cmGeneratorExpression.h
   cmGlobalGenerator.cxx
   cmGlobalGenerator.h
   cmGlobalUnixMakefileGenerator3.cxx
diff --git a/Source/cmAddTestCommand.h b/Source/cmAddTestCommand.h
index a971fd7..0b9ca3c 100644
--- a/Source/cmAddTestCommand.h
+++ b/Source/cmAddTestCommand.h
@@ -79,6 +79,29 @@ public:
       "of the executable created at build time.  "
       "If a CONFIGURATIONS option is given then the test will be executed "
       "only when testing under one of the named configurations."
+      "\n"
+      "Arguments after COMMAND may use \"generator expressions\" with the "
+      "syntax \"$<...>\".  "
+      "These expressions are evaluted during build system generation and "
+      "produce information specific to each generated build configuration.  "
+      "Valid expressions are:\n"
+      "  $<CONFIGURATION>          = configuration name\n"
+      "  $<TARGET_FILE:tgt>        = main file (.exe, .so.1.2, .a)\n"
+      "  $<TARGET_LINKER_FILE:tgt> = file used to link (.a, .lib, .so)\n"
+      "  $<TARGET_SONAME_FILE:tgt> = file with soname (.so.3)\n"
+      "where \"tgt\" is the name of a target.  "
+      "Target file expressions produce a full path, but _DIR and _NAME "
+      "versions can produce the directory and file name components:\n"
+      "  $<TARGET_FILE_DIR:tgt>/$<TARGET_FILE_NAME:tgt>\n"
+      "  $<TARGET_LINKER_FILE_DIR:tgt>/$<TARGET_LINKER_FILE_NAME:tgt>\n"
+      "  $<TARGET_SONAME_FILE_DIR:tgt>/$<TARGET_SONAME_FILE_NAME:tgt>\n"
+      "Example usage:\n"
+      "  add_test(NAME mytest\n"
+      "           COMMAND testDriver --config $<CONFIGURATION>\n"
+      "                              --exe $<TARGET_FILE:myexe>)\n"
+      "This creates a test \"mytest\" whose command runs a testDriver "
+      "tool passing the configuration name and the full path to the "
+      "executable file produced by target \"myexe\"."
       ;
     }
   
diff --git a/Source/cmGeneratorExpression.cxx b/Source/cmGeneratorExpression.cxx
new file mode 100644
index 0000000..5c409f7
--- /dev/null
+++ b/Source/cmGeneratorExpression.cxx
@@ -0,0 +1,196 @@
+/*=========================================================================
+
+  Program:   CMake - Cross-Platform Makefile Generator
+  Module:    $RCSfile$
+  Language:  C++
+  Date:      $Date$
+  Version:   $Revision$
+
+  Copyright (c) 2002 Kitware, Inc., Insight Consortium.  All rights reserved.
+  See Copyright.txt or http://www.cmake.org/HTML/Copyright.html for details.
+
+     This software is distributed WITHOUT ANY WARRANTY; without even
+     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+     PURPOSE.  See the above copyright notices for more information.
+
+=========================================================================*/
+#include "cmGeneratorExpression.h"
+
+#include "cmMakefile.h"
+#include "cmTarget.h"
+
+//----------------------------------------------------------------------------
+cmGeneratorExpression::cmGeneratorExpression(
+  cmMakefile* mf, const char* config,
+  cmListFileBacktrace const& backtrace):
+  Makefile(mf), Config(config), Backtrace(backtrace)
+{
+  this->TargetInfo.compile("^\\$<TARGET"
+                           "(|_SONAME|_LINKER)"  // File with what purpose?
+                           "_FILE(|_NAME|_DIR):" // Filename component.
+                           "([A-Za-z0-9_-]+)"    // Target name.
+                           ">$");
+}
+
+//----------------------------------------------------------------------------
+const char* cmGeneratorExpression::Process(std::string const& input)
+{
+  return this->Process(input.c_str());
+}
+
+//----------------------------------------------------------------------------
+const char* cmGeneratorExpression::Process(const char* input)
+{
+  this->Data.clear();
+
+  // We construct and evaluate expressions directly in the output
+  // buffer.  Each expression is replaced by its own output value
+  // after evaluation.  A stack of barriers records the starting
+  // indices of open (pending) expressions.
+  for(const char* c = input; *c; ++c)
+    {
+    if(c[0] == '$' && c[1] == '<')
+      {
+      this->Barriers.push(this->Data.size());
+      this->Data.push_back('$');
+      this->Data.push_back('<');
+      c += 1;
+      }
+    else if(c[0] == '>' && !this->Barriers.empty())
+      {
+      this->Data.push_back('>');
+      if(!this->Evaluate()) { break; }
+      this->Barriers.pop();
+      }
+    else
+      {
+      this->Data.push_back(c[0]);
+      }
+    }
+
+  // Return a null-terminated output value.
+  this->Data.push_back('\0');
+  return &*this->Data.begin();
+}
+
+//----------------------------------------------------------------------------
+bool cmGeneratorExpression::Evaluate()
+{
+  // The top-most barrier points at the beginning of the expression.
+  size_t barrier = this->Barriers.top();
+
+  // Construct a null-terminated representation of the expression.
+  this->Data.push_back('\0');
+  const char* expr = &*(this->Data.begin()+barrier);
+
+  // Evaluate the expression.
+  std::string result;
+  if(this->Evaluate(expr, result))
+    {
+    // Success.  Replace the expression with its evaluation result.
+    this->Data.erase(this->Data.begin()+barrier, this->Data.end());
+    this->Data.insert(this->Data.end(), result.begin(), result.end());
+    return true;
+    }
+  else
+    {
+    // Failure.  Report the error message.
+    cmOStringStream e;
+    e << "Error evaluating generator expression:\n"
+      << "  " << expr << "\n"
+      << result;
+    this->Makefile->GetCMakeInstance()
+      ->IssueMessage(cmake::FATAL_ERROR, e.str().c_str(),
+                     this->Backtrace);
+    return false;
+    }
+}
+
+//----------------------------------------------------------------------------
+bool cmGeneratorExpression::Evaluate(const char* expr, std::string& result)
+{
+  if(this->TargetInfo.find(expr))
+    {
+    if(!this->EvaluateTargetInfo(result))
+      {
+      return false;
+      }
+    }
+  else if(strcmp(expr, "$<CONFIGURATION>") == 0)
+    {
+    result = this->Config? this->Config : "";
+    }
+  else
+    {
+    result = "Expression syntax not recognized.";
+    return false;
+    }
+  return true;
+}
+
+//----------------------------------------------------------------------------
+bool cmGeneratorExpression::EvaluateTargetInfo(std::string& result)
+{
+  // Lookup the referenced target.
+  std::string name = this->TargetInfo.match(3);
+  cmTarget* target = this->Makefile->FindTargetToUse(name.c_str());
+  if(!target)
+    {
+    result = "No target \"" + name + "\"";
+    return false;
+    }
+  if(target->GetType() >= cmTarget::UTILITY &&
+     target->GetType() != cmTarget::UNKNOWN_LIBRARY)
+    {
+    result = "Target \"" + name + "\" is not an executable or library.";
+    return false;
+    }
+
+  // Lookup the target file with the given purpose.
+  std::string purpose = this->TargetInfo.match(1);
+  if(purpose == "")
+    {
+    // The target implementation file (.so.1.2, .dll, .exe, .a).
+    result = target->GetFullPath(this->Config, false, true);
+    }
+  else if(purpose == "_LINKER")
+    {
+    // The file used to link to the target (.so, .lib, .a).
+    if(!target->IsLinkable())
+      {
+      result = ("TARGET_LINKER_FILE is allowed only for libraries and "
+                "executables with ENABLE_EXPORTS.");
+      return false;
+      }
+    result = target->GetFullPath(this->Config, target->HasImportLibrary());
+    }
+  else if(purpose == "_SONAME")
+    {
+    // The target soname file (.so.1).
+    if(target->IsDLLPlatform())
+      {
+      result = "TARGET_SONAME_FILE is not allowed for DLL target platforms.";
+      return false;
+      }
+    if(target->GetType() != cmTarget::SHARED_LIBRARY)
+      {
+      result = "TARGET_SONAME_FILE is allowed only for SHARED libraries.";
+      return false;
+      }
+    result = target->GetDirectory(this->Config);
+    result += "/";
+    result += target->GetSOName(this->Config);
+    }
+
+  // Extract the requested portion of the full path.
+  std::string part = this->TargetInfo.match(2);
+  if(part == "_NAME")
+    {
+    result = cmSystemTools::GetFilenameName(result);
+    }
+  else if(part == "_DIR")
+    {
+    result = cmSystemTools::GetFilenamePath(result);
+    }
+  return true;
+}
diff --git a/Source/cmGeneratorExpression.h b/Source/cmGeneratorExpression.h
new file mode 100644
index 0000000..05a697f
--- /dev/null
+++ b/Source/cmGeneratorExpression.h
@@ -0,0 +1,55 @@
+/*=========================================================================
+
+  Program:   CMake - Cross-Platform Makefile Generator
+  Module:    $RCSfile$
+  Language:  C++
+  Date:      $Date$
+  Version:   $Revision$
+
+  Copyright (c) 2002 Kitware, Inc., Insight Consortium.  All rights reserved.
+  See Copyright.txt or http://www.cmake.org/HTML/Copyright.html for details.
+
+     This software is distributed WITHOUT ANY WARRANTY; without even
+     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+     PURPOSE.  See the above copyright notices for more information.
+
+=========================================================================*/
+#include "cmStandardIncludes.h"
+
+#include <stack>
+
+#include <cmsys/RegularExpression.hxx>
+
+class cmMakefile;
+class cmListFileBacktrace;
+
+/** \class cmGeneratorExpression
+ * \brief Evaluate generate-time query expression syntax.
+ *
+ * cmGeneratorExpression instances are used by build system generator
+ * implementations to evaluate the $<> generator expression syntax.
+ * Generator expressions are evaluated just before the generate step
+ * writes strings into the build system.  They have knowledge of the
+ * build configuration which is not available at configure time.
+ */
+class cmGeneratorExpression
+{
+public:
+  /** Construct with an evaluation context and configuration.  */
+  cmGeneratorExpression(cmMakefile* mf, const char* config,
+                        cmListFileBacktrace const& backtrace);
+
+  /** Evaluate generator expressions in a string.  */
+  const char* Process(std::string const& input);
+  const char* Process(const char* input);
+private:
+  cmMakefile* Makefile;
+  const char* Config;
+  cmListFileBacktrace const& Backtrace;
+  std::vector<char> Data;
+  std::stack<size_t> Barriers;
+  cmsys::RegularExpression TargetInfo;
+  bool Evaluate();
+  bool Evaluate(const char* expr, std::string& result);
+  bool EvaluateTargetInfo(std::string& result);
+};
diff --git a/Source/cmTestGenerator.cxx b/Source/cmTestGenerator.cxx
index 2c98910..866ffd3 100644
--- a/Source/cmTestGenerator.cxx
+++ b/Source/cmTestGenerator.cxx
@@ -16,6 +16,7 @@
 =========================================================================*/
 #include "cmTestGenerator.h"
 
+#include "cmGeneratorExpression.h"
 #include "cmLocalGenerator.h"
 #include "cmMakefile.h"
 #include "cmSystemTools.h"
@@ -94,6 +95,10 @@ void cmTestGenerator::GenerateScriptForConfig(std::ostream& os,
 {
   this->TestGenerated = true;
 
+  // Set up generator expression evaluation context.
+  cmMakefile* mf = this->Test->GetMakefile();
+  cmGeneratorExpression ge(mf, config, this->Test->GetBacktrace());
+
   // Start the test command.
   os << indent << "ADD_TEST(" << this->Test->GetName() << " ";
 
@@ -103,7 +108,6 @@ void cmTestGenerator::GenerateScriptForConfig(std::ostream& os,
   // Check whether the command executable is a target whose name is to
   // be translated.
   std::string exe = command[0];
-  cmMakefile* mf = this->Test->GetMakefile();
   cmTarget* target = mf->FindTargetToUse(exe.c_str());
   if(target && target->GetType() == cmTarget::EXECUTABLE)
     {
@@ -113,6 +117,7 @@ void cmTestGenerator::GenerateScriptForConfig(std::ostream& os,
   else
     {
     // Use the command name given.
+    exe = ge.Process(exe.c_str());
     cmSystemTools::ConvertToUnixSlashes(exe);
     }
 
@@ -122,7 +127,7 @@ void cmTestGenerator::GenerateScriptForConfig(std::ostream& os,
   for(std::vector<std::string>::const_iterator ci = command.begin()+1;
       ci != command.end(); ++ci)
     {
-    os << " " << lg->EscapeForCMake(ci->c_str());
+    os << " " << lg->EscapeForCMake(ge.Process(*ci));
     }
 
   // Finish the test command.
diff --git a/bootstrap b/bootstrap
index 4710eca..0c21c77 100755
--- a/bootstrap
+++ b/bootstrap
@@ -188,6 +188,7 @@ CMAKE_CXX_SOURCES="\
   cmExportInstallFileGenerator \
   cmInstallDirectoryGenerator \
   cmGeneratedFileStream \
+  cmGeneratorExpression \
   cmGlobalGenerator \
   cmLocalGenerator \
   cmInstallGenerator \
-- 
cgit v0.12