Skip to content

feat: native Linux support#98

Open
velomeister wants to merge 16 commits intoPathOfBuildingCommunity:masterfrom
velomeister:linux-port
Open

feat: native Linux support#98
velomeister wants to merge 16 commits intoPathOfBuildingCommunity:masterfrom
velomeister:linux-port

Conversation

@velomeister
Copy link

Linux Port

This document describes the changes made to build and run SimpleGraphic natively on Linux.

Overview

SimpleGraphic previously only built and ran on Windows. This work ports it to Linux (x86-64), allowing Path of Building to run natively without Wine. The engine compiles with GCC 15 on Fedora Linux and successfully launches PoB with a working window, renderer, passive tree, and network features.

All changes are backwards-compatible — the Windows build is unaffected.

Scope note: This PR covers the engine port and a developer install workflow only. End-user packaging (Flatpak, AppImage, distro packages) is out of scope and would require additional work on top of this foundation — primarily bundling shared GL/EGL dependencies and coordinating a release pipeline between the two repos. The intent of this PR is to unblock native Linux development and provide the binaries for a follow-up PoB-side PR that commits them to runtime/, mirroring how Windows DLL updates are handled.


How to Build (Linux)

Prerequisites: cmake, ninja, gcc (≥ 13), pkg-config, libGL, libGLESv2, libEGL, libX11, libcurl.

git clone <this repo>
cd PathOfBuilding-SimpleGraphic
cmake -B build
cmake --build build -j$(nproc)

vcpkg handles all other dependencies automatically via the manifest.

How to Install

Install directly into PoB's runtime/ directory (where the fonts and existing data already live):

cmake --install build --prefix /path/to/PathOfBuilding/runtime

This places the binaries alongside PoB's existing runtime/SimpleGraphic/Fonts/, mirroring the Windows layout:

runtime/
  PathOfBuilding          # launcher
  libSimpleGraphic.so     # engine
  socket.so, lcurl.so, lua-utf8.so, lzip.so  # Lua C modules
  SimpleGraphic/          # already in PoB repo
    Fonts/
    SimpleGraphic.cfg

How to Run

From the install

cd /path/to/PathOfBuilding
runtime/PathOfBuilding src/Launch.lua

From the build directory (development)

cd /path/to/PathOfBuilding
LD_LIBRARY_PATH=/path/to/SimpleGraphic/build \
  /path/to/SimpleGraphic/build/PathOfBuilding src/Launch.lua

The launcher resolves all paths relative to the script argument, so it must be run from the PoB repository root with src/Launch.lua as the first argument.


Changes

1. GCC 15: Missing Standard Library Headers

GCC 15 enforces stricter implicit-inclusion rules. Several translation units relied on headers being transitively included through other headers, which is no longer guaranteed.

Files: engine/common/base64.c, engine/common/base64.h, engine/core/core_compress.cpp,
engine/render/r_font.cpp, engine/render/r_main.cpp, engine/render/r_texture.cpp

  • base64.c: add <string.h> for memset/memcpy
  • base64.h: add <stddef.h> for size_t
  • core_compress.cpp: add <cstring> for memcpy
  • r_font.cpp: add <algorithm> for std::find/std::min
  • r_main.cpp: add <chrono> and <thread>
  • r_texture.cpp: add <algorithm> and <thread>

2. Type Mismatch in r_texture.cpp

4ull (unsigned long long) does not match size_t (unsigned long) on Linux, causing -Werror=narrowing failures. Replace with size_t{4}.

File: engine/render/r_texture.cpp

3. Windows-only Sleep() API

Sleep(1) in GetShaderImageSize is a Windows API. Replace with the portable
std::this_thread::sleep_for(std::chrono::milliseconds(1)).

File: engine/render/r_main.cpp

4. Error() Format String Bug

Error("Exception: ", e.what()) is missing a %s format specifier, silently discarding the exception message. This is a pre-existing bug, visible on all platforms once the exception path is hit.

File: engine/system/win/sys_main.cpp

// Before
Error("Exception: ", e.what());
// After
Error("Exception: %s", e.what());

