Move binary directory search to ProgramInfo.
authorTeemu Murtola <teemu.murtola@gmail.com>
Mon, 1 Jul 2013 05:05:54 +0000 (08:05 +0300)
committerGerrit Code Review <gerrit@gerrit.gromacs.org>
Sun, 17 Nov 2013 18:15:53 +0000 (19:15 +0100)
This makes it possible to print the real executable location in the
startup header.  Probably useful diagnostic if one has multiple Gromacs
installations in different paths.  Also, now the search for the path is
done at most once per invocation, and not for each library file.

This is mainly moving code around and converting it to C++:
- Part of get_libdir() gets moved into gmx::ProgramInfo, and the
  remainder is now using C++.
- Move gmx_is_file() to gmx::File, since it is only used in the code
  moved to gmx::ProgramInfo.

There are some minor behavior changes:
- The search now works correctly if PATH contains an empty entry
  (i.e., the working directory is in the path).
- If PATH does not contain the working directory explicity, the search
  only considers the working directory on Windows.
- If the working directory has been changed in code (but not before call
  to gmx::init()), the search still works.

Change-Id: If1d548be6b4dd031617a6019bcad20466ad91994

13 files changed:
src/gromacs/fileio/futil.cpp
src/gromacs/gmxlib/copyrite.cpp
src/gromacs/gmxlib/oenv.cpp
src/gromacs/gmxlib/statutil.cpp
src/gromacs/utility/.gitignore
src/gromacs/utility/file.cpp
src/gromacs/utility/file.h
src/gromacs/utility/path.cpp
src/gromacs/utility/path.h
src/gromacs/utility/programinfo.cpp
src/gromacs/utility/programinfo.h
src/gromacs/utility/tests/CMakeLists.txt
src/gromacs/utility/tests/programinfo.cpp [new file with mode: 0644]

index 8f571e12bf8f545d09b3a5e836332dbcd39882dd..c12aa5b3dc72e5bd5393632ae5680eac40fb0a11 100644 (file)
  * the research papers on the package. Check out http://www.gromacs.org.
  */
 #ifdef HAVE_CONFIG_H
-#include <config.h>
+#include "config.h"
 #endif
 
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
 #include <io.h>
 #endif
 
-#include "sysstuff.h"
-#include "string2.h"
-#include "futil.h"
-#include "network.h"
-#include "gmx_fatal.h"
-#include "smalloc.h"
-#include "statutil.h"
+/* Windows file stuff, only necessary for visual studio */
+#ifdef _MSC_VER
+#include <windows.h>
+#endif
 
+#include "gromacs/legacyheaders/gmx_fatal.h"
+#include "gromacs/legacyheaders/network.h"
+#include "gromacs/legacyheaders/smalloc.h"
+#include "gromacs/legacyheaders/string2.h"
 
-#ifdef GMX_THREAD_MPI
-#include "thread_mpi.h"
-#endif
+#include "gromacs/fileio/futil.h"
+#include "gromacs/utility/exceptions.h"
+#include "gromacs/utility/path.h"
+#include "gromacs/utility/programinfo.h"
+#include "gromacs/utility/stringutil.h"
 
-/* Windows file stuff, only necessary for visual studio */
-#ifdef _MSC_VER
-#include "windows.h"
+#ifdef GMX_THREAD_MPI
+#include "thread_mpi/threads.h"
 #endif
 
 /* we keep a linked list of all files opened through pipes (i.e.
@@ -347,38 +350,6 @@ gmx_bool gmx_fexist(const char *fname)
     }
 }
 
-static gmx_bool gmx_is_file(const char *fname)
-{
-    FILE *test;
-
-    if (fname == NULL)
-    {
-        return FALSE;
-    }
-    test = fopen(fname, "r");
-    if (test == NULL)
-    {
-        return FALSE;
-    }
-    else
-    {
-        fclose(test);
-        /*Windows doesn't allow fopen of directory - so we don't need to check this seperately */
-        #if (!((defined WIN32 || defined _WIN32 || defined WIN64 || defined _WIN64) && !defined __CYGWIN__ && !defined __CYGWIN32__))
-        {
-            int         status;
-            struct stat st_buf;
-            status = stat (fname, &st_buf);
-            if (status != 0 || !S_ISREG(st_buf.st_mode))
-            {
-                return FALSE;
-            }
-        }
-        #endif
-        return TRUE;
-    }
-}
-
 
 gmx_bool gmx_fexist_master(const char *fname, t_commrec *cr)
 {
@@ -815,144 +786,48 @@ static gmx_bool search_subdirs(const char *parent, char *libdir)
 }
 
 
