Paths & Installed Apps
Why an app that runs from your build folder breaks once it is installed, and the two calls that fix it for good.
Why Relative Paths Break
Code that works perfectly from a build folder can break the moment it becomes an installed application. The cause is almost always the same: a path written relative to the working directory.
std::ifstream f("assets/config.json"); // fine in a build tree, broken as an app
Launched from Finder, a macOS .app has a working directory of /. The file is not found, nothing throws, and the app quietly starts with no settings. On Windows and Linux the same code fails as soon as the user launches from anywhere but the install folder.
ml::Paths answers the two questions that replace the working directory.
| Question | Call |
|---|---|
| Where are the files that ship with my app? | Paths::resourceDir() |
| Where may I write this user's data? | Paths::userDataDir(app) |
Files That Ship With the App
resourceDir() is where read-only assets live — fonts, images, bundled data.
#include <Malena/Utilities/Paths.h>
const std::string icon = ml::Paths::join(ml::Paths::resourceDir(), "assets/icon.png");
| Platform | Resolves to |
|---|---|
| macOS (bundled) | YourApp.app/Contents/Resources |
| macOS (loose binary) | the executable's directory |
| Windows | the executable's directory |
| Linux | the executable's directory |
It is derived from the executable's own location — _NSGetExecutablePath, GetModuleFileNameW, /proc/self/exe — never from the working directory, so it is correct however the app was launched.
resourceDir() first and the bare path second means a developer running from the build tree is unaffected while installed copies get the right answer.Where Your App May Write
userDataDir() is where your app may write. Settings, caches, saved state — anything that belongs to the user rather than to the installation.
const std::string settings =
ml::Paths::join(ml::Paths::userDataDir("MyApp"), "settings.json");
| Platform | Resolves to |
|---|---|
| macOS | ~/Library/Application Support/MyApp |
| Windows | %APPDATA%\MyApp |
| Linux | $XDG_CONFIG_HOME/MyApp, else ~/.config/MyApp |
The directory is created on demand, so you can write to it immediately.
/Applications and C:\Program Files are not user-writable, and on macOS modifying a signed bundle breaks its signature — the app will refuse to launch afterwards.Moving Existing Installs
Existing installs have data in the old location. Read from both, write to the new one, and the migration happens on its own.
static std::string readPath()
{
const std::string current = ml::Paths::join(
ml::Paths::userDataDir("MyApp"), "settings.json");
if (std::ifstream(current).is_open()) return current;
if (std::ifstream("assets/settings.json").is_open()) // legacy location
return "assets/settings.json";
return current;
}
static std::string writePath() // writes always go to the new location
{
return ml::Paths::join(ml::Paths::userDataDir("MyApp"), "settings.json");
}
The first save after upgrading moves the data across. No migration step, no version check.
ml::Paths API
| Method | Returns |
|---|---|
Paths::resourceDir() | Read-only assets shipped with the app. Reference to a cached string. |
Paths::userDataDir(appName) | Per-user writable directory, created if missing. |
Paths::executablePath() | Full path to the running executable. |
Paths::join(a, b) | Join two path fragments with the platform separator. |
executablePath() is resolved once and cached, so it is cheap to call and safe during startup — including before any window exists.