Use platform-specific cache paths and respect XDG_CACHE_HOME (#3458)

## Description:

Use proper platform-specific methods to determine cache directory paths:

1. First check `XDG_CACHE_HOME` environment variable

2. On macOS:

- Use `NSFileManager.URLForDirectory` to get system cache path
(`~/Library/Caches`)
   
- Use bundle ID (`com.mitchellh.ghostty`) as base directory to follow
macOS conventions
   
3. Fall back to XDG spec defaults for other platforms (`~/.cache`)

## Changes:

- Extract common NSFileManager path lookup logic into `makeCommonPath`
helper

- Use `NSFileManager.URLForDirectory` to get proper macOS cache
directory

- Use bundle ID for macOS cache directory to match system conventions
and `appSupportDir` behavior

- Follows the [same pattern as our configuration file
path](https://ghostty.org/docs/config#macos-specific-path-(macos-only):)
(`~/Library/Application Support/com.mitchellh.ghostty/config`)
  
- Aligns with `appSupportDir` implementation which already uses bundle
ID

- Add tests to verify path construction for different platforms

## Questions for reviewers:

1. Should we add migration logic for existing cache files?

2. Or should we document this as a breaking change and let users
manually clean up?

## Testing:

1. Added unit tests for path construction:

   - macOS: verifies `~/Library/Caches/com.mitchellh.ghostty` path
   
   - Linux: verifies `~/.cache/ghostty` path (XDG spec)
   
2. Verified tests pass with `zig build test`

Fixes #3202
This commit is contained in:
Mitchell Hashimoto
2025-01-02 12:06:12 -08:00
committed by GitHub
3 changed files with 130 additions and 33 deletions

View File

@ -101,7 +101,23 @@ fn initThread(gpa: Allocator) !void {
sentry.c.sentry_options_set_before_send(opts, beforeSend, null);
// Determine the Sentry cache directory.
const cache_dir = try internal_os.xdg.cache(alloc, .{ .subdir = "ghostty/sentry" });
const cache_dir = cache_dir: {
// On macOS, we prefer to use the NSCachesDirectory value to be
// a more idiomatic macOS application. But if XDG env vars are set
// we will respect them.
if (comptime builtin.os.tag == .macos) macos: {
if (std.posix.getenv("XDG_CACHE_HOME") != null) break :macos;
break :cache_dir try internal_os.macos.cacheDir(
alloc,
"sentry",
);
}
break :cache_dir try internal_os.xdg.cache(
alloc,
.{ .subdir = "ghostty/sentry" },
);
};
sentry.c.sentry_options_set_database_path_n(
opts,
cache_dir.ptr,

View File

@ -25,41 +25,26 @@ pub fn appSupportDir(
alloc: Allocator,
sub_path: []const u8,
) AppSupportDirError![]const u8 {
comptime assert(builtin.target.isDarwin());
const NSFileManager = objc.getClass("NSFileManager").?;
const manager = NSFileManager.msgSend(
objc.Object,
objc.sel("defaultManager"),
.{},
return try commonDir(
alloc,
.NSApplicationSupportDirectory,
&.{ build_config.bundle_id, sub_path },
);
}
const url = manager.msgSend(
objc.Object,
objc.sel("URLForDirectory:inDomain:appropriateForURL:create:error:"),
.{
NSSearchPathDirectory.NSApplicationSupportDirectory,
NSSearchPathDomainMask.NSUserDomainMask,
@as(?*anyopaque, null),
true,
@as(?*anyopaque, null),
},
pub const CacheDirError = Allocator.Error || error{AppleAPIFailed};
/// Return the path to the system cache directory with the given sub path joined.
/// This allocates the result using the given allocator.
pub fn cacheDir(
alloc: Allocator,
sub_path: []const u8,
) CacheDirError![]const u8 {
return try commonDir(
alloc,
.NSCachesDirectory,
&.{ build_config.bundle_id, sub_path },
);
// I don't think this is possible but just in case.
if (url.value == null) return error.AppleAPIFailed;
// Get the UTF-8 string from the URL.
const path = url.getProperty(objc.Object, "path");
const c_str = path.getProperty(?[*:0]const u8, "UTF8String") orelse
return error.AppleAPIFailed;
const app_support_dir = std.mem.sliceTo(c_str, 0);
return try std.fs.path.join(alloc, &.{
app_support_dir,
build_config.bundle_id,
sub_path,
});
}
pub const SetQosClassError = error{
@ -110,9 +95,79 @@ pub const NSOperatingSystemVersion = extern struct {
};
pub const NSSearchPathDirectory = enum(c_ulong) {
NSCachesDirectory = 13,
NSApplicationSupportDirectory = 14,
};
pub const NSSearchPathDomainMask = enum(c_ulong) {
NSUserDomainMask = 1,
};
fn commonDir(
alloc: Allocator,
directory: NSSearchPathDirectory,
sub_paths: []const []const u8,
) (error{AppleAPIFailed} || Allocator.Error)![]const u8 {
comptime assert(builtin.target.isDarwin());
const NSFileManager = objc.getClass("NSFileManager").?;
const manager = NSFileManager.msgSend(
objc.Object,
objc.sel("defaultManager"),
.{},
);
const url = manager.msgSend(
objc.Object,
objc.sel("URLForDirectory:inDomain:appropriateForURL:create:error:"),
.{
directory,
NSSearchPathDomainMask.NSUserDomainMask,
@as(?*anyopaque, null),
true,
@as(?*anyopaque, null),
},
);
if (url.value == null) return error.AppleAPIFailed;
const path = url.getProperty(objc.Object, "path");
const c_str = path.getProperty(?[*:0]const u8, "UTF8String") orelse
return error.AppleAPIFailed;
const base_dir = std.mem.sliceTo(c_str, 0);
// Create a new array with base_dir as the first element
var paths = try alloc.alloc([]const u8, sub_paths.len + 1);
paths[0] = base_dir;
@memcpy(paths[1..], sub_paths);
defer alloc.free(paths);
return try std.fs.path.join(alloc, paths);
}
test "cacheDir paths" {
if (!builtin.target.isDarwin()) return;
const testing = std.testing;
const alloc = testing.allocator;
// Test base path
{
const cache_path = try cacheDir(alloc, "");
defer alloc.free(cache_path);
try testing.expect(std.mem.indexOf(u8, cache_path, "Caches") != null);
try testing.expect(std.mem.indexOf(u8, cache_path, build_config.bundle_id) != null);
}
// Test with subdir
{
const cache_path = try cacheDir(alloc, "test");
defer alloc.free(cache_path);
try testing.expect(std.mem.indexOf(u8, cache_path, "Caches") != null);
try testing.expect(std.mem.indexOf(u8, cache_path, build_config.bundle_id) != null);
const bundle_path = try std.fmt.allocPrint(alloc, "{s}/test", .{build_config.bundle_id});
defer alloc.free(bundle_path);
try testing.expect(std.mem.indexOf(u8, cache_path, bundle_path) != null);
}
}

View File

@ -143,6 +143,32 @@ test {
}
}
test "cache directory paths" {
const testing = std.testing;
const alloc = testing.allocator;
const mock_home = "/Users/test";
// Test when XDG_CACHE_HOME is not set
{
// Test base path
{
const cache_path = try cache(alloc, .{ .home = mock_home });
defer alloc.free(cache_path);
try testing.expectEqualStrings("/Users/test/.cache", cache_path);
}
// Test with subdir
{
const cache_path = try cache(alloc, .{
.home = mock_home,
.subdir = "ghostty",
});
defer alloc.free(cache_path);
try testing.expectEqualStrings("/Users/test/.cache/ghostty", cache_path);
}
}
}
test parseTerminalExec {
const testing = std.testing;