Skip to content

Instantly share code, notes, and snippets.

@runlevel5
Last active February 20, 2026 15:52
Show Gist options
  • Select an option

  • Save runlevel5/64e1dad006a36bb0cc3881749a37c3c9 to your computer and use it in GitHub Desktop.

Select an option

Save runlevel5/64e1dad006a36bb0cc3881749a37c3c9 to your computer and use it in GitHub Desktop.
How to run GOG Installer (mojosetup) on unsupported architecture?

Installing GOG Games on PPC64LE with Native MojoSetup

GOG's Linux game installers (.sh files) are Makeself archives that bundle a custom mojosetup binary (x86_64) to handle the installation UI and file extraction. On non-x86 architectures like PPC64LE, the bundled binary won't run natively. This document describes how to compile mojosetup from source for your architecture and use it to run any GOG .sh installer, with support for stdio, ncurses (TUI), GTK2, GTK3, and GTK4 (graphical) interfaces.

Background

What's inside a GOG .sh installer

The .sh file is a Makeself self-extracting archive containing:

bin/linux/x86_64/mojosetup          # x86_64 installer binary
bin/linux/x86_64/guis/              # GUI plugin .so files (dead weight — see note below)
startmojo.sh                        # wrapper that sets env vars and runs mojosetup
scripts/*.lua                       # architecture-independent Lua install scripts
gtk-2.0/                            # bundled GTK2 theme files

The actual game data is a ZIP payload appended to the end of the .sh file itself. mojosetup reads it by opening the .sh file and seeking to the embedded ZIP.

The bundled GUI .so files are dead weight

The guis/ directory in the Makeself tar contains dynamic GUI plugin .so files (e.g. ncurses.so, gtkplus2.so), but these are never loaded by the stock mojosetup code.

When MOJOSETUP_BASE is set (which startmojo.sh always does), mojosetup reads the ZIP payload from the .sh file. The loadDynamicGuiPlugins() function in gui.c looks for a guis/ prefix in the base archive, but the ZIP only contains data/, meta/, and scripts/ — no guis/. The MOJOSETUP_GUIPATH env var set by startmojo.sh is also dead code — never consumed by any C code.

This means GOG's original x86_64 mojosetup also has its GUI plugins statically linked. Our approach of statically linking stdio and ncurses matches the original design.

To support dynamic GUI plugins (e.g. GTK3, GTK4) loaded from the filesystem rather than the archive, we patch gui.c with a new function — see the gui.c filesystem plugin patch section.

Key insight

The Lua scripts and ZIP payload are architecture-independent. Only the mojosetup binary and GUI plugin .so files are x86_64. We can replace them with a natively compiled mojosetup.

Prerequisites

# Fedora / RHEL
sudo dnf install cmake gcc gcc-c++ make git

# Debian / Ubuntu
sudo apt install cmake gcc g++ make git

For the stdio-only build, no extra dependencies are needed.

For the ncurses GUI build (recommended), also install:

# Fedora / RHEL
sudo dnf install ncurses-devel ncurses-static

# Debian / Ubuntu
sudo apt install libncurses-dev libncursesw6  # or build ncurses from source for .a files

For the GTK2 GUI plugin (optional — closest to the original GOG installer look, with the game art background image via GTK2 theming), also install:

# Fedora / RHEL
sudo dnf install gtk2-devel gtk-murrine-engine

# Debian / Ubuntu
sudo apt install libgtk2.0-dev gtk2-engines-murrine

Note: The gtk-murrine-engine / gtk2-engines-murrine package provides the Murrine GTK2 engine used by GOG's bundled gtkrc theme. Without it, the background game art won't appear (GTK2 will still work, just without the theme).

For the GTK3 GUI plugin (optional — graphical installer when a display is available), also install:

# Fedora / RHEL
sudo dnf install gtk3-devel

# Debian / Ubuntu
sudo apt install libgtk-3-dev

For the GTK4 GUI plugin (optional — modern graphical installer, preferred on GNOME desktops with GTK4 available), also install:

# Fedora / RHEL
sudo dnf install gtk4-devel

# Debian / Ubuntu
sudo apt install libgtk-4-dev

Note: GTK4 support requires applying PR #77 from the mojosetup repo — see Step 1 for details.

Step 1: Clone and build mojosetup

cd /tmp
git clone https://github.com/icculus/mojosetup.git
cd mojosetup

Apply PR #77 for GTK4 support (optional)

GTK4 support is available via PR #77, which adds gui_gtk4.c (a full GTK4 port of the GTK3 plugin) and the corresponding CMake build options. Apply it with:

git fetch origin pull/77/head:pr-77
git cherry-pick --no-commit pr-77

This adds:

  • gui_gtk4.c — GTK4 GUI plugin (961 lines, uses GtkAlertDialog, GtkFileDialog, GtkDropDown, GdkMemoryTexture, etc.)
  • CMake options: MOJOSETUP_GUI_GTK4, MOJOSETUP_GUI_GTK4_STATIC
  • gui.h declaration: MojoGuiPlugin_gtk4()
  • gui.c static plugin entry: STATIC_GUI_PLUGIN(gtk4) (between cocoa and gtkplus3)

Patch the isatty check (required for non-TTY usage)

The stdio GUI plugin refuses to load when stdin is not a TTY (e.g. piped input, SSH without -tt, scripts). Comment out the check in gui_stdio.c:

# Find the priority function and comment out the isatty guard
sed -i '/if (!istty)/,/return MOJOGUI_PRIORITY_NEVER_TRY;/s/^/\/\//' gui_stdio.c

Or manually edit gui_stdio.c around line 90:

static uint8 MojoGui_stdio_priority(boolean istty)
{
    // PATCHED: allow stdio GUI even without a TTY, so scripted installs work.
    // Original code rejected non-TTY with MOJOGUI_PRIORITY_NEVER_TRY.
    //
    //  if (!istty)
    //      return MOJOGUI_PRIORITY_NEVER_TRY;

    return MOJOGUI_PRIORITY_TRY_ABSOLUTELY_LAST;
}

Patch gui.c for filesystem plugin loading

Stock mojosetup's loadDynamicGuiPlugins() only loads .so plugins from inside the base archive (ZIP), where GOG installers don't put any. The MOJOSETUP_GUIPATH env var set by startmojo.sh is never consumed.

This patch adds loadFilesystemGuiPlugins() — a new function that scans a guis/ directory on the filesystem (next to the binary, or via MOJOSETUP_GUIPATH) for .so plugin files. It uses the existing fork-based MojoPlatform_getGuiPriority() to safely test each plugin (preventing GTK crashes from taking down the main process).

This is needed to support GTK3 and GTK4 GUIs as dynamic .so plugins — keeping them out of the main binary avoids hard dependencies on libgtk-3.so / libgtk-4.so.

Apply the patch to gui.c:

# Add required headers at the top of gui.c (after existing #includes)
sed -i '/#include "fileio.h"/a\
#include <string.h>\
#include <limits.h>\
#include <libgen.h>' gui.c

Then add the loadFilesystemGuiPlugins() function. Insert this before the MojoGui_initGuiPlugin() function (around line 200 in the original):

// Load GUI plugins from filesystem directories (guis/ next to binary).
// This enables dynamic .so plugins (e.g. GTK3) that stock mojosetup can't
// load because loadDynamicGuiPlugins() only looks inside the base archive.
static void loadFilesystemGuiPlugins(void)
{
    char guisdir[PATH_MAX];
    boolean found = false;

    // Try MOJOSETUP_GUIPATH env var first (set by startmojo.sh)
    const char *envpath = getenv("MOJOSETUP_GUIPATH");
    if (envpath != NULL && envpath[0] != '\0')
    {
        // Try envpath/guis/ first, then envpath directly
        snprintf(guisdir, sizeof(guisdir), "%s/guis", envpath);
        DIR *d = opendir(guisdir);
        if (d != NULL) { closedir(d); found = true; }
        else {
            snprintf(guisdir, sizeof(guisdir), "%s", envpath);
            d = opendir(guisdir);
            if (d != NULL) { closedir(d); found = true; }
        }
    }

    // Fallback: binary's directory + /guis
    if (!found)
    {
        char binpath[PATH_MAX];
        ssize_t len = readlink("/proc/self/exe", binpath, sizeof(binpath) - 1);
        if (len > 0)
        {
            binpath[len] = '\0';
            char *dir = dirname(binpath);
            snprintf(guisdir, sizeof(guisdir), "%s/guis", dir);
            DIR *d = opendir(guisdir);
            if (d != NULL) { closedir(d); found = true; }
        }
    }

    if (!found)
        return;

    logInfo("Scanning for filesystem GUI plugins in %0", guisdir);

    DIR *dp = opendir(guisdir);
    if (dp == NULL) return;

    struct dirent *ep;
    while ((ep = readdir(dp)) != NULL)
    {
        const char *entry = ep->d_name;
        size_t len = strlen(entry);
        if (len < 4 || strcmp(entry + len - 3, ".so") != 0)
            continue;

        char fullpath[PATH_MAX];
        snprintf(fullpath, sizeof(fullpath), "%s/%s", guisdir, entry);
        logInfo("Testing filesystem GUI plugin %0", fullpath);

        // Read the .so file into memory (MojoPlatform_dlopen expects a buffer)
        FILE *f = fopen(fullpath, "rb");
        if (f == NULL) continue;

        fseek(f, 0, SEEK_END);
        long fsize = ftell(f);
        fseek(f, 0, SEEK_SET);

        void *buf = malloc(fsize);
        if (buf == NULL) { fclose(f); continue; }

        if ((long)fread(buf, 1, fsize, f) != fsize) { free(buf); fclose(f); continue; }
        fclose(f);

        MojoGuiPluginPriority priority =
            MojoPlatform_getGuiPriority(buf, (uint32)fsize);

        if (priority != MOJOGUI_PRIORITY_NEVER_TRY)
        {
            MojoGuiPlugin *plugin = (MojoGuiPlugin *)xmalloc(sizeof(MojoGuiPlugin));
            plugin->priority = priority;
            plugin->img = buf;
            plugin->imglen = (uint32)fsize;
            plugin->next = dynamicGuiPlugins;
            dynamicGuiPlugins = plugin;
        }
        else
        {
            free(buf);
        }
    }
    closedir(dp);
}

Finally, add the call to loadFilesystemGuiPlugins() in MojoGui_initGuiPlugin(), before the existing loadDynamicGuiPlugins() and loadStaticGuiPlugins() calls:

void MojoGui_initGuiPlugin(void)
{
    loadFilesystemGuiPlugins();   // <-- ADD THIS LINE
    loadDynamicGuiPlugins();
    loadStaticGuiPlugins();
    // ... rest of the function
}

Patch gui.c for MOJOSETUP_UI env var with filesystem plugins

The MOJOSETUP_UI env var override (e.g. MOJOSETUP_UI=gtkplus2) only works for statically linked plugins. For dynamic/filesystem .so plugins, the priority is determined by MojoPlatform_getGuiPriority() which forks a child process and calls only gui->priority() — it never checks the env var. And we can't simply load all plugins in the main process to call calcGuiPriority() because GTK2 and GTK3/GTK4 symbols crash if loaded together.

The fix: extract the plugin name from the .so filename (libmojosetupgui_<name>.so<name>) and compare it to MOJOSETUP_UI without loading the plugin. If it matches, boost the priority to USER_REQUESTED.

Add this function to gui.c (after calcGuiPriority()):

// Check if a GUI plugin filename matches the MOJOSETUP_UI env var.
// Used for dynamic/filesystem plugins where we can't call calcGuiPriority()
// because the plugin isn't loaded yet (and loading multiple GTK versions
// in the same process would crash).
// Filename format: "libmojosetupgui_<name>.so" or archive path "guis/libmojosetupgui_<name>.so"
static MojoGuiPluginPriority adjustPriorityByEnvVar(MojoGuiPluginPriority pri,
                                                     const char *fname)
{
    static const char *envr = NULL;
    static boolean envr_checked = false;
    const char *prefix = "libmojosetupgui_";
    const char *base;
    const char *start;
    const char *dot;
    char name[64];
    size_t namelen;

    if (pri == MOJOGUI_PRIORITY_NEVER_TRY)
        return pri;

    if (!envr_checked)
    {
        envr = cmdlinestr("ui", "MOJOSETUP_UI", NULL);
        envr_checked = true;
    }
    if (envr == NULL)
        return pri;

    // Find the basename (after last '/')
    base = strrchr(fname, '/');
    base = (base != NULL) ? base + 1 : fname;

    // Look for "libmojosetupgui_" prefix
    start = strstr(base, prefix);
    if (start == NULL)
        return pri;
    start += strlen(prefix);

    // Extract name up to ".so"
    dot = strstr(start, ".so");
    if (dot == NULL)
        return pri;

    namelen = (size_t)(dot - start);
    if (namelen == 0 || namelen >= sizeof(name))
        return pri;

    memcpy(name, start, namelen);
    name[namelen] = '\0';

    if (strcasecmp(envr, name) == 0)
    {
        logInfo("MOJOSETUP_UI matches plugin '%0', boosting to USER_REQUESTED", name);
        return MOJOGUI_PRIORITY_USER_REQUESTED;
    }

    return pri;
} // adjustPriorityByEnvVar

Then call it in loadFilesystemGuiPlugins(), right after MojoPlatform_getGuiPriority():

MojoGuiPluginPriority priority;
priority = MojoPlatform_getGuiPriority(img, imglen);
priority = adjustPriorityByEnvVar(priority, fullpath);  // <-- ADD THIS LINE
if (priority != MOJOGUI_PRIORITY_NEVER_TRY)

Patch lua_glue.c for GOG splash image

GOG modified their mojosetup_mainline.lua to pass 4 arguments to gui.start(): (title, package_id, splashfname, splashpos). Stock mojosetup passes 3: (title, splashfname, splashpos). The C function luahook_gui_start() in lua_glue.c reads args positionally, so with GOG's 4-arg call, it reads package_id as the splash filename — the splash image silently fails to load.

Fix: detect the arg count with lua_gettop(L) and adjust positions accordingly. In lua_glue.c, find the luahook_gui_start() function and replace the argument reading:

// lua_glue.c — luahook_gui_start()
// GOG passes 4 args: (title, package_id, splashfname, splashpos)
// Stock passes 3 args: (title, splashfname, splashpos)
int nargs = lua_gettop(L);
const char *title = luaL_checkstring(L, 1);
const char *splashfname;
const char *splashpos;
if (nargs >= 4)
{
    // GOG convention: arg 2 is package_id (skip it)
    splashfname = lua_tostring(L, 3);
    splashpos = lua_tostring(L, 4);
}
else
{
    // Stock convention: arg 2 is splashfname
    splashfname = lua_tostring(L, 2);
    splashpos = lua_tostring(L, 3);
}

This maintains compatibility with both GOG's modified Lua and stock mojosetup.

Option A: Build with only the stdio GUI

mkdir build && cd build
cmake .. \
    -DMOJOSETUP_GUI_STDIO=TRUE \
    -DMOJOSETUP_GUI_STDIO_STATIC=TRUE \
    -DMOJOSETUP_GUI_NCURSES=FALSE \
    -DMOJOSETUP_GUI_GTKPLUS3=FALSE \
    -DMOJOSETUP_GUI_WWW=FALSE
make -j$(nproc)

This produces build/mojosetup (~728KB), a self-contained native binary with the stdio GUI statically linked. No runtime dependencies beyond libc.

Option B: Build with stdio + ncurses GUI

The ncurses GUI provides a proper TUI with dialog boxes, progress bars, and mouse support. Both GUIs are statically linked — ncurses auto-activates in a real terminal (TTY), and falls back to stdio over pipes/scripts/SSH.

Why static linking is tricky: CMake's FindCurses module finds the dynamic .so files even when you request static linking. You must manually override the CURSES_* variables to point at the .a files. Additionally, libncursesw.a depends on libtinfo.a (Fedora splits ncurses into separate libs), so you must set CURSES_EXTRA_LIBRARY to include it.

mkdir build && cd build
cmake .. \
    -DMOJOSETUP_GUI_STDIO=TRUE \
    -DMOJOSETUP_GUI_STDIO_STATIC=TRUE \
    -DMOJOSETUP_GUI_NCURSES=TRUE \
    -DMOJOSETUP_GUI_NCURSES_STATIC=TRUE \
    -DMOJOSETUP_GUI_GTKPLUS2=FALSE \
    -DMOJOSETUP_GUI_GTKPLUS3=FALSE \
    -DMOJOSETUP_GUI_WWW=FALSE \
    -DCURSES_NCURSES_LIBRARY="/usr/lib64/libncursesw.a" \
    -DCURSES_CURSES_LIBRARY="/usr/lib64/libncursesw.a" \
    -DCURSES_LIBRARY="/usr/lib64/libncursesw.a" \
    -DCURSES_FORM_LIBRARY="/usr/lib64/libformw.a" \
    -DFORM_LIBRARY="/usr/lib64/libformw.a" \
    -DCURSES_EXTRA_LIBRARY="/usr/lib64/libtinfo.a"
make -j$(nproc)

This produces build/mojosetup (~2.5MB) with both stdio and ncurses GUIs statically linked. Dynamic dependencies are only libbz2, liblzma, libm, libc.

Note: On Debian/Ubuntu, the .a files may be in /usr/lib/powerpc64le-linux-gnu/ instead of /usr/lib64/. Adjust the paths accordingly. Also, libtinfo may not be split out separately — check with nm -g /usr/lib64/libncursesw.a | grep tputs.

Option C: Build with stdio + ncurses + GTK3 GUI

This is the full-featured build: stdio and ncurses are statically linked into the main binary, while GTK3 is built as a separate .so plugin loaded at runtime from a guis/ directory next to the binary.

This avoids a hard dependency on libgtk-3.so — if GTK3 isn't installed on the target system, the binary falls back to ncurses or stdio. When running on a desktop with a display, it shows a graphical GTK3 installer.

Requires: The gui.c filesystem plugin patch must be applied first.

mkdir build && cd build
cmake .. \
    -DMOJOSETUP_GUI_STDIO=TRUE \
    -DMOJOSETUP_GUI_STDIO_STATIC=TRUE \
    -DMOJOSETUP_GUI_NCURSES=TRUE \
    -DMOJOSETUP_GUI_NCURSES_STATIC=TRUE \
    -DMOJOSETUP_GUI_GTKPLUS3=TRUE \
    -DMOJOSETUP_GUI_GTKPLUS3_STATIC=FALSE \
    -DMOJOSETUP_GUI_GTKPLUS2=FALSE \
    -DMOJOSETUP_GUI_WWW=FALSE \
    -DCURSES_NCURSES_LIBRARY="/usr/lib64/libncursesw.a" \
    -DCURSES_CURSES_LIBRARY="/usr/lib64/libncursesw.a" \
    -DCURSES_LIBRARY="/usr/lib64/libncursesw.a" \
    -DCURSES_FORM_LIBRARY="/usr/lib64/libformw.a" \
    -DFORM_LIBRARY="/usr/lib64/libformw.a" \
    -DCURSES_EXTRA_LIBRARY="/usr/lib64/libtinfo.a"
make -j$(nproc)

This produces:

  • build/mojosetup (~2.5MB) — main binary with stdio + ncurses static
  • build/libmojosetupgui_gtkplus3.so (~83KB) — GTK3 plugin (dynamically links libgtk-3.so)

Copy the GTK3 plugin alongside the binary:

mkdir -p build/guis
cp build/libmojosetupgui_gtkplus3.so build/guis/

Option D: Build with stdio + ncurses + GTK3 + GTK4 GUI

Same as Option C but with both GTK3 and GTK4 as separate .so plugins. On GNOME desktops with GTK4 installed, GTK4 takes priority. Systems with only GTK3 get the GTK3 interface. Headless/SSH sessions fall back to ncurses or stdio.

Requires: PR #77 applied and the gui.c filesystem plugin patch.

mkdir build && cd build
cmake .. \
    -DMOJOSETUP_GUI_STDIO=TRUE \
    -DMOJOSETUP_GUI_STDIO_STATIC=TRUE \
    -DMOJOSETUP_GUI_NCURSES=TRUE \
    -DMOJOSETUP_GUI_NCURSES_STATIC=TRUE \
    -DMOJOSETUP_GUI_GTKPLUS3=TRUE \
    -DMOJOSETUP_GUI_GTKPLUS3_STATIC=FALSE \
    -DMOJOSETUP_GUI_GTK4=TRUE \
    -DMOJOSETUP_GUI_GTK4_STATIC=FALSE \
    -DMOJOSETUP_GUI_GTKPLUS2=FALSE \
    -DMOJOSETUP_GUI_WWW=FALSE \
    -DCURSES_NCURSES_LIBRARY="/usr/lib64/libncursesw.a" \
    -DCURSES_CURSES_LIBRARY="/usr/lib64/libncursesw.a" \
    -DCURSES_LIBRARY="/usr/lib64/libncursesw.a" \
    -DCURSES_FORM_LIBRARY="/usr/lib64/libformw.a" \
    -DFORM_LIBRARY="/usr/lib64/libformw.a" \
    -DCURSES_EXTRA_LIBRARY="/usr/lib64/libtinfo.a"
make -j$(nproc)

This produces:

  • build/mojosetup (~2.5MB) — main binary with stdio + ncurses static
  • build/libmojosetupgui_gtkplus3.so (~83KB) — GTK3 plugin
  • build/libmojosetupgui_gtk4.so (~85KB) — GTK4 plugin

Copy the GUI plugins alongside the binary:

mkdir -p build/guis
cp build/libmojosetupgui_gtkplus3.so build/guis/
cp build/libmojosetupgui_gtk4.so build/guis/

Option E: Build with all GUIs — stdio + ncurses + GTK2 + GTK3 + GTK4 (recommended)

The most complete build: stdio and ncurses are statically linked, while GTK2, GTK3, and GTK4 are all built as separate .so plugins. This covers every scenario:

  • GTK2: The original GOG installer look with game art background via GTK2 theming (force with MOJOSETUP_UI=gtkplus2)
  • GTK4: Modern graphical interface, auto-selected on GNOME desktops
  • GTK3: Fallback graphical interface on systems without GTK4
  • ncurses: TUI for terminals
  • stdio: Last resort for pipes/scripts

Requires: PR #77 applied, the gui.c filesystem plugin patch, the gui.c env var fix, and the lua_glue.c splash fix.

mkdir build && cd build
cmake .. \
    -DMOJOSETUP_GUI_STDIO=TRUE \
    -DMOJOSETUP_GUI_STDIO_STATIC=TRUE \
    -DMOJOSETUP_GUI_NCURSES=TRUE \
    -DMOJOSETUP_GUI_NCURSES_STATIC=TRUE \
    -DMOJOSETUP_GUI_GTKPLUS2=TRUE \
    -DMOJOSETUP_GUI_GTKPLUS2_STATIC=FALSE \
    -DMOJOSETUP_GUI_GTKPLUS3=TRUE \
    -DMOJOSETUP_GUI_GTKPLUS3_STATIC=FALSE \
    -DMOJOSETUP_GUI_GTK4=TRUE \
    -DMOJOSETUP_GUI_GTK4_STATIC=FALSE \
    -DMOJOSETUP_GUI_WWW=FALSE \
    -DCURSES_NCURSES_LIBRARY="/usr/lib64/libncursesw.a" \
    -DCURSES_CURSES_LIBRARY="/usr/lib64/libncursesw.a" \
    -DCURSES_LIBRARY="/usr/lib64/libncursesw.a" \
    -DCURSES_FORM_LIBRARY="/usr/lib64/libformw.a" \
    -DFORM_LIBRARY="/usr/lib64/libformw.a" \
    -DCURSES_EXTRA_LIBRARY="/usr/lib64/libtinfo.a"
make -j$(nproc)

This produces:

  • build/mojosetup (~2.5MB) — main binary with stdio + ncurses static
  • build/libmojosetupgui_gtkplus2.so (~85KB) — GTK2 plugin
  • build/libmojosetupgui_gtkplus3.so (~83KB) — GTK3 plugin
  • build/libmojosetupgui_gtk4.so (~85KB) — GTK4 plugin

Copy the GUI plugins alongside the binary:

mkdir -p build/guis
cp build/libmojosetupgui_gtkplus2.so build/guis/
cp build/libmojosetupgui_gtkplus3.so build/guis/
cp build/libmojosetupgui_gtk4.so build/guis/

Note: You cannot statically link both GTK3 and GTK4 — the CMakeLists.txt has conflict guards that prevent GUI_STATIC_LINK_GTKPLUS3 and GUI_STATIC_LINK_GTK4 from being enabled simultaneously. Dynamic .so plugins work fine together.

Important: The CURSES_EXTRA_LIBRARY flag is critical on Fedora/RHEL where ncurses is split into libncursesw + libtinfo. Without it, the link step fails with hundreds of undefined symbols (_nc_screen_of, tputs_sp, SP, stdscr, cur_term, etc.). This flag tells CMake's FindCurses to include libtinfo.a in CURSES_LIBRARIES, placing it correctly after the object files in the link command.

GUI selection behavior

mojosetup tests each plugin at startup (filesystem .so plugins first, then static plugins). The selection logic with all five GUIs:

Environment GTK4 priority() GTK3 priority() GTK2 priority() ncurses priority() stdio priority() Result
Desktop (GNOME, display set) TRY_FIRST TRY_FIRST TRY_FIRST TRY_NORMAL TRY_ABSOLUTELY_LAST GTK4 selected (loaded first)
Desktop (non-GNOME, display set) TRY_NORMAL TRY_NORMAL TRY_NORMAL TRY_NORMAL TRY_ABSOLUTELY_LAST GTK4 selected (loaded first)
Desktop, GTK4 not installed plugin not found TRY_FIRST/TRY_NORMAL TRY_FIRST/TRY_NORMAL TRY_NORMAL TRY_ABSOLUTELY_LAST GTK3 selected
Desktop, no GTK3/GTK4 installed plugin not found plugin not found TRY_FIRST/TRY_NORMAL TRY_NORMAL TRY_ABSOLUTELY_LAST GTK2 selected
Desktop, no GTK installed plugin not found plugin not found plugin not found TRY_NORMAL TRY_ABSOLUTELY_LAST ncurses selected
Real TTY (console, ssh -t) init() fails (no display) init() fails (no display) init() fails (no display) TRY_NORMAL TRY_ABSOLUTELY_LAST ncurses selected
Non-TTY (pipe, script, SSH) init() fails init() fails init() fails NEVER_TRY TRY_ABSOLUTELY_LAST (patched) stdio selected

All GTK plugins call gtk_init_check() which returns false when no display is available, so they fail gracefully and fall back to the next available GUI.

Note: GTK2 runs on X11 only. On Wayland desktops, GTK2 connects via XWayland (the X11 compatibility layer). This works transparently on GNOME with Fedora's default Wayland session.

You can force a specific GUI with environment variables:

MOJOSETUP_UI=stdio      # force text-only
MOJOSETUP_UI=ncurses    # force TUI
MOJOSETUP_UI=gtkplus2   # force GTK2 (original GOG look with game art background)
MOJOSETUP_UI=gtkplus3   # force GTK3
MOJOSETUP_UI=gtk4       # force GTK4

Note: For filesystem .so plugins, the MOJOSETUP_UI env var override requires the gui.c env var fix. Without it, the env var only works for statically linked plugins.

You can enable logging to see which GUI was selected:

MOJOSETUP_LOG=- MOJOSETUP_LOGLEVEL=info ./mojosetup 2>&1 | grep -i "gui\|plugin\|selected"

Verify

file build/mojosetup
# mojosetup: ELF 64-bit LSB executable, 64-bit PowerPC ...

ldd build/mojosetup
# Should show libbz2, liblzma, libm, libc — no libncurses (statically linked)

./build/mojosetup --help 2>&1 || true
# Will print an error about missing MOJOSETUP_BASE, which is expected

Step 2: Run a GOG installer

The critical environment variable is MOJOSETUP_BASE, which tells mojosetup where to find the .sh file containing the embedded ZIP payload.

Interactive (with a TTY)

If you have a terminal (local session, SSH with -tt, tmux, etc.):

export MOJOSETUP_BASE=/path/to/game_installer.sh
export MOJOSETUP_UI=stdio
/tmp/mojosetup/build/mojosetup

You'll get a text-mode installer: page through the EULA, choose options, pick an install directory, and watch the extraction progress.

Scripted / non-interactive

For headless or automated installs, pipe the required responses:

{
    echo "n"        # stop paging the EULA (skip to end)
    echo ""         # continue past the EULA readme page
    echo "Y"        # accept the license agreement
    echo ""         # accept default install destination (enter for #1)
    echo ""         # accept default options (enter to continue)
    echo "Always"   # if files already exist, always replace
    echo "Always"   # (extra, in case multiple prompts)
    sleep 7200      # keep stdin open while extraction runs
} | MOJOSETUP_BASE=/path/to/game_installer.sh \
    MOJOSETUP_UI=stdio \
    /tmp/mojosetup/build/mojosetup

The default install destination is typically ~/GOG Games/<Game Name>.

Over SSH (non-interactive, background)

ssh user@host 'nohup bash -c '"'"'{
    echo "n"
    echo ""
    echo "Y"
    echo ""
    echo ""
    echo "Always"
    echo "Always"
    sleep 7200
} | MOJOSETUP_BASE=/home/user/Downloads/game_installer.sh \
    MOJOSETUP_UI=stdio \
    /tmp/mojosetup/build/mojosetup'"'"' > /tmp/install.log 2>&1 &'

Monitor progress:

ssh user@host 'tail -5 /tmp/install.log; echo "---"; du -sh ~/GOG\ Games/*'

Troubleshooting

"Failed to start GUI" / "No usable GUI found"

Cause: The stdio GUI's priority function returned MOJOGUI_PRIORITY_NEVER_TRY because isatty(0) returned false (stdin is not a terminal).

Fix: Apply the patch from Step 1 above and rebuild.

How it works: In gui.c, the initialization loop calls each plugin's priority(istty) function. If it returns NEVER_TRY, the plugin is skipped entirely -- even MOJOSETUP_UI=stdio cannot override this, because the env var check only fires when priority is not NEVER_TRY:

// gui.c — calcGuiPriority()
retval = gui->priority(MojoPlatform_istty());
if (retval != MOJOGUI_PRIORITY_NEVER_TRY)   // <-- must pass this first
{
    if ((envr != NULL) && (strcasecmp(envr, gui->name()) == 0))
        retval = MOJOGUI_PRIORITY_USER_REQUESTED;
}

"File already exists! Replace?"

If a prior partial install left files behind, mojosetup prompts for each conflicting file. Include echo "Always" in your scripted input to auto-replace all. Or delete the install directory first:

rm -rf ~/GOG\ Games/Game\ Name/

mojosetup exits immediately with no output

Check that MOJOSETUP_BASE points to the actual .sh file (not a directory or extracted contents). mojosetup reads the ZIP payload directly from this file.

Build fails with missing Lua headers

The mojosetup repo includes its own Lua source. If CMake can't find it, ensure you're building from the repo root (not a subdirectory).

Step 3: Repack GOG installers (optional)

Instead of manually setting MOJOSETUP_BASE and running the native binary, you can repack the .sh installer to embed your custom mojosetup. The repacked installer works exactly like the original — just bash installer.sh.

How it works

The GOG .sh file has three sections concatenated together:

[ Shell header (519 lines) ] [ Gzip'd tar (binaries) ] [ ZIP payload (game data) ]

The repack script:

  1. Extracts the shell header (first 519 lines, controlled by OLDSKIP=520)
  2. Extracts and unpacks the gzip'd tar containing startmojo.sh, binaries, etc.
  3. Adds bin/linux/<arch>/mojosetup to the tarball (using uname -m to detect the architecture — alongside the existing x86_64 binary, which is kept for compatibility)
  4. Repacks the tar, then updates four header values to match the new tarball:
    • CRCsum= (line 17) — cksum of the new tarball
    • MD5= (line 18) — MD5 hash of the new tarball
    • filesizes= (line 26) — byte count of the new tarball
    • OLDUSIZE= (line 291) — uncompressed size in KB
  5. Reassembles: header + new tarball + original ZIP (transferred via dd, never loaded into memory — important for 8GB+ installers)

The startmojo.sh wrapper already handles architecture detection via uname -m and looks for bin/linux/<arch>/mojosetup automatically. No patches to the script are needed.

Repack script

Save this as repack_gog_mojosetup.sh:

#!/bin/bash
#
# repack_gog_mojosetup.sh - Repack a GOG .sh installer with a custom mojosetup binary
#
# Usage: ./repack_gog_mojosetup.sh <input.sh> <mojosetup_binary> [options] [output.sh]

set -euo pipefail

ARCH=$(uname -m)

if [ $# -lt 2 ]; then
    echo "Usage: $0 <input.sh> <mojosetup_binary> [--gtk2-plugin <path>] [--gtk3-plugin <path>] [--gtk4-plugin <path>] [output.sh]"
    exit 1
fi

INPUT="$1"
MOJOSETUP_BIN="$2"
shift 2

GTK2_PLUGIN=""
GTK3_PLUGIN=""
GTK4_PLUGIN=""
OUTPUT=""
while [ $# -gt 0 ]; do
    case "$1" in
        --gtk2-plugin) GTK2_PLUGIN="$2"; shift 2 ;;
        --gtk3-plugin) GTK3_PLUGIN="$2"; shift 2 ;;
        --gtk4-plugin) GTK4_PLUGIN="$2"; shift 2 ;;
        *) OUTPUT="$1"; shift ;;
    esac
done
OUTPUT="${OUTPUT:-${INPUT%.sh}.repacked.sh}"

WORKDIR=$(mktemp -d)
trap 'rm -rf "$WORKDIR"' EXIT

echo "==> Analyzing original installer..."

# Extract the shell header (first 519 lines)
head -519 "$INPUT" > "$WORKDIR/header.sh"

HEADER_BYTES=$(wc -c < "$WORKDIR/header.sh" | tr -d " ")
OLD_FILESIZES=$(head -519 "$INPUT" | grep '^filesizes=' | sed 's/filesizes="//;s/"//')
TOTAL_SIZE=$(stat -c%s "$INPUT")
GAME_DATA_OFFSET=$((HEADER_BYTES + OLD_FILESIZES))
GAME_DATA_SIZE=$((TOTAL_SIZE - GAME_DATA_OFFSET))

echo "    Header: $HEADER_BYTES bytes, Old tarball: $OLD_FILESIZES bytes"
echo "    Game data ZIP: $GAME_DATA_SIZE bytes (offset $GAME_DATA_OFFSET)"

# Extract and modify the binaries tarball
echo "==> Extracting original binaries tarball..."
dd if="$INPUT" bs=1 skip="$HEADER_BYTES" count="$OLD_FILESIZES" 2>/dev/null > "$WORKDIR/old_payload.tar.gz"
mkdir "$WORKDIR/binaries"
tar xzf "$WORKDIR/old_payload.tar.gz" -C "$WORKDIR/binaries"

echo "==> Adding mojosetup binary for $ARCH..."
mkdir -p "$WORKDIR/binaries/bin/linux/$ARCH/guis"
cp "$MOJOSETUP_BIN" "$WORKDIR/binaries/bin/linux/$ARCH/mojosetup"
chmod +x "$WORKDIR/binaries/bin/linux/$ARCH/mojosetup"

# Add GTK2 plugin if provided
if [ -n "$GTK2_PLUGIN" ]; then
    echo "==> Adding GTK2 GUI plugin..."
    cp "$GTK2_PLUGIN" "$WORKDIR/binaries/bin/linux/$ARCH/guis/libmojosetupgui_gtkplus2.so"
    chmod +x "$WORKDIR/binaries/bin/linux/$ARCH/guis/libmojosetupgui_gtkplus2.so"
fi

# Add GTK3 plugin if provided
if [ -n "$GTK3_PLUGIN" ]; then
    echo "==> Adding GTK3 GUI plugin..."
    cp "$GTK3_PLUGIN" "$WORKDIR/binaries/bin/linux/$ARCH/guis/libmojosetupgui_gtkplus3.so"
    chmod +x "$WORKDIR/binaries/bin/linux/$ARCH/guis/libmojosetupgui_gtkplus3.so"
fi

# Add GTK4 plugin if provided
if [ -n "$GTK4_PLUGIN" ]; then
    echo "==> Adding GTK4 GUI plugin..."
    cp "$GTK4_PLUGIN" "$WORKDIR/binaries/bin/linux/$ARCH/guis/libmojosetupgui_gtk4.so"
    chmod +x "$WORKDIR/binaries/bin/linux/$ARCH/guis/libmojosetupgui_gtk4.so"
fi

# Create new tarball and compute checksums
echo "==> Creating new binaries tarball..."
tar czf "$WORKDIR/new_payload.tar.gz" -C "$WORKDIR/binaries" .

NEW_FILESIZES=$(stat -c%s "$WORKDIR/new_payload.tar.gz")
NEW_USIZE_KB=$(( $(gzip -l "$WORKDIR/new_payload.tar.gz" | tail -1 | awk '{print $2}') / 1024 ))
NEW_CRC=$(cksum < "$WORKDIR/new_payload.tar.gz" | awk '{print $1}')
NEW_MD5=$(md5sum < "$WORKDIR/new_payload.tar.gz" | cut -d' ' -f1)

echo "    New tarball: $NEW_FILESIZES bytes (was $OLD_FILESIZES)"

# Update header checksums and sizes
echo "==> Updating header..."
sed -i "s/^CRCsum=\".*\"/CRCsum=\"$NEW_CRC\"/" "$WORKDIR/header.sh"
sed -i "s/^MD5=\".*\"/MD5=\"$NEW_MD5\"/" "$WORKDIR/header.sh"
sed -i "s/^filesizes=\".*\"/filesizes=\"$NEW_FILESIZES\"/" "$WORKDIR/header.sh"
sed -i "s/echo OLDUSIZE=.*/echo OLDUSIZE=$NEW_USIZE_KB/" "$WORKDIR/header.sh"

