xdg icon indicating copy to clipboard operation
xdg copied to clipboard

Incorrect Mapping of `XDG_RUNTIME_DIR` on macOS and Windows

Open gaesa opened this issue 9 months ago • 6 comments

Description

The XDG_RUNTIME_DIR mapping in adrg/xdg does not align with established practices from other cross-platform libraries like platformdirs (Python) and dirs (Rust).

Currently, adrg/xdg sets:

  • macOS → ~/Library/Application Support/
  • Windows → %LOCALAPPDATA%

However, there are two common approaches used in other libraries:

OS platformdirs (Python) dirs (Rust) adrg/xdg (Current)
Linux /run/user/$UID /run/user/$UID /run/user/$UID
macOS ~/Library/Caches/TemporaryItems None ~/Library/Application Support
Windows %TEMP% None %LOCALAPPDATA%

Two Common Approaches

  • platformdirs (Python): Fallbacks to a cache or temporary directory.
  • dirs (Rust): Returns None since XDG_RUNTIME_DIR is not officially defined for macOS and Windows.

As stated by dirs:

It is required that this directory is created when the user logs in, is only accessible by the user itself, is deleted when the user logs out, and supports all filesystem features of the operating system.

Possible Fixes

  1. Follow platformdirs: Use
    • macOS~/Library/Caches/TemporaryItems
    • Windows%TEMP%
  2. Follow dirs: Return nil for macOS/Windows.

Either would be better than the current behavior.

gaesa avatar Mar 18 '25 15:03 gaesa

Hi @gaesa,

The XDG Base Directory Specification covers only Linux environments basically, so there are no established practices when it comes to macOS and Windows. That's why the paths are different even in the libraries you mentioned.

However, let's follow what the specification mentions regarding the runtime directory.

The directory MUST be owned by the user, and they MUST be the only
one having read and write access to it. Its Unix access mode MUST be 0700.

macOS: There is no difference between ~/Library/Caches/TemporaryItems and ~/Library/Application Support in terms of permissions and ownership.

Windows:

%TEMP%         - typically %USERPROFILE%\AppData\Local\Temp
%LOCALAPPDATA% - typically %USERPROFILE%\AppData\Local

There is no difference between %TEMP% and %LOCALAPPDATA% in terms of permissions and ownership.

The lifetime of the directory MUST be bound to the user being logged in. 
It MUST be created when the user first logs in and if the user fully logs out the
directory MUST be removed. If the user logs in more than once they should get pointed
to the same directory, and it is mandatory that the directory continues to exist from
their first login to their last logout on the system, and not removed in between.
Files in the directory MUST not survive reboot or a full logout/login cycle.

This part of the specification does not apply to any of the directories used by this package or by the libraries you mentioned, making them more or less equally suited as runtime directories.

~/Library/Caches/TemporaryItems seems to be a directory used in older versions of macOS and by what I can tell, it is not guaranteed to exist on newer versions of macOS. Also, I don't believe it's automatically cleared on user logout.

Windows does not remove either the %TEMP directory or the files inside it when the user logs out. The temporary files are usually deleted using Disk Cleanup. Otherwise, they are not deleted at all.

If $XDG_RUNTIME_DIR is not set applications should fall back to a replacement
directory with similar capabilities and print a warning message.

Either would be better than the current behavior.

So there is no better in this instance. It's just a matter of preference.

Well, actually, on macOS, $TMPDIR could be better suited as a runtime directory as each user has their own temporary directory, which is also regularly cleaned up from what I can tell. So, $TMPDIR would probably be better than ~/Library/Application Support, in which case, for consistency, I could use %TEMP% on Windows instead of %LOCALAPPDATA% as well .

adrg avatar Mar 18 '25 17:03 adrg

Thanks for the detailed response! I see your point regarding permissions and ownership being similar across the proposed directories. However, I believe the key distinction lies in semantics—specifically, stability and ephemerality, rather than just access control.

