-
Notifications
You must be signed in to change notification settings - Fork 113
vs2008_wine_pluginmax_build
title: Building ryzomcore's 3ds Max plugin (PluginMax) under Wine description: Verified end-to-end — driving the real ryzomcore CMake build (WITH_NEL_MAXPLUGIN) through a wine-hosted VS2008 x86 toolchain, matching tool/quick_start's real PluginMax spec published: true date: 2026-07-07T00:00:00.000Z tags: editor: markdown dateCreated: 2026-07-07T00:00:00.000Z
Builds on Visual C++ 2008 build environment on Linux using Wine — read that first for the base Wine prefix, the toolchain archive, and the raw
cl.exe/CMake+Ninja traps. This page covers the additional setup needed to build the real ryzomcore repo's 3ds Max plugin (nel/tools/3d/plugin_maxand friends), not just standalone probes. {.is-info}
Verified 2026-07-07:
ninja -j4completed 1008/1008 objects, exit code 0, zeroFAILEDlines —nel_export_r.dlu,nel_patch_converter_r.dlm,nel_patch_edit_r.dlm,nel_patch_paint_r.dlm,nel_vertex_tree_paint_r.dlm,tile_utility_r.dlu,ligoscape_utility_r.dlxall built, plus the static NeL libs (nel3d_r.lib,nelmisc_r.lib,nelpacs_r.lib,nelgui_r.lib, ...). {.is-info}
Beyond the base VS2008 toolchain archive from the companion page, extracted into the same prefix's drive_c:
-
Max 2010 SDK →
Program Files/Autodesk/3ds Max 2010 SDK/(top-level archive folder name matches).Find3dsMaxSDK.cmakelooks formaxsdk/include/maxversion.hunder$ENV{PROGRAMFILES}/Autodesk/3ds Max <year> SDK/maxsdk. -
Externals (
2019q4_external_v90_x86, matchingnel_plugin_max_9_x86.bat/_2010_x86.bat/_2012_x86.bat's redist paths) →C:\2019q4_external_v90_x86directly (not underProgram Files) — one subfolder per library (zlib/include,zlib/lib,boost/include, ...), not a single unified tree. -
DirectX SDK (June 2010) →
Program Files/Microsoft DirectX SDK (June 2010)/— required unconditionally for any MSVC configure of this repo (see "Genuine upstream bugs" below), regardless of whether a Direct3D driver is actually being built.
Vendor SDK headers #include each other with inconsistent casing (e.g. one Max SDK header does #include "assetmanagement/AssetUser.h", another does #include "assetmanagement/assetUser.h") — harmless on a case-insensitive Windows filesystem, but on a case-sensitive Linux one this defeats #pragma once's file-identity tracking: the two casings resolve to two different paths, so the header gets parsed twice, and the second pass hits error C2011: class type redefinition. Same root cause as an ordinary missing-file case mismatch (e.g. ComCtl32.lib vs. requested ComCtl32.lib — actually stored as ComCtl32.Lib; BIPEXP.H vs. requested bipexp.h), just surfacing as a redefinition instead of a not-found.
ciopfs (a case-insensitive FUSE overlay) looked like the systematic fix, but turned out broken on a stock Ubuntu 24.04 box — an isolated test showed it doesn't list any subdirectory at all, even space-free ones:
mkdir -p /tmp/t/{src/sub,mnt} && ciopfs /tmp/t/src /tmp/t/mnt && ls /tmp/t/mnt # empty - real bug, not a config issueInstead, generate lowercase-alias symlinks across every relevant tree in one pass:
#!/usr/bin/env python3
# For every file/dir under ROOT whose name isn't already all-lowercase,
# add a lowercase-named symlink alongside it (skip if a same-named entry
# already exists - real file or prior symlink).
import os, sys
root = sys.argv[1]
created = 0
skipped_collision = 0
for dirpath, dirnames, filenames in os.walk(root):
for name in list(dirnames) + filenames:
lower = name.lower()
if name == lower:
continue
full = os.path.join(dirpath, name)
lower_full = os.path.join(dirpath, lower)
if os.path.lexists(lower_full):
if not os.path.islink(lower_full):
skipped_collision += 1
continue
os.symlink(name, lower_full)
created += 1
print(f"created={created} skipped_collision={skipped_collision}")Run against every include/lib tree that vendor code touches (roughly 2000 symlinks total on a fresh prefix, zero collisions observed):
PFX=~/.local/share/wineprefixes/vs2008/drive_c
for d in \
"$PFX/Program Files/Autodesk/3ds Max 2010 SDK/maxsdk/include" \
"$PFX/Program Files/Autodesk/3ds Max 2010 SDK/maxsdk/lib" \
"$PFX/Program Files/Microsoft Visual Studio 9.0/VC/include" \
"$PFX/Program Files/Microsoft Visual Studio 9.0/VC/lib" \
"$PFX/Program Files/Microsoft Visual Studio 9.0/VC/atlmfc" \
"$PFX/Program Files/Microsoft SDKs/Windows/v6.0A/Include" \
"$PFX/Program Files/Microsoft SDKs/Windows/v6.0A/Lib" \
"$PFX/Program Files/Microsoft DirectX SDK (June 2010)/Include" \
"$PFX/Program Files/Microsoft DirectX SDK (June 2010)/Lib" \
"$PFX/2019q4_external_v90_x86"
do
python3 lowercase_alias.py "$d"
doneThis is a superset of (and replaces) individually-discovered symlinks like ComCtl32.lib/bipexp.h — run it once up front on a fresh prefix rather than reactively chasing each mismatch as it surfaces during a build.
CMakeModules/FindMSVC.cmake derives VC_DIR by regexing /bin/.+ off CMAKE_C_COMPILER's path, assuming it points directly at a real .../VC/bin/cl.exe — breaks since our compiler is a wrapper script, not literally inside a VC/bin/ tree. Build a fake shadow directory ending in the right shape, with the wrapper scripts standing in for the real binaries and everything else symlinked through to the real toolchain archive:
SHADOW=~/.local/share/wineprefixes/vs2008-msvc-shadow/VC
PFX="$HOME/.local/share/wineprefixes/vs2008/drive_c/Program Files/Microsoft Visual Studio 9.0/VC"
mkdir -p "$SHADOW/bin"
ln -sfn "$PFX/include" "$SHADOW/include"
ln -sfn "$PFX/lib" "$SHADOW/lib"
ln -sfn "$PFX/atlmfc" "$SHADOW/atlmfc"
ln -sfn "$PFX/redist" "$SHADOW/redist"
cp ~/bin/wine-vs2008/winecl-env "$SHADOW/bin/winecl-env" # winecl-{cc,link,lib} source this by dirname($0), needs a local copy
cp ~/bin/wine-vs2008/winecl-cc "$SHADOW/bin/cl.exe"
cp ~/bin/wine-vs2008/winecl-link "$SHADOW/bin/link.exe"
cp ~/bin/wine-vs2008/winecl-lib "$SHADOW/bin/lib.exe"
chmod +x "$SHADOW/bin/cl.exe" "$SHADOW/bin/link.exe" "$SHADOW/bin/lib.exe"CMAKE_MT/CMAKE_RC_COMPILER stay pointed at the plain ~/bin/wine-vs2008/winecl-{mt,rc} wrappers — only CMAKE_C_COMPILER/CMAKE_CXX_COMPILER/CMAKE_LINKER/CMAKE_AR need to live under the shadow, since only FindMSVC.cmake's regex cares.
Set for every cmake configure invocation (not needed at build time — these feed CMake's own find_path/find_library calls on the Linux host, not the compiler):
export PROGRAMFILES="$HOME/.local/share/wineprefixes/vs2008/drive_c/Program Files"
export WINSDK_DIR="$HOME/.local/share/wineprefixes/vs2008/drive_c/Program Files/Microsoft SDKs/Windows/v6.0A"
export DXSDK_DIR="$HOME/.local/share/wineprefixes/vs2008/drive_c/Program Files/Microsoft DirectX SDK (June 2010)"
export EXTERNAL_PATH="$HOME/.local/share/wineprefixes/vs2008/drive_c/2019q4_external_v90_x86/zlib"-
PROGRAMFILES—Find3dsMaxSDK.cmake's hint path. -
WINSDK_DIR— read byFindWindowsSDK.cmake'sUSE_CURRENT_WINSDK()macro (see bugs below). Must be a Linux path (notC:\...) since thesefind_pathcalls run natively on the Linux host, not through Wine. -
DXSDK_DIR—FindDirectXSDK.cmake's first hint (its hardcodedC:/Program Files/...fallback paths are useless on Linux). -
EXTERNAL_PATH— satisfiesCMakeModules/FindExternal.cmake's existence gate (needsinclude/zlib.hat the given root); pointed at thezlibsubfolder specifically since that one folder alone has that exact shape. Real per-library resolution comes fromCMAKE_PREFIX_PATHin the toolchain file below, not this — without it,FindExternal.cmakefalls back to matching plain/usr(this Linux host's own system zlib.h exists there), silently pollutingCMAKE_INCLUDE_PATH/CMAKE_LIBRARY_PATHwith headers that will never have a matching Windows.lib.
~/.local/share/wineprefixes/vs2008-toolchain.cmake — same base as the companion page's, extended:
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86)
# ryzomcore/CMakeModules/nel.cmake uses its own TARGET_CPU convention, not
# CMake's built-in CMAKE_SYSTEM_PROCESSOR - falls back to HOST_CPU (the
# build machine's arch, x86_64) when unset, which would make TARGET_X64
# true and point every Find*.cmake module (MAXSDK, DXSDK, MFC) at the
# 64-bit library directories.
set(TARGET_CPU x86 CACHE STRING "" FORCE)
# CMAKE_C/CXX_COMPILER live under the VC_DIR shadow directory (see above).
set(CMAKE_C_COMPILER $ENV{HOME}/.local/share/wineprefixes/vs2008-msvc-shadow/VC/bin/cl.exe)
set(CMAKE_CXX_COMPILER $ENV{HOME}/.local/share/wineprefixes/vs2008-msvc-shadow/VC/bin/cl.exe)
set(CMAKE_LINKER $ENV{HOME}/.local/share/wineprefixes/vs2008-msvc-shadow/VC/bin/link.exe)
set(CMAKE_AR $ENV{HOME}/.local/share/wineprefixes/vs2008-msvc-shadow/VC/bin/lib.exe)
set(CMAKE_MT $ENV{HOME}/bin/wine-vs2008/winecl-mt)
set(CMAKE_RC_COMPILER $ENV{HOME}/bin/wine-vs2008/winecl-rc)
# NEVER, not ONLY: ONLY restricts find_path/find_library to paths prefixed
# by CMAKE_FIND_ROOT_PATH, which we never set (empty) - with an empty root
# path, ONLY mode finds nothing at all, even with correct explicit HINTS
# (verified: only engages during a real project()-driven cross-compile,
# not in a bare `cmake -P` test, which is why an isolated repro missed it
# at first). We aren't doing genuine sysroot-based cross-compiling - every
# path we hand Find*.cmake modules is already an absolute concrete Linux
# path - so the root-path-prefixing behavior isn't wanted at all.
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
# /Zi triggers cl.exe's own PDB-writing path via mspdb80.dll, which is
# broken under Wine - see the companion page's Traps section. CMake's
# default MSVC flags always include /Zi for Debug/RelWithDebInfo, even for
# the plain "does this compiler work" sanity check run before
# CMAKE_BUILD_TYPE is consulted. The toolchain file runs before
# Platform/Windows-MSVC.cmake sets the CMAKE_*_FLAGS_<CONFIG>_INIT
# defaults, so patching those here is a no-op - instead force the final
# CACHE entries directly (with FORCE), so the platform module's later
# non-FORCE `set(... CACHE ...)` becomes a no-op and our /Z7 value sticks.
#
# /MDd (default Debug CRT) needs msvcr90d.dll/msvcp90d.dll, which live in
# the toolchain archive's VC/redist/Debug_NonRedist/ but aren't installed
# to system32 (only the release CRT is, via winetricks vcrun2008) -
# running an /MDd binary fails silently (exit 53, no output). /MD is also
# the semantically correct choice regardless: Max's own exporters were
# Release builds.
set(CMAKE_C_FLAGS_DEBUG "/MD /Z7 /Ob0 /Od /RTC1" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_DEBUG "/MD /Z7 /Ob0 /Od /RTC1" CACHE STRING "" FORCE)
set(CMAKE_C_FLAGS_RELWITHDEBINFO "/MD /Z7 /O2 /Ob1 /DNDEBUG" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "/MD /Z7 /O2 /Ob1 /DNDEBUG" CACHE STRING "" FORCE)
# Manifest embedding: winecl-link strips CMake's own /MANIFEST:EMBED,ID=1
# (see companion page) - these just avoid generating it in the first
# place for the cases that don't go through winecl-link's own filtering.
set(CMAKE_EXE_LINKER_FLAGS_INIT "/MANIFEST:NO")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "/MANIFEST:NO")
set(CMAKE_MODULE_LINKER_FLAGS_INIT "/MANIFEST:NO")
# The 2019q4_external_v90_x86 archive is one subfolder per library
# (zlib/include, zlib/lib, boost/include, boost/lib, ...), not the single
# unified include/+lib/ tree CMakeModules/FindExternal.cmake's generic
# EXTERNAL_PATH mechanism expects. CMAKE_PREFIX_PATH is the standard
# mechanism each individual FindZLIB/FindBoost/etc. module actually
# searches - every subfolder here becomes a hint root for its own
# find_path/find_library.
file(GLOB VS2008_EXTERNAL_DIRS "$ENV{HOME}/.local/share/wineprefixes/vs2008/drive_c/2019q4_external_v90_x86/*")
set(CMAKE_PREFIX_PATH ${VS2008_EXTERNAL_DIRS} CACHE STRING "" FORCE)Matching tool/quick_start/configure_targets.py's real PluginMax spec branch (NeLSpecPluginMax), for an MSVC/x86/non-Hunter target — read the script itself (GenerateCMakeOptions) rather than trust a summary; it's the single source of truth for what a real build actually passes, and doesn't match what the option names suggest at a glance (e.g. WITH_TOOLS, the root-level "Build Tools" option, is not part of this spec at all — WITH_NEL_MAXPLUGIN=ON alone already satisfies nel/CMakeLists.txt's IF(WITH_NEL_TOOLS OR WITH_NEL_MAXPLUGIN) gate):
cmake -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=~/.local/share/wineprefixes/vs2008-toolchain.cmake \
-DWITH_SSE2=OFF -DWITH_SSE3=OFF \
-DWITH_EXTERNAL=OFF -DWITH_STLPORT=OFF -DWITH_NEL_TESTS=OFF -DWITH_TESTING=OFF -DWITH_STATIC=ON \
-DWITH_FFMPEG=OFF -DWITH_MSQUIC=OFF -DWITH_QT=OFF -DWITH_QT5=OFF -DWITH_QT6=OFF \
-DWITH_STATIC_LIBXML2=OFF -DWITH_STATIC_CURL=OFF -DCURL_NO_CURL_CMAKE=ON \
-DFINAL_VERSION=ON \
-DWITH_LUA51=OFF -DWITH_LUA53=ON \
-DWITH_MFC=ON \
-DWITH_DRIVER_DIRECT3D=OFF -DWITH_DRIVER_DSOUND=OFF -DWITH_DRIVER_XAUDIO2=OFF \
-DWITH_DRIVER_OPENGL=ON -DWITH_DRIVER_OPENGL3=ON -DWITH_DRIVER_OPENAL=OFF \
-DWITH_RYZOM_CLIENT=OFF -DWITH_RYZOM_SERVER=OFF \
-DWITH_NEL_TOOLS=OFF -DWITH_RYZOM_TOOLS=OFF -DWITH_ASSIMP=OFF \
-DWITH_NEL_SAMPLES=OFF -DWITH_SNOWBALLS=OFF -DWITH_NELNS=OFF \
-DWITH_RYZOM=OFF -DWITH_NEL_MAXPLUGIN=ON \
-DMAXSDK_DIR="$HOME/.local/share/wineprefixes/vs2008/drive_c/Program Files/Autodesk/3ds Max 2010 SDK/maxsdk" \
/path/to/ryzomcoreNotes on deviations from the script's full generality:
-
WITH_DRIVER_DIRECT3D/WITH_DRIVER_DSOUNDareONin the realPluginMaxspec (any spec other thanServerwith aDirectXSDKpath set gets them). LeftOFFhere — untested scope-reduction for a narrower "does plugin_max build" goal, not a correctness fix; flip them on and add the Direct3D driver's own build to fully match. -
WITH_DRIVER_OPENAL=OFFworks around a genuine pre-existing bug innel/src/sound/driver/openal/CMakeLists.txt(mixed keyword/plaintarget_link_librariessignatures for the same target — invalid regardless of platform, just never exercised because this driver isn't normally part of a plugin_max-only build path). Not something this Wine setup caused. -
WITH_STATIC_CURLisn't an actual option anywhere in this codebase's CMake — CMake warns it's unused. Harmless; kept for fidelity with the real script.
Two real, pre-existing defects in CMakeModules/FindWindowsSDK.cmake's USE_CURRENT_WINSDK() macro, surfaced only because this is (as far as anyone can tell) the first time this exact code path has run without a Windows registry to short-circuit past it — on real Windows, VS2008's SDK registers in the registry, so DETECT_WINSDK_VERSION_HELPER succeeds and this macro's fallback logic never runs. Both fixed locally (not yet upstreamed/committed as of writing):
Bug 1 — SET(WINSDK_DIR "") defeats the subsequent find_path(). A plain (non-cache) set() creates a normal variable that shadows the cache entry of the same name; find_path() then treats the mere existence of that normal variable (even empty) as "already resolved" and silently skips its own search — confirmed in complete isolation, independent of any cross-compiling or root-path setting:
# Reproduces with or without a pre-existing valid CACHE value for MYVAR:
set(MYVAR "")
find_path(MYVAR Windows.h HINTS /real/path/with/Include)
# MYVAR stays emptyFix: UNSET(WINSDK_DIR CACHE) instead of SET(WINSDK_DIR "").
Bug 2 — the find_path target was wrong, once bug 1 stopped masking it. FIND_PATH(WINSDK_DIR Windows.h HINTS ${WINSDKENV_DIR}/Include/um ${WINSDKENV_DIR}/Include) searches for a bare filename with the hints already pointing inside Include/ — find_path returns the directory containing the found file, so WINSDK_DIR ends up as .../Include itself, not the SDK root the rest of the macro (and everything downstream) expects (which then appends /Include again, doubling the path). Fix: search for the nested path Include/Windows.h from the candidate root instead — the same pattern Find3dsMaxSDK.cmake (include/maxversion.h) and FindDirectXSDK.cmake (Include/dxsdkver.h) already use correctly:
FIND_PATH(WINSDK_DIR Include/Windows.h HINTS ${WINSDKENV_DIR})
IF(NOT WINSDK_DIR)
FIND_PATH(WINSDK_DIR Include/um/Windows.h HINTS ${WINSDKENV_DIR}) # newer Windows Kits layout
ENDIF()A third apparent blocker — error C2719: formal parameter with __declspec(align('16')) won't be aligned on std::vector<CBone> (VS2008's STL takes resize()'s fill value by value, which 32-bit MSVC can't do for an SSE-aligned type; confirmed even a bare default-constructed std::vector<AlignedType> member triggers it, no specific call site needed) — turned out not to be a bug at all: tool/quick_start/configure_targets.py already forces -DWITH_SSE2=OFF -DWITH_SSE3=OFF for any x86 target, which makes NL_ALIGN_SSE2 a no-op and sidesteps the whole class of issue. The fix was matching that existing convention, not patching NeL's alignment macros or the vendor STL.
The same toolchain (all of the setup above: archives, symlink farm, VC_DIR shadow, environment variables, toolchain file) also builds the standalone pipeline_max export tools — the .max-processing reimplementation that runs the Max export pipeline without 3ds Max. This became possible once those tools stopped depending on libgsf (a Linux-only OLE reader) and got a native cross-platform OLE backend. Building them under VS2008 gives an x87 / no-SSE2 reference matching the Max 2010 that produced the corpus's reference exports, which is the instrument for chasing the remaining float-precision residual (see the design doc's §2b VS2008 x87 reference build for the porting details, the two latent bugs it surfaced — an empty-std::list dereference caught by MSVC's checked iterators, and a SuperClassId static-initialization-order fiasco — and the measured precision improvement).
Use a separate build dir next to the plugin one (both are full NeL builds under Wine — keep them; a clean rebuild is ~500 objects). The configure differs from the plugin spec by turning the plugin off and the tools + 3D + native OLE on; no Max SDK is needed:
cmake -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=~/.local/share/wineprefixes/vs2008-toolchain.cmake \
-DWITH_SSE2=OFF -DWITH_SSE3=OFF \
-DWITH_EXTERNAL=OFF -DWITH_STLPORT=OFF -DWITH_NEL_TESTS=OFF -DWITH_TESTING=OFF -DWITH_STATIC=ON \
-DWITH_FFMPEG=OFF -DWITH_MSQUIC=OFF -DWITH_QT=OFF -DWITH_QT5=OFF -DWITH_QT6=OFF \
-DWITH_STATIC_LIBXML2=OFF -DWITH_STATIC_CURL=OFF -DCURL_NO_CURL_CMAKE=ON \
-DFINAL_VERSION=ON -DWITH_LUA51=OFF -DWITH_LUA53=ON -DWITH_MFC=ON \
-DWITH_DRIVER_DIRECT3D=OFF -DWITH_DRIVER_DSOUND=OFF -DWITH_DRIVER_XAUDIO2=OFF \
-DWITH_DRIVER_OPENGL=ON -DWITH_DRIVER_OPENGL3=ON -DWITH_DRIVER_OPENAL=OFF \
-DWITH_RYZOM_CLIENT=OFF -DWITH_RYZOM_SERVER=OFF -DWITH_RYZOM_TOOLS=OFF -DWITH_ASSIMP=OFF \
-DWITH_NEL_SAMPLES=OFF -DWITH_SNOWBALLS=OFF -DWITH_NELNS=OFF -DWITH_RYZOM=OFF \
-DWITH_NEL_MAXPLUGIN=OFF -DWITH_NEL_TOOLS=ON -DWITH_3D=ON \
-DWITH_PIPELINE_NATIVE_OLE=ON -DWITH_LIBGSF=OFF \
/path/to/ryzomcoreTargets are .exe (ninja pipeline_max_export_zone.exe, …_export_shape.exe, pipeline_max_corpus_test.exe, …). Running an exe needs the release external DLLs (libxml2/jpeg62/libpng16/zlib/freetype/…) from 2019q4_external_v90_x86/*/bin/ on its search path (copy them next to the exe) and Windows-form path arguments (winepath -w); a thin per-tool wrapper that does both lets the x64 corpus drivers (zone_corpus.py, etc.) drive the VS2008 binaries via --bin.
- Visual C++ 2008 build environment on Linux using Wine — the base toolchain setup this page extends
- NeL 3ds Max Plugins — compatibility table showing which Visual C++ version each plugin build targets