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.
pull/7676/head
Mitchell Hashimoto 2025-06-24 07:54:37 -04:00 committed by GitHub
commit 5f6cdb0c4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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 = config.@"shell-integration",
.shell_integration_features = config.@"shell-integration-features", .shell_integration_features = config.@"shell-integration-features",
.working_directory = config.@"working-directory", .working_directory = config.@"working-directory",
.resources_dir = global_state.resources_dir, .resources_dir = global_state.resources_dir.host(),
.term = config.term, .term = config.term,
// Get the cgroup if we're on linux and have the decl. I'd love // 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 App = struct {};
pub const Window = struct {}; pub const Window = struct {};

View File

@ -23,6 +23,8 @@ const Config = configpkg.Config;
const log = std.log.scoped(.embedded_window); const log = std.log.scoped(.embedded_window);
pub const resourcesDir = internal_os.resourcesDir;
pub const App = struct { pub const App = struct {
/// Because we only expect the embedding API to be used in embedded /// Because we only expect the embedding API to be used in embedded
/// environments, the options are extern so that we can expose it /// 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); const log = std.log.scoped(.glfw);
pub const resourcesDir = internal_os.resourcesDir;
pub const App = struct { pub const App = struct {
app: *CoreApp, app: *CoreApp,
config: Config, config: Config,

View File

@ -2,6 +2,7 @@
pub const App = @import("gtk/App.zig"); pub const App = @import("gtk/App.zig");
pub const Surface = @import("gtk/Surface.zig"); pub const Surface = @import("gtk/Surface.zig");
pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir;
test { test {
@import("std").testing.refAllDecls(@This()); @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 App = struct {};
pub const Surface = 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 stderr = std.io.getStdErr().writer();
const stdout = std.io.getStdOut().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 " ++ try stderr.print("Could not find the Ghostty resources directory. Please ensure " ++
"that Ghostty is installed correctly.\n", .{}); "that Ghostty is installed correctly.\n", .{});

View File

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

View File

@ -9,6 +9,7 @@ const harfbuzz = @import("harfbuzz");
const oni = @import("oniguruma"); const oni = @import("oniguruma");
const crash = @import("crash/main.zig"); const crash = @import("crash/main.zig");
const renderer = @import("renderer.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 /// 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 /// 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 /// The app resources directory, equivalent to zig-out/share when we build
/// from source. This is null if we can't detect it. /// from source. This is null if we can't detect it.
resources_dir: ?[]const u8, resources_dir: internal_os.ResourcesDir,
/// Where logging should go /// Where logging should go
pub const Logging = union(enum) { pub const Logging = union(enum) {
@ -62,7 +63,7 @@ pub const GlobalState = struct {
.action = null, .action = null,
.logging = .{ .stderr = {} }, .logging = .{ .stderr = {} },
.rlimits = .{}, .rlimits = .{},
.resources_dir = null, .resources_dir = .{},
}; };
errdefer self.deinit(); errdefer self.deinit();
@ -170,11 +171,11 @@ pub const GlobalState = struct {
// Find our resources directory once for the app so every launch // Find our resources directory once for the app so every launch
// hereafter can use this cached value. // hereafter can use this cached value.
self.resources_dir = try internal_os.resourcesDir(self.alloc); self.resources_dir = try apprt.runtime.resourcesDir(self.alloc);
errdefer if (self.resources_dir) |dir| self.alloc.free(dir); errdefer self.resources_dir.deinit(self.alloc);
// Setup i18n // 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}); 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 /// Cleans up the global state. This doesn't _need_ to be called but
/// doing so in dev modes will check for memory leaks. /// doing so in dev modes will check for memory leaks.
pub fn deinit(self: *GlobalState) void { 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 // Flush our crash logs
crash.deinit(); crash.deinit();

View File

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

View File

@ -2,13 +2,42 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const Allocator = std.mem.Allocator; 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 /// Gets the directory to the bundled resources directory, if it
/// exists (not all platforms or packages have it). The output is /// exists (not all platforms or packages have it). The output is
/// owned by the caller. /// owned by the caller.
/// ///
/// This is highly Ghostty-specific and can likely be generalized at /// This is highly Ghostty-specific and can likely be generalized at
/// some point but we can cross that bridge if we ever need to. /// 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. // Use the GHOSTTY_RESOURCES_DIR environment variable in release builds.
// //
// In debug builds we try using terminfo detection first instead, since // 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. // freed, do not try to use internal_os.getenv or posix getenv.
if (comptime builtin.mode != .Debug) { if (comptime builtin.mode != .Debug) {
if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| { 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) { } else |err| switch (err) {
error.EnvironmentVariableNotFound => {}, error.EnvironmentVariableNotFound => {},
else => return err, else => return err,
@ -38,7 +67,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
// Get the path to our running binary // Get the path to our running binary
var exe_buf: [std.fs.max_path_bytes]u8 = undefined; 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 // We have an exe path! Climb the tree looking for the terminfo
// bundle as we expect it. // 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()) { if (comptime builtin.target.os.tag.isDarwin()) {
inline for (sentinels) |sentinel| { inline for (sentinels) |sentinel| {
if (try maybeDir(&dir_buf, dir, "Contents/Resources", sentinel)) |v| { 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", if (builtin.target.os.tag == .freebsd) "local/share" else "share",
sentinel, sentinel,
)) |v| { )) |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. // fallback and use the provided resources dir.
if (comptime builtin.mode == .Debug) { if (comptime builtin.mode == .Debug) {
if (std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR")) |dir| { 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) { } else |err| switch (err) {
error.EnvironmentVariableNotFound => {}, error.EnvironmentVariableNotFound => {},
else => return err, else => return err,
} }
} }
return null; return .{};
} }
/// Little helper to check if the "base/sub/suffix" directory exists and /// Little helper to check if the "base/sub/suffix" directory exists and