From c300150d89ea69bc5eec428f6250a5748fa12bc4 Mon Sep 17 00:00:00 2001 From: Ed Johnson-Williams Date: Wed, 18 Mar 2026 23:17:07 +0000 Subject: [PATCH 1/2] macOS: Resolve data directory at runtime for app bundle support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, DATADIR was hardcoded at compile time, which meant the binary could only find its resources (UI files, CSS, device configs) at the path baked in by ./configure. This prevents packaging as a macOS .app bundle, since bundles can be placed anywhere on disk. Add get_data_dir() which uses _NSGetExecutablePath() on macOS to locate the executable and resolve the data directory relative to the bundle structure (../Resources/share/elektroid). Falls back to the compile-time DATADIR if not running from a bundle or on other platforms — no change in behaviour for Linux. Co-Authored-By: Claude Opus 4.6 --- src/connectors/elektron.c | 2 +- src/connectors/volca_sample_2.c | 6 ++++- src/elektroid.c | 20 ++++++++++------ src/mactions/microbrute.c | 6 +++-- src/utils.c | 42 +++++++++++++++++++++++++++++++++ src/utils.h | 2 ++ 6 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/connectors/elektron.c b/src/connectors/elektron.c index 1e17ede2..aafe2e95 100644 --- a/src/connectors/elektron.c +++ b/src/connectors/elektron.c @@ -4009,7 +4009,7 @@ elektron_configure_device (struct backend *backend, guint8 id) if (err) { - filename = strdup (DATADIR DEVICES_FILE); + filename = g_build_filename (get_data_dir (), DEVICES_FILE, NULL); err = elektron_configure_device_from_file (backend, id, filename); g_free (filename); } diff --git a/src/connectors/volca_sample_2.c b/src/connectors/volca_sample_2.c index f4c6af65..7499a6cd 100644 --- a/src/connectors/volca_sample_2.c +++ b/src/connectors/volca_sample_2.c @@ -832,8 +832,12 @@ volca_sample_2_pattern_clear (struct backend *backend, const gchar *path) gint err; struct idata init_pattern; - err = file_load (DATADIR "/volca_sample_2/init_pattern.vlcsplpattb", + gchar *pattern_path = g_build_filename (get_data_dir (), + "volca_sample_2", + "init_pattern.vlcsplpattb", NULL); + err = file_load (pattern_path, &init_pattern, NULL); + g_free (pattern_path); if (err) { return err; diff --git a/src/elektroid.c b/src/elektroid.c index 9dc35c22..8d5d5631 100644 --- a/src/elektroid.c +++ b/src/elektroid.c @@ -1545,11 +1545,14 @@ elektroid_startup (GApplication *gapp, gpointer *user_data) } builder = gtk_builder_new (); - gtk_builder_add_from_file (builder, DATADIR "/elektroid.ui", NULL); + gchar *ui_path = g_build_filename (get_data_dir (), "elektroid.ui", NULL); + gtk_builder_add_from_file (builder, ui_path, NULL); + g_free (ui_path); css_provider = gtk_css_provider_new (); - gtk_css_provider_load_from_path (css_provider, DATADIR "/elektroid.css", - NULL); + gchar *css_path = g_build_filename (get_data_dir (), "elektroid.css", NULL); + gtk_css_provider_load_from_path (css_provider, css_path, NULL); + g_free (css_path); gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), GTK_STYLE_PROVIDER (css_provider), @@ -1561,11 +1564,14 @@ elektroid_startup (GApplication *gapp, gpointer *user_data) GTK_ABOUT_DIALOG (gtk_builder_get_object (builder, "about_dialog")); gtk_about_dialog_set_version (about_dialog, PACKAGE_VERSION); - elektroid_about_add_credit_section (_("Libraries"), - DATADIR "/libraries.html"); + gchar *libraries_path = g_build_filename (get_data_dir (), + "libraries.html", NULL); + elektroid_about_add_credit_section (_("Libraries"), libraries_path); + g_free (libraries_path); - elektroid_about_add_credit_section (_("Acknowledgements"), - DATADIR "/THANKS"); + gchar *thanks_path = g_build_filename (get_data_dir (), "THANKS", NULL); + elektroid_about_add_credit_section (_("Acknowledgements"), thanks_path); + g_free (thanks_path); maction_context.box = GTK_WIDGET (gtk_builder_get_object (builder, "menu_actions_box")); diff --git a/src/mactions/microbrute.c b/src/mactions/microbrute.c index edbdda23..033d8e00 100644 --- a/src/mactions/microbrute.c +++ b/src/mactions/microbrute.c @@ -259,8 +259,10 @@ void microbrute_init () { GtkBuilder *builder = gtk_builder_new (); - gtk_builder_add_from_file (builder, DATADIR "/microbrute/microbrute.ui", - NULL); + gchar *mb_ui_path = g_build_filename (get_data_dir (), "microbrute", + "microbrute.ui", NULL); + gtk_builder_add_from_file (builder, mb_ui_path, NULL); + g_free (mb_ui_path); config_window = GTK_WIDGET (gtk_builder_get_object (builder, "config_window")); gtk_window_resize (GTK_WINDOW (config_window), 1, 1); diff --git a/src/utils.c b/src/utils.c index 04949d16..b9b2a940 100644 --- a/src/utils.c +++ b/src/utils.c @@ -25,6 +25,10 @@ #endif #include #include +#ifdef __APPLE__ +#include +#include +#endif #include "utils.h" #define DEBUG_SHORT_HEX_LEN 64 @@ -188,6 +192,44 @@ get_user_dir (const char *rel_dir) } } +const gchar * +get_data_dir () +{ +#ifdef __APPLE__ + static gchar *data_dir = NULL; + if (!data_dir) + { + char path[PATH_MAX]; + uint32_t size = sizeof (path); + if (_NSGetExecutablePath (path, &size) == 0) + { + char *real = realpath (path, NULL); + if (real) + { + // Check for .app bundle: MacOS/bin -> Resources/share/elektroid + char *dir = dirname (real); + char *candidate = g_build_filename (dir, "..", "Resources", + "share", PACKAGE, NULL); + char *resolved = realpath (candidate, NULL); + g_free (candidate); + if (resolved) + { + data_dir = resolved; + } + free (real); + } + } + if (!data_dir) + { + data_dir = DATADIR; + } + } + return data_dir; +#else + return DATADIR; +#endif +} + char * get_system_startup_path (const gchar *local_dir) { diff --git a/src/utils.h b/src/utils.h index 34b34edb..f5259510 100644 --- a/src/utils.h +++ b/src/utils.h @@ -139,6 +139,8 @@ gint filename_get_lenght_without_ext (const gchar * name); gchar *get_user_dir (const gchar *); +const gchar *get_data_dir (); + gchar *get_system_startup_path (const gchar *); void free_msg (gpointer); From 0fef3c4a859c50209c2e07d94b82d4087cd8e274 Mon Sep 17 00:00:00 2001 From: Ed Johnson-Williams Date: Wed, 18 Mar 2026 23:17:11 +0000 Subject: [PATCH 2/2] macOS: Add app bundle build script Script to package Elektroid as a macOS .app with proper icon, Info.plist, and bundled resources. Requires rsvg-convert for icon generation. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + macos-bundle.sh | 105 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100755 macos-bundle.sh diff --git a/.gitignore b/.gitignore index fcb9baa6..7a76a506 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,4 @@ docs/.jekyll-metadata docs/.bundle/ flatpak/build/ .flatpak-builder/ +Elektroid.app/ diff --git a/macos-bundle.sh b/macos-bundle.sh new file mode 100755 index 00000000..9dd37312 --- /dev/null +++ b/macos-bundle.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# Build a macOS .app bundle for Elektroid. +# Requires: rsvg-convert (librsvg), Homebrew GTK dependencies. +# The binary resolves its data directory at runtime from the bundle +# location, so no special configure prefix is needed. +set -euo pipefail + +APP_NAME="Elektroid" +APP_BUNDLE="${APP_NAME}.app" +CONTENTS="${APP_BUNDLE}/Contents" +MACOS="${CONTENTS}/MacOS" +RESOURCES="${CONTENTS}/Resources" +SRC_DIR="$(cd "$(dirname "$0")" && pwd)" +NPROC=$(sysctl -n hw.ncpu) + +cd "${SRC_DIR}" + +# Build if needed +if [ ! -f src/elektroid ]; then + echo "==> Configuring..." + test -f configure || autoreconf -fi + ./configure 2>&1 | tail -5 + echo "==> Building..." + make -j${NPROC} 2>&1 | tail -3 +fi + +echo "==> Creating app bundle structure..." +rm -rf "${APP_BUNDLE}" +mkdir -p "${MACOS}" "${RESOURCES}/share/elektroid" \ + "${RESOURCES}/share/icons/hicolor/scalable/apps" + +# Copy binaries +cp src/elektroid "${MACOS}/elektroid-bin" +cp src/elektroid-cli "${MACOS}/" + +# Copy data files +cp res/elektroid.ui res/elektroid.css res/libraries.html res/THANKS \ + "${RESOURCES}/share/elektroid/" +cp -R res/elektron "${RESOURCES}/share/elektroid/" +test -d res/microbrute && cp -R res/microbrute "${RESOURCES}/share/elektroid/" +test -d res/volca_sample_2 && cp -R res/volca_sample_2 "${RESOURCES}/share/elektroid/" + +# Copy icons +cp res/*.svg "${RESOURCES}/share/icons/hicolor/scalable/apps/" + +# Generate .icns from SVG +echo "==> Generating app icon..." +ICONSET_DIR=$(mktemp -d)/Elektroid.iconset +mkdir -p "${ICONSET_DIR}" +SVG="res/io.github.dagargo.Elektroid.svg" + +for size in 16 32 64 128 256 512; do + rsvg-convert -w ${size} -h ${size} "${SVG}" -o "${ICONSET_DIR}/icon_${size}x${size}.png" +done +for size in 32 64 128 256 512 1024; do + half=$((size / 2)) + rsvg-convert -w ${size} -h ${size} "${SVG}" -o "${ICONSET_DIR}/icon_${half}x${half}@2x.png" +done +iconutil -c icns "${ICONSET_DIR}" -o "${RESOURCES}/Elektroid.icns" +rm -rf "$(dirname "${ICONSET_DIR}")" + +# Create launcher script +cat > "${MACOS}/Elektroid" << 'LAUNCHER' +#!/bin/bash +DIR="$(cd "$(dirname "$0")" && pwd)" +RESOURCES="$(cd "${DIR}/../Resources" && pwd)" +export XDG_DATA_DIRS="${RESOURCES}/share:${XDG_DATA_DIRS:-/opt/homebrew/share:/usr/local/share:/usr/share}" +exec "${DIR}/elektroid-bin" "$@" +LAUNCHER +chmod +x "${MACOS}/Elektroid" + +# Create Info.plist +VERSION=$(grep 'AC_INIT' configure.ac | sed 's/.*\[\([0-9.]*\)\].*/\1/') +cat > "${CONTENTS}/Info.plist" << PLIST + + + + + CFBundleName + ${APP_NAME} + CFBundleDisplayName + ${APP_NAME} + CFBundleIdentifier + io.github.dagargo.Elektroid + CFBundleVersion + ${VERSION} + CFBundleShortVersionString + ${VERSION} + CFBundleExecutable + Elektroid + CFBundleIconFile + Elektroid + CFBundlePackageType + APPL + NSHighResolutionCapable + + LSMinimumSystemVersion + 11.0 + + +PLIST + +echo "==> Done! Created ${APP_BUNDLE}" +echo " cp -r ${APP_BUNDLE} /Applications/" +echo " open /Applications/${APP_BUNDLE}"