5. IndexUTF8ToUTF32 Guarded by #ifdef _WIN32

IndexUTF8ToUTF32 uses only standard C++ (std::string_view, char32_t, std::vector) but was inside a #ifdef _WIN32 block, making it unavailable on Linux. It is called from r_font.cpp and r_main.cpp on all platforms.

File: engine/common/common.cpp — move the function above the #endif.

6. SpawnProcess and PlatformOpenURL for Linux

Both functions had stub implementations that printed #warning and returned an error string.

File: engine/system/win/sys_main.cpp

  • SpawnProcess: implemented using fork/execvp. Arguments are split on whitespace, which is sufficient for PoB's usage (relaunching itself with a version flag).
  • PlatformOpenURL: implemented using xdg-open, the standard Linux mechanism for opening a URL in the user's default browser.
  • Remove <uuid/uuid.h> which is not available on all distributions and was unused.

7. Runtime Data Path: SG_BASE_PATH

On Windows, the launcher executable lives in runtime/, so FindBasePath() (which returns the directory of the current executable via /proc/self/exe) naturally points at the right place. On Linux, the binary lives in the CMake build directory, so the data files (SimpleGraphic/Fonts/, etc.) are not found.

File: engine/system/win/sys_main.cpp

A new environment variable SG_BASE_PATH is checked at the start of FindBasePath() on Linux. If set, it is used directly. The launcher sets it to its own directory, which contains SimpleGraphic/Fonts/ whether running from the build directory or an installed prefix.

#elif __linux__
    if (const char* sgBasePath = ::getenv("SG_BASE_PATH")) {
        progPath = sgBasePath;
        progPath = weakly_canonical(progPath);
        return progPath;
    }
    // ... existing /proc/self/exe fallback

8. CMake Build System: Cross-Platform Lua Module Fixes

File: CMakeLists.txt, cmake/FindLuaJIT.cmake

Position-Independent Code

Static libraries (cmp_core, imgui) linked into libSimpleGraphic.so must be compiled with -fPIC. Added POSITION_INDEPENDENT_CODE ON to both targets.

-Wno-template-body

GCC 15 introduced a new warning triggered by sol2's template definitions. Suppressed with -Wno-template-body on Linux.

zstd: Static Fallback

vcpkg provides zstd::libzstd_static on Linux rather than zstd::libzstd_shared. Use a generator expression to fall back gracefully:

$<IF:$<TARGET_EXISTS:zstd::libzstd_shared>,zstd::libzstd_shared,zstd::libzstd_static>

Lua Module lib Prefix

On Linux, CMake adds a lib prefix to SHARED libraries by default (e.g. libsocket.so). Lua's require("socket") looks for socket.so, not libsocket.so. Added PREFIX "" to all Lua module targets on non-Windows: lcurl, lua-utf8, luasocket, lzip.

lcurl: Duplicate Symbol

LuaJIT 2.1 exports luaL_setfuncs as part of its Lua 5.2 compatibility layer, and
l52util.c (bundled with lcurl) also defines it as a Lua 5.1 shim. On Linux this causes a duplicate symbol error at link time. Resolved with -Wl,--allow-multiple-definition.

lua-utf8: LUA_BUILD_AS_DLL / INT_MAX

LUA_BUILD_AS_DLL is an MSVC-specific define. On Linux, replace it with
-include limits.h to provide INT_MAX (needed by the UTF-8 library).

luasocket: Platform Socket Backend

wsocket.c (Windows Sockets) is replaced with usocket.c (POSIX sockets) on Linux. wsock32 and ws2_32 link dependencies are also made Windows-only.

FindLuaJIT.cmake

Added luajit-2.1 and luajit-5.1 as fallback search names for the LuaJIT header directory and library, matching the naming used by the Linux vcpkg install layout.

9. ImGui: System GLES3 Instead of ANGLE

Files: CMakeLists.txt, engine/render/r_main.cpp

On Windows, ImGui is compiled with IMGUI_IMPL_OPENGL_ES2 and linked against ANGLE's static libGLESv2. On Linux the engine creates a GLES3 context via the system EGL driver (not ANGLE), so ANGLE's static library has no active context. Every call to glCreateShader inside ImGui returns 0, and shader compilation fails silently on every frame.

