core, gtk: implement host resources dir for Flatpak (#6661)

Introduces host resources directory as a new concept: A directory
containing application resources that can only be accessed from the host
operating system. This is significant for sandboxed application runtimes
like Flatpak where shells spawned on the host should have access to
application resources to enable integrations.

Alongside this, apprt is now allowed to override the resources lookup
logic.
This commit is contained in:
Mitchell Hashimoto
2025-06-24 07:54:37 -04:00
committed by GitHub
12 changed files with 86 additions and 16 deletions

View File

@@ -546,7 +546,7 @@ pub fn init(
.shell_integration = config.@"shell-integration",
.shell_integration_features = config.@"shell-integration-features",
.working_directory = config.@"working-directory",
.resources_dir = global_state.resources_dir,
.resources_dir = global_state.resources_dir.host(),
.term = config.term,
// Get the cgroup if we're on linux and have the decl. I'd love

View File

@@ -1,2 +1,4 @@
const internal_os = @import("../os/main.zig");
pub const resourcesDir = internal_os.resourcesDir;
pub const App = struct {};
pub const Window = struct {};

View File

@@ -23,6 +23,8 @@ const Config = configpkg.Config;
const log = std.log.scoped(.embedded_window);
pub const resourcesDir = internal_os.resourcesDir;
pub const App = struct {
/// Because we only expect the embedding API to be used in embedded
/// environments, the options are extern so that we can expose it

View File

@@ -35,6 +35,8 @@ const darwin_enabled = builtin.target.os.tag.isDarwin() and
const log = std.log.scoped(.glfw);
pub const resourcesDir = internal_os.resourcesDir;
pub const App = struct {
app: *CoreApp,
config: Config,

View File

@@ -2,6 +2,7 @@
pub const App = @import("gtk/App.zig");
pub const Surface = @import("gtk/Surface.zig");
pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir;
test {
@import("std").testing.refAllDecls(@This());

29
src/apprt/gtk/flatpak.zig Normal file
View File

@@ -0,0 +1,29 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_config = @import("../../build_config.zig");
const internal_os = @import("../../os/main.zig");
const glib = @import("glib");
pub fn resourcesDir(alloc: Allocator) !internal_os.ResourcesDir {
if (comptime build_config.flatpak) {
// Only consult Flatpak runtime data for host case.
if (internal_os.isFlatpak()) {
var result: internal_os.ResourcesDir = .{
.app_path = try alloc.dupe(u8, "/app/share/ghostty"),
};
errdefer alloc.free(result.app_path.?);
const keyfile = glib.KeyFile.new();
defer keyfile.unref();
if (keyfile.loadFromFile("/.flatpak-info", .{}, null) == 0) return result;
const app_dir = std.mem.span(keyfile.getString("Instance", "app-path", null)) orelse return result;
defer glib.free(app_dir.ptr);
result.host_path = try std.fs.path.join(alloc, &[_][]const u8{ app_dir, "share", "ghostty" });
return result;
}
}
return try internal_os.resourcesDir(alloc);
}

View File

@@ -1,2 +1,4 @@
const internal_os = @import("../os/main.zig");
pub const resourcesDir = internal_os.resourcesDir;
pub const App = struct {};
pub const Surface = struct {};

View File

@@ -115,7 +115,8 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
const stderr = std.io.getStdErr().writer();
const stdout = std.io.getStdOut().writer();
if (global_state.resources_dir == null)
const resources_dir = global_state.resources_dir.app();
if (resources_dir == null)
try stderr.print("Could not find the Ghostty resources directory. Please ensure " ++
"that Ghostty is installed correctly.\n", .{});

View File

@@ -56,7 +56,7 @@ pub const Location = enum {
},
.resources => try std.fs.path.join(arena_alloc, &.{
global_state.resources_dir orelse return null,
global_state.resources_dir.app() orelse return null,
"themes",
}),
};

View File

@@ -9,6 +9,7 @@ const harfbuzz = @import("harfbuzz");
const oni = @import("oniguruma");
const crash = @import("crash/main.zig");
const renderer = @import("renderer.zig");
const apprt = @import("apprt.zig");
/// We export the xev backend we want to use so that the rest of
/// Ghostty can import this once and have access to the proper
@@ -35,7 +36,7 @@ pub const GlobalState = struct {
/// The app resources directory, equivalent to zig-out/share when we build
/// from source. This is null if we can't detect it.
resources_dir: ?[]const u8,
resources_dir: internal_os.ResourcesDir,
/// Where logging should go
pub const Logging = union(enum) {
@@ -62,7 +63,7 @@ pub const GlobalState = struct {
.action = null,
.logging = .{ .stderr = {} },
.rlimits = .{},
.resources_dir = null,
.resources_dir = .{},
};
errdefer self.deinit();
@@ -170,11 +171,11 @@ pub const GlobalState = struct {
// Find our resources directory once for the app so every launch
// hereafter can use this cached value.
self.resources_dir = try internal_os.resourcesDir(self.alloc);
errdefer if (self.resources_dir) |dir| self.alloc.free(dir);
self.resources_dir = try apprt.runtime.resourcesDir(self.alloc);
errdefer self.resources_dir.deinit(self.alloc);
// Setup i18n
if (self.resources_dir) |v| internal_os.i18n.init(v) catch |err| {
if (self.resources_dir.app()) |v| internal_os.i18n.init(v) catch |err| {
std.log.warn("failed to init i18n, translations will not be available err={}", .{err});
};
}
@@ -182,7 +183,7 @@ pub const GlobalState = struct {
/// Cleans up the global state. This doesn't _need_ to be called but
/// doing so in dev modes will check for memory leaks.
pub fn deinit(self: *GlobalState) void {
if (self.resources_dir) |dir| self.alloc.free(dir);
self.resources_dir.deinit(self.alloc);
// Flush our crash logs
crash.deinit();

View File

@@ -56,6 +56,7 @@ pub const open = openpkg.open;
pub const OpenType = openpkg.Type;
pub const pipe = pipepkg.pipe;
pub const resourcesDir = resourcesdir.resourcesDir;
pub const ResourcesDir = resourcesdir.ResourcesDir;
pub const ShellEscapeWriter = shell.ShellEscapeWriter;
test {

View File

@@ -2,13 +2,42 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
pub const ResourcesDir = struct {
/// Avoid accessing these directly, use the app() and host() methods instead.
app_path: ?[]const u8 = null,
host_path: ?[]const u8 = null,
/// Free resources held. Requires the same allocator as when resourcesDir()
/// is called.
pub fn deinit(self: *ResourcesDir, alloc: Allocator) void {
if (self.app_path) |p| alloc.free(p);
if (self.host_path) |p| alloc.free(p);
}
/// Get the directory to the bundled resources directory accessible
/// by the application.
pub fn app(self: *ResourcesDir) ?[]const u8 {
return self.app_path;
}
/// Get the directory to the bundled resources directory accessible
/// by the host environment (i.e. for sandboxed applications). The
/// returned directory might not be accessible from the application
/// itself.
///
/// In non-sandboxed environment, this should be the same as app().
pub fn host(self: *ResourcesDir) ?[]const u8 {
return self.host_path orelse self.app_path;
}
};
/// Gets the directory to the bundled resources directory, if it
/// exists (not all platforms or packages have it). The output is
/// owned by the caller.
///
/// This is highly Ghostty-specific and can likely be generalized at
/// some point but we can cross that bridge if we ever need to.
pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
pub fn resourcesDir(alloc: Allocator) !ResourcesDir {
// Use the GHOSTTY_RESOURCES_DIR environment variable in release builds.
//
// In debug builds we try using terminfo detection first instead, since
@@ -20,7 +49,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// freed, do not try to use internal_os.getenv or posix getenv.
if (comptime builtin.mode != .Debug) {
if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| {
if (dir.len > 0) return dir;
if (dir.len > 0) return .{ .app_path = dir };
} else |err| switch (err) {
error.EnvironmentVariableNotFound => {},
else => return err,
@@ -38,7 +67,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// Get the path to our running binary
var exe_buf: [std.fs.max_path_bytes]u8 = undefined;
var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return null;
var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return .{};
// We have an exe path! Climb the tree looking for the terminfo
// bundle as we expect it.
@@ -50,7 +79,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
if (comptime builtin.target.os.tag.isDarwin()) {
inline for (sentinels) |sentinel| {
if (try maybeDir(&dir_buf, dir, "Contents/Resources", sentinel)) |v| {
return try std.fs.path.join(alloc, &.{ v, "ghostty" });
return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) };
}
}
}
@@ -65,7 +94,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
if (builtin.target.os.tag == .freebsd) "local/share" else "share",
sentinel,
)) |v| {
return try std.fs.path.join(alloc, &.{ v, "ghostty" });
return .{ .app_path = try std.fs.path.join(alloc, &.{ v, "ghostty" }) };
}
}
}
@@ -74,14 +103,14 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// fallback and use the provided resources dir.
if (comptime builtin.mode == .Debug) {
if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| {
if (dir.len > 0) return dir;
if (dir.len > 0) return .{ .app_path = dir };
} else |err| switch (err) {
error.EnvironmentVariableNotFound => {},
else => return err,
}
}
return null;
return .{};
}
/// Little helper to check if the "base/sub/suffix" directory exists and