vs2008_linux_wine - ryzom/ryzomcore GitHub Wiki
title: Visual C++ 2008 build environment on Linux using Wine description: Running the VS2008 x86 command-line toolchain (cl/link) in a 32-bit Wine prefix โ for plugin_max builds and pipeline_max reference-arithmetic probes published: true date: 2026-08-06T00:00:00.000Z tags: editor: markdown dateCreated: 2026-07-07T00:00:00.000Z
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. {.is-info}
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. {.is-info}
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 xvfbxvfb 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. {.is-warning}
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 dotnet20Verify 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), andredist/x86/(VC90 CRT/MFC assemblies, for packaging and for running/MDbinaries). x86 only;bin/amd64etc. are irrelevant here. -
Program Files/Microsoft Visual Studio 9.0/Common7/IDE/โcl.exeloadsmspdb80.dll(and friendsmsobj80.dll,mspdbcore.dll) from here; either keep the directory and add it toPATH, or copy those DLLs next tocl.exe. Same install asVC/binโ mixed service-pack trees giveC1902(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/, andBin/(RC.Exe+RCDLL.dll,mt.exe).Bootstrapper/,Samples/and the x64/IA64Binsubdirectories 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.exeExpect 0x1.555556p-2 (the hex bit pattern of 1.0f/3.0f).
-
/Z7, never/Zi./Zispawnsmspdbsrv.exe(the PDB server process), which is unreliable under Wine and hangs builds./Z7embeds 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 /calone always succeeds; the DLLs can be internally consistent and still hit this) โ it'slink.exe's use ofmspdb80.dllfor its own PDB-based incremental-link/debug bookkeeping breaking under Wine. Triggered by/DEBUGon the link line, which/Z7(or/Zi) adds implicitly even though/Z7needs no separate.pdb. Fix: pass/link /DEBUG:NONE /INCREMENTAL:NOโ/INCREMENTAL:NOalone is not sufficient,/DEBUG:NONEis the switch that actually matters. Confirmed harmless to the object code: the resulting.exestill runs and produces correct output,/Z7's debug info stays embedded in the.objregardless 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; thewinecl-linkwrapper appends both switches unconditionally. -
Stale precompiled headers after header edits. ninja's dependency tracking through the Wine-hosted
cl.exerecords no header dependencies for the PCH object itself, so after editing a header the stale.pchsilently 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 thecmake_pch.cxx.objfiles) and rebuild. -
RC.Exerejects/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), notname.exe. Use the older no-colon form/Fename.exeinstead; 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/โ thetranslate_argswrapper below handles bare tokens but needs a glued-prefix case to also cover this one. -
Case sensitivity. Windows-authored sources
#include <WinDef.h>with arbitrary casing, and โ worse โ vendor SDK headers sometimes#includeeach 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 redefinitionis 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. -
Response files and long command lines work fine (
cl @args.rsp); prefer them over heroic shell quoting of\-ridden paths. -
Parallelism:
cl /MPworks, but eachclis 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) - The CRT initializes the x87 control word to 53-bit precision (
_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. - Link
/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 viaNL_WINE_VS2008_PREFIX, default$HOME/toolchain_v90_prefix),INCLUDE/LIB/WINEPATH, andtranslate_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โ thincl.exe/lib.exe/RC.Exeinvocations throughtranslate_args. -
winecl-linkโ additionally strips/MANIFEST:EMBED*(CMake'svs_linkhelper emits that VS2010+ syntax for MSVC+Ninja; VS2008'slink.exefails withLNK1117) and appends/DEBUG:NONE /INCREMENTAL:NO(the LNK1101 trap above). -
winecl-mtโ translates-manifestand-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:dosdeviceslinks, theVC_DIRshadow 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 modesNEVER, the/Z7+/MDper-config flag overrides (/Zibreaks under Wine, see Traps;/MDdbinaries fail silently since only the release CRT is installed),CMAKE_PREFIX_PATHfrom the per-library externals tree, andNL_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:
-
CMake 3.30+ forwards
--msvc-verto itsvs_linktool, detects the pre-VS2010 linker, and runs the classic pass itself. Thewinecl-mtunconditional path translation is what makes its invocation survive Wine. -
Older CMake assumes the VS2010+
/MANIFEST:EMBEDlinker syntax (stripped bywinecl-link) and never invokes mt;NL_EMBED_SXS_MANIFEST_MTinCMakeModules/nel.cmakefills the gap with a POST_BUILDtool/wine_vs2008/embed_manifest.shpass, 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.exeCMake'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. {.is-warning}
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.
- Pipeline Max โ Design and Coherency Contract, ยง10i for the provenance note this environment exists to address
- NeL 3ds Max Plugins โ compatibility table showing which Visual C++ version each plugin build targets
- 3ds Max 9 on Linux using Wine โ the companion page for the Max 9 trial install and general Wine prefix conventions