From 328e8eef78044215cd1efe3f6be533716b0b1493 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Villemot?= <sebastien@dynare.org>
Date: Mon, 11 Dec 2023 19:01:00 +0100
Subject: [PATCH] Change default location for configuration file
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

– under Linux and macOS, use the “dynare” subdirectory of the configuration
  directory specified by the XDG specification
– under Windows, use the “dynare” subdirectory of the Application Data folder

The old location is kept for backward compatibility, with a warning.
---
 src/Configuration.cc | 129 ++++++++++++++++++++++++++++---------------
 src/Configuration.hh |   9 ++-
 src/DynareMain.cc    |   2 +-
 3 files changed, 95 insertions(+), 45 deletions(-)

diff --git a/src/Configuration.cc b/src/Configuration.cc
index 94aa5049..1703d115 100644
--- a/src/Configuration.cc
+++ b/src/Configuration.cc
@@ -21,7 +21,12 @@
 #include <iostream>
 #include <utility>
 
+#ifdef _WIN32
+# include <shlobj.h>
+#endif
+
 #include "Configuration.hh"
+#include "DataTree.hh" // For strsplit()
 
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Wold-style-cast"
@@ -109,64 +114,59 @@ Configuration::Configuration(bool parallel_arg, bool parallel_test_arg,
 }
 
 void
-Configuration::getConfigFileInfo(const filesystem::path& conffile_option)
+Configuration::getConfigFileInfo(const filesystem::path& conffile_option,
+                                 WarningConsolidation& warnings)
 {
   using namespace boost;
-  ifstream configFile;
 
-  if (conffile_option.empty())
+  filesystem::path config_file {conffile_option};
+
+  if (config_file.empty())
     {
-      filesystem::path defaultConfigFile;
-      // Test OS and try to open default file
-#if defined(_WIN32) || defined(__CYGWIN32__)
-      if (auto appdata = getenv("APPDATA"); appdata)
-        defaultConfigFile = filesystem::path {appdata} / "dynare.ini";
-      else
-        {
-          if (parallel || parallel_test)
-            cerr << "ERROR: ";
-          else
-            cerr << "WARNING: ";
-          cerr << "APPDATA environment variable not found." << endl;
+      config_file = findConfigFile("dynare.ini");
 
-          if (parallel || parallel_test)
-            exit(EXIT_FAILURE);
-        }
-#else
-      if (auto home = getenv("HOME"); home)
-        defaultConfigFile = filesystem::path {home} / ".dynare";
-      else
+      if (config_file.empty()) // Try old default location (Dynare ⩽ 5) for backward compatibility
         {
-          if (parallel || parallel_test)
-            cerr << "ERROR: ";
-          else
-            cerr << "WARNING: ";
-          cerr << "HOME environment variable not found." << endl;
-          if (parallel || parallel_test)
-            exit(EXIT_FAILURE);
-        }
+          filesystem::path old_default_config_file;
+#ifdef _WIN32
+          array<wchar_t, MAX_PATH + 1> appdata;
+          if (SHGetFolderPathW(nullptr, CSIDL_APPDATA | CSIDL_FLAG_DONT_VERIFY, nullptr,
+                               SHGFP_TYPE_CURRENT, appdata.data())
+              == S_OK)
+            old_default_config_file = filesystem::path {appdata.data()} / "dynare.ini";
+#else
+          if (auto home = getenv("HOME"); home)
+            old_default_config_file = filesystem::path {home} / ".dynare";
 #endif
-      configFile.open(defaultConfigFile, fstream::in);
-      if (!configFile.is_open())
-        {
-          if (parallel || parallel_test)
+          if (!old_default_config_file.empty() && exists(old_default_config_file))
             {
-              cerr << "ERROR: Could not open the default config file ("
-                   << defaultConfigFile.string() << ")" << endl;
-              exit(EXIT_FAILURE);
+              warnings << "WARNING: the location " << old_default_config_file.string()
+                       << " for the configuration file is obsolete; please see the reference"
+                       << " manual for the new location." << endl;
+              config_file = old_default_config_file;
             }
-          else
-            return;
         }
     }
-  else
+
+  if (config_file.empty())
     {
-      configFile.open(conffile_option, fstream::in);
-      if (!configFile.is_open())
+      if (parallel || parallel_test)
         {
-          cerr << "ERROR: Couldn't open file " << conffile_option.string() << endl;
+          cerr << "ERROR: the parallel or parallel_test option was passed but no configuration "
+               << "file was found" << endl;
           exit(EXIT_FAILURE);
         }
+      else
+        return;
+    }
+
+  ifstream configFile;
+  configFile.open(config_file, fstream::in);
+
+  if (!configFile.is_open())
+    {
+      cerr << "ERROR: Couldn't open configuration file " << config_file.string() << endl;
+      exit(EXIT_FAILURE);
     }
 
   string name, computerName, port, userName, password, remoteDrive, remoteDirectory, programPath,
@@ -812,3 +812,46 @@ Configuration::writeEndParallel(ostream& output) const
          << "     closeSlave(options_.parallel,options_.parallel_info.RemoteTmpFolder);" << endl
          << "end" << endl;
 }
+
+filesystem::path
+Configuration::findConfigFile(const string& filename)
+{
+#ifdef _WIN32
+  array<wchar_t, MAX_PATH + 1> appdata;
+  if (SHGetFolderPathW(nullptr, CSIDL_APPDATA | CSIDL_FLAG_DONT_VERIFY, nullptr, SHGFP_TYPE_CURRENT,
+                       appdata.data())
+      == S_OK)
+    {
+      filesystem::path candidate {filesystem::path {appdata.data()} / "dynare" / filename};
+      if (exists(candidate))
+        return candidate;
+    }
+#else
+  filesystem::path xdg_config_home;
+  if (auto xdg_config_home_env = getenv("XDG_CONFIG_HOME"); xdg_config_home_env)
+    xdg_config_home = xdg_config_home_env;
+  if (auto home = getenv("HOME"); xdg_config_home.empty() && home)
+    xdg_config_home = filesystem::path {home} / ".config";
+
+  if (!xdg_config_home.empty())
+    {
+      filesystem::path candidate {xdg_config_home / "dynare" / filename};
+      if (exists(candidate))
+        return candidate;
+    }
+
+  string xdg_config_dirs;
+  if (auto xdg_config_dirs_env = getenv("XDG_CONFIG_DIRS"); xdg_config_dirs_env)
+    xdg_config_dirs = xdg_config_dirs_env;
+  if (xdg_config_dirs.empty())
+    xdg_config_dirs = "/etc/xdg";
+  for (const auto& dir : DataTree::strsplit(xdg_config_dirs, ':'))
+    {
+      filesystem::path candidate {filesystem::path {dir} / "dynare" / filename};
+      if (exists(candidate))
+        return candidate;
+    }
+#endif
+
+  return {};
+}
diff --git a/src/Configuration.hh b/src/Configuration.hh
index 5b6014c2..4f4fea72 100644
--- a/src/Configuration.hh
+++ b/src/Configuration.hh
@@ -114,10 +114,17 @@ private:
                                   const string& programPath, const string& programConfig,
                                   const string& matlabOctavePath, bool singleCompThread,
                                   int numberOfThreadsPerJob, const string& operatingSystem);
+  /* Given a filename (e.g. dynare.ini), looks for it in the configuration directory:
+     – if under Linux or macOS, look into the “dynare” subdirectory of the XDG
+       configuration directories (following the default values and the precedence order specified in
+       the XDG specification)
+     – if under Windows, look into %APPDATA%\dynare\
+     The returned path will be empty if the file is not found. */
+  [[nodiscard]] static filesystem::path findConfigFile(const string& filename);
 
 public:
   //! Parse config file
-  void getConfigFileInfo(const filesystem::path& conffile_option);
+  void getConfigFileInfo(const filesystem::path& conffile_option, WarningConsolidation& warnings);
   //! Check Pass
   void checkPass(WarningConsolidation& warnings) const;
   //! Check Pass
diff --git a/src/DynareMain.cc b/src/DynareMain.cc
index 8bf68792..4e9ff55a 100644
--- a/src/DynareMain.cc
+++ b/src/DynareMain.cc
@@ -463,7 +463,7 @@ main(int argc, char** argv)
   // Process config file
   Configuration config {parallel, parallel_test, parallel_follower_open_mode, parallel_use_psexec,
                         cluster_name};
-  config.getConfigFileInfo(conffile);
+  config.getConfigFileInfo(conffile, warnings);
   config.checkPass(warnings);
   config.transformPass();
 
-- 
GitLab