ml::docs | Your First App Manifests Events Messaging Plugins Scenes Resources
See all tutorials →
Beginner 5 sections ~8 min C++17/20

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.

Introduction

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.

This failure is quiet, which is what makes it expensive. Assets silently do not load, settings silently do not save, and the app looks like it started correctly.

ml::Paths answers the two questions that replace the working directory.

QuestionCall
Where are the files that ship with my app?Paths::resourceDir()
Where may I write this user's data?Paths::userDataDir(app)
1Reading

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");
PlatformResolves to
macOS (bundled)YourApp.app/Contents/Resources
macOS (loose binary)the executable's directory
Windowsthe executable's directory
Linuxthe 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.

Keep the old relative path as a fallback while migrating. Trying resourceDir() first and the bare path second means a developer running from the build tree is unaffected while installed copies get the right answer.
2Writing

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");
PlatformResolves 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.

Never write next to the executable. /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.
3Upgrading

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.

4Reference

ml::Paths API

MethodReturns
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.