The XDG spec suggests that the runtime directory should be temporary, tied to the user session, and safe to remove without consequence. This means it should not only be user-owned but also unstable in existence—not assumed to persist across sessions, and applications should be able to safely recreate it if needed.

I may have been mistaken in suggesting ~/Library/Caches/TemporaryItems as a better alternative—it's not guaranteed to exist on newer macOS versions, which makes it unreliable. Given this, $TMPDIR is a much better fit, as it is user-specific, session-bound, and explicitly intended for temporary storage.

On Windows, while %TEMP% is not always cleared, its role and intended semantics align much better with the expectations of a runtime directory than %LOCALAPPDATA%, which is meant for persistent application data. %TEMP% is the conventional location for temporary, ephemeral files, making it the more appropriate choice.

That said, I agree with your assessment that $TMPDIR on macOS would be better suited as a runtime directory. For consistency, using %TEMP% on Windows instead of %LOCALAPPDATA% would also be a better fit.

gaesa avatar Mar 19 '25 03:03 gaesa

On FreeBSD, /run/user/ isn't part of the filesystem hierarchy documented in hier(7). When the documentation mentions a corresponding directory, it is /var/run/user/. ~~I think /var/run/user/ is a reasonable default for FreeBSD.~~ It turns out that since version 14.1, FreeBSD automatically creates /var/run/xdg/<username>/ for you. Note this is the user name in the path, not their UID. There a topic on the FreeBSD Forums about this. I am surprised it wasn't in the release notes.

See a related issue for Python platformdirs: https://github.com/tox-dev/platformdirs/issues/190.

Edit: Removed the part about other BSDs because it is uncertain. Added information about FreeBSD 14.1.

dbohdan avatar Aug 24 '25 11:08 dbohdan

Hi @dbohdan. Thank you for the info.

So XDG_RUNTIME_DIR used to be /var/run/user/$UID prior to FreeBSD 14.1 and now the directory is /var/run/xdg/$USER? I assume /var/run/user/$UID was also automatically created, right? Does it still exist on FreeBSD 14.1 and beyond?

I need to look into the location of the runtime directory on other BSD distros.

adrg avatar Aug 28 '25 10:08 adrg

Hi! You're welcome.

On older FreeBSD releases, /var/run/user/$UID/ is not created automatically. You can set it up yourself, for example, by installing and configuring sysutils/pam_xdg. The Wayland setup instructions I linked also tell the user to configure it manually.

I am pretty confident NetBSD and OpenBSD do not create any kind of XDG_RUNTIME_DIR by default. The best way to determine the conventions on these systems is to check what is assumed in the ports (packages).

dbohdan avatar Aug 28 '25 11:08 dbohdan

In case it helps someone, here is the approach I currently use in pago to decide where to put a socket.

func DefaultSocket() (string, error) {
	currentUser, err := user.Current()
	if err != nil {
		return "", err
	}

	// Build candidate directories in priority order.
	candidates := []string{}
	subdir := "pago"

	if envDir := os.Getenv("XDG_RUNTIME_DIR"); envDir != "" {
		candidates = append(candidates, envDir)
	}

	if runtime.GOOS == "freebsd" {
		candidates = append(candidates, filepath.Join("/var/run/xdg", currentUser.Username))
	}

	candidates = append(
		candidates,
		filepath.Join("/run/user", currentUser.Uid),
		filepath.Join("/var/run/user", currentUser.Uid),
	)

	// Find the first candidate that exists.
	var runtimeDir string
	for _, candidateDir := range candidates {
		if _, err := os.Stat(candidateDir); err == nil {
			runtimeDir = candidateDir
			break
		}
	}

	// If no candidate exists, fall back to the temporary directory.
	if runtimeDir == "" {
		runtimeDir = os.TempDir()
		subdir = "pago-" + currentUser.Username
	}

	return filepath.Join(runtimeDir, subdir, "socket"), nil
}

Edit: Rewritten to stat each path no more than once.

dbohdan avatar Aug 28 '25 11:08 dbohdan