Skip to content

Linux support: build, runtime, and importer issues (172/177 tests passing) #45

@vivekgr92

Description

@vivekgr92

Linux support: 6 patches landing 172/177 automation tests, with one cluster of 5 Linux-specific failures

Summary

URLab's README hedges "Windows (Win64). Linux is experimental", and Scripts/build_and_test.sh exists but currently hardcodes Win64 binaries. With six small patches I was able to compile URLab against UE 5.7.4 binary distribution on Ubuntu 22.04 / NVIDIA L40S and run the URLab.* automation suite to 172 / 177 passing.

Posting an issue first per CONTRIBUTING.md ("for large changes, open an issue first to discuss the approach") before sending a PR. The 5 remaining failures cluster on one root cause that I'd like guidance on before submitting.

What problem does this solve?

Researchers and CI environments running on Linux currently can't use URLab without first hitting six undocumented build- and runtime-time blockers. Several of them are real bugs (incorrect preprocessor branching, case-sensitivity, hardcoded .dll names) that are non-functional on Linux but currently no-op on Windows because of defined(_WIN32) semantics or case-insensitive filesystems.

Environment

OS Ubuntu 22.04
GPU NVIDIA L40S (Vulkan SM5)
UE 5.7.4 binary (Linux_Unreal_Engine_5.7.x.zip)
Toolchain UE-bundled clang 20.1.8 + libc++
CMake 3.31 (system 3.22 is below CoACD's required 3.24)

Patches (each minimal, all Linux-only conditional except where noted)

1. Source/URLab/URLab.Build.cs — drop _WIN32=0 define on Linux

-if (Target.Platform == UnrealTargetPlatform.Linux)
-{
-    PublicDefinitions.Add("_WIN32=0");
-    PublicDefinitions.Add("USE_DECLSPEC=1");
-    PublicDefinitions.Add("__linux__=1");
-    PublicDefinitions.Add("__unix__=1");
-}

#if defined _WIN32 is true even when _WIN32 is 0. Defining it on Linux forces MuJoCo's mjexport.h down the __declspec(dllimport) branch, which clang on Linux can't parse.

2. Source/URLab/Public/URLab.h — guard _WIN32 redefine

-#ifndef _WIN32
-    #define _WIN32 PLATFORM_WINDOWS
-#endif
+#if PLATFORM_WINDOWS && !defined(_WIN32)
+    #define _WIN32 1
+#endif

Same root cause as #1 from a different angle.

3. Source/URLab/Public/MuJoCo/Components/Sensors/MjCamera.h — fix doc comment

-/** @brief The ZMQ Endpoint for this specific camera (e.g., tcp://*:5558). Must be unique per camera. */
+/** @brief The ZMQ Endpoint for this specific camera (e.g., tcp://0.0.0.0:5558). Must be unique per camera. */

The ://* inside a /** … */ block triggers -Wcomment ("/* within block comment"), which is -Werror in UE's build. Cosmetic but a hard build failure.

4. Source/URLab/Public/CoACD/CoacdInterface.h — fix include casing

-#include "Coacd/coacd.h"
+#include "CoACD/coacd.h"

The installed header is at third_party/install/CoACD/include/CoACD/coacd.h. Linux fs is case-sensitive; Windows isn't.

5. Source/URLab/Private/URLab.cpp + MuJoCo/Core/MjPhysicsEngine.cpp — platform-aware shared-library names

LoadDependencyDLL("mujoco.dll", …) and GetDllHandle(TEXT("mujoco.dll")) fail on Linux. Branched the names on PLATFORM_WINDOWS / PLATFORM_LINUX and the install subdirectory (bin/ Windows, lib/ Linux, matching what the build scripts produce).

6. CoACD upstream header — _WIN32 undef guard

third_party/CoACD/src/public/coacd.h line 10:

-#if _WIN32
+#if defined(_WIN32)

This is a CoACD upstream issue triggered by UE's -Wundef -Werror. This patch lives in a third-party submodule, so it can't go in URLab proper. I'd suggest applying it via the existing third_party/CoACD_custom/ overlay pattern that URLab already uses to swap CoACD's CMakeLists. Open question for maintainers (see below).

Build-script changes

  • Scripts/build_and_test.sh is hardcoded to Win64 paths (UnrealBuildTool.exe, UnrealEditor-Cmd.exe, Win64 Development) — the Linux invocation in CONTRIBUTING.md would actually fail. I have a Linux variant ready (build_and_test_linux.sh) that mirrors the summary-block format. Open question whether you'd prefer a unified platform-detecting script or a parallel _linux.sh file.
  • third_party/build_all.sh works as-is, but only with the system gcc + libstdc++, which produces libs that fail to link into UE plugins on Linux (UE uses bundled libc++ — std symbol mismatch at plugin link time). I have a wrapper that exports CC=<UE-clang>, CXX=<UE-clang++>, CXXFLAGS="-stdlib=libc++ -nostdinc++ …" and other flags to make the third-party builds ABI-compatible with UE's link.
  • Editor builds on Linux don't stage RuntimeDependencies next to the plugin .so, so the URLab plugin's third-party shared libs aren't dlopen-able from Plugins/.../Binaries/Linux/ without LD_LIBRARY_PATH. The build_and_test script and a launcher script handle this; for shipped plugin packaging this would need proper RPATH or symlinking. Open question whether you have a preferred staging approach.

Test results (with patches 1–5; #6 applied via direct edit, not yet via overlay)

=== URLab build+test summary ===
Timestamp : 2026-04-27 00:40:49 UTC
Git HEAD  : 7417660c (main)
Engine    : UE 5.7.4 (Linux binary distribution)
Build     : Succeeded
Tests     : 172 / 177 passed (5 failed)  [177 tests performed]
Log       : sha256:9bec19373b91454d (4.8 MB; available on request)
================================

The 5 failures

All 5 cluster on MJCF default-class inheritance:

Test Assertion Note
URLab.Import.DefaultClassJointAxis inherited jnt_axis[1] ≈ 1 not true child joint isn't getting axis from <default> class
URLab.Import.DefaultFromTo inherited fromto mismatch same family
URLab.Import.RoundTrip_Defaults friction[0] expected 0.700, got 1.000 got MuJoCo's hardcoded fallback — class default never applied
URLab.Muscle.Arm26_ActuatorParams nu mismatch: ref=6 got=3 URLab importer dropped half the muscles vs mj_loadXML reference
URLab.Muscle.Arm26_Counts ntendon expected 6 got 3, nu expected 6 got 3 same — exactly half

Two reads on this:

  • (a) genuine Linux-only bug in URLab's import path. The 6→3 exactly half Arm26 pattern strongly suggests something is iterating with stride 2 or skipping odd/even, or that default-class lookups are returning null and only the muscles with all attributes inline survive. A plausible root cause is TCHAR being UTF-16 on Windows but UTF-8 (1-byte) on Linux — a buffer-size or pointer-arithmetic assumption made on the Windows side could be skipping every other class lookup on Linux. (Speculative; haven't traced the import code.)

  • (b) you may already know about this. Wanted to surface it before going deeper.

How to reproduce locally

UE 5.7.4 binary distribution from https://www.unrealengine.com/linux, host project with the URLab plugin in Plugins/, then:

# 1. Init submodules + build third-party with UE's bundled clang + libc++
#    (paths assumed: $UE_ROOT, $URLAB_ROOT)
cd $URLAB_ROOT
git submodule update --init --recursive
UE_TC=$UE_ROOT/Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/Linux_x64/v26_clang-20.1.8-rockylinux8/x86_64-unknown-linux-gnu
CC=$UE_TC/bin/clang CXX=$UE_TC/bin/clang++ \
CXXFLAGS="-stdlib=libc++ -nostdinc++ -isystem $UE_TC/include/c++/v1 -fPIC -Qunused-arguments -Wno-unknown-warning-option -Wno-missing-template-arg-list-after-template-kw" \
CFLAGS="-fPIC -Qunused-arguments -Wno-unknown-warning-option" \
LDFLAGS="-stdlib=libc++ -fuse-ld=lld -L$UE_TC/lib64 -Wl,-rpath,$UE_TC/lib64" \
bash third_party/build_all.sh --no-submodule-sync

# 2. drop libzmq.a so UBT links against libzmq.so (avoids
#    pthread_cond_clockwait undefined-symbol from static archive)
rm third_party/install/libzmq/lib/libzmq.a

# 3. Generate project files + build editor target
$UE_ROOT/Engine/Build/BatchFiles/Linux/GenerateProjectFiles.sh -project=<host>.uproject -game -engine
$UE_ROOT/Engine/Build/BatchFiles/Linux/Build.sh <Project>Editor Linux Development -Project=<host>.uproject

# 4. Launch editor with LD_LIBRARY_PATH to the third-party install dirs
URLAB_3P=$URLAB_ROOT/third_party/install
LD_LIBRARY_PATH="$URLAB_3P/MuJoCo/lib:$URLAB_3P/CoACD/lib:$URLAB_3P/libzmq/lib" \
DISPLAY=:1 $UE_ROOT/Engine/Binaries/Linux/UnrealEditor <host>.uproject

Questions before I send a PR

  1. Are you interested in a Linux-support PR at all? Status of the 0.1-alpha "experimental" tag suggests yes, but you may have your own Linux roadmap.
  2. CoACD _WIN32 patch — apply via CoACD_custom/ overlay (replicates your existing pattern), or upstream PR to CoACD? (Overlay is faster but adds a maintenance touchpoint.)
  3. Build script — unified platform-detecting build_and_test.sh, or a parallel _linux.sh?
  4. Third-party build with UE toolchain — is the right place to express this a new third_party/build_all_linux.sh, or extend build_all.sh with platform detection?
  5. Default-class inheritance bug — would you prefer me to investigate and fix as part of the same PR, or carve it out into a separate tracking issue and ship the build/runtime support first?
  6. Plugin runtime staging on Linux — is LD_LIBRARY_PATH acceptable for editor dev workflows (with packaging adding proper RPATH), or do you want a different convention?

Happy to do the work either way — just want to align on shape before I open a multi-file PR you might prefer differently scoped. The patches are small and tested locally; I can have a PR up within a day of your guidance.

Importance

Research / exploration. Not blocking my project (I have a Linux build working locally), but Linux-only research labs would currently be unable to evaluate URLab without rediscovering all six issues themselves.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions