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.
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 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.
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.
# Fedora / RHEL
sudo dnf install cmake gcc gcc-c++ make git
# Debian / Ubuntu
sudo apt install cmake gcc g++ make gitFor 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 filesFor 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-murrineNote: The
gtk-murrine-engine/gtk2-engines-murrinepackage 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-devFor 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-devNote: GTK4 support requires applying PR #77 from the mojosetup repo — see Step 1 for details.
cd /tmp
git clone https://github.com/icculus/mojosetup.git
cd mojosetupGTK4 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-77This adds:
gui_gtk4.c— GTK4 GUI plugin (961 lines, usesGtkAlertDialog,GtkFileDialog,GtkDropDown,GdkMemoryTexture, etc.)- CMake options:
MOJOSETUP_GUI_GTK4,MOJOSETUP_GUI_GTK4_STATIC gui.hdeclaration:MojoGuiPlugin_gtk4()gui.cstatic plugin entry:STATIC_GUI_PLUGIN(gtk4)(between cocoa and gtkplus3)
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.cOr 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;
}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.cThen 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
}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;
} // adjustPriorityByEnvVarThen 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)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.
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.
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
.afiles may be in/usr/lib/powerpc64le-linux-gnu/instead of/usr/lib64/. Adjust the paths accordingly. Also,libtinfomay not be split out separately — check withnm -g /usr/lib64/libncursesw.a | grep tputs.
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 staticbuild/libmojosetupgui_gtkplus3.so(~83KB) — GTK3 plugin (dynamically linkslibgtk-3.so)
Copy the GTK3 plugin alongside the binary:
mkdir -p build/guis
cp build/libmojosetupgui_gtkplus3.so build/guis/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 staticbuild/libmojosetupgui_gtkplus3.so(~83KB) — GTK3 pluginbuild/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/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 staticbuild/libmojosetupgui_gtkplus2.so(~85KB) — GTK2 pluginbuild/libmojosetupgui_gtkplus3.so(~83KB) — GTK3 pluginbuild/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_GTKPLUS3andGUI_STATIC_LINK_GTK4from being enabled simultaneously. Dynamic.soplugins work fine together.
Important: The
CURSES_EXTRA_LIBRARYflag is critical on Fedora/RHEL where ncurses is split intolibncursesw+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'sFindCursesto includelibtinfo.ainCURSES_LIBRARIES, placing it correctly after the object files in the link command.
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 GTK4Note: For filesystem
.soplugins, theMOJOSETUP_UIenv 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"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 expectedThe critical environment variable is MOJOSETUP_BASE, which tells mojosetup
where to find the .sh file containing the embedded ZIP payload.
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/mojosetupYou'll get a text-mode installer: page through the EULA, choose options, pick an install directory, and watch the extraction progress.
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/mojosetupThe default install destination is typically ~/GOG Games/<Game Name>.
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/*'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;
}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/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.
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).
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.
The GOG .sh file has three sections concatenated together:
[ Shell header (519 lines) ] [ Gzip'd tar (binaries) ] [ ZIP payload (game data) ]
The repack script:
- Extracts the shell header (first 519 lines, controlled by
OLDSKIP=520) - Extracts and unpacks the gzip'd tar containing
startmojo.sh, binaries, etc. - Adds
bin/linux/<arch>/mojosetupto the tarball (usinguname -mto detect the architecture — alongside the existingx86_64binary, which is kept for compatibility) - Repacks the tar, then updates four header values to match the new tarball:
CRCsum=(line 17) —cksumof the new tarballMD5=(line 18) — MD5 hash of the new tarballfilesizes=(line 26) — byte count of the new tarballOLDUSIZE=(line 291) — uncompressed size in KB
- 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.
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"# 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.
Running GOG's x86_64 mojosetup through box64 was attempted but failed due to multiple compounding issues:
- ncurses ABI mismatch: The bundled mojosetup links against
libncurses.so.5but modern distros ship.so.6. Box64 can't resolve this. - PTY input handling: Box64 cannot properly handle interactive PTY input required by the installer's text UI.
- isatty gate: Even with stdio fallback, the
isatty()check ingui_stdio.cblocks 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.
- 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, usesbg_pixmaptheming for backgrounds)gui_gtkplus3.c— GTK3 GUI plugin (882 lines, graphical dialogs)gui_gtk4.c— GTK4 GUI plugin (961 lines, from PR #77; usesGtkAlertDialog,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()scansguis/dir next to binary;adjustPriorityByEnvVar()enablesMOJOSETUP_UIoverride for.sopluginsgui.h— GUI interface declarations (patched: addsMojoGuiPlugin_gtk4())lua_glue.c— Lua/C bridge (patched:luahook_gui_start()handles both GOG's 4-arg and stock 3-arggui.start()calling convention for splash images)platform_unix.c—MojoPlatform_dlopen()(lines 944-986, writes.soto temp file then dlopens),MojoPlatform_getGuiPriority()(lines 1008-1043, forks a child to safely test each plugin)fileio.c—MojoArchive_initBaseArchive()(lines 1433-1521, handlesMOJOSETUP_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)