Build an application bundle on macOS
All checks were successful
Alpine 3.22 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success

This is far from done, but nonetheless constitutes a big improvement.

macOS application bundles are more or less necessary for:
 - showing a nice icon;
 - having spawned off instances actually be brought to the foreground;
 - file associations (yet files currently do not open properly);
 - having a reasonable method of distribution.

Also resolving a bunch of minor issues:
 - The context menu had duplicate items,
   and might needlessly end up with (null) labels.
This commit is contained in:
2025-11-08 18:47:51 +01:00
parent a7ff9f220d
commit 690e60cd74
8 changed files with 440 additions and 23 deletions

170
fiv.c
View File

@@ -73,6 +73,138 @@ slist_to_strv(GSList *slist)
return strv;
}
// --- macOS utilities ---------------------------------------------------------
#ifdef __APPLE__
#include <CoreFoundation/CoreFoundation.h>
static gchar *
cfurlref_to_path(CFURLRef urlref)
{
CFStringRef path = CFURLCopyFileSystemPath(urlref, kCFURLPOSIXPathStyle);
if (!path)
return NULL;
CFIndex size = CFStringGetMaximumSizeForEncoding(
CFStringGetLength(path), kCFStringEncodingUTF8) + 1;
gchar *string = g_malloc(size);
Boolean ok = CFStringGetCString(path, string, size, kCFStringEncodingUTF8);
CFRelease(path);
if (!ok) {
g_free(string);
return NULL;
}
return string;
}
static gchar *
get_application_bundle_path(void)
{
gchar *result = NULL;
CFBundleRef bundle = CFBundleGetMainBundle();
if (!bundle)
goto fail_1;
// When launched from outside a bundle, it will make up one,
// but these paths will then be equal.
CFURLRef bundle_url = CFBundleCopyBundleURL(bundle);
if (!bundle_url)
goto fail_1;
CFURLRef resources_url = CFBundleCopyResourcesDirectoryURL(bundle);
if (!resources_url)
goto fail_2;
if (!CFEqual(bundle_url, resources_url))
result = cfurlref_to_path(bundle_url);
CFRelease(resources_url);
fail_2:
CFRelease(bundle_url);
fail_1:
return result;
}
static gchar *
prepend_path_string(const gchar *prepended, const gchar *original)
{
if (!prepended)
return g_strdup(original ? original : "");
if (!original || !*original)
return g_strdup(prepended);
GHashTable *seen = g_hash_table_new(g_str_hash, g_str_equal);
GPtrArray *unique = g_ptr_array_new();
g_ptr_array_add(unique, (gpointer) prepended);
g_hash_table_add(seen, (gpointer) prepended);
gchar **components = g_strsplit(original, ":", -1);
for (gchar **p = components; *p; p++) {
if (g_hash_table_contains(seen, *p))
continue;
g_ptr_array_add(unique, *p);
g_hash_table_add(seen, *p);
}
g_ptr_array_add(unique, NULL);
gchar *result = g_strjoinv(":", (gchar **) unique->pdata);
g_hash_table_destroy(seen);
g_ptr_array_free(unique, TRUE);
g_strfreev(components);
return result;
}
// We reuse foreign dependencies, so we need to prevent them from loading
// any system-wide files, and point them in the right direction.
static void
adjust_environment(void)
{
gchar *bundle_dir = get_application_bundle_path();
if (!bundle_dir)
return;
gchar *contents_dir = g_build_filename(bundle_dir, "Contents", NULL);
gchar *macos_dir = g_build_filename(contents_dir, "MacOS", NULL);
gchar *resources_dir = g_build_filename(contents_dir, "Resources", NULL);
gchar *datadir = g_build_filename(resources_dir, "share", NULL);
gchar *libdir = g_build_filename(resources_dir, "lib", NULL);
g_free(bundle_dir);
gchar *new_path = prepend_path_string(macos_dir, g_getenv("PATH"));
g_setenv("PATH", new_path, TRUE);
g_free(new_path);
const gchar *data_dirs = g_getenv("XDG_DATA_DIRS");
gchar *new_data_dirs = data_dirs && *data_dirs
? prepend_path_string(datadir, data_dirs)
: prepend_path_string(datadir, "/usr/local/share:/usr/share");
g_setenv("XDG_DATA_DIRS", new_data_dirs, TRUE);
g_free(new_data_dirs);
gchar *schemas_dir = g_build_filename(datadir, "glib-2.0", "schemas", NULL);
g_setenv("GSETTINGS_SCHEMA_DIR", schemas_dir, TRUE);
g_free(schemas_dir);
gchar *gdk_pixbuf_module_file =
g_build_filename(libdir, "gdk-pixbuf-2.0", "loaders.cache", NULL);
g_setenv("GDK_PIXBUF_MODULE_FILE", gdk_pixbuf_module_file, TRUE);
g_free(gdk_pixbuf_module_file);
// GTK+ is smart enough to also consider application bundles,
// but let there be a single source of truth.
g_setenv("GTK_EXE_PREFIX", resources_dir, TRUE);
g_free(libdir);
g_free(datadir);
g_free(resources_dir);
g_free(macos_dir);
g_free(contents_dir);
}
#endif
// --- Keyboard shortcuts ------------------------------------------------------
// Fuck XML, this can be easily represented in static structures.
// Though it would be nice if the accelerators could be customized.
@@ -1092,9 +1224,22 @@ on_next(void)
static gchar **
build_spawn_argv(const char *uri)
{
// Because we only pass URIs, there is no need to prepend "--" here.
GPtrArray *a = g_ptr_array_new();
g_ptr_array_add(a, g_strdup(PROJECT_NAME));
#ifdef __APPLE__
// Otherwise we would always launch ourselves in the background.
gchar *bundle_dir = get_application_bundle_path();
if (bundle_dir) {
g_ptr_array_add(a, g_strdup("open"));
g_ptr_array_add(a, g_strdup("-a"));
g_ptr_array_add(a, bundle_dir);
// At least with G_APPLICATION_NON_UNIQUE, this is necessary:
g_ptr_array_add(a, g_strdup("-n"));
g_ptr_array_add(a, g_strdup("--args"));
}
#endif
// Because we only pass URIs, there is no need to prepend "--" after this.
if (!a->len)
g_ptr_array_add(a, g_strdup(PROJECT_NAME));
// Process-local VFS URIs need to be resolved to globally accessible URIs.
// It doesn't seem possible to reliably tell if a GFile is process-local,
@@ -1403,15 +1548,24 @@ on_window_state_event(G_GNUC_UNUSED GtkWidget *widget,
static void
show_help_contents(void)
{
gchar *filename = g_strdup_printf("%s.html", PROJECT_NAME);
#ifdef G_OS_WIN32
gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
#elif defined __APPLE__
gchar *prefix = get_application_bundle_path();
if (!prefix) {
show_error_dialog(g_error_new(
G_FILE_ERROR, G_FILE_ERROR_FAILED, "Cannot locate bundle"));
return;
}
#else
gchar *prefix = g_strdup(PROJECT_PREFIX);
#endif
gchar *filename = g_strdup_printf("%s.html", PROJECT_NAME);
gchar *path = g_build_filename(prefix, PROJECT_DOCDIR, filename, NULL);
g_free(prefix);
#else
gchar *path = g_build_filename(PROJECT_DOCDIR, filename, NULL);
#endif
g_free(filename);
GError *error = NULL;
gchar *uri = g_filename_to_uri(path, NULL, &error);
g_free(path);
@@ -2640,6 +2794,10 @@ main(int argc, char *argv[])
{},
};
#ifdef __APPLE__
adjust_environment();
#endif
// We never get the ::open signal, thanks to G_OPTION_ARG_FILENAME_ARRAY.
GtkApplication *app = gtk_application_new(NULL, G_APPLICATION_NON_UNIQUE);
g_application_set_option_context_parameter_string(