-/* Check if the program name begins with "/" on unix/cygwin, or
- * with "\" or "X:\" on windows. If not, the program name
- * is relative to the current directory.
- */
-static gmx_bool filename_is_absolute(char *name)
-{
-#ifdef GMX_NATIVE_WINDOWS
-    return ((name[0] == DIR_SEPARATOR) || ((strlen(name) > 3) && strncmp(name+1, ":\\", 2)) == 0);
-#else
-    return (name[0] == DIR_SEPARATOR);
-#endif
-}
-
 void get_libdir(char *libdir)
 {
-#define GMX_BINNAME_MAX 512
-    char     bin_name[GMX_BINNAME_MAX];
-    char     buf[GMX_BINNAME_MAX];
-    char     full_path[GMX_PATH_MAX+GMX_BINNAME_MAX];
-    char     system_path[GMX_PATH_MAX];
-    char    *dir, *ptr, *s;
-    gmx_bool found = FALSE;
-    int      i;
-
-    if (Program() != NULL)
+    // TODO: There is a potential buffer overrun in the way libdir is passed in.
+    try
     {
+        std::string fullPath = gmx::ProgramInfo::getInstance().fullBinaryPath();
 
-        /* First - detect binary name */
-        if (strlen(Program()) >= GMX_BINNAME_MAX)
-        {
-            gmx_fatal(FARGS, "The name of the binary is longer than the allowed buffer size (%d):\n'%s'", GMX_BINNAME_MAX, Program());
-        }
-        strncpy(bin_name, Program(), GMX_BINNAME_MAX-1);
-
-        /* On windows & cygwin we need to add the .exe extension
-         * too, or we wont be able to detect that the file exists
-         */
-#if (defined GMX_NATIVE_WINDOWS || defined GMX_CYGWIN)
-        if (strlen(bin_name) < 3 || gmx_strncasecmp(bin_name+strlen(bin_name)-4, ".exe", 4))
-        {
-            strcat(bin_name, ".exe");
-        }
-#endif
-
-        /* Only do the smart search part if we got a real name */
-        if (NULL != bin_name && strncmp(bin_name, "GROMACS", GMX_BINNAME_MAX))
-        {
-
-            if (!strchr(bin_name, DIR_SEPARATOR))
-            {
-                /* No slash or backslash in name means it must be in the path - search it! */
-                /* Add the local dir since it is not in the path on windows */
-                gmx_getcwd(system_path, sizeof(system_path));
-                sprintf(full_path, "%s%c%s", system_path, DIR_SEPARATOR, bin_name);
-                found = gmx_is_file(full_path);
-                if (!found && (s = getenv("PATH")) != NULL)
-                {
-                    char *dupped;
-
-                    dupped = gmx_strdup(s);
-                    s      = dupped;
-                    while (!found && (dir = gmx_strsep(&s, PATH_SEPARATOR)) != NULL)
-                    {
-                        sprintf(full_path, "%s%c%s", dir, DIR_SEPARATOR, bin_name);
-                        found = gmx_is_file(full_path);
-                    }
-                    sfree(dupped);
-                }
-                if (!found)
-                {
-                    strcpy(libdir, GMXLIB_FALLBACK);
-                    return;
-                }
-            }
-            else if (!filename_is_absolute(bin_name))
-            {
-                /* name contains directory separators, but
-                 * it does not start at the root, i.e.
-                 * name is relative to the current dir
-                 */
-                gmx_getcwd(buf, sizeof(buf));
-                sprintf(full_path, "%s%c%s", buf, DIR_SEPARATOR, bin_name);
-            }
-            else
-            {
-                strncpy(full_path, bin_name, GMX_PATH_MAX);
-            }
-
-            /* Now we should have a full path and name in full_path,
-             * but on unix it might be a link, or a link to a link to a link..
-             */
-#ifndef GMX_NATIVE_WINDOWS
-            while ( (i = readlink(full_path, buf, sizeof(buf)-1)) > 0)
-            {
-                buf[i] = '\0';
-                /* If it doesn't start with "/" it is relative */
-                if (buf[0] != DIR_SEPARATOR)
-                {
-                    strncpy(strrchr(full_path, DIR_SEPARATOR)+1, buf, GMX_PATH_MAX);
-                }
-                else
-                {
-                    strncpy(full_path, buf, GMX_PATH_MAX);
-                }
-            }
-#endif
-
-            /* If running directly from the build tree, try to use the source
-             * directory.
-             */
+        // If running directly from the build tree, try to use the source
+        // directory.
 #if (defined CMAKE_SOURCE_DIR && defined CMAKE_BINARY_DIR)
-            if (strncmp(full_path, CMAKE_BINARY_DIR, strlen(CMAKE_BINARY_DIR)) == 0)
+        // TODO: Consider adding Path::startsWith(), as this may not work as
+        // expected.
+        if (gmx::startsWith(fullPath, CMAKE_BINARY_DIR))
+        {
+            if (search_subdirs(CMAKE_SOURCE_DIR, libdir))
             {
-                if (search_subdirs(CMAKE_SOURCE_DIR, libdir))
-                {
-                    return;
-                }
+                return;
             }
+        }
 #endif
 
-            /* Remove the executable name - it always contains at least one slash */
-            *(strrchr(full_path, DIR_SEPARATOR)+1) = '\0';
-            /* Now we have the full path to the gromacs executable.
-             * Use it to find the library dir.
-             */
-            found = FALSE;
-            while (!found && ( (ptr = strrchr(full_path, DIR_SEPARATOR)) != NULL ) )
+        // Remove the executable name
+        fullPath = gmx::Path::splitToPathAndFilename(fullPath).first;
+        // Now we have the full path to the gromacs executable.
+        // Use it to find the library dir.
+        while (!fullPath.empty())
+        {
+            if (search_subdirs(fullPath.c_str(), libdir))
             {
-                *ptr  = '\0';
-                found = search_subdirs(full_path, libdir);
+                return;
             }
+            fullPath = gmx::Path::splitToPathAndFilename(fullPath).first;
         }
     }
+    GMX_CATCH_ALL_AND_EXIT_WITH_FATAL_ERROR;
+
     /* End of smart searching. If we didn't find it in our parent tree,
      * or if the program name wasn't set, at least try some standard
      * locations before giving up, in case we are running from e.g.
      * a users home directory. This only works on unix or cygwin...
      */
+    bool found = false;
 #ifndef GMX_NATIVE_WINDOWS
     if (!found)
     {
index 2369081992d1a151c0efc2c0c2b20f3e54058aae..afdf09293c9ea5705d0841f49ddfd6582527e211 100644 (file)
@@ -696,7 +696,7 @@ void printBinaryInformation(FILE *fp, const ProgramInfo &programInfo,
     fprintf(fp, "%sGROMACS:    %s, %s%s%s\n", prefix, name.c_str(),
             GromacsVersion(), precisionString, suffix);
     fprintf(fp, "%sExecutable: %s%s\n", prefix,
-            programInfo.programNameWithPath().c_str(), suffix);
+            programInfo.fullBinaryPath().c_str(), suffix);
     fprintf(fp, "%sCommand line:%s\n%s  %s%s\n",
             prefix, suffix, prefix, programInfo.commandLine().c_str(), suffix);
     if (settings.bExtendedInfo_)
index 95281af76257364fd605344ac79cd57c761b2320..8a86133c6731e1343201f3b8e2db9bfa3a9fcaa4 100644 (file)
@@ -208,7 +208,7 @@ const char *output_env_get_program_name(const output_env_t oenv)
 {
     try
     {
-        return oenv->programInfo.programNameWithPath().c_str();
+        return oenv->programInfo.fullBinaryPath().c_str();
     }
     GMX_CATCH_ALL_AND_EXIT_WITH_FATAL_ERROR;
 }
index 429c36857ff11a6c41439fcb7928ff0dbd2a33af..2a0d04ce5f57bfcf6206e999ae9456751562ad1c 100644 (file)
@@ -98,7 +98,7 @@ const char *Program(void)
 {
     try
     {
-        return gmx::ProgramInfo::getInstance().programNameWithPath().c_str();
+        return gmx::ProgramInfo::getInstance().fullBinaryPath().c_str();
     }
     GMX_CATCH_ALL_AND_EXIT_WITH_FATAL_ERROR;
 }
index 97e9c2eb99a0f265070a2d6c9bf476e0ffb4361f..b9c354dc7c236d2c9533a3ac32f65610b7d452e6 100644 (file)
@@ -1,2 +1,3 @@
 gmx_header_config_gen.h
 gitversion.c
+tests/test-bin
index df080911817df533f677537e422d8ef97835f198..af49d16cd7b07bec8522d4b12f7ade83e3445785 100644 (file)
 #include <string>
 #include <vector>
 
-#include "gromacs/fileio/futil.h"
+#include <sys/stat.h>
 
 #include "gromacs/utility/exceptions.h"
 #include "gromacs/utility/gmxassert.h"
 #include "gromacs/utility/stringutil.h"
 
+#include "gmx_header_config.h"
+
 namespace gmx
 {
 
@@ -256,7 +258,30 @@ void File::writeLine()
 // static
 bool File::exists(const char *filename)
 {
-    return gmx_fexist(filename);
+    if (filename == NULL)
+    {
+        return false;
+    }
+    FILE *test = fopen(filename, "r");
+    if (test == NULL)
+    {
+        return false;
+    }
+    else
+    {
+        fclose(test);
+        // Windows doesn't allow fopen of directory, so we don't need to check
+        // this separately.
+#ifndef GMX_NATIVE_WINDOWS
+        struct stat st_buf;
+        int         status = stat(filename, &st_buf);
+        if (status != 0 || !S_ISREG(st_buf.st_mode))
+        {
+            return false;
+        }
+#endif
+        return true;
+    }
 }
 
 // static
index f676ea9cc2affe31ed7334c43ab204b403e3a81c..6c16cce92d4436b256822352db4bd2a74c7cf9da 100644 (file)
@@ -193,7 +193,7 @@ class File
         void writeLine();
 
         /*! \brief
-         * Checks whether a file exists.
+         * Checks whether a file exists and is a regular file.
          *
          * \param[in] filename  Path to the file to check.
          * \returns   true if \p filename exists and is accessible.
index 44282b43657951721fe1456d755a13d3f762b783..c1d03479530f05d97f370a4666bdc1fc2a6e0ead 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * This file is part of the GROMACS molecular simulation package.
  *
- * Copyright (c) 2011,2012, by the GROMACS development team, led by
+ * Copyright (c) 2011,2012,2013, by the GROMACS development team, led by
  * David van der Spoel, Berk Hess, Erik Lindahl, and including many
  * others, as listed in the AUTHORS file in the top-level source
  * directory and at http://www.gromacs.org.
  * \author Teemu Murtola <teemu.murtola@gmail.com>
  * \ingroup module_utility
  */
-#include "path.h"
+#include "gromacs/utility/path.h"
 
-#include "gmx_header_config.h"
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
 
-#include <errno.h>
-#include <sys/stat.h>
+#include <algorithm>
+
+#include "config.h"
 
+#include <sys/stat.h>
 #ifdef GMX_NATIVE_WINDOWS
 #include <direct.h>
+#else
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
 #endif
 
+#include "gromacs/fileio/futil.h"
+
 namespace
 {
 
@@ -58,11 +68,48 @@ const char cDirSeparator = '/';
 //! Directory separators to use when parsing paths.
 const char cDirSeparators[] = "/\\";
 
+//! Check whether a given character is a directory separator.
+bool isDirSeparator(char chr)
+{
+    return std::strchr(cDirSeparators, chr);
+}
+
 } // namespace
 
 namespace gmx
 {
 
+/********************************************************************
+ * Path
+ */
+
+bool Path::containsDirectory(const std::string &path)
+{
+    return path.find_first_of(cDirSeparators) != std::string::npos;
+}
+
+/* Check if the program name begins with "/" on unix/cygwin, or
+ * with "\" or "X:\" on windows. If not, the program name
+ * is relative to the current directory.
+ */
+bool Path::isAbsolute(const char *path)
+{
+    if (isDirSeparator(path[0]))
+    {
+        return true;
+    }
+#ifdef GMX_NATIVE_WINDOWS
+    return path[0] != '\0' && path[1] == ':' && isDirSeparator(path[2]);
+#else
+    return false;
+#endif
+}
+
+bool Path::isAbsolute(const std::string &path)
+{
+    return isAbsolute(path.c_str());
+}
+
 std::string Path::join(const std::string &path1,
                        const std::string &path2)
 {
@@ -90,6 +137,89 @@ Path::splitToPathAndFilename(const std::string &path)
     return std::make_pair(path.substr(0, pos), path.substr(pos+1));
 }
 
+std::string Path::normalize(const std::string &path)
+{
+    std::string result(path);
+    if (DIR_SEPARATOR != '/')
+    {
+        std::replace(result.begin(), result.end(), '/', DIR_SEPARATOR);
+    }
+    return result;
+}
+
+bool Path::exists(const char *path)
+{
+    return gmx_fexist(path);
+}
+
+bool Path::exists(const std::string &path)
+{
+    return exists(path.c_str());
+}
+
+std::string Path::getWorkingDirectory()
+{
+    // TODO: Use exceptions instead of gmx_fatal().
+    char cwd[GMX_PATH_MAX];
+    gmx_getcwd(cwd, sizeof(cwd));
+    return cwd;
+}
+
+void Path::splitPathEnvironment(const std::string        &pathEnv,
+                                std::vector<std::string> *result)
+{
+    size_t                   prevPos = 0;
+    size_t                   separator;
+    do
+    {
+        separator = pathEnv.find_first_of(PATH_SEPARATOR, prevPos);
+        result->push_back(pathEnv.substr(prevPos, separator - prevPos));
+        prevPos = separator + 1;
+    }
+    while (separator != std::string::npos);
+}
+
+std::vector<std::string> Path::getExecutablePaths()
+{
+    std::vector<std::string> result;
+#ifdef GMX_NATIVE_WINDOWS
+    // Add the local dir since it is not in the path on Windows.
+    result.push_back("");
+#endif
+    const char *path = std::getenv("PATH");
+    if (path != NULL)
+    {
+        splitPathEnvironment(path, &result);
+    }
+    return result;
+}
+
+std::string Path::resolveSymlinks(const std::string &path)
+{
+    std::string result(path);
+#ifndef GMX_NATIVE_WINDOWS
+    char        buf[GMX_PATH_MAX];
+    int         length;
+    while ((length = readlink(result.c_str(), buf, sizeof(buf)-1)) > 0)
+    {
+        buf[length] = '\0';
+        if (isAbsolute(buf))
+        {
+            result = buf;
+        }
+        else
+        {
+            result = join(splitToPathAndFilename(result).first, buf);
+        }
+    }
+#endif
+    return result;
+}
+
+
+/********************************************************************
+ * Directory
+ */
 
 int Directory::create(const char *path)
 {
index 4b1d2bde43fed5592176d1bc8d19b40c2c2e5738..833284bdab06ec53252dd17907779b04dd385a70 100644 (file)
@@ -45,6 +45,7 @@
 
 #include <string>
 #include <utility>
+#include <vector>
 
 namespace gmx
 {
@@ -52,6 +53,10 @@ namespace gmx
 class Path
 {
     public:
+        static bool containsDirectory(const std::string &path);
+        static bool isAbsolute(const char *path);
+        static bool isAbsolute(const std::string &path);
+
         static std::string join(const std::string &path1,
                                 const std::string &path2);
         static std::string join(const std::string &path1,
@@ -59,6 +64,17 @@ class Path
                                 const std::string &path3);
         static std::pair<std::string, std::string>
         splitToPathAndFilename(const std::string &path);
+        static std::string normalize(const std::string &path);
+
+        static bool exists(const char *path);
+        static bool exists(const std::string &path);
+        static std::string getWorkingDirectory();
+
+        static void splitPathEnvironment(const std::string        &pathEnv,
+                                         std::vector<std::string> *result);
+        static std::vector<std::string> getExecutablePaths();
+
+        static std::string resolveSymlinks(const std::string &path);
 
     private:
         // Disallow instantiation.
index 7d4d857ff2451e44177e1582e9365c7a8a4f1235..32d6b452d9be49d34bea3a961c7eff2d1a0928a0 100644 (file)
@@ -41,7 +41,6 @@
  */
 #include "programinfo.h"
 
-// For GMX_BINARY_SUFFIX
 #ifdef HAVE_CONFIG_H
 #include "config.h"
 #endif
 #include <cstdlib>
 #include <cstring>
 
-#include <algorithm>
 #include <string>
+#include <vector>
 
 #include <boost/scoped_ptr.hpp>
 
-#include "gromacs/fileio/futil.h"
 #include "gromacs/legacyheaders/thread_mpi/mutex.h"
 
 #include "gromacs/utility/exceptions.h"
+#include "gromacs/utility/file.h"
 #include "gromacs/utility/gmxassert.h"
 #include "gromacs/utility/path.h"
 #include "gromacs/utility/stringutil.h"
@@ -89,6 +88,87 @@ std::string quoteIfNecessary(const char *str)
     return str;
 }
 
+/*! \brief
+ * Default implementation for ExecutableEnvironmentInterface.
+ *
+ * Used if ExecutableEnvironmentInterface is not explicitly provided when
+ * constructing ProgramInfo.
+ */
+class DefaultExecutableEnvironment : public ExecutableEnvironmentInterface
+{
+    public:
+        //! Allocates a default environment.
+        static ExecutableEnvironmentPointer create()
+        {
+            return ExecutableEnvironmentPointer(new DefaultExecutableEnvironment());
+        }
+
+        DefaultExecutableEnvironment()
+            : initialWorkingDirectory_(Path::getWorkingDirectory())
+        {
+        }
+
+        virtual std::string getWorkingDirectory() const
+        {
+            return initialWorkingDirectory_;
+        }
+        virtual std::vector<std::string> getExecutablePaths() const
+        {
+            return Path::getExecutablePaths();
+        }
+
+    private:
+        std::string   initialWorkingDirectory_;
+};
+
+/*! \brief
+ * Finds the absolute path of the binary from \c argv[0].
+ *
+ * \param[in] invokedName \c argv[0] the binary was invoked with.
+ * \param[in] env         Executable environment.
+ * \returns   The full path of the binary.
+ *
+ * If a binary with the given name cannot be located, \p invokedName is
+ * returned.
+ */
+std::string findFullBinaryPath(const std::string                    &invokedName,
+                               const ExecutableEnvironmentInterface &env)
+{
+    std::string searchName = invokedName;
+    // On Windows & Cygwin we need to add the .exe extension,
+    // or we wont be able to detect that the file exists.
+#if (defined GMX_NATIVE_WINDOWS || defined GMX_CYGWIN)
+    if (!endsWith(searchName, ".exe"))
+    {
+        searchName.append(".exe");
+    }
+#endif
+    if (!Path::containsDirectory(searchName))
+    {
+        // No directory in name means it must be in the path - search it!
+        std::vector<std::string>                 pathEntries = env.getExecutablePaths();
+        std::vector<std::string>::const_iterator i;
+        for (i = pathEntries.begin(); i != pathEntries.end(); ++i)
+        {
+            const std::string &dir      = i->empty() ? env.getWorkingDirectory() : *i;
+            std::string        testPath = Path::join(dir, searchName);
+            if (File::exists(testPath))
+            {
+                return testPath;
+            }
+        }
+    }
+    else if (!Path::isAbsolute(searchName))
+    {
+        // Name contains directories, but is not absolute, i.e.,
+        // it is relative to the current directory.
+        std::string cwd      = env.getWorkingDirectory();
+        std::string testPath = Path::join(cwd, searchName);
+        return testPath;
+    }
+    return searchName;
+}
+
 //! \}
 
 }   // namespace
@@ -101,39 +181,35 @@ class ProgramInfo::Impl
 {
     public:
         Impl();
-        Impl(const char *realBinaryName, int argc, const char *const argv[]);
-
-        std::string             realBinaryName_;
-        std::string             fullInvokedProgram_;
-        std::string             programName_;
-        std::string             invariantProgramName_;
-        std::string             commandLine_;
-        std::string             displayName_;
-        mutable tMPI::mutex     displayNameMutex_;
+        Impl(const char *realBinaryName, int argc, const char *const argv[],
+             ExecutableEnvironmentPointer env);
+
+        ExecutableEnvironmentPointer  executableEnv_;
+        std::string                   realBinaryName_;
+        std::string                   invokedName_;
+        std::string                   programName_;
+        std::string                   invariantProgramName_;
+        std::string                   displayName_;
+        std::string                   commandLine_;
+        mutable std::string           fullBinaryPath_;
+        mutable tMPI::mutex           displayNameMutex_;
+        mutable tMPI::mutex           binaryPathMutex_;
 };
 
 ProgramInfo::Impl::Impl()
-    : realBinaryName_("GROMACS"), fullInvokedProgram_("GROMACS"),
+    : realBinaryName_("GROMACS"),
       programName_("GROMACS"), invariantProgramName_("GROMACS")
 {
 }
 
 ProgramInfo::Impl::Impl(const char *realBinaryName,
-                        int argc, const char *const argv[])
-    : realBinaryName_(realBinaryName != NULL ? realBinaryName : ""),
-      fullInvokedProgram_(argc != 0 ? argv[0] : ""),
-      programName_(Path::splitToPathAndFilename(fullInvokedProgram_).second)
+                        int argc, const char *const argv[],
+                        ExecutableEnvironmentPointer env)
+    : executableEnv_(move(env)),
+      realBinaryName_(realBinaryName != NULL ? realBinaryName : "")
 {
-    // Temporary hack to make things work on Windows while waiting for #950.
-    // Some places in the existing code expect to have DIR_SEPARATOR in all
-    // input paths, but Windows may also give '/' (and does that, e.g., for
-    // tests invoked through CTest).
-    // When removing this, remove also the #include "gromacs/fileio/futil.h".
-    if (DIR_SEPARATOR == '\\')
-    {
-        std::replace(fullInvokedProgram_.begin(), fullInvokedProgram_.end(),
-                     '/', '\\');
-    }
+    invokedName_          = (argc != 0 ? argv[0] : "");
+    programName_          = Path::splitToPathAndFilename(invokedName_).second;
     programName_          = stripSuffixIfPresent(programName_, ".exe");
     invariantProgramName_ = programName_;
 #ifdef GMX_BINARY_SUFFIX
@@ -201,18 +277,28 @@ ProgramInfo::ProgramInfo()
 }
 
 ProgramInfo::ProgramInfo(const char *realBinaryName)
-    : impl_(new Impl(realBinaryName, 1, &realBinaryName))
+    : impl_(new Impl(realBinaryName, 1, &realBinaryName,
+                     DefaultExecutableEnvironment::create()))
 {
 }
 
 ProgramInfo::ProgramInfo(int argc, const char *const argv[])
-    : impl_(new Impl(NULL, argc, argv))
+    : impl_(new Impl(NULL, argc, argv,
+                     DefaultExecutableEnvironment::create()))
 {
 }
 
 ProgramInfo::ProgramInfo(const char *realBinaryName,
                          int argc, const char *const argv[])
-    : impl_(new Impl(realBinaryName, argc, argv))
+    : impl_(new Impl(realBinaryName, argc, argv,
+                     DefaultExecutableEnvironment::create()))
+{
+}
+
+ProgramInfo::ProgramInfo(const char *realBinaryName,
+                         int argc, const char *const argv[],
+                         ExecutableEnvironmentPointer env)
+    : impl_(new Impl(realBinaryName, argc, argv, move(env)))
 {
 }
 
@@ -233,11 +319,6 @@ const std::string &ProgramInfo::realBinaryName() const
     return impl_->realBinaryName_;
 }
 
-const std::string &ProgramInfo::programNameWithPath() const
-{
-    return impl_->fullInvokedProgram_;
-}
-
 const std::string &ProgramInfo::programName() const
 {
     return impl_->programName_;
@@ -261,4 +342,22 @@ const std::string &ProgramInfo::commandLine() const
     return impl_->commandLine_;
 }
 
+const std::string &ProgramInfo::fullBinaryPath() const
+{
+    tMPI::lock_guard<tMPI::mutex> lock(impl_->binaryPathMutex_);
+    if (impl_->fullBinaryPath_.empty())
+    {
+        impl_->fullBinaryPath_ =
+            Path::normalize(
+                    Path::resolveSymlinks(
+                            findFullBinaryPath(impl_->invokedName_,
+                                               *impl_->executableEnv_)));
+        // TODO: Investigate/Consider using a dladdr()-based solution.
+        // Potentially less portable, but significantly simpler, and also works
+        // with user binaries even if they are located in some arbitrary location,
+        // as long as shared libraries are used.
+    }
+    return impl_->fullBinaryPath_;
+}
+
 } // namespace gmx
index 98ad2874eff3487eb2e49876da6edc2101ea91f0..7f9cfa191d19b6e96747d0c486d4cfe876ab5db6 100644 (file)
 #define GMX_UTILITY_PROGRAMINFO_H
 
 #include <string>
+#include <vector>
 
 #include "common.h"
+#include "uniqueptr.h"
 
 namespace gmx
 {
 
+//! \addtogroup module_utility
+//! \{
+
+/*! \libinternal \brief
+ * Allows customization of the way various directories are found by
+ * ProgramInfo.
+ *
+ * For the ProgramInfo constructors that do not take this interface as a
+ * parameter, a default implementation is used that forwards the calls to the
+ * corresponding methods in gmx::Path.
+ *
+ * \inlibraryapi
+ */
+class ExecutableEnvironmentInterface
+{
+    public:
+        virtual ~ExecutableEnvironmentInterface() {}
+
+        /*! \brief
+         * Returns the working directory when the program was launched.
+         */
+        virtual std::string getWorkingDirectory() const = 0;
+        /*! \brief
+         * Returns list of paths where executables are searched for.
+         *
+         * The returned list should be in priority order.  An empty string in
+         * the returned list corresponds to getWorkindDirectory().
+         */
+        virtual std::vector<std::string> getExecutablePaths() const = 0;
+};
+
+//! Shorthand for a smart pointer to ExecutableEnvironmentInterface.
+typedef gmx_unique_ptr<ExecutableEnvironmentInterface>::type
+    ExecutableEnvironmentPointer;
+
 /*! \libinternal \brief
  * Helper class for managing information about the running binary.
  *
@@ -75,7 +112,6 @@ namespace gmx
  * exceptions.
  *
  * \inlibraryapi
- * \ingroup module_utility
  */
 class ProgramInfo
 {
@@ -155,6 +191,28 @@ class ProgramInfo
          */
         ProgramInfo(const char *realBinaryName,
                     int argc, const char *const argv[]);
+        /*! \brief
+         * Initializes a program information object based on binary name and
+         * command line.
+         *
+         * \param[in] realBinaryName  Name of the binary
+         *     (without Gromacs binary suffix or .exe on Windows).
+         * \param[in] argc  argc value passed to main().
+         * \param[in] argv  argv array passed to main().
+         * \param[in] env   Customizes the way the binary name is handled.
+         *
+         * This overload allows one to customize the way the binary is located
+         * by providing a custom ExecutableEnvironmentInterface implementation.
+         * This is mainly useful for testing purposes to make it possible to
+         * test different paths without setting environment variables, changing
+         * the working directory or doing other process-wide operations.
+         * It may also be useful for making Gromacs behave better when linked
+         * into a non-Gromacs executable (with possible extensions in
+         * ExecutableEnvironmentInterface).
+         */
+        ProgramInfo(const char *realBinaryName,
+                    int argc, const char *const argv[],
+                    ExecutableEnvironmentPointer env);
         ~ProgramInfo();
 
         /*! \brief
@@ -177,12 +235,6 @@ class ProgramInfo
          * Does not throw.
          */
         const std::string &realBinaryName() const;
-        /*! \brief
-         * Returns the path and name of the binary as it was invoked.
-         *
-         * Does not throw.
-         */
-        const std::string &programNameWithPath() const;
         /*! \brief
          * Returns the name of the binary as it was invoked without any path.
          *
@@ -214,6 +266,15 @@ class ProgramInfo
          */
         const std::string &commandLine() const;
 
+        /*! \brief
+         * Returns the full path of the invoked binary.
+         *
+         * Returns argv[0] if there was an error in finding the absolute path.
+         *
+         * Does not throw.
+         */
+        const std::string &fullBinaryPath() const;
+
     private:
         class Impl;
 
index 6c9beab06ba9d9f5347d8cf204055d2ce667a622..21cfdb921bea64224de0e10e1eeac08346a05a66 100644 (file)
@@ -1,7 +1,7 @@
 #
 # This file is part of the GROMACS molecular simulation package.
 #
-# Copyright (c) 2012, by the GROMACS development team, led by
+# Copyright (c) 2012,2013, by the GROMACS development team, led by
 # David van der Spoel, Berk Hess, Erik Lindahl, and including many
 # others, as listed in the AUTHORS file in the top-level source
 # directory and at http://www.gromacs.org.
 # To help us fund GROMACS development, we humbly ask that you cite
 # the research papers on the package. Check out http://www.gromacs.org.
 
+set(EXECUTABLE_EXTENSION "")
+if (GMX_NATIVE_WINDOWS OR GMX_CYGWIN)
+    set(EXECUTABLE_EXTENSION ".exe")
+endif ()
+set(PATH_SEARCH_TEST_DIR ${CMAKE_CURRENT_BINARY_DIR}/test-bin)
+file(MAKE_DIRECTORY ${PATH_SEARCH_TEST_DIR}/bin)
+file(WRITE ${PATH_SEARCH_TEST_DIR}/bin/test-exe${EXECUTABLE_EXTENSION}
+     "Test executable for path searching")
+if (UNIX)
+    execute_process(
+        COMMAND ${CMAKE_COMMAND} -E create_symlink
+            test-exe
+            ${PATH_SEARCH_TEST_DIR}/bin/test-rel-link)
+    execute_process(
+        COMMAND ${CMAKE_COMMAND} -E create_symlink
+            ${PATH_SEARCH_TEST_DIR}/bin/test-exe
+            ${PATH_SEARCH_TEST_DIR}/bin/test-abs-link)
+endif ()
+
 gmx_add_unit_test(UtilityUnitTests utility-test
+                  programinfo.cpp
                   stringutil.cpp)
diff --git a/src/gromacs/utility/tests/programinfo.cpp b/src/gromacs/utility/tests/programinfo.cpp
new file mode 100644 (file)
index 0000000..59cd367
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * This file is part of the GROMACS molecular simulation package.
+ *
+ * Copyright (c) 2013, by the GROMACS development team, led by
+ * David van der Spoel, Berk Hess, Erik Lindahl, and including many
+ * others, as listed in the AUTHORS file in the top-level source
+ * directory and at http://www.gromacs.org.
+ *
+ * GROMACS is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1
+ * of the License, or (at your option) any later version.
+ *
+ * GROMACS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GROMACS; if not, see
+ * http://www.gnu.org/licenses, or write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
+ *
+ * If you want to redistribute modifications to GROMACS, please
+ * consider that scientific software is very special. Version
+ * control is crucial - bugs must be traceable. We will be happy to
+ * consider code for inclusion in the official distribution, but
+ * derived work must not be called official GROMACS. Details are found
+ * in the README & COPYING files - if they are missing, get the
+ * official version at http://www.gromacs.org.
+ *
+ * To help us fund GROMACS development, we humbly ask that you cite
+ * the research papers on the package. Check out http://www.gromacs.org.
+ */
+/*! \internal \file
+ * \brief
+ * Tests for gmx::ProgramInfo.
+ *
+ * \author Teemu Murtola <teemu.murtola@gmail.com>
+ * \ingroup module_utility
+ */
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+#include "gromacs/utility/common.h"
+#include "gromacs/utility/path.h"
+#include "gromacs/utility/programinfo.h"
+#include "gromacs/utility/uniqueptr.h"
+
+#include "testutils/cmdlinetest.h"
+
+#include "config.h"
+
+using gmx::test::CommandLine;
+using gmx::Path;
+
+#if (defined GMX_NATIVE_WINDOWS || defined GMX_CYGWIN)
+//! Extension for executable files on the platform.
+#define EXECUTABLE_EXTENSION ".exe"
+#else
+//! Extension for executable files on the platform.
+#define EXECUTABLE_EXTENSION ""
+//! Defined if the platform supports symlinks and those can be tested.
+#define TEST_SYMLINKS
+#endif
+
+namespace
+{
+
+class TestExecutableEnvironment : public gmx::ExecutableEnvironmentInterface
+{
+    public:
+        TestExecutableEnvironment()
+            : workingDirectory_(CMAKE_BINARY_DIR "/src/gromacs/utility/tests/test-bin")
+        {
+        }
+
+        virtual std::string getWorkingDirectory() const
+        {
+            return workingDirectory_;
+        }
+        virtual std::vector<std::string> getExecutablePaths() const
+        {
+            return path_;
+        }
+
+        std::string               workingDirectory_;
+        std::vector<std::string>  path_;
+
+        GMX_DISALLOW_COPY_AND_ASSIGN(TestExecutableEnvironment);
+};
+
+//! Shorthand for a smart pointer to TestExecutableEnvironment.
+typedef gmx::gmx_unique_ptr<TestExecutableEnvironment>::type
+    TestExecutableEnvironmentPointer;
+
+class ProgramInfoTest : public ::testing::Test
+{
+    public:
+        ProgramInfoTest()
+            : env_(new TestExecutableEnvironment())
+        {
+            expectedExecutable_ =
+                Path::normalize(
+                        Path::join(env_->getWorkingDirectory(),
+                                   "bin/test-exe" EXECUTABLE_EXTENSION));
+        }
+
+        void testBinaryPathSearch(const char *argv0)
+        {
+            ASSERT_TRUE(env_.get() != NULL);
+            gmx::ProgramInfo  info(NULL, 1, &argv0, move(env_));
+            EXPECT_EQ(expectedExecutable_, info.fullBinaryPath());
+        }
+        void testBinaryPathSearch(const std::string &argv0)
+        {
+            testBinaryPathSearch(argv0.c_str());
+        }
+
+        std::string                      expectedExecutable_;
+        TestExecutableEnvironmentPointer env_;
+};
+
+TEST_F(ProgramInfoTest, FindsBinaryWithAbsolutePath)
+{
+    testBinaryPathSearch(Path::join(env_->getWorkingDirectory(), "bin/test-exe"));
+}
+
+TEST_F(ProgramInfoTest, FindsBinaryWithRelativePath)
+{
+    testBinaryPathSearch("bin/test-exe");
+}
+
+TEST_F(ProgramInfoTest, FindsBinaryFromPath)
+{
+    env_->path_.push_back(Path::join(env_->getWorkingDirectory(), "bin"));
+    testBinaryPathSearch("test-exe");
+}
+
+TEST_F(ProgramInfoTest, FindsBinaryFromCurrentDirectory)
+{
+    env_->workingDirectory_ = Path::join(env_->getWorkingDirectory(), "bin");
+    env_->path_.push_back("");
+    testBinaryPathSearch("test-exe");
+}
+
+#ifdef TEST_SYMLINKS
+TEST_F(ProgramInfoTest, FindsBinaryFromAbsoluteSymLink)
+{
+    testBinaryPathSearch("bin/test-abs-link");
+}
+
+TEST_F(ProgramInfoTest, FindsBinaryFromRelativeSymLink)
+{
+    testBinaryPathSearch("bin/test-rel-link");
+}
+#endif
+
+} // namespace