Skip to content

vs2008_linux_wine

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

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_export reference 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, the plugin_max binaries themselves. {.is-info}

Install Wine itself

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 ~/.wine prefix defaults to 64-bit on a 64-bit host, and there is no in-place conversion afterward (only delete-and-redo). If ~/.wine doesn't exist yet, make sure the first wine/wineboot invocation ever run on the box sets WINEARCH=win32 explicitly, even if you only ever intend to use the dedicated vs2008 prefix below — e.g.:

WINEARCH=win32 WINEDLLOVERRIDES="mscoree,mshtml=" xvfb-run -a wineboot -i

Verify with grep '#arch' ~/.wine/system.reg — should read #arch=win32. {.is-warning}

Create the Wine prefix

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

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.

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.

Getting the toolchain into the prefix

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/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
  • Program Files/Microsoft SDKs/Windows/v6.0A/Include/ and Lib/ (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.

Environment wrapper

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

Traps

  • /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.
  • /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.
  • Case sensitivity. Windows-authored sources #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.
  • Response files and long command lines work fine (cl @args.rsp); prefer them over heroic shell quoting of \-ridden paths.
  • Parallelism: 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.

Flags for reference-arithmetic probes

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.

Using Max 2010 SDK code in probes

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.

Driving the compiler from native Linux CMake + Ninja

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:NO

winecl-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.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).

CMAKE_FIND_ROOT_PATH_MODE_LIBRARY/_INCLUDE are set to ONLY above, which is fine for this trivial standalone-probe example (no find_package calls to trip it), but is a real trap for anything using Find*.cmake modules: with CMAKE_FIND_ROOT_PATH empty (never set here), ONLY mode makes find_path/find_library find nothing at all, even with correct explicit HINTS — it only engages during a genuine project()-driven cross-compile, not in a bare cmake -P test, which made it easy to miss at first. Building the real ryzomcore PluginMax target uses NEVER for 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.

Troubleshooting

cl.exe exits immediately with no output

mspdb80.dll not found — verify WINEPATH includes Common7\IDE, or copy mspdb80.dll, mspdbcore.dll, msobj80.dll next to cl.exe.

C1902: Program database manager mismatch

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.

LNK1104: cannot open file 'kernel32.lib'

LIB doesn't reach the Windows SDK Lib directory — the VC lib directory alone only has the CRT.

See also

Clone this wiki locally