Fix: On Linux, compile ImGui with IMGUI_IMPL_OPENGL_ES3 and link against the system GLESv2 library. This makes ImGui's GL calls share the same context as the engine.

Additionally, ImGui_ImplOpenGL3_Init was previously called with the hardcoded string "#version 100" (GLES2). Change to nullptr so ImGui queries the active context for the correct GLSL version automatically. This is correct on both Windows (resolves to "#version 100" via IMGUI_IMPL_OPENGL_ES2) and Linux (resolves to "#version 300 es").

10. Linux Launcher Binary

File: linux/launcher.c, CMakeLists.txt

A small C launcher binary (PathOfBuilding) is built on Linux. It is the entry point for running PoB and handles the environment setup that runtime/Path of Building.exe handles implicitly on Windows via its install layout.

Responsibilities:

  1. Load the engine: dlopens libSimpleGraphic.so from its own directory with RTLD_LAZY | RTLD_GLOBAL (RTLD_GLOBAL is required so LuaJIT's symbols are visible to dlopen'd Lua C modules like socket.so).

  2. Resolve the script path: realpath is called on the Lua script argument so that basePath inside the engine (derived from /proc/self/exe, which points at the build directory) does not interfere with relative path resolution.

  3. Set SG_BASE_PATH: Points the engine at the launcher's own directory for data files (fonts, config). This works for both the build directory and any installed prefix because SimpleGraphic/Fonts/ is installed alongside the launcher.

  4. Set LUA_PATH: Adds <pob_root>/runtime/lua/?.lua so Lua's require finds the bundled Lua modules (e.g. xml, base64, sha1). The PoB root is derived from the script path: src/Launch.lua...

  5. Set LUA_CPATH: Adds the launcher's directory so Lua's require finds the C modules (socket.so, lcurl.so, lua-utf8.so, lzip.so).

11. vcpkg Overlay Ports

Files: vcpkg-ports/, vcpkg-configuration.json