# Assemble the repacked installer
echo "==> Assembling repacked installer..."
cat "$WORKDIR/header.sh" > "$OUTPUT"
cat "$WORKDIR/new_payload.tar.gz" >> "$OUTPUT"
dd if="$INPUT" bs=1M iflag=skip_bytes,count_bytes skip=$GAME_DATA_OFFSET count=$GAME_DATA_SIZE >> "$OUTPUT" 2>/dev/null
chmod +x "$OUTPUT"

echo "==> Done! $(stat -c%s "$OUTPUT" | numfmt --to=iec-i)  $OUTPUT"
echo "Run with:  bash $OUTPUT"

Usage

# Repack with just the mojosetup binary (stdio + ncurses)
./repack_gog_mojosetup.sh \
    ~/Downloads/pillars_of_eternity_3_8_0_88149_85633.sh \
    /tmp/mojosetup/build/mojosetup

# Repack with GTK3 plugin included
./repack_gog_mojosetup.sh \
    ~/Downloads/pillars_of_eternity_3_8_0_88149_85633.sh \
    /tmp/mojosetup/build/mojosetup \
    --gtk3-plugin /tmp/mojosetup/build/libmojosetupgui_gtkplus3.so

# Repack with all GTK plugins (recommended)
./repack_gog_mojosetup.sh \
    ~/Downloads/pillars_of_eternity_3_8_0_88149_85633.sh \
    /tmp/mojosetup/build/mojosetup \
    --gtk2-plugin /tmp/mojosetup/build/libmojosetupgui_gtkplus2.so \
    --gtk3-plugin /tmp/mojosetup/build/libmojosetupgui_gtkplus3.so \
    --gtk4-plugin /tmp/mojosetup/build/libmojosetupgui_gtk4.so

# Run the repacked installer — works natively!
bash ~/Downloads/pillars_of_eternity_3_8_0_88149_85633.repacked.sh

# Run with the original GOG look (GTK2 with game art background)
MOJOSETUP_UI=gtkplus2 bash ~/Downloads/pillars_of_eternity_3_8_0_88149_85633.repacked.sh

# Makeself verification passes:
#   Verifying archive integrity... MD5 checksums are OK. All good.

The repacked installer is the same size as the original (plus ~2.5MB for the native binary and ~255KB for all three GTK plugins). The original x86_64 binary is preserved, so the repacked installer still works on x86_64 systems too.

Why not box64?

Running GOG's x86_64 mojosetup through box64 was attempted but failed due to multiple compounding issues:

  1. ncurses ABI mismatch: The bundled mojosetup links against libncurses.so.5 but modern distros ship .so.6. Box64 can't resolve this.
  2. PTY input handling: Box64 cannot properly handle interactive PTY input required by the installer's text UI.
  3. isatty gate: Even with stdio fallback, the isatty() check in gui_stdio.c blocks the stdio GUI from loading under box64's emulated environment, and patching is not possible on the x86_64 binary.

Building mojosetup natively is both simpler and more reliable.

Reference

  • mojosetup source: https://github.com/icculus/mojosetup
  • GTK4 PR: icculus/mojosetup#77
  • Makeself: https://makeself.io/ (the self-extracting archive format GOG uses)
  • Key source files:
    • gui_stdio.c — stdio GUI plugin (priority function, init, all UI prompts)
    • gui_ncurses.c — ncurses GUI plugin (1497 lines, TUI dialogs)
    • gui_gtkplus2.c — GTK2 GUI plugin (stock mojosetup, uses bg_pixmap theming for backgrounds)
    • gui_gtkplus3.c — GTK3 GUI plugin (882 lines, graphical dialogs)
    • gui_gtk4.c — GTK4 GUI plugin (961 lines, from PR #77; uses GtkAlertDialog, GtkFileDialog, GtkDropDown, GdkMemoryTexture, CSS margins)
    • gui.c — GUI plugin loader: staticGui[] array (includes GTK4 between cocoa and gtkplus3), loadDynamicGuiPlugins(), MojoGui_initGuiPlugin()
    • gui.c (patched) — loadFilesystemGuiPlugins() scans guis/ dir next to binary; adjustPriorityByEnvVar() enables MOJOSETUP_UI override for .so plugins
    • gui.h — GUI interface declarations (patched: adds MojoGuiPlugin_gtk4())
    • lua_glue.c — Lua/C bridge (patched: luahook_gui_start() handles both GOG's 4-arg and stock 3-arg gui.start() calling convention for splash images)
    • platform_unix.cMojoPlatform_dlopen() (lines 944-986, writes .so to temp file then dlopens), MojoPlatform_getGuiPriority() (lines 1008-1043, forks a child to safely test each plugin)
    • fileio.cMojoArchive_initBaseArchive() (lines 1433-1521, handles MOJOSETUP_BASE)
    • CMakeLists.txt — build options for GUI plugin selection (ncurses at lines 444-471, GTK3 at lines 520-550, GTK4 added by PR #77 with conflict guards for static GTK3+GTK4)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment