system: resolve user-facing paths against the app root so a macOS bundle keeps them beside the .app

This commit is contained in:
mchughalex 2026-07-21 11:01:12 +01:00
parent 705b78f53e
commit 3e842e62a6
5 changed files with 42 additions and 5 deletions

View file

@ -36,6 +36,14 @@ std::filesystem::path GetExecutablePath();
// Get executable folder. // Get executable folder.
std::filesystem::path GetExecutableFolder(); std::filesystem::path GetExecutableFolder();
// Get the application's root folder: the executable folder, or, when the
// executable lives inside a macOS application bundle
// ("<root>/<name>.app/Contents/MacOS/<exe>"), the folder containing the
// bundle. User-facing files (config, game data, saves, DLC, logs) resolve
// against this so a bundled app keeps them beside the bundle rather than
// inside it.
std::filesystem::path GetAppRootFolder();
// Get user folder. // Get user folder.
std::filesystem::path GetUserFolder(); std::filesystem::path GetUserFolder();

View file

@ -95,6 +95,21 @@ std::filesystem::path GetExecutableFolder() {
return GetExecutablePath().parent_path(); return GetExecutablePath().parent_path();
} }
std::filesystem::path GetAppRootFolder() {
auto folder = GetExecutableFolder();
#if REX_PLATFORM_MAC
// <root>/<name>.app/Contents/MacOS/<exe> -> <root>
if (folder.filename() == "MacOS") {
const auto contents = folder.parent_path();
const auto bundle = contents.parent_path();
if (contents.filename() == "Contents" && bundle.extension() == ".app") {
return bundle.parent_path();
}
}
#endif
return folder;
}
std::filesystem::path GetUserFolder() { std::filesystem::path GetUserFolder() {
// get preferred data home // get preferred data home
char* home = std::getenv("XDG_DATA_HOME"); char* home = std::getenv("XDG_DATA_HOME");

View file

@ -65,6 +65,12 @@ std::filesystem::path GetExecutableFolder() {
return GetExecutablePath().parent_path(); return GetExecutablePath().parent_path();
} }
std::filesystem::path GetAppRootFolder() {
// Application bundles are a macOS concept; the executable folder is the
// application root everywhere else.
return GetExecutableFolder();
}
std::filesystem::path GetUserFolder() { std::filesystem::path GetUserFolder() {
std::filesystem::path result; std::filesystem::path result;
PWSTR path; PWSTR path;

View file

@ -16,6 +16,7 @@
#include <rex/assert.h> #include <rex/assert.h>
#include <rex/chrono/clock.h> #include <rex/chrono/clock.h>
#include <rex/cvar.h> #include <rex/cvar.h>
#include <rex/filesystem.h>
#include <rex/input/flags.h> #include <rex/input/flags.h>
#include <rex/input/sdl/sdl_input_driver.h> #include <rex/input/sdl/sdl_input_driver.h>
#include <rex/logging.h> #include <rex/logging.h>
@ -96,18 +97,23 @@ void SDLInputDriver::OnWindowAvailable(rex::ui::Window* window) {
} }
SDL_Gamepad_initialized_ = true; SDL_Gamepad_initialized_ = true;
// Load custom controller mappings if available // Load custom controller mappings if available. A relative path is
// resolved against the application root rather than the working
// directory, which is undefined when launched from a bundle/Finder.
if (!REXCVAR_GET(hid_mappings_file).empty()) { if (!REXCVAR_GET(hid_mappings_file).empty()) {
std::filesystem::path mappings_path(REXCVAR_GET(hid_mappings_file)); std::filesystem::path mappings_path(REXCVAR_GET(hid_mappings_file));
if (mappings_path.is_relative()) {
mappings_path = rex::filesystem::GetAppRootFolder() / mappings_path;
}
if (!std::filesystem::exists(mappings_path)) { if (!std::filesystem::exists(mappings_path)) {
REXLOG_WARN("SDL GameControllerDB: file '{}' does not exist.", REXLOG_WARN("SDL GameControllerDB: file '{}' does not exist.",
REXCVAR_GET(hid_mappings_file)); mappings_path.string());
} else { } else {
auto mappings_result = auto mappings_result =
SDL_AddGamepadMappingsFromFile(REXCVAR_GET(hid_mappings_file).c_str()); SDL_AddGamepadMappingsFromFile(mappings_path.string().c_str());
if (mappings_result < 0) { if (mappings_result < 0) {
REXLOG_ERROR("SDL GameControllerDB: error loading file '{}': {}.", REXLOG_ERROR("SDL GameControllerDB: error loading file '{}': {}.",
REXCVAR_GET(hid_mappings_file), mappings_result); mappings_path.string(), mappings_result);
} else { } else {
REXLOG_INFO("SDL GameControllerDB: loaded {} mappings.", mappings_result); REXLOG_INFO("SDL GameControllerDB: loaded {} mappings.", mappings_result);
} }

View file

@ -312,7 +312,9 @@ bool ReXApp::OnInitialize() {
} }
bool ReXApp::SetupEnvironment() { bool ReXApp::SetupEnvironment() {
auto exe_dir = rex::filesystem::GetExecutableFolder(); // App root, not executable folder: inside a macOS .app bundle the config
// and logs stay next to the bundle, where users can find them.
auto exe_dir = rex::filesystem::GetAppRootFolder();
auto config_path = exe_dir / (std::string(GetName()) + ".toml"); auto config_path = exe_dir / (std::string(GetName()) + ".toml");
// Load config before resolving cvar-backed paths such as game_data_root. // Load config before resolving cvar-backed paths such as game_data_root.