From 3e840b3c8c0652ca22d36002fdd5ec66ef99c47f Mon Sep 17 00:00:00 2001 From: Andrew Rabert Date: Fri, 26 Dec 2025 03:51:15 -0500 Subject: [PATCH] Add portable mode for Windows Detect portable marker file (portable or portable.txt) next to exe and store data/cache in app directory instead of AppData. ZIP package includes marker file for out-of-box portable use. --- CMakeModules/PreparePortableZip.cmake.in | 41 ++++++++++++++++++ src/main.cpp | 10 ++++- src/shared/Paths.cpp | 55 +++++++++++++++++++++++- src/shared/Paths.h | 3 ++ 4 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 CMakeModules/PreparePortableZip.cmake.in diff --git a/CMakeModules/PreparePortableZip.cmake.in b/CMakeModules/PreparePortableZip.cmake.in new file mode 100644 index 0000000..bafbe3c --- /dev/null +++ b/CMakeModules/PreparePortableZip.cmake.in @@ -0,0 +1,41 @@ +# Prepare portable ZIP contents +# Copies installed files to a separate directory with runtime DLLs in root + +set(SOURCE_DIR "@CMAKE_INSTALL_PREFIX@") +set(ZIP_DIR "@CMAKE_CURRENT_BINARY_DIR@/portable") +set(PROJECT_SOURCE_DIR "@PROJECT_SOURCE_DIR@") + +# Clean and create ZIP staging directory +file(REMOVE_RECURSE "${ZIP_DIR}") +file(MAKE_DIRECTORY "${ZIP_DIR}") + +# Copy all files except redist folder and vc_redist installer +file(GLOB_RECURSE ALL_FILES RELATIVE "${SOURCE_DIR}" "${SOURCE_DIR}/*") +foreach(f ${ALL_FILES}) + # Skip redist folder and VCRedist installer + if(f MATCHES "^redist/" OR f STREQUAL "vc_redist.x64.exe") + continue() + endif() + + get_filename_component(dir "${f}" DIRECTORY) + if(dir) + file(MAKE_DIRECTORY "${ZIP_DIR}/${dir}") + endif() + file(COPY "${SOURCE_DIR}/${f}" DESTINATION "${ZIP_DIR}/${dir}") +endforeach() + +# Copy runtime DLLs to root (for portable use) +set(VCRUNTIME_DIR "${PROJECT_SOURCE_DIR}/deps/vcruntime") +if(EXISTS "${VCRUNTIME_DIR}") + file(GLOB RUNTIME_DLLS "${VCRUNTIME_DIR}/*.dll") + foreach(dll ${RUNTIME_DLLS}) + file(COPY "${dll}" DESTINATION "${ZIP_DIR}") + endforeach() + message(STATUS "Bundled VC runtime DLLs for portable package") +endif() + +# Create portable marker file +file(WRITE "${ZIP_DIR}/portable" "") +message(STATUS "Created portable marker file") + +message(STATUS "Portable package prepared in: ${ZIP_DIR}") diff --git a/src/main.cpp b/src/main.cpp index 233abe4..34ad5e4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -193,7 +193,15 @@ int main(int argc, char *argv[]) return EXIT_SUCCESS; } - // Handle config-dir first (affects where profiles.json is located) + // Check for portable mode (marker file next to exe) + if (Paths::detectAndEnablePortableMode()) + { + // Portable mode sets its own paths, skip config-dir + QSettings::setPath(QSettings::NativeFormat, QSettings::UserScope, + Paths::globalDataDir()); + } + + // Handle config-dir (overrides portable mode if explicitly set) auto configDir = parser.value("config-dir"); if (!configDir.isEmpty()) { diff --git a/src/shared/Paths.cpp b/src/shared/Paths.cpp index 67a9918..b4f031b 100644 --- a/src/shared/Paths.cpp +++ b/src/shared/Paths.cpp @@ -15,7 +15,9 @@ ///////////////////////////////////////////////////////////////////////////////////////// static QString g_configDirOverride; +static QString g_cacheDirOverride; static QString g_activeProfileId; +static bool g_portableMode = false; ///////////////////////////////////////////////////////////////////////////////////////// void Paths::setConfigDir(const QString& path) @@ -23,6 +25,42 @@ void Paths::setConfigDir(const QString& path) g_configDirOverride = path; } +///////////////////////////////////////////////////////////////////////////////////////// +void Paths::setCacheDir(const QString& path) +{ + g_cacheDirOverride = path; +} + +///////////////////////////////////////////////////////////////////////////////////////// +bool Paths::isPortableMode() +{ + return g_portableMode; +} + +///////////////////////////////////////////////////////////////////////////////////////// +bool Paths::detectAndEnablePortableMode() +{ +#ifdef Q_OS_WIN + QString appDir = QGuiApplication::applicationDirPath(); + + // Check for portable marker file + if (QFile::exists(appDir + "/portable") || QFile::exists(appDir + "/portable.txt")) + { + QString dataPath = appDir + "/data"; + QString cachePath = appDir + "/cache"; + + g_portableMode = true; + g_configDirOverride = dataPath; + g_cacheDirOverride = cachePath; + + qInfo() << "Portable mode enabled, data:" << dataPath; + return true; + } +#endif + + return false; +} + ///////////////////////////////////////////////////////////////////////////////////////// static QDir writableLocation(QStandardPaths::StandardLocation loc) { @@ -93,7 +131,22 @@ QString Paths::globalDataDir(const QString& file) ///////////////////////////////////////////////////////////////////////////////////////// QString Paths::globalCacheDir(const QString& file) { - QDir d = writableLocation(QStandardPaths::GenericCacheLocation); + QDir d; + + if (!g_cacheDirOverride.isEmpty()) + { + d = QDir(g_cacheDirOverride); + if (!d.mkpath(d.absolutePath())) + { + qWarning() << "Failed to create cache override directory:" << d.absolutePath(); + d = writableLocation(QStandardPaths::GenericCacheLocation); + } + } + else + { + d = writableLocation(QStandardPaths::GenericCacheLocation); + } + if (file.isEmpty()) return d.absolutePath(); return d.filePath(file); diff --git a/src/shared/Paths.h b/src/shared/Paths.h index f4cc83d..41b52d5 100644 --- a/src/shared/Paths.h +++ b/src/shared/Paths.h @@ -12,6 +12,9 @@ namespace Paths { void setConfigDir(const QString& path); + void setCacheDir(const QString& path); + bool isPortableMode(); + bool detectAndEnablePortableMode(); QString resourceDir(const QString& file = QString()); QString socketName(const QString& serverName); QString soundsPath(const QString& sound);