-
Notifications
You must be signed in to change notification settings - Fork 113
vs2008_linux_wine
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-07-07T00: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 msxml6Verify 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.
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) and msxml6 (some SDK tools want it; harmless to have). The .NET components are only needed if you attempt the full installer (below) — the toolchain itself does not use .NET.
vcrun2008/msxml6 are reliable unattended either way; the dotnet35 verb (Route B only) is slow and flaky under -q, one more reason to prefer Route A.
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/(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 -
Program Files/Microsoft SDKs/Windows/v6.0A/—Include/andLib/(the Windows SDK headers/libs VS2008 shipped with)
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. -
/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.
Wrapper scripts — ~/bin/wine-vs2008/winecl-{cc,link,lib,mt,rc}, sharing ~/bin/wine-vs2008/winecl-env:
# winecl-env (sourced by the others)
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;$VS\\VC\\atlmfc\\include;$SDK\\Include"
export LIB="$VS\\VC\\lib;$VS\\VC\\atlmfc\\lib;$SDK\\Lib"
export WINEPATH="$VS\\VC\\bin;$VS\\Common7\\IDE"
export WINEDEBUG=-all
# Translate absolute Unix-path arguments to Windows form. MSVC's cl.exe/
# link.exe treat a leading '/' as a switch marker, so an absolute Unix path
# passed as a standalone token (e.g. the bare source file CMake's ABI-check
# passes) gets misparsed as an unrecognized switch. Paths glued directly
# after most recognized prefixes (/Fo<path>, /I<path>, ...) are unaffected
# since cl.exe only inspects the leading characters of the token for its
# prefix, not any '/' that appears deeper in - but /FI (force-include) is
# stricter and rejects a glued value that itself starts with '/' ("/FI
# requires an argument", confirmed empirically against CMake's PCH command
# line) - so glued-prefix forms need the same translation as bare tokens,
# just with the short alpha prefix preserved and only the path part (from
# the second '/' onward) converted.
translate_args() {
ARGS=()
for a in "$@"; do
case "$a" in
/*)
if [ -e "$a" ]; then
ARGS+=("$(winepath -w "$a" 2>/dev/null)")
elif [[ "$a" =~ ^(/[A-Za-z]{1,4})(/.*)$ ]] && [ -e "${BASH_REMATCH[2]}" ]; then
ARGS+=("${BASH_REMATCH[1]}$(winepath -w "${BASH_REMATCH[2]}" 2>/dev/null)")
else
ARGS+=("$a")
fi
;;
*)
ARGS+=("$a")
;;
esac
done
}# winecl-cc
#!/bin/bash
. "$(dirname "$0")/winecl-env"
translate_args "$@"
exec wine "C:\\Program Files\\Microsoft Visual Studio 9.0\\VC\\bin\\cl.exe" "${ARGS[@]}"# winecl-link
#!/bin/bash
. "$(dirname "$0")/winecl-env"
translate_args "$@"
# CMake's own `cmake -E vs_link_exe --manifests` helper unconditionally
# appends /MANIFEST:EMBED,ID=1 for MSVC+Ninja regardless of target MSVC
# version - that syntax postdates VS2008's link.exe (LNK1117 syntax
# error). Manifest embedding doesn't matter for these CLI tool/plugin
# builds, so strip it rather than fight CMake's helper or touch the
# project's own (deliberately version-general) linker flags.
FILTERED=()
for a in "${ARGS[@]}"; do
case "$a" in
/MANIFEST:EMBED*) ;;
*) FILTERED+=("$a") ;;
esac
done
exec wine "C:\\Program Files\\Microsoft Visual Studio 9.0\\VC\\bin\\link.exe" "${FILTERED[@]}" /MANIFEST:NO /DEBUG:NONE /INCREMENTAL:NOwinecl-lib, winecl-mt, winecl-rc follow winecl-cc's simple pattern, pointing at lib.exe/mt.exe/RC.Exe respectively (mt.exe/RC.Exe live under the Windows SDK Bin/, not VC/bin/).
No xvfb-run needed for any of these — only the initial wineboot -i/winetricks setup needs a virtual display; plain compiler/linker invocations run fine without one.
Toolchain file — ~/.local/share/wineprefixes/vs2008-toolchain.cmake:
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86)
set(CMAKE_C_COMPILER $ENV{HOME}/bin/wine-vs2008/winecl-cc)
set(CMAKE_CXX_COMPILER $ENV{HOME}/bin/wine-vs2008/winecl-cc)
set(CMAKE_LINKER $ENV{HOME}/bin/wine-vs2008/winecl-link)
set(CMAKE_AR $ENV{HOME}/bin/wine-vs2008/winecl-lib)
set(CMAKE_MT $ENV{HOME}/bin/wine-vs2008/winecl-mt)
set(CMAKE_RC_COMPILER $ENV{HOME}/bin/wine-vs2008/winecl-rc)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
# /Zi triggers cl.exe's own PDB-writing path via mspdb80.dll, which is
# broken under Wine (C1902 at compile time, LNK1101 at link time) - not a
# DLL version mismatch. 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 goes through mt.exe's /notify_update incremental
# contract, which signals via specific exit codes CMake expects - our
# wine-wrapped mt.exe doesn't reproduce that contract and the merge step
# fails (separately from the /Zi issue above). Irrelevant for CLI
# tool/probe binaries anyway - disable it outright rather than debug
# mt.exe's exit-code semantics under Wine.
set(CMAKE_EXE_LINKER_FLAGS_INIT "/MANIFEST:NO")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "/MANIFEST:NO")
set(CMAKE_MODULE_LINKER_FLAGS_INIT "/MANIFEST:NO")Usage, on any trivial CMakeLists.txt + Ninja generator:
cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=~/.local/share/wineprefixes/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).
CMAKE_FIND_ROOT_PATH_MODE_LIBRARY/_INCLUDEare set toONLYabove, which is fine for this trivial standalone-probe example (nofind_packagecalls to trip it), but is a real trap for anything usingFind*.cmakemodules: withCMAKE_FIND_ROOT_PATHempty (never set here),ONLYmode makesfind_path/find_libraryfind nothing at all, even with correct explicitHINTS— it only engages during a genuineproject()-driven cross-compile, not in a barecmake -Ptest, which made it easy to miss at first. Building the real ryzomcore PluginMax target usesNEVERfor all three instead. {.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