From 6ddf11bdb43554d784c70946ec684a81182e3eb7 Mon Sep 17 00:00:00 2001 From: Vivek GR Date: Mon, 27 Apr 2026 00:57:37 +0000 Subject: [PATCH 01/13] fix(linux): make URLab plugin build and dlopen on UE Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six small Linux-only changes that unblock building the plugin against UE 5.7's Linux toolchain (clang 20 + libc++) and let it locate MuJoCo / CoACD / libzmq at runtime. - URLab.Build.cs: drop _WIN32=0 / __linux__=1 defines on Linux. With _WIN32 defined (even as 0), MuJoCo's mjexport.h takes the __declspec(dllimport) branch, which clang on Linux can't parse. __linux__ / __unix__ are already defined by the compiler. - URLab.h: gate the _WIN32 redefine block on PLATFORM_WINDOWS so it no longer fires on Linux. Same root cause as Build.cs above. - MjCamera.h: replace tcp://*:5558 with tcp://0.0.0.0:5558 in a /** doc comment **/ — the literal `://*` triggers -Wcomment which is -Werror in UE's build. - CoacdInterface.h: case-correct include "Coacd/coacd.h" -> "CoACD/coacd.h" to match the installed header path. Linux fs is case-sensitive. - URLab.cpp + MjPhysicsEngine.cpp: branch the third-party shared-lib names on PLATFORM_WINDOWS / PLATFORM_LINUX so dlopen can find them by their actual SONAMEs (libmujoco.so.3.7.0, lib_coacd.so, libzmq.so.5) and the per-package install//lib subdirectory. With these patches the URLab.* automation suite on Linux runs to 172/177 passing. The 5 remaining failures cluster on a separate MJCF default-class inheritance issue tracked in #45. Refs: #45 --- .../Private/MuJoCo/Core/MjPhysicsEngine.cpp | 13 +++++++-- Source/URLab/Private/URLab.cpp | 29 ++++++++++++------- Source/URLab/Public/CoACD/CoacdInterface.h | 2 +- .../MuJoCo/Components/Sensors/MjCamera.h | 2 +- Source/URLab/Public/URLab.h | 4 +-- Source/URLab/URLab.Build.cs | 13 ++------- 6 files changed, 35 insertions(+), 28 deletions(-) diff --git a/Source/URLab/Private/MuJoCo/Core/MjPhysicsEngine.cpp b/Source/URLab/Private/MuJoCo/Core/MjPhysicsEngine.cpp index a639ed6..948624a 100644 --- a/Source/URLab/Private/MuJoCo/Core/MjPhysicsEngine.cpp +++ b/Source/URLab/Private/MuJoCo/Core/MjPhysicsEngine.cpp @@ -109,13 +109,20 @@ static void URLab_InstallMujocoCallbacks() { if (GMujocoCallbacksInstalled) return; - // mujoco.dll is delay-loaded in URLab's Build.cs, and the linker refuses + // mujoco is delay-loaded in URLab's Build.cs, and the linker refuses // to bind data symbols through a delayed import. Resolve the two // mju_user_* function pointers manually via GetDllExport. - void* Handle = FPlatformProcess::GetDllHandle(TEXT("mujoco.dll")); +#if PLATFORM_WINDOWS + const TCHAR* MujocoLibName = TEXT("mujoco.dll"); +#elif PLATFORM_LINUX + const TCHAR* MujocoLibName = TEXT("libmujoco.so.3.7.0"); +#else + const TCHAR* MujocoLibName = TEXT("mujoco"); +#endif + void* Handle = FPlatformProcess::GetDllHandle(MujocoLibName); if (!Handle) { - UE_LOG(LogURLab, Warning, TEXT("[URLab] Could not resolve mujoco.dll to install error callbacks")); + UE_LOG(LogURLab, Warning, TEXT("[URLab] Could not resolve %s to install error callbacks"), MujocoLibName); return; } diff --git a/Source/URLab/Private/URLab.cpp b/Source/URLab/Private/URLab.cpp index acdffa6..5d25b21 100644 --- a/Source/URLab/Private/URLab.cpp +++ b/Source/URLab/Private/URLab.cpp @@ -37,13 +37,16 @@ void FURLabModule::StartupModule() FString PluginDir = IPluginManager::Get().FindPlugin("UnrealRoboticsLab")->GetBaseDir(); FString InstallDir = FPaths::Combine(PluginDir, TEXT("third_party/install")); - // Function to load a DLL and log success/failure - auto LoadDependencyDLL = [&](const FString& LibraryName, const FString& SubDir) { + // Function to load a shared library and log success/failure. BinSubDir is + // the per-package subdirectory under third_party/install// that + // holds the loadable artifact: "bin" on Windows (DLLs), "lib" on Linux + // (.so files). + auto LoadDependencyDLL = [&](const FString& LibraryName, const FString& SubDir, const FString& BinSubDir) { // Try plugin third-party path first (editor / development) - FString DLLPath = FPaths::Combine(InstallDir, SubDir, TEXT("bin"), LibraryName); + FString DLLPath = FPaths::Combine(InstallDir, SubDir, BinSubDir, LibraryName); if (!FPaths::FileExists(DLLPath)) { - // Packaged build: DLLs staged next to the executable + // Packaged build: shared libs staged next to the executable DLLPath = FPaths::Combine(FPlatformProcess::GetModulesDirectory(), LibraryName); } if (FPaths::FileExists(DLLPath)) { @@ -60,17 +63,21 @@ void FURLabModule::StartupModule() return false; }; +#if PLATFORM_WINDOWS // Load MuJoCo. Since 3.7.0 the obj/stl decoders are compiled into // mujoco.dll itself (changelog item 9); loading the standalone // obj_decoder.dll / stl_decoder.dll would cause a plugin-registration // collision and crash during module init. - LoadDependencyDLL(TEXT("mujoco.dll"), TEXT("MuJoCo")); - - // Load ZMQ - LoadDependencyDLL(TEXT("libzmq-v143-mt-4_3_6.dll"), TEXT("libzmq")); - - // Load CoACD (Shared library) - LoadDependencyDLL(TEXT("lib_coacd.dll"), TEXT("CoACD")); + LoadDependencyDLL(TEXT("mujoco.dll"), TEXT("MuJoCo"), TEXT("bin")); + LoadDependencyDLL(TEXT("libzmq-v143-mt-4_3_6.dll"), TEXT("libzmq"), TEXT("bin")); + LoadDependencyDLL(TEXT("lib_coacd.dll"), TEXT("CoACD"), TEXT("bin")); +#elif PLATFORM_LINUX + // Linux .so layout: third_party/install//lib/. Names are SONAMEs + // produced by the upstream cmake builds. + LoadDependencyDLL(TEXT("libmujoco.so.3.7.0"), TEXT("MuJoCo"), TEXT("lib")); + LoadDependencyDLL(TEXT("libzmq.so.5"), TEXT("libzmq"), TEXT("lib")); + LoadDependencyDLL(TEXT("lib_coacd.so"), TEXT("CoACD"), TEXT("lib")); +#endif // Some CoACD dependencies like TBB or runtimes might be in CoACD/bin // They should be loaded automatically if in search path, but we can verify here if needed. diff --git a/Source/URLab/Public/CoACD/CoacdInterface.h b/Source/URLab/Public/CoACD/CoacdInterface.h index f70ff72..5d7ea7a 100644 --- a/Source/URLab/Public/CoACD/CoacdInterface.h +++ b/Source/URLab/Public/CoACD/CoacdInterface.h @@ -27,7 +27,7 @@ #include #include #include "Chaos/ArrayCollectionArray.h" -#include "Coacd/coacd.h" +#include "CoACD/coacd.h" #include "Misc/FileHelper.h" namespace CoacdInterface { diff --git a/Source/URLab/Public/MuJoCo/Components/Sensors/MjCamera.h b/Source/URLab/Public/MuJoCo/Components/Sensors/MjCamera.h index 523ffb4..0d6ce37 100644 --- a/Source/URLab/Public/MuJoCo/Components/Sensors/MjCamera.h +++ b/Source/URLab/Public/MuJoCo/Components/Sensors/MjCamera.h @@ -137,7 +137,7 @@ class URLAB_API UMjCamera : public UMjComponent UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera|Network") bool bEnableZmqBroadcast = false; - /** @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. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MuJoCo|Camera|Network") FString ZmqEndpoint = TEXT("tcp://*:5558"); diff --git a/Source/URLab/Public/URLab.h b/Source/URLab/Public/URLab.h index 3e8c857..c359503 100644 --- a/Source/URLab/Public/URLab.h +++ b/Source/URLab/Public/URLab.h @@ -24,8 +24,8 @@ #include "CoreMinimal.h" #include "Modules/ModuleManager.h" -#ifndef _WIN32 - #define _WIN32 PLATFORM_WINDOWS +#if PLATFORM_WINDOWS && !defined(_WIN32) + #define _WIN32 1 #endif class FURLabModule : public IModuleInterface { diff --git a/Source/URLab/URLab.Build.cs b/Source/URLab/URLab.Build.cs index a2daeeb..5b2289a 100644 --- a/Source/URLab/URLab.Build.cs +++ b/Source/URLab/URLab.Build.cs @@ -83,16 +83,9 @@ public URLab(ReadOnlyTargetRules Target) : base(Target) if (Target.Platform == UnrealTargetPlatform.Linux) { - // Ensure Unreal recognizes MuJoCo's export macros - } - - - if (Target.Platform == UnrealTargetPlatform.Linux) - { - PublicDefinitions.Add("_WIN32=0"); - PublicDefinitions.Add("USE_DECLSPEC=1"); - PublicDefinitions.Add("__linux__=1"); - PublicDefinitions.Add("__unix__=1"); + // Don't define _WIN32 on Linux: `#if defined _WIN32` is true even + // when _WIN32 is 0, which sends MuJoCo's mjexport.h down the + // __declspec(dllimport) branch and breaks clang on Linux. } From 1eb63164759bdcb6bd7b2e1c5b3b427b1b180dc6 Mon Sep 17 00:00:00 2001 From: Vivek GR Date: Mon, 27 Apr 2026 00:57:45 +0000 Subject: [PATCH 02/13] feat(linux): add Scripts/build_and_test_linux.sh Mirrors build_and_test.sh's summary-block format but uses the Linux UnrealBuildTool / UnrealEditor-Cmd binaries, builds the host project's Editor target on the Linux platform, and exports LD_LIBRARY_PATH at the third-party install dirs (RuntimeDependencies aren't staged on Linux editor builds, so the plugin's libmujoco / lib_coacd / libzmq aren't otherwise dlopen-able). Note: the existing build_and_test.sh hardcodes Win64 binary names and a Win64 build platform, so the Linux invocation in CONTRIBUTING.md would currently fail. Whether this should be a parallel _linux.sh or a unified platform-detecting script is an open question; see #45. Refs: #45 --- Scripts/build_and_test_linux.sh | 206 ++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100755 Scripts/build_and_test_linux.sh diff --git a/Scripts/build_and_test_linux.sh b/Scripts/build_and_test_linux.sh new file mode 100755 index 0000000..e9608b3 --- /dev/null +++ b/Scripts/build_and_test_linux.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# --- LEGAL DISCLAIMER --- +# UnrealRoboticsLab is an independent software plugin. It is NOT affiliated with, +# endorsed by, or sponsored by Epic Games, Inc. "Unreal" and "Unreal Engine" are +# trademarks or registered trademarks of Epic Games, Inc. in the US and elsewhere. +# +# This plugin incorporates third-party software: MuJoCo (Apache 2.0), +# CoACD (MIT), and libzmq (MPL 2.0). See ThirdPartyNotices.txt for details. + +# Linux variant of build_and_test.sh. Same summary block format so the PR +# template is satisfied. Differs from the upstream script in: +# - Uses the Linux Build.sh / UnrealEditor-Cmd binaries +# - Builds the project's Editor target on the Linux platform +# - Stages third-party shared libs into the plugin's Binaries/Linux/ via +# setup_runtime_linux.sh after build (RuntimeDependencies aren't auto- +# staged for editor builds on Linux). +# +# Usage: +# ./Scripts/build_and_test_linux.sh \ +# --engine /home/ubuntu/UnrealEngine \ +# --project /home/ubuntu/URLabTest/URLabTest.uproject \ +# [--target CustomEditorTargetName] \ +# [--filter URLab] \ +# [--log /tmp/urlab_test.log] +# +# --target defaults to Editor derived from the .uproject filename. +# Only override for projects that don't follow UE's naming convention. +# +# Exit codes: 0 ok, 1 build failed, 2 tests failed, 3 bad args. + +set -eu + +TARGET="" +FILTER="URLab" +LOG="/tmp/urlab_test.log" +ENGINE="" +PROJECT="" + +usage() { + cat >&2 <<'HELP' +build_and_test_linux.sh — build the project's Editor target and run the URLab automation suite (Linux). + +Usage: + ./Scripts/build_and_test_linux.sh \ + --engine /home/ubuntu/UnrealEngine \ + --project /home/ubuntu/URLabTest/URLabTest.uproject \ + [--target CustomEditorTargetName] \ + [--filter URLab] \ + [--log /tmp/urlab_test.log] + +--target defaults to Editor derived from the .uproject filename. + +Exit codes: 0 ok, 1 build failed, 2 tests failed, 3 bad args. +HELP + exit 3 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --engine) ENGINE="$2"; shift 2 ;; + --project) PROJECT="$2"; shift 2 ;; + --target) TARGET="$2"; shift 2 ;; + --filter) FILTER="$2"; shift 2 ;; + --log) LOG="$2"; shift 2 ;; + -h|--help) usage ;; + *) echo "Unknown arg: $1" >&2; usage ;; + esac +done + +[[ -z "$ENGINE" ]] && { echo "Missing --engine" >&2; usage; } +[[ -z "$PROJECT" ]] && { echo "Missing --project" >&2; usage; } + +# Derive the editor target from the .uproject filename if not explicitly +# supplied. UE's convention is Editor — e.g. MyGame.uproject -> +# MyGameEditor. Override with --target for projects that don't follow this. +if [[ -z "$TARGET" ]]; then + PROJECT_BASENAME=$(basename "$PROJECT") + TARGET="${PROJECT_BASENAME%.*}Editor" +fi + +BUILD_SH="$ENGINE/Engine/Build/BatchFiles/Linux/Build.sh" +CMD="$ENGINE/Engine/Binaries/Linux/UnrealEditor-Cmd" + +[[ -x "$BUILD_SH" ]] || { echo "Build.sh not found: $BUILD_SH" >&2; exit 3; } +[[ -x "$CMD" ]] || { echo "UnrealEditor-Cmd not found: $CMD" >&2; exit 3; } + +# Pre-flight: refuse to run while an editor instance has the project locked. +# Match only the actual UnrealEditor binary, not the project string in our own +# command line. +if pgrep -fa "/UnrealEditor( |$)" >/dev/null 2>&1; then + echo "ERROR: an UnrealEditor process is running. Close it first." >&2 + exit 3 +fi + +# Plugin runtime dep path. Editor builds don't stage RuntimeDependencies on +# Linux, so the URLab plugin .so cannot dlopen libmujoco/lib_coacd/libzmq +# without LD_LIBRARY_PATH pointing at the third_party install dirs. +URLAB_3P="$(cd "$(dirname "$0")/../third_party/install" 2>/dev/null && pwd || true)" +if [[ -d "$URLAB_3P" ]]; then + export LD_LIBRARY_PATH="$URLAB_3P/MuJoCo/lib:$URLAB_3P/CoACD/lib:$URLAB_3P/libzmq/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +fi + +: > "$LOG" + +# --- Build ----------------------------------------------------------------- +echo ">>> Building $TARGET (Linux Development)..." +BUILD_OUT=$("$BUILD_SH" "$TARGET" Linux Development "-Project=$PROJECT" -WaitMutex 2>&1 || true) +echo "$BUILD_OUT" | tail -10 +if echo "$BUILD_OUT" | grep -q "Result: Succeeded\|Target is up to date"; then + BUILD_STATUS="Succeeded" +else + BUILD_STATUS="Failed" +fi + +# --- Test ------------------------------------------------------------------ +PASS=0 +FAIL=0 +TOTAL=0 +TESTS_PERFORMED_LINE="" + +if [[ "$BUILD_STATUS" == "Succeeded" ]]; then + echo ">>> Running automation tests (filter=$FILTER, log=$LOG)..." + "$CMD" "$PROJECT" \ + -ExecCmds="Automation RunTests $FILTER" \ + -Unattended -NullRHI -NoSound -NoSplash -stdout -log \ + -TestExit="Automation Test Queue Empty" \ + > "$LOG" 2>&1 || true + + if [[ ! -s "$LOG" ]]; then + echo "ERROR: test log is empty — editor likely held the project lock." >&2 + fi + + PASS=$(grep -cE 'Result=\{Success\}' "$LOG" || true) + FAIL=$(grep -cE 'Result=\{(Fail|Error)\}' "$LOG" || true) + TOTAL=$((PASS + FAIL)) + TESTS_PERFORMED_LINE=$(grep -oE '[0-9]+ tests performed' "$LOG" | tail -1 || true) +fi + +# --- Fingerprint + summary ------------------------------------------------- +# Note: Host and Log-path lines are intentionally omitted from the summary +# block because they leak the reporter's hostname / username on local runs. +# The SHA-256 of the log is sufficient for reviewers to verify content +# integrity when they re-run the suite themselves. +TS=$(date -u '+%Y-%m-%d %H:%M:%S UTC') +GIT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +GIT_SHA=$(git -C "$GIT_DIR" rev-parse --short=8 HEAD 2>/dev/null || echo "unknown") +GIT_BRANCH=$(git -C "$GIT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") + +# Strip the engine path down to its last segment (e.g. 'UnrealEngine'). The +# full path can contain the user's install root on a non-standard layout. +ENGINE_LABEL=$(basename "$ENGINE") +[[ -z "$ENGINE_LABEL" ]] && ENGINE_LABEL="$ENGINE" + +# Third-party dep SHAs from third_party/install//INSTALLED_SHA.txt +# (written by the CMake build scripts). Lets a reviewer verify they're +# comparing against the same binary toolchain. Silent skip if missing. +DEPS_LINE="" +for pair in "mj:MuJoCo" "coacd:CoACD" "zmq:libzmq"; do + key="${pair%%:*}" + dir="${pair##*:}" + sha_file="$GIT_DIR/third_party/install/$dir/INSTALLED_SHA.txt" + if [[ -s "$sha_file" ]]; then + sha=$(head -c 7 "$sha_file") + if [[ -n "$DEPS_LINE" ]]; then DEPS_LINE="$DEPS_LINE "; fi + DEPS_LINE="${DEPS_LINE}${key}=${sha}" + fi +done +[[ -z "$DEPS_LINE" ]] && DEPS_LINE="unavailable" + +LOG_HASH="n/a" +if [[ -s "$LOG" ]]; then + if command -v sha256sum >/dev/null 2>&1; then + LOG_HASH=$(sha256sum "$LOG" | cut -c1-16) + fi +fi + +cat < Date: Tue, 28 Apr 2026 01:35:31 +0000 Subject: [PATCH 03/13] fix(linux): keep TCHAR_TO_UTF8 conversions alive across mjs_* calls URLab stored TCHAR_TO_UTF8 macro output in a long-lived `const char*`, which dangles immediately because the macro expands to a pointer into a temporary FTCHARToUTF8 whose lifetime ends at the end of the full expression. On Windows/MSVC the stack memory often still held the original bytes when the pointer was used a few lines later, so the bug went unnoticed. On Linux/clang the stack is reclaimed aggressively and the pointer dereferences garbage. Two consequences observed on Linux: - FMujocoSpecWrapper::AddDefault registered each class under a corrupted name, so subsequent mjs_findDefault lookups by the joint/geom/actuator import paths failed silently and MuJoCo fell back to its built-in defaults. This caused all five Linux-only test failures from the previous run: URLab.Import.DefaultClassJointAxis URLab.Import.DefaultFromTo URLab.Import.RoundTrip_Defaults URLab.Muscle.Arm26_ActuatorParams URLab.Muscle.Arm26_Counts After this fix all 177 URLab.* tests pass on Linux. - UMjTendon::RegisterToSpec did the same thing with the side-site string for wrapping. No failing test caught it but the pattern is identical so it's fixed here too. Both call sites now construct an explicit FTCHARToUTF8 with function-scope lifetime so the converted bytes outlive the mjs_addDefault / mjs_wrapGeom call. Refs: #45 --- .../Private/MuJoCo/Components/Tendons/MjTendon.cpp | 7 ++++++- .../Private/MuJoCo/Core/Spec/MjSpecWrapper.cpp | 14 +++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Source/URLab/Private/MuJoCo/Components/Tendons/MjTendon.cpp b/Source/URLab/Private/MuJoCo/Components/Tendons/MjTendon.cpp index fb753b1..e1094f7 100644 --- a/Source/URLab/Private/MuJoCo/Components/Tendons/MjTendon.cpp +++ b/Source/URLab/Private/MuJoCo/Components/Tendons/MjTendon.cpp @@ -245,7 +245,12 @@ void UMjTendon::RegisterToSpec(FMujocoSpecWrapper& Wrapper, mjsBody* ParentBody) case EMjTendonWrapType::Geom: { - const char* SideSiteStr = Wrap.SideSite.IsEmpty() ? "" : TCHAR_TO_UTF8(*Wrap.SideSite); + // FTCHARToUTF8 keeps the converted string alive for the + // duration of the wrapGeom call. Storing TCHAR_TO_UTF8 macro + // output in a const char* would dangle on Linux (see + // MjSpecWrapper::AddDefault for the same fix). + FTCHARToUTF8 SideSiteConv(*Wrap.SideSite); + const char* SideSiteStr = Wrap.SideSite.IsEmpty() ? "" : SideSiteConv.Get(); mjsWrap* W = mjs_wrapGeom(Tendon, TCHAR_TO_UTF8(*Wrap.TargetName), SideSiteStr); if (!W) { diff --git a/Source/URLab/Private/MuJoCo/Core/Spec/MjSpecWrapper.cpp b/Source/URLab/Private/MuJoCo/Core/Spec/MjSpecWrapper.cpp index 976bb52..f36d453 100644 --- a/Source/URLab/Private/MuJoCo/Core/Spec/MjSpecWrapper.cpp +++ b/Source/URLab/Private/MuJoCo/Core/Spec/MjSpecWrapper.cpp @@ -159,12 +159,20 @@ void FMujocoSpecWrapper::AddDefault(UMjDefault* DefaultComp) { if (!DefaultComp) return; - const char* ClassName = DefaultComp->ClassName.IsEmpty() ? nullptr : TCHAR_TO_UTF8(*DefaultComp->ClassName); - + // TCHAR_TO_UTF8 returns a pointer into a temporary that dies at the end of + // the full expression. Storing it in a long-lived variable produces a + // dangling pointer; on Linux/clang the stack is reclaimed before the value + // is used and the class registers under a garbage name, breaking + // childclass / inheritance resolution. Use FTCHARToUTF8 with explicit + // function-scope lifetime instead. + FTCHARToUTF8 ClassNameConv(*DefaultComp->ClassName); + const char* ClassName = DefaultComp->ClassName.IsEmpty() ? nullptr : ClassNameConv.Get(); + mjsDefault* parentDef = nullptr; if (!DefaultComp->ParentClassName.IsEmpty()) { - parentDef = mjs_findDefault(Spec, TCHAR_TO_UTF8(*DefaultComp->ParentClassName)); + FTCHARToUTF8 ParentNameConv(*DefaultComp->ParentClassName); + parentDef = mjs_findDefault(Spec, ParentNameConv.Get()); if (parentDef) { UE_LOG(LogURLabWrapper, Log, TEXT("[AddDefault] Linked Class '%s' to Parent '%s'"), *DefaultComp->ClassName, *DefaultComp->ParentClassName); From 4166c5801b54f1e4c4ca7a32398a1fa5c690863b Mon Sep 17 00:00:00 2001 From: Vivek GR Date: Tue, 28 Apr 2026 01:57:58 +0000 Subject: [PATCH 04/13] feat(linux): stage third-party libs into Binaries/Linux/ for $ORIGIN RPATH UBT's auto-computed RPATH for the URLab plugin .so on Linux resolves incorrectly when the plugin lives outside the host project tree (e.g. symlinked in from a separate clone). The relative path it emits is ${ORIGIN}/../../../../../UnrealEngine/../URLabTest/Plugins/.../lib which resolves to a non-existent /home//URLabTest/... rather than the plugin's real on-disk location. Result: the loader can't find libmujoco / lib_coacd / libzmq at editor startup, so the plugin module fails to load without LD_LIBRARY_PATH. Side-step the relative-path issue by symlinking third-party shared libs into the plugin's own Binaries/Linux/ directory after build. UBT already adds ${ORIGIN} to the plugin .so's RPATH (correctly, this time), so the loader finds the libs in the same dir as the plugin .so. No LD_LIBRARY_PATH needed at editor or packaging time. Changes: - URLab.Build.cs (Linux branch): drop the broken PublicRuntimeLibraryPaths.Add() entries (they end up generating bad RPATH); use RuntimeDependencies with $(BinaryOutputDir) target so packaging stages the libs alongside the plugin .so. Glob *.so* (not just *.so) so SONAME-versioned real files like libmujoco.so.3.7.0 also get staged. - Scripts/setup_runtime_linux.sh: new helper that symlinks third_party/install//lib/*.so* into Binaries/Linux/ for editor builds (where RuntimeDependencies aren't auto-staged on Linux). Idempotent. - Scripts/build_and_test_linux.sh: invoke setup_runtime_linux.sh after the plugin builds, drop the LD_LIBRARY_PATH fallback (no longer needed). After: editor launches and the URLab.* automation suite runs to 177/177 passing with LD_LIBRARY_PATH unset. Refs: #45 --- Scripts/build_and_test_linux.sh | 18 +++++---- Scripts/setup_runtime_linux.sh | 66 +++++++++++++++++++++++++++++++++ Source/URLab/URLab.Build.cs | 28 +++++++++++--- 3 files changed, 99 insertions(+), 13 deletions(-) create mode 100755 Scripts/setup_runtime_linux.sh diff --git a/Scripts/build_and_test_linux.sh b/Scripts/build_and_test_linux.sh index e9608b3..496ff02 100755 --- a/Scripts/build_and_test_linux.sh +++ b/Scripts/build_and_test_linux.sh @@ -106,14 +106,6 @@ if pgrep -fa "/UnrealEditor( |$)" >/dev/null 2>&1; then exit 3 fi -# Plugin runtime dep path. Editor builds don't stage RuntimeDependencies on -# Linux, so the URLab plugin .so cannot dlopen libmujoco/lib_coacd/libzmq -# without LD_LIBRARY_PATH pointing at the third_party install dirs. -URLAB_3P="$(cd "$(dirname "$0")/../third_party/install" 2>/dev/null && pwd || true)" -if [[ -d "$URLAB_3P" ]]; then - export LD_LIBRARY_PATH="$URLAB_3P/MuJoCo/lib:$URLAB_3P/CoACD/lib:$URLAB_3P/libzmq/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" -fi - : > "$LOG" # --- Build ----------------------------------------------------------------- @@ -126,6 +118,16 @@ else BUILD_STATUS="Failed" fi +# --- Stage runtime libs --------------------------------------------------- +# UBT's auto-RPATH for plugins symlinked outside the host project resolves +# incorrectly. Symlink third-party .so files into the plugin's Binaries/Linux/ +# so the loader resolves them via ${ORIGIN} (which UBT does add correctly). +SETUP_RUNTIME="$(dirname "$0")/setup_runtime_linux.sh" +if [[ "$BUILD_STATUS" == "Succeeded" && -x "$SETUP_RUNTIME" ]]; then + echo ">>> Staging third-party runtime libs..." + bash "$SETUP_RUNTIME" || echo "WARN: setup_runtime_linux.sh failed (continuing)" >&2 +fi + # --- Test ------------------------------------------------------------------ PASS=0 FAIL=0 diff --git a/Scripts/setup_runtime_linux.sh b/Scripts/setup_runtime_linux.sh new file mode 100755 index 0000000..5e2a41c --- /dev/null +++ b/Scripts/setup_runtime_linux.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 Jonathan Embley-Riches. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# setup_runtime_linux.sh — symlink URLab's third-party shared libraries into +# the plugin's Binaries/Linux/ directory so the editor's dynamic loader can +# resolve them via ${ORIGIN} (UBT already adds ${ORIGIN} to the plugin .so's +# RPATH). +# +# Why this is needed: +# UBT's auto-computed relative RPATH for PublicAdditionalLibraries on Linux +# assumes the plugin lives inside the host project's Plugins/ tree under a +# path UBT can express relative to ${ORIGIN}. For plugins symlinked in from +# outside the host project that calculation produces RPATH entries that +# resolve to non-existent directories, so libmujoco / lib_coacd / libzmq +# can't be found at editor startup. +# +# Run this after building both the third-party libs and the plugin .so. It is +# idempotent; existing symlinks are replaced. +# +# Usage: ./Scripts/setup_runtime_linux.sh +# (run from the plugin root, or any cwd — paths are resolved relative +# to this script's location) + +set -eu + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PLUGIN_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +THIRD_PARTY="$PLUGIN_ROOT/third_party/install" +BIN_DIR="$PLUGIN_ROOT/Binaries/Linux" + +if [[ ! -d "$THIRD_PARTY" ]]; then + echo "ERROR: third_party/install not found at $THIRD_PARTY" >&2 + echo "Run third_party/build_all.sh first." >&2 + exit 1 +fi + +if [[ ! -d "$BIN_DIR" ]]; then + echo "ERROR: plugin Binaries/Linux/ not found at $BIN_DIR" >&2 + echo "Build the plugin first (Build.sh URLabTestEditor Linux Development)." >&2 + exit 1 +fi + +count=0 +for pkg_lib in "$THIRD_PARTY/MuJoCo/lib" "$THIRD_PARTY/CoACD/lib" "$THIRD_PARTY/libzmq/lib"; do + [[ -d "$pkg_lib" ]] || continue + for so in "$pkg_lib"/*.so*; do + [[ -e "$so" ]] || continue + target="$BIN_DIR/$(basename "$so")" + ln -sfn "$so" "$target" + count=$((count + 1)) + done +done + +echo "Linked $count third-party shared libraries into $BIN_DIR" diff --git a/Source/URLab/URLab.Build.cs b/Source/URLab/URLab.Build.cs index 5b2289a..e821cd0 100644 --- a/Source/URLab/URLab.Build.cs +++ b/Source/URLab/URLab.Build.cs @@ -143,7 +143,19 @@ private void AddThirdPartyLibrary(string LibraryName, ReadOnlyTargetRules Target } else if (Target.Platform == UnrealTargetPlatform.Linux) { - // Link all static libraries and shared objects for Linux + // On Linux the URLab plugin .so already gets `${ORIGIN}` added to + // its RPATH by UBT, so the runtime loader resolves third-party + // shared libs (libmujoco / lib_coacd / libzmq) from the plugin's + // own Binaries/Linux/ directory. The `setup_rpath_linux.sh` + // helper symlinks third_party/install//lib/*.so* into + // Binaries/Linux/ after the plugin builds, so no LD_LIBRARY_PATH + // is needed at editor / packaging time. + // + // (UBT's auto-computed relative RPATH from absolute lib paths + // resolves incorrectly when the plugin lives outside the host + // project — the ${ORIGIN}/../../../../UnrealEngine/.. chain it + // emits assumes engine and project share a common ancestor. + // Staging via $ORIGIN sidesteps that entirely.) string LibPath = Path.Combine(FullPath, "lib"); if (Directory.Exists(LibPath)) { @@ -151,19 +163,25 @@ private void AddThirdPartyLibrary(string LibraryName, ReadOnlyTargetRules Target { PublicAdditionalLibraries.Add(LibFile); } + // Link against the unversioned .so (symlink) so the linker + // records the SONAME, not an absolute versioned path. foreach (string LibFile in Directory.GetFiles(LibPath, "*.so", SearchOption.AllDirectories)) { PublicAdditionalLibraries.Add(LibFile); - RuntimeDependencies.Add(LibFile); - PublicDelayLoadDLLs.Add(Path.GetFileName(LibFile)); + } + // Stage every *.so* (including version-suffixed real files) + // next to the plugin .so so the SONAME chain resolves at + // runtime under ${ORIGIN}. + foreach (string LibFile in Directory.GetFiles(LibPath, "*.so*", SearchOption.AllDirectories)) + { + RuntimeDependencies.Add("$(BinaryOutputDir)/" + Path.GetFileName(LibFile), LibFile, StagedFileType.NonUFS); } } - // Add binaries (often plugin/shared objects) for Linux string BinPath = Path.Combine(FullPath, "bin"); if (Directory.Exists(BinPath)) { - foreach (string BinFile in Directory.GetFiles(BinPath, "*.so", SearchOption.AllDirectories)) + foreach (string BinFile in Directory.GetFiles(BinPath, "*.so*", SearchOption.AllDirectories)) { RuntimeDependencies.Add(BinFile); PublicDelayLoadDLLs.Add(Path.GetFileName(BinFile)); From b7e64a4df053a52e4c1b68254bc1aa9bfc47af6a Mon Sep 17 00:00:00 2001 From: Vivek GR Date: Tue, 28 Apr 2026 02:20:50 +0000 Subject: [PATCH 05/13] build(linux): apply CoACD _WIN32 patch via CoACD_custom overlay; drop libzmq.a Stop directly editing the CoACD submodule's public/coacd.h. Instead overlay a fixed copy via the existing CoACD_custom/ pattern that URLab already uses for CMakeLists.txt + cmake/. The fixed copy relaxes `#if _WIN32` to `#if defined(_WIN32)` so consumers compiled with `-Wundef -Werror` (UE on Linux) accept the header without warnings becoming errors. Both build.sh and build.ps1 grow a parallel block to copy CoACD_custom/public/* onto src/public/. Also disable libzmq's static archive on Linux (BUILD_STATIC=OFF). The static archive's mailbox_safe.cpp pulls libc++ condition_variable_any::wait_until -> pthread_cond_clockwait via inlining, which UE's link sysroot can't resolve. The .so version links its own runtime deps internally and works fine, so the static lib is unused and only causes the URLab plugin link to fail (undefined symbol pthread_cond_clockwait). Restricted to Linux via a case on uname so the Windows path is unchanged. After both changes: 177/177 URLab.* automation tests still pass on Linux from a clean third-party rebuild. Refs: #45 --- third_party/CoACD/build.ps1 | 5 ++ third_party/CoACD/build.sh | 7 +++ third_party/CoACD_custom/public/coacd.h | 75 +++++++++++++++++++++++++ third_party/libzmq/build.sh | 11 +++- 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 third_party/CoACD_custom/public/coacd.h diff --git a/third_party/CoACD/build.ps1 b/third_party/CoACD/build.ps1 index 810e772..fc56abd 100644 --- a/third_party/CoACD/build.ps1 +++ b/third_party/CoACD/build.ps1 @@ -53,6 +53,11 @@ if (Test-Path "../../CoACD_custom/cmake") { if (-not (Test-Path "cmake")) { New-Item -ItemType Directory -Path "cmake" | Out-Null } Copy-Item -Path "../../CoACD_custom/cmake/*" -Destination "cmake/" -Recurse -Force } +# Overlay header patches (see build.sh comment for rationale). +if (Test-Path "../../CoACD_custom/public") { + if (-not (Test-Path "public")) { New-Item -ItemType Directory -Path "public" | Out-Null } + Copy-Item -Path "../../CoACD_custom/public/*" -Destination "public/" -Recurse -Force +} if (-not (Test-Path "build")) { New-Item -ItemType Directory -Path "build" } cd build diff --git a/third_party/CoACD/build.sh b/third_party/CoACD/build.sh index 6d4d5b7..4bf5e92 100644 --- a/third_party/CoACD/build.sh +++ b/third_party/CoACD/build.sh @@ -53,6 +53,13 @@ if [ -d "../../CoACD_custom/cmake" ]; then mkdir -p cmake cp -rf ../../CoACD_custom/cmake/* cmake/ fi +# Overlay header patches (e.g. public/coacd.h relaxes `#if _WIN32` to +# `#if defined(_WIN32)` so URLab's build does not trip clang's -Wundef under +# UE on Linux). +if [ -d "../../CoACD_custom/public" ]; then + mkdir -p public + cp -rf ../../CoACD_custom/public/* public/ +fi mkdir -p build cd build diff --git a/third_party/CoACD_custom/public/coacd.h b/third_party/CoACD_custom/public/coacd.h new file mode 100644 index 0000000..21385bc --- /dev/null +++ b/third_party/CoACD_custom/public/coacd.h @@ -0,0 +1,75 @@ +// URLab CoACD overlay header. Build scripts copy this onto +// third_party/CoACD/src/public/coacd.h before configure so the upstream +// `#if _WIN32` test (which evaluates an undefined macro to 0 and trips +// `-Werror=undef` under UE's clang on Linux) becomes a `defined()` check. +// +// Diff vs upstream: line 10, `#if _WIN32` -> `#if defined(_WIN32)`. + +#pragma once +#include +#include +#include +#include +#include + +namespace coacd { + +#if defined(_WIN32) +#define COACD_API __declspec(dllexport) +#else +#define COACD_API +#endif + +struct Mesh { + std::vector> vertices; + std::vector> indices; +}; + +std::vector CoACD(Mesh const &input, double threshold = 0.05, + int max_convex_hull = -1, std::string preprocess = "auto", + int prep_resolution = 50, int sample_resolution = 2000, + int mcts_nodes = 20, int mcts_iteration = 150, + int mcts_max_depth = 3, bool pca = false, + bool merge = true, bool decimate = false, int max_ch_vertex = 256, + bool extrude = false, double extrude_margin = 0.01, + std::string apx_mode = "ch", unsigned int seed = 0, + bool real_metric = false); +void set_log_level(std::string_view level); + +} // namespace coacd + +extern "C" { + +struct CoACD_Mesh { + double *vertices_ptr; + uint64_t vertices_count; + int *triangles_ptr; + uint64_t triangles_count; +}; + +struct CoACD_MeshArray { + CoACD_Mesh *meshes_ptr; + uint64_t meshes_count; +}; + +void COACD_API CoACD_freeMeshArray(CoACD_MeshArray arr); + +constexpr int preprocess_auto = 0; +constexpr int preprocess_on = 1; +constexpr int preprocess_off = 2; + +constexpr int apx_ch = 0; +constexpr int apx_box = 1; + +CoACD_MeshArray COACD_API CoACD_run(CoACD_Mesh const &input, double threshold, + int max_convex_hull, int preprocess_mode, + int prep_resolution, int sample_resolution, + int mcts_nodes, int mcts_iteration, + int mcts_max_depth, bool pca, bool merge, + bool decimate, int max_ch_vertex, + bool extrude, double extrude_margin, + int apx_mode, unsigned int seed, + bool real_metric); + +void COACD_API CoACD_setLogLevel(char const *level); +} diff --git a/third_party/libzmq/build.sh b/third_party/libzmq/build.sh index 03fb0a1..f15d37d 100644 --- a/third_party/libzmq/build.sh +++ b/third_party/libzmq/build.sh @@ -48,10 +48,19 @@ mkdir -p build cd build echo "Configuring libzmq..." +# BUILD_STATIC=OFF on Linux: the libzmq static archive's mailbox_safe.cpp +# pulls libc++ wait_until -> pthread_cond_clockwait, which UE's link +# sysroot doesn't resolve. The shared .so links its own deps internally +# and works fine, so just don't build the static lib on Linux. +LIBZMQ_STATIC_FLAG="" +case "$(uname -s)" in + Linux) LIBZMQ_STATIC_FLAG="-DBUILD_STATIC=OFF" ;; +esac cmake .. -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ -DZMQ_BUILD_TESTS=OFF \ -DWITH_PERF_TOOL=OFF \ - -DENABLE_DRAFTS=OFF + -DENABLE_DRAFTS=OFF \ + $LIBZMQ_STATIC_FLAG echo "Building libzmq..." cmake --build . --config "$BUILD_TYPE" From 221ccca4d628ea022cd27d181751d8bec379a4a0 Mon Sep 17 00:00:00 2001 From: Vivek GR Date: Tue, 28 Apr 2026 02:22:31 +0000 Subject: [PATCH 06/13] docs: add Linux setup guide and drop the experimental hedge Replace the "Linux is experimental" line in the README requirements with a pointer to docs/linux_setup.md, mention the Linux build script alongside the Windows one in the install section, and add the new doc to the mkdocs nav under Guides. The new docs/linux_setup.md captures: - Prerequisites: UE 5.7 Linux binary, CMake 3.24+, host UE5 C++ project with the plugin in Plugins/. - Why a special build flow: UE's bundled clang + libc++ require ABI-compatible third-party libs, so the system gcc + libstdc++ default of build_all.sh produces unlinkable binaries. - The full env-var sandwich (CC/CXX/AR/RANLIB/CFLAGS/CXXFLAGS/LDFLAGS) pointing the third-party CMake builds at UE's clang. - The role of Scripts/setup_runtime_linux.sh in staging the third- party .so files into Binaries/Linux/ so $ORIGIN-RPATH resolves them without LD_LIBRARY_PATH. - How to run the automation suite via Scripts/build_and_test_linux.sh, including the editor-must-be-closed gotcha. - A short list of known caveats (plugin must be inside the host project's Plugins/, first-launch shader compile time, etc.). Refs: #45 --- README.md | 14 ++++-- docs/linux_setup.md | 103 ++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 docs/linux_setup.md diff --git a/README.md b/README.md index aa1fc7d..1ab16d6 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,9 @@ URLab communicates with external systems over ZMQ. The companion package [**urla ## Requirements -- **Unreal Engine 5.7+** -- **Windows** (Win64). Linux is experimental. +- **Unreal Engine 5.7+** -- Windows binary or Linux binary distribution. +- **Windows (Win64)** with **Visual Studio 2022**, or **Linux (x86_64)** with UE's bundled clang. See [Linux setup](docs/linux_setup.md) for the Linux-specific build flow. - **MuJoCo 3.7+** -- bundled in `third_party/`, built from source. -- **Visual Studio 2022** or compatible C++ toolchain. - **CMake 3.24+** -- for building third-party libraries. - **Python 3.11+** -- optional, for `urlab_bridge` policies. - **[uv](https://github.com/astral-sh/uv)** -- optional, for Python dependency management. @@ -71,10 +70,17 @@ git clone https://github.com/URLab-Sim/UnrealRoboticsLab.git ### 2. Build Dependencies Navigate to the plugin's `third_party` folder and run the build script to fetch and compile MuJoCo, CoACD, and ZMQ: ```powershell +# Windows (PowerShell) cd UnrealRoboticsLab/third_party .\build_all.ps1 ``` -*(If this script fails with a **Stack Overflow** error, see [Troubleshooting](#troubleshooting) below).* +```bash +# Linux: see docs/linux_setup.md for the env vars needed to build +# the third-party libs against UE's bundled clang + libc++. +cd UnrealRoboticsLab/third_party +./build_all.sh +``` +*(If the Windows script fails with a **Stack Overflow** error, see [Troubleshooting](#troubleshooting) below).* ### 3. Compile & Launch 1. Right-click your `.uproject` and select **Generate Visual Studio project files**. diff --git a/docs/linux_setup.md b/docs/linux_setup.md new file mode 100644 index 0000000..fd8d5ad --- /dev/null +++ b/docs/linux_setup.md @@ -0,0 +1,103 @@ +# Linux Setup + +URLab supports Linux against Unreal Engine 5.7+ binary distribution. The plugin builds and the full automation suite (`URLab.*`) passes 177/177 with a working editor — but the build flow is more involved than on Windows because UE's bundled clang + libc++ require the third-party dependencies to be ABI-compatible. + +If you hit anything not covered here please open an issue. + +## Prerequisites + +- **UE 5.7+** Linux binary distribution from . Extract anywhere; the rest of this guide refers to the extract root as `$UE_ROOT` (e.g. `/home//UnrealEngine`). +- **CMake 3.24+** — Ubuntu 22.04 ships 3.22, which is below CoACD's minimum. Easiest fix: + ```bash + pip install --user "cmake>=3.24,<4" + export PATH="$HOME/.local/bin:$PATH" + ``` +- **A host UE5 C++ project** with `UnrealRoboticsLab` cloned into its `Plugins/` (or symlinked there). + +The plugin's third-party submodules (`MuJoCo`, `CoACD`, `libzmq`) must be initialised: +```bash +cd UnrealRoboticsLab +git submodule update --init --recursive +``` + +## Why a special build flow + +UE on Linux uses its own bundled clang (currently 20.1.x at `$UE_ROOT/Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/...`) and links against its bundled libc++. If you build the third-party libs with the system gcc + libstdc++ (which is what `third_party/build_all.sh` does by default), the resulting `.so` files have a different C++ ABI than the UE plugin link expects, and you'll get a wall of `std::*` undefined-symbol errors at link time. + +Solution: point the third-party CMake builds at UE's clang and libc++ via env vars. Once the libs are built that way, the plugin links and runs against them cleanly. + +## Step-by-step + +```bash +UE_ROOT=/path/to/UnrealEngine +URLAB_ROOT=$UE_ROOT/../URLabTest/Plugins/UnrealRoboticsLab # adjust to your layout +UE_TC=$UE_ROOT/Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/Linux_x64/v26_clang-20.1.8-rockylinux8/x86_64-unknown-linux-gnu +``` + +### 1. Build third-party libs against UE's toolchain + +```bash +cd "$URLAB_ROOT/third_party" + +CC="$UE_TC/bin/clang" \ +CXX="$UE_TC/bin/clang++" \ +AR="$UE_TC/bin/llvm-ar" \ +RANLIB="$UE_TC/bin/llvm-ranlib" \ +CFLAGS="-fPIC -Qunused-arguments -Wno-unknown-warning-option" \ +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" \ +LDFLAGS="-stdlib=libc++ -fuse-ld=lld -L$UE_TC/lib64 -Wl,-rpath,$UE_TC/lib64" \ +bash ./build_all.sh +``` + +Expected on success: `third_party/install/{MuJoCo,CoACD,libzmq}/` populated with headers + `.so` files. + +Notes: +- `-Wno-unknown-warning-option` lets clang ignore `-Werror=stringop-overflow` (a GCC-only flag) that TBB (CoACD's transitive dep) tries to use. +- `-Wno-missing-template-arg-list-after-template-kw` keeps clang 20 from rejecting OpenVDB's `OpT::template eval(...)` syntax. +- `-Qunused-arguments` quiets MuJoCo's `-Werror -Wunused-command-line-argument` noise from `-stdlib=libc++` on compile-only steps. +- `BUILD_STATIC=OFF` is now applied automatically for libzmq on Linux (in `third_party/libzmq/build.sh`) — the static archive's `mailbox_safe.cpp` pulls `pthread_cond_clockwait` which UE's link sysroot can't resolve, and the `.so` works fine. + +### 2. Generate UE project files and build the editor target + +```bash +cd "$UE_ROOT" +Engine/Build/BatchFiles/Linux/GenerateProjectFiles.sh -project=/path/to/HostProject.uproject -game -engine +Engine/Build/BatchFiles/Linux/Build.sh HostProjectEditor Linux Development -Project=/path/to/HostProject.uproject +``` + +### 3. Stage runtime libs + +UE on Linux doesn't auto-stage `RuntimeDependencies` for editor builds, and UBT's auto-computed RPATH for plugins symlinked outside the host project can resolve incorrectly. URLab ships a helper that symlinks the third-party `.so` files into the plugin's `Binaries/Linux/` so the loader finds them via `${ORIGIN}` (which UBT does add correctly): + +```bash +"$URLAB_ROOT/Scripts/setup_runtime_linux.sh" +``` + +This is idempotent. The `Scripts/build_and_test_linux.sh` runner invokes it automatically after the build. + +### 4. Launch the editor + +```bash +DISPLAY=:1 "$UE_ROOT/Engine/Binaries/Linux/UnrealEditor" /path/to/HostProject.uproject +``` + +`URLab` should appear under loaded plugins, the **MuJoCo** asset category should register, and the MJCF importer should respond when you drag an `.xml` into the Content Browser. + +## Running the automation suite + +```bash +cd "$URLAB_ROOT" +./Scripts/build_and_test_linux.sh \ + --engine "$UE_ROOT" \ + --project /path/to/HostProject.uproject +``` + +Produces the same summary block as `build_and_test.{sh,ps1}` for pasting into a PR. Expected: `177 / 177 passed`. + +Close the editor before running — the test harness needs the project lock free. + +## Known caveats + +- **Plugin must be located inside (or symlinked inside) the host project's `Plugins/` directory.** UBT's auto-RPATH calculation makes assumptions about the relative position of the plugin and the engine; we work around them by staging libs into `${ORIGIN}`, but the plugin still needs to be findable to UBT through the host project's tree. +- **PIE editor first launch compiles Vulkan SM5 shaders + a derived-data cache** (~10–20 GB). Plan for it; subsequent launches are fast. +- **The `urlab_bridge` Python package** (separate repo) hasn't been smoke-tested against this Linux flow yet. Open an issue if you hit problems. diff --git a/mkdocs.yml b/mkdocs.yml index a944f57..cc77da4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -72,6 +72,7 @@ nav: - Home: index.md - Guides: - Getting Started: getting_started.md + - Linux Setup: linux_setup.md - Features: features.md - Architecture: architecture.md - MJCF Import: guides/mujoco_import.md From acbdd385bfc38e3fbe4fcbdc4975003d63fa09df Mon Sep 17 00:00:00 2001 From: Vivek GR Date: Tue, 28 Apr 2026 17:45:24 +0000 Subject: [PATCH 07/13] build(linux): fold env-var sandwich into build_all.sh --engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accept --engine on third_party/build_all.sh. When given on Linux, glob Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/Linux_x64/ v*_clang-*/x86_64-unknown-linux-gnu so the script survives UE version bumps (v26 -> v27) without a script edit, then export CC / CXX / AR / RANLIB and the matching CFLAGS / CXXFLAGS / LDFLAGS internally so child build.sh invocations inherit them. Replaces the explicit env-var sandwich docs/linux_setup.md previously asked the user to type. The new flow is a single command: ./third_party/build_all.sh --engine $UE_ROOT Without --engine the script falls through to the host toolchain (unchanged behaviour for Windows / macOS, and for any Linux user who deliberately wants the system gcc + libstdc++). Why this matters: with the wrong toolchain, system gcc + libstdc++ either produces .so files whose C++ ABI doesn't match UE's plugin link (wall of std::* undefined-symbol errors), or — worse, with some combinations — emits LTO bitcode objects that masquerade as .so but fail to load with a cryptic "file too short" at editor startup. Both are real failure modes that cost real debugging time. --- docs/linux_setup.md | 19 +++----- third_party/build_all.sh | 97 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 99 insertions(+), 17 deletions(-) diff --git a/docs/linux_setup.md b/docs/linux_setup.md index fd8d5ad..7b3d9d2 100644 --- a/docs/linux_setup.md +++ b/docs/linux_setup.md @@ -31,31 +31,26 @@ Solution: point the third-party CMake builds at UE's clang and libc++ via env va ```bash UE_ROOT=/path/to/UnrealEngine URLAB_ROOT=$UE_ROOT/../URLabTest/Plugins/UnrealRoboticsLab # adjust to your layout -UE_TC=$UE_ROOT/Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/Linux_x64/v26_clang-20.1.8-rockylinux8/x86_64-unknown-linux-gnu ``` ### 1. Build third-party libs against UE's toolchain ```bash cd "$URLAB_ROOT/third_party" - -CC="$UE_TC/bin/clang" \ -CXX="$UE_TC/bin/clang++" \ -AR="$UE_TC/bin/llvm-ar" \ -RANLIB="$UE_TC/bin/llvm-ranlib" \ -CFLAGS="-fPIC -Qunused-arguments -Wno-unknown-warning-option" \ -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" \ -LDFLAGS="-stdlib=libc++ -fuse-ld=lld -L$UE_TC/lib64 -Wl,-rpath,$UE_TC/lib64" \ -bash ./build_all.sh +./build_all.sh --engine "$UE_ROOT" ``` +`--engine` is what makes the build use UE's bundled clang + libc++ rather than the host's gcc + libstdc++. Skip it and the resulting `.so` files have a different C++ ABI than UE's plugin link expects, which surfaces as either a wall of `std::*` undefined-symbol errors at link time, or — worse, with some toolchain combinations — system gcc emits LTO bitcode objects rather than real `.so` files which fail to load with a cryptic "file too short" at editor startup. + +The script globs `Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/Linux_x64/v*_clang-*/` so future UE version bumps (v26 → v27) don't break it. It then exports the CC / CXX / AR / RANLIB pointing at UE's tools and the matching CFLAGS / CXXFLAGS / LDFLAGS internally; no env var sandwich on the user's side. + Expected on success: `third_party/install/{MuJoCo,CoACD,libzmq}/` populated with headers + `.so` files. -Notes: +Build flags applied internally (for reference, in case you need to debug a per-dep failure): - `-Wno-unknown-warning-option` lets clang ignore `-Werror=stringop-overflow` (a GCC-only flag) that TBB (CoACD's transitive dep) tries to use. - `-Wno-missing-template-arg-list-after-template-kw` keeps clang 20 from rejecting OpenVDB's `OpT::template eval(...)` syntax. - `-Qunused-arguments` quiets MuJoCo's `-Werror -Wunused-command-line-argument` noise from `-stdlib=libc++` on compile-only steps. -- `BUILD_STATIC=OFF` is now applied automatically for libzmq on Linux (in `third_party/libzmq/build.sh`) — the static archive's `mailbox_safe.cpp` pulls `pthread_cond_clockwait` which UE's link sysroot can't resolve, and the `.so` works fine. +- `BUILD_STATIC=OFF` is applied automatically for libzmq on Linux (in `third_party/libzmq/build.sh`) — the static archive's `mailbox_safe.cpp` pulls `pthread_cond_clockwait` which UE's link sysroot can't resolve, and the `.so` works fine. ### 2. Generate UE project files and build the editor target diff --git a/third_party/build_all.sh b/third_party/build_all.sh index 0f793d3..c6c49fa 100644 --- a/third_party/build_all.sh +++ b/third_party/build_all.sh @@ -1,24 +1,111 @@ #!/bin/bash -# Master Build Script for URLab Third-Party Dependencies (Linux) +# Master Build Script for URLab Third-Party Dependencies (Linux/macOS) # # Each dep's build.sh will sync its submodule (third_party//src) to the # SHA URLab expects before building. Pass --no-submodule-sync to skip the # sync across all three deps, e.g. when iterating on a submodule locally. +# +# Linux note: pass --engine to build against UE's bundled clang + +# libc++. Without it the system gcc + libstdc++ are used, which produces +# .so files whose C++ ABI doesn't match UE's plugin link (you'll get a wall +# of std::* undefined-symbol errors during the URLab plugin link step). +# Worse, with some toolchain combinations the system gcc emits LTO bitcode +# objects rather than real .so files, which fail to load with a cryptic +# "file too short" at editor startup. The --engine path makes the toolchain +# explicit. + +set -e -ROOT_DIR=$(pwd) +ROOT_DIR=$(cd "$(dirname "$0")" && pwd) INSTALL_DIR="$ROOT_DIR/install" BUILD_TYPE="Release" +ENGINE="" + +usage() { + cat >&2 <<'HELP' +build_all.sh — build URLab's third-party dependencies (MuJoCo, CoACD, libzmq). +Usage: + ./third_party/build_all.sh [--engine ] [--no-submodule-sync] + +Options: + --engine Linux only: point CC/CXX/AR/RANLIB at UE's bundled + clang + libc++ under /Engine/Extras/..., + and inject the matching CFLAGS/CXXFLAGS/LDFLAGS so + the resulting .so files are ABI-compatible with + UE's plugin link. Strongly recommended on Linux. + --no-submodule-sync Skip `git submodule update` on each dep's src/. + Use when iterating on a submodule locally. +HELP + exit 3 +} + +# Pre-parse args. We collect --no-submodule-sync into SHARED_ARGS so it +# propagates to each per-dep build.sh; --engine is consumed here and used +# only to set CC/CXX/CFLAGS/etc. SHARED_ARGS=() -for arg in "$@"; do - if [ "$arg" = "--no-submodule-sync" ]; then SHARED_ARGS+=("--no-submodule-sync"); fi +while [[ $# -gt 0 ]]; do + case "$1" in + --engine) ENGINE="$2"; shift 2 ;; + --no-submodule-sync) SHARED_ARGS+=("--no-submodule-sync"); shift ;; + -h|--help) usage ;; + *) echo "Unknown arg: $1" >&2; usage ;; + esac done -# Ensure directories exist +# Linux: when --engine is given, locate UE's bundled clang under +# Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/Linux_x64// +# x86_64-unknown-linux-gnu/. Glob the v*_clang-* directory so future UE +# bumps (v26 -> v27) survive without a script edit. +if [[ -n "$ENGINE" && "$(uname -s)" = "Linux" ]]; then + SDK_BASE="$ENGINE/Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/Linux_x64" + if [[ ! -d "$SDK_BASE" ]]; then + echo "ERROR: --engine path doesn't contain UE's Linux SDK base:" >&2 + echo " expected: $SDK_BASE" >&2 + exit 3 + fi + UE_TC="" + for cand in "$SDK_BASE"/v*_clang-*/x86_64-unknown-linux-gnu; do + if [[ -x "$cand/bin/clang++" ]]; then + UE_TC="$cand" + break + fi + done + if [[ -z "$UE_TC" ]]; then + echo "ERROR: no v*_clang-*/x86_64-unknown-linux-gnu toolchain found under $SDK_BASE" >&2 + exit 3 + fi + echo "Using UE toolchain: $UE_TC" + + export CC="$UE_TC/bin/clang" + export CXX="$UE_TC/bin/clang++" + export AR="$UE_TC/bin/llvm-ar" + export RANLIB="$UE_TC/bin/llvm-ranlib" + + # Notes on the CXX/LDFLAGS choices: + # -nostdinc++ -isystem : force libc++ headers from UE's SDK + # so we don't accidentally pick up libstdc++ headers from the host. + # -stdlib=libc++: link against UE's libc++ at link time. + # -fuse-ld=lld: lld is what UE uses; avoids surprises on plugin link. + # -Qunused-arguments: silence the "argument unused during compilation" + # warning -stdlib=libc++ produces on .c-mode compile-only steps + # (some MuJoCo deps build with -Werror). + # -Wno-unknown-warning-option: lets clang ignore -Werror=stringop-overflow + # and similar GCC-only flags TBB (CoACD's transitive dep) uses. + # -Wno-missing-template-arg-list-after-template-kw: clang 20 (UE 5.7's + # bundled) rejects OpenVDB's `OpT::template eval(...)` syntax that + # older clang accepted. CoACD pulls OpenVDB transitively. + export CFLAGS="-fPIC -Qunused-arguments -Wno-unknown-warning-option" + export 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" + export LDFLAGS="-stdlib=libc++ -fuse-ld=lld -L$UE_TC/lib64 -Wl,-rpath,$UE_TC/lib64" +fi + mkdir -p "$INSTALL_DIR" echo -e "\e[36mStarting Unified Build Process...\e[0m" +cd "$ROOT_DIR" + # 1. CoACD echo -e "\n\e[33m--- Building CoACD ---\e[0m" cd CoACD From 7854ace88f5691b9e2b5d757d971e714fee5d0c8 Mon Sep 17 00:00:00 2001 From: Vivek GR Date: Tue, 28 Apr 2026 17:47:17 +0000 Subject: [PATCH 08/13] build(linux): auto-stage Binaries/Linux symlinks from per-dep build.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each MuJoCo / CoACD / libzmq build.sh now invokes Scripts/setup_runtime_linux.sh after install on Linux. This catches the partial-rebuild case: bumping a single submodule SHA and running only that dep's build.sh (skipping build_all.sh and build_and_test_linux.sh) would otherwise leave Binaries/Linux/ holding stale symlinks pointing into the wiped install//lib/ — the editor fails to load with no obvious cause. setup_runtime_linux.sh now warn-and-skips with exit 0 when Binaries/Linux/ doesn't exist yet, so a first-time fresh checkout (third-party built before the plugin .so) doesn't error out. The next plugin build creates Binaries/Linux/, the next setup_runtime_linux.sh call (from any of the build paths) populates it. Linux-only block — guarded with `if [ "$(uname -s)" = "Linux" ]` — so Windows / macOS paths are unchanged. --- Scripts/setup_runtime_linux.sh | 10 +++++++--- third_party/CoACD/build.sh | 8 ++++++++ third_party/MuJoCo/build.sh | 12 ++++++++++++ third_party/libzmq/build.sh | 8 ++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Scripts/setup_runtime_linux.sh b/Scripts/setup_runtime_linux.sh index 5e2a41c..b58aa3e 100755 --- a/Scripts/setup_runtime_linux.sh +++ b/Scripts/setup_runtime_linux.sh @@ -47,9 +47,13 @@ if [[ ! -d "$THIRD_PARTY" ]]; then fi if [[ ! -d "$BIN_DIR" ]]; then - echo "ERROR: plugin Binaries/Linux/ not found at $BIN_DIR" >&2 - echo "Build the plugin first (Build.sh URLabTestEditor Linux Development)." >&2 - exit 1 + # Fresh checkout: plugin .so hasn't been built yet. Skip silently with a + # one-line note so per-dep build.sh invocations during initial + # third-party setup don't error out. The next plugin build will create + # Binaries/Linux/, and the next call to this script (from build.sh, + # build_all.sh, or build_and_test_linux.sh) will populate the symlinks. + echo "Skipping runtime stage: $BIN_DIR doesn't exist yet (build the plugin first)." + exit 0 fi count=0 diff --git a/third_party/CoACD/build.sh b/third_party/CoACD/build.sh index 4bf5e92..afce0c7 100644 --- a/third_party/CoACD/build.sh +++ b/third_party/CoACD/build.sh @@ -78,3 +78,11 @@ cd ../.. # Record the exact source SHA we just installed from (see MuJoCo/build.ps1). echo "$INSTALLED_SHA" > "$INSTALL_DIR/INSTALLED_SHA.txt" echo "Recorded INSTALLED_SHA=$INSTALLED_SHA at $INSTALL_DIR/INSTALLED_SHA.txt" + +# Linux: re-stage Binaries/Linux/ symlinks. See MuJoCo/build.sh for rationale. +if [ "$(uname -s)" = "Linux" ]; then + SETUP_RUNTIME="$(cd "$(dirname "$0")/../../Scripts" 2>/dev/null && pwd)/setup_runtime_linux.sh" + if [ -x "$SETUP_RUNTIME" ]; then + bash "$SETUP_RUNTIME" || echo "WARN: setup_runtime_linux.sh failed (continuing)" >&2 + fi +fi diff --git a/third_party/MuJoCo/build.sh b/third_party/MuJoCo/build.sh index 473c0e8..0192d25 100644 --- a/third_party/MuJoCo/build.sh +++ b/third_party/MuJoCo/build.sh @@ -71,3 +71,15 @@ cd ../.. # Record the exact source SHA we just installed from (see MuJoCo/build.ps1). echo "$INSTALLED_SHA" > "$INSTALL_DIR/INSTALLED_SHA.txt" echo "Recorded INSTALLED_SHA=$INSTALLED_SHA at $INSTALL_DIR/INSTALLED_SHA.txt" + +# Linux: re-stage the plugin's Binaries/Linux/ symlinks so a per-dep rebuild +# (e.g. someone bumps MuJoCo's submodule SHA and runs only this script) +# doesn't leave stale symlinks pointing into a wiped install/MuJoCo/lib/. +# The helper exits cleanly with a one-line note if Binaries/Linux/ doesn't +# exist yet (first-time fresh checkout — plugin .so not built). +if [ "$(uname -s)" = "Linux" ]; then + SETUP_RUNTIME="$(cd "$(dirname "$0")/../../Scripts" 2>/dev/null && pwd)/setup_runtime_linux.sh" + if [ -x "$SETUP_RUNTIME" ]; then + bash "$SETUP_RUNTIME" || echo "WARN: setup_runtime_linux.sh failed (continuing)" >&2 + fi +fi diff --git a/third_party/libzmq/build.sh b/third_party/libzmq/build.sh index f15d37d..6d31a78 100644 --- a/third_party/libzmq/build.sh +++ b/third_party/libzmq/build.sh @@ -73,3 +73,11 @@ cd ../.. # Record the exact source SHA we just installed from (see MuJoCo/build.ps1). echo "$INSTALLED_SHA" > "$INSTALL_DIR/INSTALLED_SHA.txt" echo "Recorded INSTALLED_SHA=$INSTALLED_SHA at $INSTALL_DIR/INSTALLED_SHA.txt" + +# Linux: re-stage Binaries/Linux/ symlinks. See MuJoCo/build.sh for rationale. +if [ "$(uname -s)" = "Linux" ]; then + SETUP_RUNTIME="$(cd "$(dirname "$0")/../../Scripts" 2>/dev/null && pwd)/setup_runtime_linux.sh" + if [ -x "$SETUP_RUNTIME" ]; then + bash "$SETUP_RUNTIME" || echo "WARN: setup_runtime_linux.sh failed (continuing)" >&2 + fi +fi From 0993efef840dce91befb1c587ac0187989c87821 Mon Sep 17 00:00:00 2001 From: Vivek GR Date: Tue, 28 Apr 2026 17:47:58 +0000 Subject: [PATCH 09/13] docs(linux): add UE5-on-Linux installation prerequisites linux_setup.md previously assumed a working $UE_ROOT. Add a short "Unreal Engine 5" subsection at the top of Prerequisites covering: - where to get the precompiled Linux binary (epicgames.com/linux) - the .zip flow (~25 GB compressed, ~43 GB extracted) - disk-space rule of thumb (~70 GB free for initial setup including shader / DDC cache) - display options for headless servers (NICE DCV / X2Go / VNC) - common apt prereqs (libsdl2, libvulkan1) for fresh Ubuntu 22.04 Mentions the source-build path exists but is not required (most users land on this page from a fresh distro and don't realise the binary distribution exists). --- docs/linux_setup.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/linux_setup.md b/docs/linux_setup.md index 7b3d9d2..5c7e897 100644 --- a/docs/linux_setup.md +++ b/docs/linux_setup.md @@ -6,13 +6,33 @@ If you hit anything not covered here please open an issue. ## Prerequisites -- **UE 5.7+** Linux binary distribution from . Extract anywhere; the rest of this guide refers to the extract root as `$UE_ROOT` (e.g. `/home//UnrealEngine`). +### Unreal Engine 5 + +Epic ships a precompiled UE5 binary for Linux — no source build, no GitHub access, no Setup.sh. It's the path of least friction; the source-build path is also supported by Epic but takes much longer (~hours of compile, ~150 GB disk) and is not required. + +1. Sign in to your Epic Games account at (top of the page). +2. Download **Linux Unreal Engine** — currently a single `.zip` named like `Linux_Unreal_Engine_5.7.x.zip`, ~25 GB compressed / ~43 GB extracted. +3. Extract anywhere. The rest of this guide refers to the extract root as `$UE_ROOT` (e.g. `/home//UnrealEngine`). +4. Confirm the editor binary exists and is executable: + + ```bash + ls -la $UE_ROOT/Engine/Binaries/Linux/UnrealEditor + ``` + +**Disk-space rule of thumb:** 70 GB free during initial setup is comfortable: ~43 GB UE binaries, ~10–20 GB shader / DDC cache after first launch, and a few GB for a host project + the URLab plugin. + +**Display:** the editor needs a Wayland or X11 session. On a headless server, common options are NICE DCV, X2Go, or a TigerVNC `:1` display (export `DISPLAY=:1` before launching). + +**System libs:** UE bundles most of what it needs (libc++, ICU, etc.). On a fresh Ubuntu 22.04 you may still need `apt install libsdl2-2.0-0 libvulkan1` if the editor can't open a window — that's the symptom you'd see after launch with a missing-library message in the log. + +### Other prerequisites + - **CMake 3.24+** — Ubuntu 22.04 ships 3.22, which is below CoACD's minimum. Easiest fix: ```bash pip install --user "cmake>=3.24,<4" export PATH="$HOME/.local/bin:$PATH" ``` -- **A host UE5 C++ project** with `UnrealRoboticsLab` cloned into its `Plugins/` (or symlinked there). +- **A host UE5 C++ project** with `UnrealRoboticsLab` cloned into its `Plugins/` (or symlinked there). The host project must be C++; if your project is Blueprints-only, add a dummy C++ class in the editor first (Tools → New C++ Class). The plugin's third-party submodules (`MuJoCo`, `CoACD`, `libzmq`) must be initialised: ```bash From 2b22ddb27876dae8daf76f87dd76c39a9b7badcb Mon Sep 17 00:00:00 2001 From: Vivek GR Date: Wed, 29 Apr 2026 22:27:37 +0000 Subject: [PATCH 10/13] fix(linux): direct-assign mju_user_* on Linux instead of GetDllHandle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review feedback: Linux no longer adds the third-party .so to PublicDelayLoadDLLs (RPATH staging supersedes the delay-load workaround), so URLab_InstallMujocoCallbacks doesn't need to GetDllHandle("libmujoco.so.X.Y.Z") + GetDllExport at all on Linux. The mju_user_error / mju_user_warning symbols are declared `MJAPI extern void (*...)(const char*)` in mujoco.h and link directly through the .so we're already linked against, so direct assignment is enough. Drops the hardcoded libmujoco.so.3.7.0 SONAME literal — one fewer spot to update on the next MuJoCo pin bump. Windows path is unchanged: still GetDllHandle-based because mujoco.dll genuinely is delay-loaded there. --- .../Private/MuJoCo/Core/MjPhysicsEngine.cpp | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Source/URLab/Private/MuJoCo/Core/MjPhysicsEngine.cpp b/Source/URLab/Private/MuJoCo/Core/MjPhysicsEngine.cpp index 948624a..8f32ffc 100644 --- a/Source/URLab/Private/MuJoCo/Core/MjPhysicsEngine.cpp +++ b/Source/URLab/Private/MuJoCo/Core/MjPhysicsEngine.cpp @@ -109,20 +109,15 @@ static void URLab_InstallMujocoCallbacks() { if (GMujocoCallbacksInstalled) return; - // mujoco is delay-loaded in URLab's Build.cs, and the linker refuses - // to bind data symbols through a delayed import. Resolve the two - // mju_user_* function pointers manually via GetDllExport. #if PLATFORM_WINDOWS - const TCHAR* MujocoLibName = TEXT("mujoco.dll"); -#elif PLATFORM_LINUX - const TCHAR* MujocoLibName = TEXT("libmujoco.so.3.7.0"); -#else - const TCHAR* MujocoLibName = TEXT("mujoco"); -#endif - void* Handle = FPlatformProcess::GetDllHandle(MujocoLibName); + // mujoco.dll is delay-loaded on Windows (URLab.Build.cs adds it via + // PublicDelayLoadDLLs), and the linker refuses to bind data symbols + // through a delayed import. Resolve the two mju_user_* function + // pointers manually via GetDllExport. + void* Handle = FPlatformProcess::GetDllHandle(TEXT("mujoco.dll")); if (!Handle) { - UE_LOG(LogURLab, Warning, TEXT("[URLab] Could not resolve %s to install error callbacks"), MujocoLibName); + UE_LOG(LogURLab, Warning, TEXT("[URLab] Could not resolve mujoco.dll to install error callbacks")); return; } @@ -134,6 +129,16 @@ static void URLab_InstallMujocoCallbacks() if (PErr) { *PErr = &URLab_OnMujocoError; } if (PWarn) { *PWarn = &URLab_OnMujocoWarning; } UE_LOG(LogURLab, Log, TEXT("[URLab] MuJoCo error callbacks installed (err=%p warn=%p)"), (void*)PErr, (void*)PWarn); +#else + // On Linux/macOS the lib is linked directly (no delay-load), so + // mju_user_error / mju_user_warning are resolvable as ordinary BSS + // data symbols at link time. Direct assignment is enough — no + // GetDllHandle / GetDllExport, no hardcoded SONAME literal needed. + mju_user_error = &URLab_OnMujocoError; + mju_user_warning = &URLab_OnMujocoWarning; + UE_LOG(LogURLab, Log, TEXT("[URLab] MuJoCo error callbacks installed (direct)")); +#endif + GMujocoCallbacksInstalled = true; } From 10d7276992125a2b4db4c2642711373a6d025159 Mon Sep 17 00:00:00 2001 From: Vivek GR Date: Wed, 29 Apr 2026 22:27:42 +0000 Subject: [PATCH 11/13] build(linux): version-sort UE toolchain glob in build_all.sh Per review feedback: bash globs sort lexicographically, so `for cand in $SDK_BASE/v*_clang-*/...; do ... break; done` picks v25 over v26 when both exist. Latent today since UE ships exactly one Linux toolchain per engine version, but it would silently regress on a future UE bump if a transitional engine ever shipped two. Replace the loop with `ls -d ... | sort -V | tail -1` (version-aware sort, picks the highest) and keep the executability check. --- third_party/build_all.sh | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/third_party/build_all.sh b/third_party/build_all.sh index c6c49fa..ec4b4d7 100644 --- a/third_party/build_all.sh +++ b/third_party/build_all.sh @@ -64,14 +64,12 @@ if [[ -n "$ENGINE" && "$(uname -s)" = "Linux" ]]; then echo " expected: $SDK_BASE" >&2 exit 3 fi - UE_TC="" - for cand in "$SDK_BASE"/v*_clang-*/x86_64-unknown-linux-gnu; do - if [[ -x "$cand/bin/clang++" ]]; then - UE_TC="$cand" - break - fi - done - if [[ -z "$UE_TC" ]]; then + # Pick the highest-numbered v*_clang-* toolchain. Bash globs sort + # lexicographically so v25 sorts before v26 — version-aware sort with + # `sort -V` ensures v27 wins over v26 on a future UE bump. Latent today + # since UE ships one toolchain per engine version, but cheap insurance. + UE_TC=$(ls -d "$SDK_BASE"/v*_clang-*/x86_64-unknown-linux-gnu 2>/dev/null | sort -V | tail -1) + if [[ -z "$UE_TC" || ! -x "$UE_TC/bin/clang++" ]]; then echo "ERROR: no v*_clang-*/x86_64-unknown-linux-gnu toolchain found under $SDK_BASE" >&2 exit 3 fi From bcb3df4ce7169b7988de34d5743843afa131f0aa Mon Sep 17 00:00:00 2001 From: Vivek GR Date: Wed, 29 Apr 2026 22:27:50 +0000 Subject: [PATCH 12/13] docs(linux): restructure for one-time-setup vs day-to-day workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review feedback on docs/linux_setup.md: 1. Add a source-build option (UnrealEngine clone + Setup.sh + GenerateProjectFiles.sh + make UnrealEditor) under Prerequisites. The precompiled binary is still recommended as the path of least friction; the source build is for engine-level debugging. 2. Split the doc into clearly-labelled sections: - One-time setup (the existing 4-step walkthrough) - Day-to-day workflow (just `git pull` + build_and_test_linux.sh) - Troubleshooting / Advanced (build flags reference, env-var sandwich for manual per-dep rebuilds, known caveats). The Reviewer's read was that the doc currently looked like a first-setup guide with no signposting for what the loop is after that. Now there's a header pointer at the top and the day-to-day section is its own anchor. 3. Move the "Build flags applied internally" reference list out of step 1 (where most users won't need it) and into Troubleshooting / Advanced — it's debugging context, not setup material. No content removed, just reshelved. --- docs/linux_setup.md | 99 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 77 insertions(+), 22 deletions(-) diff --git a/docs/linux_setup.md b/docs/linux_setup.md index 5c7e897..a479820 100644 --- a/docs/linux_setup.md +++ b/docs/linux_setup.md @@ -1,6 +1,8 @@ # Linux Setup -URLab supports Linux against Unreal Engine 5.7+ binary distribution. The plugin builds and the full automation suite (`URLab.*`) passes 177/177 with a working editor — but the build flow is more involved than on Windows because UE's bundled clang + libc++ require the third-party dependencies to be ABI-compatible. +URLab supports Linux against Unreal Engine 5.7+. The plugin builds and the full automation suite (`URLab.*`) passes 177/177 with a working editor — but the build flow is more involved than on Windows because UE's bundled clang + libc++ require the third-party dependencies to be ABI-compatible. + +This page is a **one-time setup** walkthrough. Jump to [Day-to-day workflow](#day-to-day-workflow) once you have a working build; the [Troubleshooting / Advanced](#troubleshooting--advanced) section covers debugging tips and the build flags applied internally by `build_all.sh --engine`. If you hit anything not covered here please open an issue. @@ -8,7 +10,11 @@ If you hit anything not covered here please open an issue. ### Unreal Engine 5 -Epic ships a precompiled UE5 binary for Linux — no source build, no GitHub access, no Setup.sh. It's the path of least friction; the source-build path is also supported by Epic but takes much longer (~hours of compile, ~150 GB disk) and is not required. +You have two options. The binary is dramatically faster to set up; the source build is useful when you want to debug into the engine itself. + +#### Option A — precompiled binary (recommended for most users) + +Epic ships a precompiled UE5 binary for Linux — no source build, no GitHub access, no `Setup.sh`. 1. Sign in to your Epic Games account at (top of the page). 2. Download **Linux Unreal Engine** — currently a single `.zip` named like `Linux_Unreal_Engine_5.7.x.zip`, ~25 GB compressed / ~43 GB extracted. @@ -19,22 +25,47 @@ Epic ships a precompiled UE5 binary for Linux — no source build, no GitHub acc ls -la $UE_ROOT/Engine/Binaries/Linux/UnrealEditor ``` -**Disk-space rule of thumb:** 70 GB free during initial setup is comfortable: ~43 GB UE binaries, ~10–20 GB shader / DDC cache after first launch, and a few GB for a host project + the URLab plugin. +#### Option B — source build (useful for engine-level debugging) + +Lets you set breakpoints in engine code, step through UBT, and patch UE itself. Costs a multi-hour clone + compile and ~150 GB disk; rarely needed for plugin work but invaluable when you do need it. + +1. [Link your Epic Games account to GitHub](https://www.unrealengine.com/en-US/ue-on-github) so you can access . +2. Clone the `5.7` branch (or `release` for whatever Epic considers stable): -**Display:** the editor needs a Wayland or X11 session. On a headless server, common options are NICE DCV, X2Go, or a TigerVNC `:1` display (export `DISPLAY=:1` before launching). + ```bash + git clone -b 5.7 https://github.com/EpicGames/UnrealEngine.git + cd UnrealEngine + ``` + +3. Run the bundled setup + project-files scripts: -**System libs:** UE bundles most of what it needs (libc++, ICU, etc.). On a fresh Ubuntu 22.04 you may still need `apt install libsdl2-2.0-0 libvulkan1` if the editor can't open a window — that's the symptom you'd see after launch with a missing-library message in the log. + ```bash + ./Setup.sh # downloads ~30 GB of binary deps via Git LFS + ./GenerateProjectFiles.sh # generates Makefile / IDE project files + make UnrealEditor # ~hours on first build, parallel-safe + ``` + +4. `$UE_ROOT` is your `UnrealEngine/` clone root for the rest of this doc. + +#### General notes (apply to both options) + +- **Disk-space rule of thumb:** 70 GB free for option A initial setup is comfortable (~43 GB UE binaries + ~10–20 GB shader / DDC cache + a few GB for project + plugin). Option B needs ~150 GB. +- **Display:** the editor needs a Wayland or X11 session. On a headless server, common options are NICE DCV, X2Go, or a TigerVNC `:1` display (`export DISPLAY=:1` before launching). +- **System libs:** UE bundles most of what it needs (libc++, ICU, etc.). On a fresh Ubuntu 22.04 you may still need `apt install libsdl2-2.0-0 libvulkan1` if the editor can't open a window — that's the symptom you'd see after launch with a missing-library message in the log. ### Other prerequisites - **CMake 3.24+** — Ubuntu 22.04 ships 3.22, which is below CoACD's minimum. Easiest fix: + ```bash pip install --user "cmake>=3.24,<4" export PATH="$HOME/.local/bin:$PATH" ``` + - **A host UE5 C++ project** with `UnrealRoboticsLab` cloned into its `Plugins/` (or symlinked there). The host project must be C++; if your project is Blueprints-only, add a dummy C++ class in the editor first (Tools → New C++ Class). The plugin's third-party submodules (`MuJoCo`, `CoACD`, `libzmq`) must be initialised: + ```bash cd UnrealRoboticsLab git submodule update --init --recursive @@ -42,11 +73,11 @@ git submodule update --init --recursive ## Why a special build flow -UE on Linux uses its own bundled clang (currently 20.1.x at `$UE_ROOT/Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/...`) and links against its bundled libc++. If you build the third-party libs with the system gcc + libstdc++ (which is what `third_party/build_all.sh` does by default), the resulting `.so` files have a different C++ ABI than the UE plugin link expects, and you'll get a wall of `std::*` undefined-symbol errors at link time. +UE on Linux uses its own bundled clang (currently 20.1.x at `$UE_ROOT/Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/...`) and links against its bundled libc++. If you build the third-party libs with the system gcc + libstdc++ (which is what `third_party/build_all.sh` does *without* `--engine`), the resulting `.so` files have a different C++ ABI than the UE plugin link expects, and you'll get a wall of `std::*` undefined-symbol errors at link time. -Solution: point the third-party CMake builds at UE's clang and libc++ via env vars. Once the libs are built that way, the plugin links and runs against them cleanly. +`build_all.sh --engine $UE_ROOT` points the third-party CMake builds at UE's clang + libc++. Once the libs are built that way, the plugin links and runs against them cleanly. -## Step-by-step +## One-time setup ```bash UE_ROOT=/path/to/UnrealEngine @@ -60,17 +91,9 @@ cd "$URLAB_ROOT/third_party" ./build_all.sh --engine "$UE_ROOT" ``` -`--engine` is what makes the build use UE's bundled clang + libc++ rather than the host's gcc + libstdc++. Skip it and the resulting `.so` files have a different C++ ABI than UE's plugin link expects, which surfaces as either a wall of `std::*` undefined-symbol errors at link time, or — worse, with some toolchain combinations — system gcc emits LTO bitcode objects rather than real `.so` files which fail to load with a cryptic "file too short" at editor startup. - -The script globs `Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/Linux_x64/v*_clang-*/` so future UE version bumps (v26 → v27) don't break it. It then exports the CC / CXX / AR / RANLIB pointing at UE's tools and the matching CFLAGS / CXXFLAGS / LDFLAGS internally; no env var sandwich on the user's side. - Expected on success: `third_party/install/{MuJoCo,CoACD,libzmq}/` populated with headers + `.so` files. -Build flags applied internally (for reference, in case you need to debug a per-dep failure): -- `-Wno-unknown-warning-option` lets clang ignore `-Werror=stringop-overflow` (a GCC-only flag) that TBB (CoACD's transitive dep) tries to use. -- `-Wno-missing-template-arg-list-after-template-kw` keeps clang 20 from rejecting OpenVDB's `OpT::template eval(...)` syntax. -- `-Qunused-arguments` quiets MuJoCo's `-Werror -Wunused-command-line-argument` noise from `-stdlib=libc++` on compile-only steps. -- `BUILD_STATIC=OFF` is applied automatically for libzmq on Linux (in `third_party/libzmq/build.sh`) — the static archive's `mailbox_safe.cpp` pulls `pthread_cond_clockwait` which UE's link sysroot can't resolve, and the `.so` works fine. +`--engine` makes the script glob `Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/Linux_x64/v*_clang-*/` (version-sorted, so future UE bumps from v26 → v27 are picked up automatically), then export `CC / CXX / AR / RANLIB / CFLAGS / CXXFLAGS / LDFLAGS` for each per-dep `build.sh`. ### 2. Generate UE project files and build the editor target @@ -88,7 +111,7 @@ UE on Linux doesn't auto-stage `RuntimeDependencies` for editor builds, and UBT' "$URLAB_ROOT/Scripts/setup_runtime_linux.sh" ``` -This is idempotent. The `Scripts/build_and_test_linux.sh` runner invokes it automatically after the build. +It's idempotent and warn-skips when `Binaries/Linux/` doesn't exist yet (first-time fresh checkout). Each per-dep `build.sh` and `Scripts/build_and_test_linux.sh` invoke it automatically too, so you usually don't run it directly. ### 4. Launch the editor @@ -98,20 +121,52 @@ DISPLAY=:1 "$UE_ROOT/Engine/Binaries/Linux/UnrealEditor" /path/to/HostProject.up `URLab` should appear under loaded plugins, the **MuJoCo** asset category should register, and the MJCF importer should respond when you drag an `.xml` into the Content Browser. -## Running the automation suite +## Day-to-day workflow + +After the one-time setup, normal iteration is just: ```bash -cd "$URLAB_ROOT" +git pull ./Scripts/build_and_test_linux.sh \ --engine "$UE_ROOT" \ --project /path/to/HostProject.uproject ``` -Produces the same summary block as `build_and_test.{sh,ps1}` for pasting into a PR. Expected: `177 / 177 passed`. +That builds the editor target (incremental), re-stages the runtime symlinks if a third-party dep was rebuilt, runs the `URLab.*` automation suite, and emits the build+test summary block to paste into a PR. Expected: `177 / 177 passed`. Close the editor before running — the test harness needs the project lock free. -## Known caveats +If you only changed plugin C++ and want to skip the test pass, the editor `Build.sh` step from one-time setup #2 is the inner loop. + +## Troubleshooting / Advanced + +### Build flags applied internally by `build_all.sh --engine` + +Reference, in case you need to debug a per-dep failure or replicate the build manually: + +- `-Wno-unknown-warning-option` lets clang ignore `-Werror=stringop-overflow` (a GCC-only flag) that TBB (CoACD's transitive dep) tries to use. +- `-Wno-missing-template-arg-list-after-template-kw` keeps clang 20 from rejecting OpenVDB's `OpT::template eval(...)` syntax. +- `-Qunused-arguments` quiets MuJoCo's `-Werror -Wunused-command-line-argument` noise from `-stdlib=libc++` on compile-only steps. +- `BUILD_STATIC=OFF` is applied automatically for libzmq on Linux (in `third_party/libzmq/build.sh`) — the static archive's `mailbox_safe.cpp` pulls `pthread_cond_clockwait` which UE's link sysroot can't resolve, and the `.so` works fine. + +### Building one dep manually (env-var sandwich) + +If you want to rebuild only one dep with the same flags `build_all.sh --engine` would apply (e.g. iterating on MuJoCo locally), the equivalent invocation is: + +```bash +UE_TC=$(ls -d "$UE_ROOT/Engine/Extras/ThirdPartyNotUE/SDKs/HostLinux/Linux_x64"/v*_clang-*/x86_64-unknown-linux-gnu | sort -V | tail -1) + +CC="$UE_TC/bin/clang" \ +CXX="$UE_TC/bin/clang++" \ +AR="$UE_TC/bin/llvm-ar" \ +RANLIB="$UE_TC/bin/llvm-ranlib" \ +CFLAGS="-fPIC -Qunused-arguments -Wno-unknown-warning-option" \ +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" \ +LDFLAGS="-stdlib=libc++ -fuse-ld=lld -L$UE_TC/lib64 -Wl,-rpath,$UE_TC/lib64" \ +bash third_party/MuJoCo/build.sh +``` + +### Known caveats - **Plugin must be located inside (or symlinked inside) the host project's `Plugins/` directory.** UBT's auto-RPATH calculation makes assumptions about the relative position of the plugin and the engine; we work around them by staging libs into `${ORIGIN}`, but the plugin still needs to be findable to UBT through the host project's tree. - **PIE editor first launch compiles Vulkan SM5 shaders + a derived-data cache** (~10–20 GB). Plan for it; subsequent launches are fast. From 6cba181e86dc4394779dce7a00ee4d7285809bab Mon Sep 17 00:00:00 2001 From: Vivek GR Date: Wed, 29 Apr 2026 22:57:31 +0000 Subject: [PATCH 13/13] docs(linux): move runtime-staging step out of one-time setup Per the second half of jonathanembleyriches's nitpick on linux_setup.md: setup_runtime_linux.sh is now invoked automatically by every per-dep build.sh and by build_and_test_linux.sh, so users never need to run it manually during normal setup or iteration. Keeping it as Step 3 of the one-time setup walkthrough was misleading. Removed it from the One-time setup section. Added a "How runtime staging works" subsection in Troubleshooting / Advanced that explains what the script does, when it auto-fires, and how to run it manually in the rare case you need to. Renumbered "Launch the editor" from step 4 to step 3. The brief mention is preserved at the end of step 2 for users who want the link to the explanation, but the action is gone. --- docs/linux_setup.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/linux_setup.md b/docs/linux_setup.md index a479820..816fd11 100644 --- a/docs/linux_setup.md +++ b/docs/linux_setup.md @@ -103,17 +103,9 @@ Engine/Build/BatchFiles/Linux/GenerateProjectFiles.sh -project=/path/to/HostProj Engine/Build/BatchFiles/Linux/Build.sh HostProjectEditor Linux Development -Project=/path/to/HostProject.uproject ``` -### 3. Stage runtime libs +The third-party `.so` files (libmujoco, lib_coacd, libzmq) get symlinked into the plugin's `Binaries/Linux/` automatically by each per-dep `build.sh` and by `Scripts/build_and_test_linux.sh` — see [How runtime staging works](#how-runtime-staging-works) in Troubleshooting / Advanced if you need to do it manually. -UE on Linux doesn't auto-stage `RuntimeDependencies` for editor builds, and UBT's auto-computed RPATH for plugins symlinked outside the host project can resolve incorrectly. URLab ships a helper that symlinks the third-party `.so` files into the plugin's `Binaries/Linux/` so the loader finds them via `${ORIGIN}` (which UBT does add correctly): - -```bash -"$URLAB_ROOT/Scripts/setup_runtime_linux.sh" -``` - -It's idempotent and warn-skips when `Binaries/Linux/` doesn't exist yet (first-time fresh checkout). Each per-dep `build.sh` and `Scripts/build_and_test_linux.sh` invoke it automatically too, so you usually don't run it directly. - -### 4. Launch the editor +### 3. Launch the editor ```bash DISPLAY=:1 "$UE_ROOT/Engine/Binaries/Linux/UnrealEditor" /path/to/HostProject.uproject @@ -140,6 +132,20 @@ If you only changed plugin C++ and want to skip the test pass, the editor `Build ## Troubleshooting / Advanced +### How runtime staging works + +UE on Linux doesn't auto-stage `RuntimeDependencies` for editor builds, and UBT's auto-computed RPATH for plugins symlinked outside the host project can resolve incorrectly. URLab works around this by symlinking the third-party `.so` files into the plugin's `Binaries/Linux/` so the loader resolves them via `${ORIGIN}` (which UBT does add correctly). + +The helper `Scripts/setup_runtime_linux.sh` does the symlinking. It's idempotent and warn-skips when `Binaries/Linux/` doesn't exist yet (first-time fresh checkout, before the plugin .so has been built). You **don't normally call it directly** — both `build_all.sh` (via each per-dep `build.sh`) and `Scripts/build_and_test_linux.sh` invoke it after their respective build steps. + +You'd run it manually only if you fiddled with `third_party/install//lib/` outside of those scripts and need to re-sync the symlinks: + +```bash +"$URLAB_ROOT/Scripts/setup_runtime_linux.sh" +``` + +For packaged (non-editor) builds, `RuntimeDependencies.Add("$(BinaryOutputDir)/...", LibFile, NonUFS)` from `URLab.Build.cs` stages the libs through `BuildCookRun`, and UBT's `${ORIGIN}` RPATH resolves them — no manual step needed. + ### Build flags applied internally by `build_all.sh --engine` Reference, in case you need to debug a per-dep failure or replicate the build manually: