Companion page to 3ds Max 9 on Linux using Wine, which covers the usual prefix conventions. This page covers the command-line toolchain only (
cl.exe,link.exe,lib.exe,nmake.exe) — not the Visual Studio IDE, which is not worth fighting under Wine.
Why this exists: the
~/pipeline_exportreference outputs were produced by 3ds Max 2010, a VS2008 x86 build (see the provenance note in the pipeline_max design document, §10i). Reproducing the reference exporter's float arithmetic bit-for-bit therefore means VS2008 x86 codegen. This environment is for building small kernel probe harnesses (decompose/normal/vertex-path kernels compiled with the same compiler, run under Wine, outputs compared against the headless x64 exporters) and, if ever needed, theplugin_maxbinaries themselves.
Ubuntu's own repos are enough — no WineHQ PPA required. wine pulls in both wine64 and wine32:i386 (i.e. 32-bit support) automatically:
sudo dpkg --add-architecture i386 # skip if `dpkg --print-foreign-architectures` already lists i386
sudo apt update
sudo apt install -y wine winetricks xvfb
xvfb is needed on any box with no $DISPLAY (plain ssh, CI, tty-only session) — some Wine/winetricks component installers try to open a window even when silenced, and hang without a virtual display to open it on.
The default
~/.wineprefix defaults to 64-bit on a 64-bit host, and there is no in-place conversion afterward (only delete-and-redo). If~/.winedoesn't exist yet, make sure the firstwine/winebootinvocation ever run on the box setsWINEARCH=win32explicitly, even if you only ever intend to use the dedicatedvs2008prefix below — e.g.:WINEARCH=win32 WINEDLLOVERRIDES="mscoree,mshtml=" xvfb-run -a wineboot -iVerify with
grep '#arch' ~/.wine/system.reg— should read#arch=win32.
Same conventions as the Max 9 page: a dedicated 32-bit prefix under ~/.local/share/wineprefixes/. Unlike ~/.wine, Wine will not create missing parent directories for a custom WINEPREFIX — create the directory yourself first, or the first command below fails with chdir to ... No such file or directory.
Headless / scripted setup — winetricks' unattended mode makes the whole prefix reproducible from a script. Every command sets its own env vars inline (VAR=val command) rather than export, so nothing persists to the shell — repeat the WINEARCH/WINEPREFIX pair on every command that touches this prefix:
mkdir -p ~/.local/share/wineprefixes/vs2008/
WINEARCH=win32 WINEPREFIX=~/.local/share/wineprefixes/vs2008/ WINEDLLOVERRIDES="mscoree,mshtml=" xvfb-run -a wineboot -i
WINEARCH=win32 WINEPREFIX=~/.local/share/wineprefixes/vs2008/ xvfb-run -a winetricks -q vcrun2008 msxml6
WINEARCH=win32 WINEPREFIX=~/.local/share/wineprefixes/vs2008/ W_OPT_UNATTENDED=1 xvfb-run -a winetricks -q dotnet20
Verify with grep '#arch' ~/.local/share/wineprefixes/vs2008/system.reg (expect #arch=win32), and confirm vcrun2008/msxml6 landed: ls ~/.local/share/wineprefixes/vs2008/drive_c/windows/system32/{msvcr90,msxml6}.dll.
A wall of err:ole:StdMarshalImpl_MarshalInterface/err:mscoree:LoadLibraryShim ... registry key for installroot noise during wineboot -i and the vcrun2008 install is expected and harmless — RPC/OLE services and .NET detection aren't available in a minimal headless prefix, and neither is needed here.
dotnet20 (real .NET 2.0) is what makes mt.exe work: the SDK's manifest tool instantiates the .NET CorMetaDataDispenser COM object even for plain native manifests, and without a CLR metadata engine behind mscoree it dereferences a NULL interface and page-faults. Root-caused by elimination (2026-08-05): native imagehlp/dbghelp don't help, wine-mono doesn't either (it never implemented the unmanaged metadata API) — only the real .NET 2.0 runtime does. With it, mt embeds manifests correctly and the traditional VS2008 manifest flow works end to end (see Manifest embedding below).
Interactive alternative (needs a display): drop the xvfb-run -a/-q/WINEDLLOVERRIDES and run winecfg/winetricks directly; in Winetricks, Install a Windows DLL or component → check vcrun2008 (the CRT the produced binaries and the toolchain itself expect), msxml6 (some SDK tools want it; harmless to have) and dotnet20 (for mt.exe, above).
Route A — copy an existing install (recommended, and the route this page assumes). The compiler does not need registry state; a working install tree is enough. From the existing local VS2008 install, copy into the prefix's drive_c:
Program Files/Microsoft Visual Studio 9.0/VC/ — bin/, include/, lib/, atlmfc/include/ + atlmfc/lib/ (the MFC tools need them), and redist/x86/ (VC90 CRT/MFC assemblies, for packaging and for running /MD binaries). x86 only; bin/amd64 etc. are irrelevant here.Program Files/Microsoft Visual Studio 9.0/Common7/IDE/ — cl.exe loads mspdb80.dll (and friends msobj80.dll, mspdbcore.dll) from here; either keep the directory and add it to PATH, or copy those DLLs next to cl.exe. Same install as VC/bin — mixed service-pack trees give C1902 (see Troubleshooting). Prefer SP1 (cl 15.00.30729.01) throughout: it is what the era's reference binaries were built with.Program Files/Microsoft SDKs/Windows/v6.0A/ — Include/, Lib/, and Bin/ (RC.Exe + RCDLL.dll, mt.exe). Bootstrapper/, Samples/ and the x64/IA64 Bin subdirectories are dead weight — skip them.If the local install already lives in a Wine prefix (e.g. it was used to build plugin_max), point the environment below at that prefix instead — nothing more to do.
Route B — run the installer under Wine. Known-troublesome: the VS2008 setup chain wants Windows Installer sequencing and .NET 3.5 (winetricks dotnet35 in a win32 prefix, slow and fragile), and partial-failure states are common. If the installer must be used, deselect everything except Visual C++ and expect to iterate. Route A is strictly less pain when any Windows machine or existing install is available.
Wine maps the Unix environment into the Windows process environment, so vcvars32.bat is unnecessary — export the three variables from the shell. Keep a wrapper script, e.g. ~/bin/winecl:
#!/bin/sh
export WINEPREFIX=~/.local/share/wineprefixes/vs2008/
VS='C:\Program Files\Microsoft Visual Studio 9.0'
SDK='C:\Program Files\Microsoft SDKs\Windows\v6.0A'
export INCLUDE="$VS\\VC\\include;$SDK\\Include"
export LIB="$VS\\VC\\lib;$SDK\\Lib"
export WINEPATH="$VS\\VC\\bin;$VS\\Common7\\IDE"
export WINEDEBUG=-all
exec wine "$VS\\VC\\bin\\cl.exe" "$@"
(WINEPATH appends to the Windows PATH; it is how cl.exe finds mspdb80.dll in Common7/IDE without copying DLLs around.)
Smoke test:
printf '#include <stdio.h>\nint main(){printf("%%a\\n", 1.0f/3.0f);return 0;}\n' > /tmp/t.c
winecl /nologo /O2 /Z7 /Fet.exe t.c /link /DEBUG:NONE /INCREMENTAL:NO # run from the source directory
wine t.exe
Expect 0x1.555556p-2 (the hex bit pattern of 1.0f/3.0f).
/Z7, never /Zi. /Zi spawns mspdbsrv.exe (the PDB server process), which is unreliable under Wine and hangs builds. /Z7 embeds the debug info in the object files and never starts it. For probe harnesses just omit debug info entirely.LINK : fatal error LNK1101: incorrect MSPDB80.DLL version. Not actually a DLL version mismatch (verified: cl.exe /c alone always succeeds; the DLLs can be internally consistent and still hit this) — it's link.exe's use of mspdb80.dll for its own PDB-based incremental-link/debug bookkeeping breaking under Wine. Triggered by /DEBUG on the link line, which /Z7 (or /Zi) adds implicitly even though /Z7 needs no separate .pdb. Fix: pass /link /DEBUG:NONE /INCREMENTAL:NO — /INCREMENTAL:NO alone is not sufficient, /DEBUG:NONE is the switch that actually matters. Confirmed harmless to the object code: the resulting .exe still runs and produces correct output, /Z7's debug info stays embedded in the .obj regardless of what the linker does with /DEBUG. Re-verified 2026-08-05 on a pristine WinXP-sourced toolchain copy — this is permanent Wine behavior, not a damaged-DLL artifact; the winecl-link wrapper appends both switches unconditionally.cl.exe records no header dependencies for the PCH object itself, so after editing a header the stale .pch silently replays the old header contents into every TU that uses it (symptoms range from "my change has no effect" to inexplicable errors). After any shared-header edit, find <builddir> -name '*.pch' -delete (plus the cmake_pch.cxx.obj files) and rebuild.RC.Exe rejects /nologo (fatal error RC1106: invalid option: -ologo) — the SDK v6.0A resource compiler predates the flag; just don't pass it./Fe:name.exe (colon-attached output name) silently miscompiles the filename — under this Wine setup it writes to a file literally named :name.exe (leading colon), not name.exe. Use the older no-colon form /Fename.exe instead; only the output-filename switch is affected.cl : Command line error D8004 : '/FI' requires an argument. /FI<path> (force-include, e.g. CMake's PCH mechanism) is stricter than /Fo<path>//I<path> about a glued value starting with / — the translate_args wrapper below handles bare tokens but needs a glued-prefix case to also cover this one.#include <WinDef.h> with arbitrary casing, and — worse — vendor SDK headers sometimes #include each other with inconsistent casing (two different casings of the same file defeat #pragma once's file-identity tracking, silently double-processing the header — C2011: class type redefinition is the symptom). ciopfs (case-insensitive FUSE overlay) looked like the fix but turned out to be broken on a stock Ubuntu 24.04 box (doesn't list any subdirectory, even space-free ones, in an isolated test) — a lowercase-alias symlink farm across the SDK trees is the practical fix; see Building ryzomcore's 3ds Max plugin (PluginMax) under Wine for the script.cl @args.rsp); prefer them over heroic shell quoting of \-ridden paths.cl /MP works, but each cl is a Wine process — for the small probe harnesses this page targets, plain serial compiles are simpler and fast enough.To match the Max 2010-era plugin codegen, compile probes the way the plugin was built:
/O2 /fp:precise (VS2008 defaults for release; no /arch:SSE* — the x87 blended default is the point of the exercise)_PC_53); a probe that wants Max-identical behavior should leave it alone (do not call _controlfp), since the reference ran with the CRT default./SUBSYSTEM:CONSOLE; dump results as text (hex float bits, printf("%08x") on the bit pattern) so outputs diff cleanly against the Linux x64 exporters' dumps.decomp_affine and the other decomp.h/geom utilities are exports of Max's own geom.dll (import library geom.lib in the Max 2010 SDK) — they are Autodesk-compiled code, not headers, which is exactly why no recompilation on the Linux side can reproduce their bits. A probe exe that links geom.lib and sits next to Max 2010's geom.dll (plus its direct dependencies; check with winedump -j import geom.dll) runs the actual reference decompose. That is the scoped experiment for the DefaultRotQuat/Scale byte-identity question (design doc §10i). Deeper Max internals (Mesh::buildRenderNormals in mesh.dll) pull progressively more of the Max runtime as dependencies — feasibility drops fast; the angle-weighted replication in pipeline_max_export_shape plus its 1e-4 verdict tier is the accepted answer there.
Verified end-to-end (2026-07-07): CMake's Ninja generator can drive this Wine-hosted cl.exe/link.exe directly — cmake -G Ninja configures, ninja builds, and the resulting .exe runs under wine and produces correct output, no manual winecl invocation needed. Four Wine-specific issues had to be worked around, on top of the raw-invocation traps above; all four are baked into the toolchain file and wrapper scripts below rather than left as manual steps.
The wrapper scripts and toolchain file live in the ryzomcore repo: tool/wine_vs2008/ — that directory is the canonical, maintained copy (this page used to inline hand-rolled ~/bin/wine-vs2008 versions; the repo layer supersedes them):
winecl-env — shared environment: prefix location (parameterized via NL_WINE_VS2008_PREFIX, default $HOME/toolchain_v90_prefix), INCLUDE/LIB/WINEPATH, and translate_args, which converts absolute Unix-path arguments to Windows form. MSVC tools treat a leading / as a switch marker, so a bare Unix path (e.g. the source file CMake's ABI check passes) gets misparsed as an option; glued-prefix forms (/FI/abs/path) need the same treatment.winecl-cc, winecl-lib, winecl-rc — thin cl.exe/lib.exe/RC.Exe invocations through translate_args.winecl-link — additionally strips /MANIFEST:EMBED* (CMake's vs_link helper emits that VS2010+ syntax for MSVC+Ninja; VS2008's link.exe fails with LNK1117) and appends /DEBUG:NONE /INCREMENTAL:NO (the LNK1101 trap above).winecl-mt — translates -manifest and -outputresource: path arguments unconditionally (mt parses any leading-/ argument as an option, and the paths involved may not exist yet or anymore at wrapper time, so the generic existence-checked translation is not enough). Requires the prefix's .NET 2.0 (see above).setup.sh — one-shot prefix preparation: dosdevices links, the VC_DIR shadow directory, the lowercase-alias symlink farm, and the Max SDK header patch (all described on the PluginMax page).toolchain.cmake — the CMake toolchain file: shadow-dir compiler paths, TARGET_CPU x86, find-root modes NEVER, the /Z7+/MD per-config flag overrides (/Zi breaks under Wine, see Traps; /MDd binaries fail silently since only the release CRT is installed), CMAKE_PREFIX_PATH from the per-library externals tree, and NL_EMBED_SXS_MANIFEST_MT (manifest flow below).The environment expects the prefix at $HOME/toolchain_v90_prefix by default; export NL_WINE_VS2008_PREFIX=~/.local/share/wineprefixes/vs2008 (or wherever the prefix from this page lives) before configuring and building.
With .NET 2.0 in the prefix, mt.exe works and binaries get their VC90 SxS manifests through the traditional VS2008 three-stage flow: link /MANIFEST writes a <output>.manifest sidecar, and an mt pass embeds it as the RT_MANIFEST resource (id 1 for EXEs, id 2 for DLLs). The linker-generated manifest is exact per target — CRT and, when linked, MFC dependent assemblies. Who runs the mt pass depends on the CMake generation:
--msvc-ver to its vs_link tool, detects the pre-VS2010 linker, and runs the classic pass itself. The winecl-mt unconditional path translation is what makes its invocation survive Wine./MANIFEST:EMBED linker syntax (stripped by winecl-link) and never invokes mt; NL_EMBED_SXS_MANIFEST_MT in CMakeModules/nel.cmake fills the gap with a POST_BUILD tool/wine_vs2008/embed_manifest.sh pass, which treats an already-consumed sidecar as success so both generations coexist.NL_EMBED_SXS_MANIFEST_RC (a resource-compiled static manifest) remains as fallback for prefixes without working mt. Without any embedded manifest, /MD binaries fail on real Windows with LoadLibrary error 126 — the CRT only resolves through WinSxS there.
Usage, on any trivial CMakeLists.txt + Ninja generator:
export NL_WINE_VS2008_PREFIX=~/.local/share/wineprefixes/vs2008
cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=/path/to/ryzomcore/tool/wine_vs2008/toolchain.cmake /path/to/src
ninja
wine ./hello.exe
CMake's compiler-ID detection (The C compiler identification is MSVC 15.0.30729.1) and ABI detection both work transparently through the wrapper — no special-casing needed there, since try_compile's marker-string check just inspects the compiled object, and only actually running a cross-compiled binary would need CMAKE_CROSSCOMPILING_EMULATOR (not set up here, not needed for configure/build).
The repo toolchain file sets all three
CMAKE_FIND_ROOT_PATH_MODE_*toNEVER.ONLYwith an emptyCMAKE_FIND_ROOT_PATH(an earlier iteration of this setup) makesfind_path/find_libraryfind nothing at all, even with correct explicitHINTS— and only during a genuineproject()-driven cross-compile, not in a barecmake -Ptest, which made it easy to miss. Nothing here is sysroot-style cross-compiling: every path handed to the Find modules is already a concrete Linux path.
Pointing this at the real ryzomcore build: see Building ryzomcore's 3ds Max plugin (PluginMax) under Wine — verified end-to-end, covers the additional environment variables, the VC_DIR shadow-directory trick FindMSVC.cmake needs, the case-insensitivity symlink farm the Max SDK/Windows SDK/externals need, the exact CMake flags matching tool/quick_start/configure_targets.py's real PluginMax spec, and two genuine upstream CMakeModules/FindWindowsSDK.cmake bugs found along the way.
mspdb80.dll not found — verify WINEPATH includes Common7\IDE, or copy mspdb80.dll, mspdbcore.dll, msobj80.dll next to cl.exe.
Mixed mspdb* DLL versions between VC/bin and Common7/IDE (e.g. trees copied from different service-pack levels). Copy both directories from the same install.
LIB doesn't reach the Windows SDK Lib directory — the VC lib directory alone only has the CRT.