Skip to content

vs2008_wine_pluginmax_build

Jan Boon edited this page Jul 7, 2026 · 2 revisions

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_max and friends), not just standalone probes. {.is-info}

Verified 2026-07-07: ninja -j4 completed 1008/1008 objects, exit code 0, zero FAILED lines — 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.dlx all built, plus the static NeL libs (nel3d_r.lib, nelmisc_r.lib, nelpacs_r.lib, nelgui_r.lib, ...). {.is-info}

Archives needed

Beyond the base VS2008 toolchain archive from the companion page, extracted into the same prefix's drive_c:

  • Max 2010 SDKProgram Files/Autodesk/3ds Max 2010 SDK/ (top-level archive folder name matches). Find3dsMaxSDK.cmake looks for maxsdk/include/maxversion.h under $ENV{PROGRAMFILES}/Autodesk/3ds Max <year> SDK/maxsdk.
  • Externals (2019q4_external_v90_x86, matching nel_plugin_max_9_x86.bat/_2010_x86.bat/_2012_x86.bat's redist paths) → C:\2019q4_external_v90_x86 directly (not under Program 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.

Case-insensitivity: the symlink farm

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 issue

Instead, 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"
done

This 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.

VC_DIR shadow directory

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.

Environment variables

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"
  • PROGRAMFILESFind3dsMaxSDK.cmake's hint path.
  • WINSDK_DIR — read by FindWindowsSDK.cmake's USE_CURRENT_WINSDK() macro (see bugs below). Must be a Linux path (not C:\...) since these find_path calls run natively on the Linux host, not through Wine.
  • DXSDK_DIRFindDirectXSDK.cmake's first hint (its hardcoded C:/Program Files/... fallback paths are useless on Linux).
  • EXTERNAL_PATH — satisfies CMakeModules/FindExternal.cmake's existence gate (needs include/zlib.h at the given root); pointed at the zlib subfolder specifically since that one folder alone has that exact shape. Real per-library resolution comes from CMAKE_PREFIX_PATH in the toolchain file below, not this — without it, FindExternal.cmake falls back to matching plain /usr (this Linux host's own system zlib.h exists there), silently polluting CMAKE_INCLUDE_PATH/CMAKE_LIBRARY_PATH with headers that will never have a matching Windows .lib.

Toolchain file

~/.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)

The CMake configure command

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/ryzomcore

Notes on deviations from the script's full generality:

  • WITH_DRIVER_DIRECT3D/WITH_DRIVER_DSOUND are ON in the real PluginMax spec (any spec other than Server with a DirectXSDK path set gets them). Left OFF here — 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=OFF works around a genuine pre-existing bug in nel/src/sound/driver/openal/CMakeLists.txt (mixed keyword/plain target_link_libraries signatures 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_CURL isn't an actual option anywhere in this codebase's CMake — CMake warns it's unused. Harmless; kept for fidelity with the real script.

Genuine upstream bugs found (independent of Wine)

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 empty

Fix: 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.

See also

Clone this wiki locally