Three additional overlay ports are required for the Linux build:

  • angle (chromium_5414#9): GLES/EGL implementation. Used by ImGui and the render system. The overlay pins a specific Chromium snapshot with patches for build system compatibility.
  • openssl (3.5.4): Required by lcurl for HTTPS. The overlay overrides the vcpkg baseline to use a newer version with GCC 15 compatibility.
  • pthread-stubs (0.4): Provides stub implementations of pthread functions required by EGL on Linux.

12. LuaJIT Port Version 3: Linux GC64 and Null-Safe getenv

Files: vcpkg-ports/ports/luajit/, vcpkg-ports/versions/

Two Linux-specific fixes were added to the LuaJIT vcpkg port, bumping it from port-version 1 to port-version 3.

LUAJIT_ENABLE_GC64 (port-version 2)

When LuaJIT is statically linked into libSimpleGraphic.so and the library is loaded via dlopen, the dynamic linker places the .so at a high virtual address (above 4 GB). By default, LuaJIT allocates all GC-managed memory within 4 GB of its own code section, using 32-bit relative offsets. At high addresses this constraint cannot be satisfied and lua_newstate returns NULL, crashing immediately.

Fix: Build LuaJIT with XCFLAGS=-DLUAJIT_ENABLE_GC64, which lifts the 4 GB constraint by using full 64-bit pointers for GC objects. Applied via portfile.cmake inside the elseif(VCPKG_TARGET_IS_LINUX) block.

006-fix-getenvcopy-linux.patch (port-version 3)

The pob-wide-crt.patch (applied in an earlier port version) introduces a Windows-friendly _lua_getenvcopy abstraction. On non-Windows platforms it defines:

#define _lua_getenvcopy(name) strdup(getenv(name))

When getenv returns NULL (variable not set), strdup(NULL) is undefined behaviour and crashes. This affects two call sites: luaopen_package (on startup, when LUA_PATH is unset) and lj_cf_os_getenv (at runtime, whenever Lua code calls os.getenv for an unset variable).

Fix: Replace the unsafe macro with a null-safe inline function:

static inline char* _lua_getenvcopy(const char* name) {
    const char* v = getenv(name);
    return v ? strdup(v) : NULL;
}
static inline void _lua_getenvfree(const char* v) { free((void*)v); }

Also add #include <stdlib.h> and #include <string.h> inside the #else block so the inline functions have access to getenv, strdup, and free when the header is compiled as a standalone translation unit.

The portfile also fixes two minor Linux-specific build issues:

  • The install step created a circular symlink (bin/luajit → luajit). Fixed by copying the real binary from the build tree after installation.
  • configure was not marked executable before vcpkg_configure_make. Fixed with file(CHMOD ...).

13. Install Target

File: CMakeLists.txt

cmake --install build --prefix /path/to/PathOfBuilding/runtime places the Linux binaries directly into PoB's runtime/ directory, where SimpleGraphic/Fonts/ already lives. The launcher's SG_BASE_PATH = dir logic then naturally finds the fonts alongside the installed binaries, matching the Windows layout exactly.

One CMake fix was needed: LIBRARY DESTINATION "." added to all install(TARGETS ... SHARED) rules. Without this, CMake's GNUInstallDirs places .so files in lib64/ rather than the prefix root, breaking the launcher's dlopen path.


Known Limitations

  • SpawnProcess argument splitting: The Linux implementation splits argList on whitespace. Arguments containing spaces are not quoted. This is sufficient for PoB's current usage (relaunching itself) but is not a general-purpose solution.

  • No system font fallback: The engine loads only the pre-rendered bitmap fonts shipped in data/SimpleGraphic/Fonts/. There is no fallback to system fonts.

GCC 15 enforces stricter implicit inclusion rules. Add missing headers
that were previously available transitively:
- base64.c: <string.h> for memset/memcpy
- base64.h: <stddef.h> for size_t
- core_compress.cpp: <cstring> for memcpy
- r_font.cpp: <algorithm> for std::find/std::min
4ull (unsigned long long) doesn't match size_t (unsigned long) on Linux,
causing -Werror failures. Replace with size_t{4} to match the type of the
surrounding expression. Also add missing <algorithm> and <thread> includes.
Sleep() is a Windows-only API. Replace with the portable
std::this_thread::sleep_for(). Add the required <chrono> and <thread>
includes.
The function only uses standard C++ (std::string_view, char32_t,
std::vector) and is called from r_font.cpp and r_main.cpp on all
platforms. Move it out of the #ifdef _WIN32 block.
Error("Exception: ", e.what()) is missing the %s format specifier,
causing the exception message to be silently dropped.
SpawnProcess: use fork/execvp to launch child processes.
PlatformOpenURL: use xdg-open to open URLs in the default browser.
Also remove <uuid/uuid.h> (not available on all distros, unused) and
add <sstream>/<vector> required by SpawnProcess.
On Windows the launcher exe lives in runtime/ so basePath resolves
correctly. On Linux the binary is in the build dir, so we allow the
launcher to set SG_BASE_PATH to point FindBasePath() at the correct
runtime data directory (fonts, config, etc.).
- cmp_core, imgui: enable POSITION_INDEPENDENT_CODE (required when
  linking static libs into a shared library)
- SimpleGraphic: suppress -Wno-template-body for sol2/GCC 15 issue
- zstd: fall back to static target when shared is unavailable
- lcurl: remove lib prefix on Linux; add -Wl,--allow-multiple-definition
  to resolve luaL_setfuncs symbol collision between LuaJIT and l52util
- lua-utf8: remove lib prefix on Linux; use -include limits.h instead of
  LUA_BUILD_AS_DLL (which is MSVC-specific)
- luasocket: switch from wsocket.c to usocket.c on Linux; remove lib
  prefix; move Windows-only wsock32/ws2_32 to a platform variable
- lzip: remove lib prefix on Linux
- FindLuaJIT.cmake: add luajit-2.1 and luajit-5.1 as fallback names for
  the Linux install layout
On Linux, ImGui was compiled with IMGUI_IMPL_OPENGL_ES2 and linked
against ANGLE's static libGLESv2. ANGLE has no active EGL context
(the engine uses system GLES3 via EGL), so shader compilation fails
silently every frame.

Fix: on Linux use IMGUI_IMPL_OPENGL_ES3 with the system GLESv2 library
so ImGui's GL calls go through the same context as the engine. Change
ImGui_ImplOpenGL3_Init(nullptr) to let ImGui auto-detect the correct
GLSL version from the active context.
Pass nullptr to ImGui_ImplOpenGL3_Init() so ImGui queries the active
context for the correct GLSL version, rather than hardcoding "#version 100"
which is invalid on GLES3 contexts.
Add linux/launcher.c which:
- Locates and dlopen's libSimpleGraphic.so from its own directory
- Resolves the Lua script path to absolute (so basePath inside the
  engine resolves correctly)
- Sets SG_BASE_PATH to <pob_root>/runtime so the engine finds its
  data files (fonts, config)
- Sets LUA_PATH to include <pob_root>/runtime/lua
- Sets LUA_CPATH to include the build directory for Lua C modules
- Calls RunLuaFileAsWin(argc-1, argv+1)

Usage: ./PathOfBuilding src/Launch.lua
These ports are required by the Linux build:
- angle: GLES/EGL implementation (used by ImGui and the render system)
- openssl: required by lcurl for HTTPS support
- pthread-stubs: satisfies EGL's pthread dependency on Linux
Two Linux-specific fixes applied via the build system and a new patch:

1. LUAJIT_ENABLE_GC64: LuaJIT statically linked into a dlopen'd shared
   library is loaded at a high virtual address. Without GC64, LuaJIT
   requires all GC memory within 4GB of its code section, which fails
   at these addresses and crashes in lua_newstate.

2. 006-fix-getenvcopy-linux.patch: The pob-wide-crt patch defines
   _lua_getenvcopy on non-Windows as strdup(getenv(name)). When the
   env var is unset, getenv returns NULL and strdup(NULL) is undefined
   behaviour, crashing in luaopen_package and lj_cf_os_getenv. Replace
   with a null-safe inline function.

Also fix the circular luajit symlink in the install step and make the
configure script executable before vcpkg_configure_make.
cmake --install now produces a flat directory that mirrors the Windows
runtime/ layout:

  <prefix>/
    PathOfBuilding          # launcher
    libSimpleGraphic.so     # engine
    socket.so, lcurl.so, lua-utf8.so, lzip.so  # Lua C modules
    SimpleGraphic/
      Fonts/                # pre-rendered bitmap fonts
      SimpleGraphic.cfg     # default engine config

Font bitmaps are added to data/SimpleGraphic/Fonts/ in this repository
(they are engine assets previously bundled with PoB's runtime/).

Two CMake fixes enable the flat layout:
- Add LIBRARY DESTINATION "." to all SHARED target install rules so
  Linux .so files land in the prefix root instead of lib64/.
- Install data/SimpleGraphic into the prefix alongside the binaries.

The launcher now sets SG_BASE_PATH to its own directory, so fonts are
found whether running from the build directory or an installed prefix.
Font bitmaps and SimpleGraphic.cfg already live in PoB's runtime/
directory (the same as on Windows), so there is no need to duplicate
them in this repository.

Remove data/SimpleGraphic/ and drop the install(DIRECTORY) rule.
The natural install prefix is now the PoB runtime/ directory, where
the launcher's SG_BASE_PATH logic finds the fonts automatically.
@velomeister
Copy link
Author

A note on authorship: This port was developed with the assistance of Claude. I want to be fully transparent about that. That said, I'm a professional software engineer and cybersecurity practitioner — I have reviewed every change in this PR, I understand what each one does and why, and I am not just blindly applying AI suggestions. I take full ownership of this work and commit to maintaining Linux support going forward, including addressing any regressions or issues that arise on this platform.

@LocalIdentity
Copy link
Contributor

@velomeister we have a PoB dev discord. Send me a message (Localidentity) and I'll give you an inv

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants