From 3426c9ff21ae44bd9ab8e8a995e136ca7db2591f Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Fri, 22 Aug 2025 21:49:08 -0500 Subject: [PATCH] App: Migrate directory handling to helper class Use std::filesystem wherever possible, replacing most uses of std::string when the object is actually a path. This is the first stage of refactoring, and does not make any changes to Application that affect client code. Access to the new directory-handling class is implemented, but is unused by any external code. --- src/App/Application.cpp | 59 +-- src/App/Application.h | 7 + src/App/ApplicationDirectories.cpp | 581 +++++++++++++++++++++++++++++ src/App/ApplicationDirectories.h | 179 +++++++++ src/App/CMakeLists.txt | 2 + src/App/PreCompiled.h | 9 + 6 files changed, 796 insertions(+), 41 deletions(-) create mode 100644 src/App/ApplicationDirectories.cpp create mode 100644 src/App/ApplicationDirectories.h diff --git a/src/App/Application.cpp b/src/App/Application.cpp index b76a8a3b11..760f52de0e 100644 --- a/src/App/Application.cpp +++ b/src/App/Application.cpp @@ -102,6 +102,7 @@ #include "Annotation.h" #include "Application.h" +#include "ApplicationDirectories.h" #include "CleanupProcess.h" #include "ComplexGeoData.h" #include "Services.h" @@ -185,6 +186,7 @@ Base::ConsoleObserverStd *Application::_pConsoleObserverStd = nullptr; Base::ConsoleObserverFile *Application::_pConsoleObserverFile = nullptr; AppExport std::map Application::mConfig; +std::shared_ptr Application::_appDirs; //************************************************************************** @@ -1113,7 +1115,7 @@ int64_t Application::applicationPid() std::string Application::getHomePath() { - return mConfig["AppHomePath"]; + return Base::FileInfo::pathToString(Application::directories()->getHomePath()) + PATHSEP; } std::string Application::getExecutableName() @@ -1140,78 +1142,53 @@ bool Application::isDevelopmentVersion() return suffix == "dev"; } +std::shared_ptr Application::directories() { + return _appDirs; +} + std::string Application::getTempPath() { - return mConfig["AppTempPath"]; + return Base::FileInfo::pathToString(_appDirs->getTempPath()) + PATHSEP; } std::string Application::getTempFileName(const char* FileName) { - return Base::FileInfo::getTempFileName(FileName, getTempPath().c_str()); + return Base::FileInfo::pathToString(_appDirs->getTempFileName(FileName)) + PATHSEP; } std::string Application::getUserCachePath() { - return mConfig["UserCachePath"]; + return Base::FileInfo::pathToString(_appDirs->getUserCachePath()) + PATHSEP; } std::string Application::getUserConfigPath() { - return mConfig["UserConfigPath"]; + return Base::FileInfo::pathToString(_appDirs->getUserConfigPath()) + PATHSEP; } std::string Application::getUserAppDataDir() { - return mConfig["UserAppData"]; + return Base::FileInfo::pathToString(_appDirs->getUserAppDataDir()) + PATHSEP; } std::string Application::getUserMacroDir() { - return mConfig["UserMacroPath"]; + return Base::FileInfo::pathToString(_appDirs->getUserMacroDir()) + PATHSEP; } std::string Application::getResourceDir() { -#ifdef RESOURCEDIR - // #6892: Conda may inject null characters => remove them using c_str() - std::string path = std::string(RESOURCEDIR).c_str(); - path += PATHSEP; - const QDir dir(QString::fromStdString(path)); - if (dir.isAbsolute()) - return path; - return mConfig["AppHomePath"] + path; -#else - return mConfig["AppHomePath"]; -#endif + return Base::FileInfo::pathToString(_appDirs->getResourceDir()) + PATHSEP; } std::string Application::getLibraryDir() { -#ifdef LIBRARYDIR - // #6892: Conda may inject null characters => remove them using c_str() - std::string path = std::string(LIBRARYDIR).c_str(); - const QDir dir(QString::fromStdString(path)); - if (dir.isAbsolute()) - return path; - return mConfig["AppHomePath"] + path; -#else - return mConfig["AppHomePath"] + "lib"; -#endif + return Base::FileInfo::pathToString(_appDirs->getLibraryDir()) + PATHSEP; } std::string Application::getHelpDir() { -#ifdef DOCDIR - // #6892: Conda may inject null characters => remove them using c_str() - std::string path = std::string(DOCDIR).c_str(); - path += PATHSEP; - const QDir dir(QString::fromStdString(path)); - if (dir.isAbsolute()) - return path; - return mConfig["AppHomePath"] + path; -#else - return mConfig["DocPath"]; -#endif + return Base::FileInfo::pathToString(_appDirs->getHelpDir()) + PATHSEP; } int Application::checkLinkDepth(int depth, MessageOption option) @@ -2535,7 +2512,7 @@ void processProgramOptions(const boost::program_options::variables_map& vm, std: void Application::initConfig(int argc, char ** argv) { // find the home path.... - mConfig["AppHomePath"] = FindHomePath(argv[0]); + mConfig["AppHomePath"] = ApplicationDirectories::findHomePath(argv[0]).string(); // Version of the application extracted from SubWCRef into src/Build/Version.h // We only set these keys if not yet defined. Therefore it suffices to search @@ -2593,7 +2570,7 @@ void Application::initConfig(int argc, char ** argv) } // extract home paths - ExtractUserPath(); + _appDirs = std::make_shared(mConfig); if (vm.contains("safe-mode")) { SafeMode::StartSafeMode(); diff --git a/src/App/Application.h b/src/App/Application.h index 2a2d9e8f76..6abc36aa4a 100644 --- a/src/App/Application.h +++ b/src/App/Application.h @@ -53,6 +53,7 @@ namespace App class Document; class DocumentObject; +class ApplicationDirectories; class ApplicationObserver; class Property; class AutoTransaction; @@ -423,6 +424,10 @@ public: static std::string getExecutableName(); static std::string getNameWithVersion(); static bool isDevelopmentVersion(); + + /// Access to the various directories for the program a replacement for the get*Path methods below + static std::shared_ptr directories(); + /*! Returns the temporary directory. By default, this is set to the system's temporary directory but can be customized by the user. @@ -626,6 +631,8 @@ private: static void SaveEnv(const char *); /// startup configuration container static std::map mConfig; + /// Management of and access to applications directories + static std::shared_ptr _appDirs; static int _argc; static char ** _argv; //@} diff --git a/src/App/ApplicationDirectories.cpp b/src/App/ApplicationDirectories.cpp new file mode 100644 index 0000000000..e66765e31f --- /dev/null +++ b/src/App/ApplicationDirectories.cpp @@ -0,0 +1,581 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/*************************************************************************************************** + * * + * Copyright (c) 2002 Jürgen Riegel * + * Copyright (c) 2025 The FreeCAD project association AISBL * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD 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. * + * * + * FreeCAD 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 FreeCAD. * + * If not, see . * + * * + **************************************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +#include +#include +#include +#include +#include +#include +#endif + +#include "ApplicationDirectories.h" + +#if defined(FC_OS_LINUX) || defined(FC_OS_MACOSX) || defined(FC_OS_BSD) +#include +#endif + +#include +#include + +#include +#include + + +using namespace App; +namespace fs = std::filesystem; + +fs::path qstringToPath(const QString& path) +{ +#if defined(FC_OS_WIN32) + return {path.toStdWString()}; +#else + return {path.toStdString()}; +#endif +} + +ApplicationDirectories::ApplicationDirectories(std::map &config) +{ + try { + int major = std::stoi(config.at("BuildVersionMajor")); + int minor = std::stoi(config.at("BuildVersionMinor")); + _currentVersion = std::make_tuple(major, minor); + } catch (const std::exception& e) { + throw Base::RuntimeError("Failed to parse version from config: " + std::string(e.what())); + } + configurePaths(config); + configureResourceDirectory(config); + configureLibraryDirectory(config); + configureHelpDirectory(config); +} + +const fs::path& ApplicationDirectories::getHomePath() const +{ + return this->_home; +} + +const fs::path& ApplicationDirectories::getTempPath() const { + return this->_temp; +} + +fs::path ApplicationDirectories::getTempFileName(const std::string & filename) const { + return Base::FileInfo::getTempFileName(filename.c_str(), getTempPath().string().c_str()); +} + +const fs::path& ApplicationDirectories::getUserCachePath() const +{ + return this->_userCache; +} + +const fs::path& ApplicationDirectories::getUserAppDataDir() const +{ + return this->_userAppData; +} + +const fs::path& ApplicationDirectories::getUserMacroDir() const +{ + return this->_userMacro; +} + +const fs::path& ApplicationDirectories::getResourceDir() const +{ + return this->_resource; +} + +const fs::path& ApplicationDirectories::getHelpDir() const +{ + return this->_help; +} + +const fs::path& ApplicationDirectories::getUserConfigPath() const { + return this->_userConfig; +} + +const fs::path& ApplicationDirectories::getLibraryDir() const { + return this->_library; +} + + +/*! + * \brief findPath + * Returns the path where to store application files to. + * If \a customHome is not empty, it will be used, otherwise a path starting from \a stdHome will be used. + */ +fs::path ApplicationDirectories::findPath(const fs::path& stdHome, const fs::path& customHome, + const std::vector& subdirs, bool create) { + fs::path appData = customHome; + if (appData.empty()) { + appData = stdHome; + } + + // If a custom user home path is given, then don't modify it + if (customHome.empty()) { + for (const auto& it : subdirs) { + appData = appData / it; + } + } + + // To write to our data path, we must create some directories, first. + if (create && !fs::exists(appData) && !Py_IsInitialized()) { + try { + fs::create_directories(appData); + } catch (const fs::filesystem_error& e) { + throw Base::FileSystemError("Could not create directories. Failed with: " + e.code().message()); + } + } + + return appData; +} + +void ApplicationDirectories::configurePaths(std::map& mConfig) +{ + bool keepDeprecatedPaths = mConfig.contains("KeepDeprecatedPaths"); + + // std paths + _home = fs::path(mConfig.at("AppHomePath")); + mConfig["BinPath"] = mConfig.at("AppHomePath") + "bin" + PATHSEP; + mConfig["DocPath"] = mConfig.at("AppHomePath") + "doc" + PATHSEP; + + // this is to support a portable version of FreeCAD + auto [customHome, customData, customTemp] = getCustomPaths(); + + // get the system standard paths + auto [configHome, dataHome, cacheHome, tempPath] = getStandardPaths(); + + // User home path + // + fs::path homePath = findUserHomePath(customHome); + mConfig["UserHomePath"] = Base::FileInfo::pathToString(homePath); + + // the old path name to save config and data files + std::vector subdirs; + if (keepDeprecatedPaths) { + configHome = homePath; + dataHome = homePath; + cacheHome = homePath; + getOldDataLocation(mConfig, subdirs); + } + else { + getSubDirectories(mConfig, subdirs); + } + + // User data path + // + fs::path data = findPath(dataHome, customData, subdirs, true); + _userAppData = data; + mConfig["UserAppData"] = Base::FileInfo::pathToString(_userAppData) + PATHSEP; + + + // User config path + // + fs::path config = findPath(configHome, customHome, subdirs, true); + _userConfig = config; + mConfig["UserConfigPath"] = Base::FileInfo::pathToString(_userConfig) + PATHSEP; + + + // User cache path + // + std::vector cachedirs = subdirs; + cachedirs.emplace_back("Cache"); + fs::path cache = findPath(cacheHome, customTemp, cachedirs, true); + _userCache = cache; + mConfig["UserCachePath"] = Base::FileInfo::pathToString(cache) + PATHSEP; + + + // Set application temporary directory + // + std::vector empty; + fs::path tmp = findPath(tempPath, customTemp, empty, true); + _temp = tmp; + mConfig["AppTempPath"] = Base::FileInfo::pathToString(tmp) + PATHSEP; + + + // Set the default macro directory + // + std::vector macrodirs = std::move(subdirs); // Last use in this method, just move + macrodirs.emplace_back("Macro"); + fs::path macro = findPath(dataHome, customData, macrodirs, true); + _userMacro = macro; + mConfig["UserMacroPath"] = Base::FileInfo::pathToString(macro) + PATHSEP; +} + +void ApplicationDirectories::configureResourceDirectory(const std::map& mConfig) { +#ifdef RESOURCEDIR + // #6892: Conda may inject null characters => remove them using c_str() + fs::path path {std::string(RESOURCEDIR).c_str()}; + if (path.is_absolute()) { + _resource = path; + } else { + _resource = Base::FileInfo::stringToPath(mConfig.at("AppHomePath")) / path; + } +#else + _resource = fs::path(mConfig.at("AppHomePath")); +#endif +} + +void ApplicationDirectories::configureLibraryDirectory(const std::map& mConfig) { +#ifdef LIBRARYDIR + // #6892: Conda may inject null characters => remove them using c_str() + fs::path path {std::string(LIBRARYDIR).c_str()}; + if (path.is_absolute()) { + _library = path; + } else { + _library = Base::FileInfo::stringToPath(mConfig.at("AppHomePath")) / path; + } +#else + _library = Base::FileInfo::stringToPath(mConfig.at("AppHomePath")) / "lib"; +#endif +} + + +void ApplicationDirectories::configureHelpDirectory(const std::map& mConfig) +{ +#ifdef DOCDIR + // #6892: Conda may inject null characters => remove them using c_str() + fs::path path {std::string(DOCDIR).c_str()}; + if (path.is_absolute()) { + _help = path; + } else { + _help = Base::FileInfo::stringToPath(mConfig.at("AppHomePath")) / path; + } +#else + _help = Base::FileInfo::stringToPath(mConfig.at("DocPath")); +#endif +} + + +fs::path ApplicationDirectories::getUserHome() +{ + fs::path path; +#if defined(FC_OS_LINUX) || defined(FC_OS_CYGWIN) || defined(FC_OS_BSD) || defined(FC_OS_MACOSX) + // Default paths for the user-specific stuff + struct passwd pwd {}; + struct passwd *result {}; + constexpr std::size_t bufferLength = 16384; + std::vector buffer(bufferLength); + const int error = getpwuid_r(getuid(), &pwd, buffer.data(), buffer.size(), &result); + if (!result || error != 0) { + throw Base::RuntimeError("Getting HOME path from system failed!"); + } + path = Base::FileInfo::stringToPath(result->pw_dir); +#else + path = Base::FileInfo::stringToPath(QStandardPaths::writableLocation(QStandardPaths::HomeLocation).toStdString()); +#endif + return path; +} + +#if defined(FC_OS_WIN32) // This is ONLY used on Windows now, so don't even compile it elsewhere +#include +#include "ShlObj.h" +QString ApplicationDirectories::getOldGenericDataLocation() +{ + WCHAR szPath[MAX_PATH]; + std::wstring_convert> converter; + if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, szPath))) { + return QString::fromStdString(converter.to_bytes(szPath)); + } + return {}; +} +#endif + +void ApplicationDirectories::getSubDirectories(const std::map& mConfig, + std::vector& appData) +{ + // If 'AppDataSkipVendor' is defined, the value of 'ExeVendor' must not be part of + // the path. + if (!mConfig.contains("AppDataSkipVendor") && mConfig.contains("ExeVendor")) { + appData.push_back(mConfig.at("ExeVendor")); + } + appData.push_back(mConfig.at("ExeName")); +} + +void ApplicationDirectories::getOldDataLocation(const std::map& mConfig, + std::vector& appData) +{ + // The name of the directory where the parameters are stored should be the name of + // the application (for branding reasons). +#if defined(FC_OS_LINUX) || defined(FC_OS_CYGWIN) || defined(FC_OS_BSD) + // If 'AppDataSkipVendor' is defined, the value of 'ExeVendor' must not be part of + // the path. + if (!mConfig.contains("AppDataSkipVendor")) { + appData.push_back(std::string(".") + mConfig.at("ExeVendor")); + appData.push_back(mConfig.at("ExeName")); + } else { + appData.push_back(std::string(".") + mConfig.at("ExeName")); + } + +#elif defined(FC_OS_MACOSX) || defined(FC_OS_WIN32) + getSubDirectories(mConfig, appData); +#endif +} + +fs::path ApplicationDirectories::findUserHomePath(const fs::path& userHome) +{ + return userHome.empty() ? getUserHome() : userHome; +} + +std::tuple ApplicationDirectories::getCustomPaths() +{ + const QProcessEnvironment env(QProcessEnvironment::systemEnvironment()); + QString userHome = env.value(QStringLiteral("FREECAD_USER_HOME")); + QString userData = env.value(QStringLiteral("FREECAD_USER_DATA")); + QString userTemp = env.value(QStringLiteral("FREECAD_USER_TEMP")); + + auto toNativePath = [](QString& path) { + if (!path.isEmpty()) { + if (const QDir dir(path); dir.exists()) { + path = QDir::toNativeSeparators(dir.canonicalPath()); + } + else { + path.clear(); + } + } + }; + + // verify env. variables + toNativePath(userHome); + toNativePath(userData); + toNativePath(userTemp); + + // if FREECAD_USER_HOME is set but not FREECAD_USER_DATA + if (!userHome.isEmpty() && userData.isEmpty()) { + userData = userHome; + } + + // if FREECAD_USER_HOME is set but not FREECAD_USER_TEMP + if (!userHome.isEmpty() && userTemp.isEmpty()) { + const QDir dir(userHome); + dir.mkdir(QStringLiteral("temp")); + const QFileInfo fi(dir, QStringLiteral("temp")); + userTemp = fi.absoluteFilePath(); + } + + return {qstringToPath(userHome), + qstringToPath(userData), + qstringToPath(userTemp)}; +} + +std::tuple ApplicationDirectories::getStandardPaths() +{ + QString configHome = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); + QString dataHome = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); + QString cacheHome = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); + QString tempPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + + // Keep the old behaviour +#if defined(FC_OS_WIN32) + configHome = getOldGenericDataLocation(); + dataHome = configHome; + + // On systems with non-7-bit-ASCII application data directories, + // GetTempPathW will return a path in DOS format. This path will be + // accepted by boost's file_lock class. + // Since boost 1.76, there is now a version that accepts a wide string. +#if (BOOST_VERSION < 107600) + tempPath = QString::fromStdString(Base::FileInfo::getTempPath()); + cacheHome = tempPath; +#endif +#endif + + return {qstringToPath(configHome), + qstringToPath(dataHome), + qstringToPath(cacheHome), + qstringToPath(tempPath)}; +} + +// TODO: Consider using this for all UNIX-like OSes +#if defined(__OpenBSD__) +#include +#include +#include +#include + +fs::path ApplicationDirectories::findHomePath(const char* sCall) +{ + // We have three ways to start this application either use one of the two executables or + // import the FreeCAD.so module from a running Python session. In the latter case the + // Python interpreter is already initialized. + std::string absPath; + std::string homePath; + if (Py_IsInitialized()) { + // Note: `realpath` is known to cause a buffer overflow because it + // expands the given path to an absolute path of unknown length. + // Even setting PATH_MAX does not necessarily solve the problem + // for sure, but the risk of overflow is rather small. + char resolved[PATH_MAX]; + char* path = realpath(sCall, resolved); + if (path) + absPath = path; + } + else { + int argc = 1; + QCoreApplication app(argc, (char**)(&sCall)); + absPath = QCoreApplication::applicationFilePath().toStdString(); + } + + // should be an absolute path now + std::string::size_type pos = absPath.find_last_of("/"); + homePath.assign(absPath,0,pos); + pos = homePath.find_last_of("/"); + homePath.assign(homePath,0,pos+1); + + return Base::FileInfo::stringToPath(homePath); +} + +#elif defined (FC_OS_LINUX) || defined(FC_OS_CYGWIN) || defined(FC_OS_BSD) +#include +#include +#include + +fs::path ApplicationDirectories::findHomePath(const char* sCall) +{ + // We have three ways to start this application either use one of the two executables or + // import the FreeCAD.so module from a running Python session. In the latter case the + // Python interpreter is already initialized. + std::string absPath; + std::string homePath; + if (Py_IsInitialized()) { + // Note: `realpath` is known to cause a buffer overflow because it + // expands the given path to an absolute path of unknown length. + // Even setting PATH_MAX does not necessarily solve the problem + // for sure, but the risk of overflow is rather small. + char resolved[PATH_MAX]; + char* path = realpath(sCall, resolved); + if (path) + absPath = path; + } + else { + // Find the path of the executable. Theoretically, there could occur a + // race condition when using readlink, but we only use this method to + // get the absolute path of the executable to compute the actual home + // path. In the worst case we simply get q wrong path, and FreeCAD is not + // able to load its modules. + char resolved[PATH_MAX]; +#if defined(FC_OS_BSD) + int mib[4]; + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PATHNAME; + mib[3] = -1; + size_t cb = sizeof(resolved); + sysctl(mib, 4, resolved, &cb, NULL, 0); + int nchars = strlen(resolved); +#else + int nchars = readlink("/proc/self/exe", resolved, PATH_MAX); +#endif + if (nchars < 0 || nchars >= PATH_MAX) + throw Base::FileSystemError("Cannot determine the absolute path of the executable"); + resolved[nchars] = '\0'; // enforce null termination + absPath = resolved; + } + + // should be an absolute path now + std::string::size_type pos = absPath.find_last_of("/"); + homePath.assign(absPath,0,pos); + pos = homePath.find_last_of("/"); + homePath.assign(homePath,0,pos+1); + + return Base::FileInfo::stringToPath(homePath); +} + +#elif defined(FC_OS_MACOSX) +#include +#include +#include +#include + +fs::path ApplicationDirectories::findHomePath(const char* sCall) +{ + // If Python is initialized at this point, then we're being run from + // MainPy.cpp, which hopefully rewrote argv[0] to point at the + // FreeCAD shared library. + if (!Py_IsInitialized()) { + uint32_t sz = 0; + + // function only returns "sz" if the first arg is too small to hold value + _NSGetExecutablePath(nullptr, &sz); + + if (const auto buf = new char[++sz]; _NSGetExecutablePath(buf, &sz) == 0) { + std::array resolved{}; + const char* path = realpath(buf, resolved.data()); + delete [] buf; + + if (path) { + const std::string Call(resolved.data()); + std::string TempHomePath; + std::string::size_type pos = Call.find_last_of(fs::path::preferred_separator); + TempHomePath.assign(Call,0,pos); + pos = TempHomePath.find_last_of(fs::path::preferred_separator); + TempHomePath.assign(TempHomePath,0,pos+1); + return Base::FileInfo::stringToPath(TempHomePath); + } + } else { + delete [] buf; + } + } + + return Base::FileInfo::stringToPath(sCall); +} + +#elif defined (FC_OS_WIN32) +fs::path ApplicationDirectories::findHomePath(const char* sCall) +{ + // We have several ways to start this application: + // * use one of the two executables + // * import the FreeCAD.pyd module from a running Python session. In this case the + // Python interpreter is already initialized. + // * use a custom dll that links FreeCAD core dlls and that is loaded by its host application + // In this case the calling name should be set to FreeCADBase.dll or FreeCADApp.dll in order + // to locate the correct home directory + wchar_t szFileName [MAX_PATH]; + QString dll(QString::fromUtf8(sCall)); + if (Py_IsInitialized() || dll.endsWith(QLatin1String(".dll"))) { + GetModuleFileNameW(GetModuleHandleA(sCall),szFileName, MAX_PATH-1); + } + else { + GetModuleFileNameW(0, szFileName, MAX_PATH-1); + } + + std::wstring Call(szFileName), homePath; + std::wstring::size_type pos = Call.find_last_of(fs::path::preferred_separator); + homePath.assign(Call,0,pos); + pos = homePath.find_last_of(fs::path::preferred_separator); + homePath.assign(homePath,0,pos+1); + + // fixes #0001638 to avoid loading DLLs from Windows' system directories before FreeCAD's bin folder + std::wstring binPath = homePath; + binPath += L"bin"; + SetDllDirectoryW(binPath.c_str()); + + // https://stackoverflow.com/questions/5625884/conversion-of-stdwstring-to-qstring-throws-linker-error +#ifdef _MSC_VER + QString str = QString::fromUtf16(reinterpret_cast(homePath.c_str())); +#else + QString str = QString::fromStdWString(homePath); +#endif + return qstringToPath(str); +} + +#else +# error "std::string ApplicationDirectories::FindHomePath(const char*) not implemented" +#endif diff --git a/src/App/ApplicationDirectories.h b/src/App/ApplicationDirectories.h new file mode 100644 index 0000000000..a9e91aac8c --- /dev/null +++ b/src/App/ApplicationDirectories.h @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/*************************************************************************************************** + * * + * Copyright (c) 2002 Jürgen Riegel * + * Copyright (c) 2025 The FreeCAD project association AISBL * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD 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. * + * * + * FreeCAD 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 FreeCAD. * + * If not, see . * + * * + **************************************************************************************************/ + +#ifndef SRC_APP_APPLICATIONDIRECTORIES_H_ +#define SRC_APP_APPLICATIONDIRECTORIES_H_ + +#include "FCGlobal.h" + +#include +#include +#include +#include + +namespace App { + + /// A helper class to handle application-wide directory management on behalf of the main + /// App::Application class + class AppExport ApplicationDirectories { + + public: + + /// Constructor + /// \param currentVersion A tuple {major, minor} used to create/manage the versioned + /// directories, if any + explicit ApplicationDirectories(std::map &config); + + /// Given argv[0], figure out the home path + static std::filesystem::path findHomePath(const char* sCall); + + /// "Home" here is the parent directory of the actual executable file being run. It is + /// exposed here primarily for historical compatibility reasons, and new code should almost + /// certainly NOT use this path for anything. See alternatives in the + /// `App::ApplicationDirectories` class. + const std::filesystem::path& getHomePath() const; + + /// Temp path is the location of all temporary files: it is not guaranteed to preserve + /// information between runs of the program, but *is* guaranteed to exist for the duration + /// of a single program execution (that is, files are not deleted from it *during* the run). + const std::filesystem::path& getTempPath() const; + + /// Get a file in the temp directory. WARNING: NOT THREAD-SAFE! Currently just forwards to + /// the FileInfo class. TODO: Rewrite to be thread safe + std::filesystem::path getTempFileName(const std::string & filename = "") const; + + /// The user cache path can be used to store files that the program *prefers* not be deleted + /// between runs, but that will be recreated or otherwise handled if they do not exist due + /// to the cache being cleared. There is no guarantee that the files will exist from + /// run-to-run, but an effort is made to preserve them (unlike the temp directory, which + /// should never be used to save data between runs). + const std::filesystem::path& getUserCachePath() const; + + /// The primary directory used to store per-user application data. This is the parent + /// directory of all installed addons, per-user configuration files, etc. Developers looking + /// for a place to put per-user data should begin here. Common subdirectories include "Mod", + /// "Macros", "Materials" and many others that don't begin with the letter "M". This is + /// typically a versioned directory, though users may choose to use a single path for + /// multiple versions of the software. + const std::filesystem::path& getUserAppDataDir() const; + + /// Historically, a single directory was used to store user-created (or user-installed) + /// macro files. This is the path to that directory. Note that going forward it should *not* + /// be assumed that all installed macros are located in this directory. This is typically a + /// versioned directory, though users may choose to use a single path for multiple versions + /// of the software. + const std::filesystem::path& getUserMacroDir() const; + + /// The "resource" directory should be used to store non-ephemeral resources such as icons, + /// templates, hardware setup, etc. -- items that should be preserved from run-to-run of the + /// program, and between versions. This is *not* a versioned directory, and multiple + /// versions of the software may access the same data. + const std::filesystem::path& getResourceDir() const; + + /// Nominally, this is the directory where "help" files are stored, though for historical + /// reasons several other informational files are stored here as well. It should only be + /// used for user-facing informational files. + const std::filesystem::path& getHelpDir() const; + + /// The root path of user config files `user.cfg` and `system.cfg`. + const std::filesystem::path& getUserConfigPath() const; + + /// The directory of all extension modules. Added to `sys.path` during Python + /// initialization. + const std::filesystem::path& getLibraryDir() const; + + /// Get the user's home directory + static std::filesystem::path getUserHome(); + +#ifdef FC_OS_WIN32 + /// On Windows, gets the location of the user's "AppData" directory. Invalid on other OSes. + QString getOldGenericDataLocation(); +#endif + /// Adds subdirectory information to the appData vector for use in constructing full paths to config files, etc. + static void getSubDirectories(const std::map& mConfig, + std::vector& appData); + /// To a given path it adds the subdirectories where to store application-specific files. + /// On Linux or BSD a hidden directory (i.e. starting with a dot) is added. + static void getOldDataLocation(const std::map& mConfig, + std::vector& appData); + /// If the passed path name is not empty, it will be returned, otherwise the user home path of the system will + /// be returned. + static std::filesystem::path findUserHomePath(const std::filesystem::path& userHome); + + bool isVersionedPath(const std::filesystem::path &startingPath) const; + std::string mostRecentAvailableConfigVersion(const std::filesystem::path &startingPath) const; + std::filesystem::path mostRecentConfigFromBase(const std::filesystem::path &startingPath) const; + + static void migrateConfig(const std::filesystem::path &oldPath, const std::filesystem::path &newPath); + + + protected: + static std::filesystem::path findPath( + const std::filesystem::path& stdHome, + const std::filesystem::path& customHome, + const std::vector& paths, + bool create); + + void configurePaths(std::map &config); + void configureResourceDirectory(const std::map& mConfig); + void configureLibraryDirectory(const std::map& mConfig); + void configureHelpDirectory(const std::map& mConfig); + + /*! + * \brief getCustomPaths + * Returns a tuple of path names where to store config, data, and temp. files. + * The method therefore reads the environment variables: + * \list + * \li FREECAD_USER_HOME + * \li FREECAD_USER_DATA + * \li FREECAD_USER_TEMP + * \endlist + */ + static std::tuple getCustomPaths(); + + /*! + * \brief getStandardPaths + * Returns a tuple of XDG-compliant standard paths names where to store config, data and cached files. + * The method therefore reads the environment variables: + * \list + * \li XDG_CONFIG_HOME + * \li XDG_DATA_HOME + * \li XDG_CACHE_HOME + * \endlist + */ + std::tuple getStandardPaths(); + + private: + std::tuple _currentVersion; + std::filesystem::path _home; + std::filesystem::path _temp; + std::filesystem::path _userCache; + std::filesystem::path _userConfig; + std::filesystem::path _userAppData; + std::filesystem::path _userMacro; + std::filesystem::path _resource; + std::filesystem::path _library; + std::filesystem::path _help; + }; + +} // App + +#endif //SRC_APP_APPLICATIONDIRECTORIES_H_ diff --git a/src/App/CMakeLists.txt b/src/App/CMakeLists.txt index f274bb674b..cf2f2415bb 100644 --- a/src/App/CMakeLists.txt +++ b/src/App/CMakeLists.txt @@ -273,6 +273,7 @@ SET(FreeCADApp_CPP_SRCS ${Document_CPP_SRCS} ${Properties_CPP_SRCS} Application.cpp + ApplicationDirectories.cpp ApplicationPy.cpp AutoTransaction.cpp Branding.cpp @@ -303,6 +304,7 @@ SET(FreeCADApp_HPP_SRCS ${Document_HPP_SRCS} ${Properties_HPP_SRCS} Application.h + ApplicationDirectories.h AutoTransaction.h Branding.h CleanupProcess.h diff --git a/src/App/PreCompiled.h b/src/App/PreCompiled.h index 982e2a8125..b531b93ef9 100644 --- a/src/App/PreCompiled.h +++ b/src/App/PreCompiled.h @@ -70,6 +70,9 @@ // STL #include #include +#if defined(FC_OS_WIN32) +#include +#endif #include #include #include @@ -101,6 +104,12 @@ #include +// Qt -- only QtCore +#include +#include +#include +#include + #endif //_PreComp_ #endif // APP_PRECOMPILED_H