Skip to content

clangcl_linux_sysroot

Jan Boon edited this page Nov 2, 2026 · 1 revision

title: Building the VS2022 (v143) target from Linux with clang-cl (draft) description: A Windows x64 cross toolchain on Linux without Wine, from clang/lld, a packaged MSVC + Windows SDK sysroot and a case-insensitive VFS overlay, linking the stock 2026q4_external_v143_x64 externals published: true date: 2026-10-31T00:00:00.000Z tags: editor: markdown dateCreated: 2026-10-31T00:00:00.000Z

Status: trial validated (2026-10-31). A program compiled and linked by clang-cl and lld-link in an Ubuntu 26.04 container, against the unchanged 2026q4_external_v143_x64 package, runs natively on Windows: zlib and libpng through their import libraries, Lua, the MSVC-built luabind static library with STL types in its interface, the MS STL and the UCRT. Ryzom Core itself has not been built this way yet; see "Open items". Companion pages: VS2008 under Wine and VC6 under Wine for the two legacy toolchains, which have no Wine-free option because those compilers only exist as Windows binaries. This page is the modern counterpart: native Linux, parallel, no Wine. {.is-warning}

Why

clang-cl is not a different ABI. It targets x86_64-pc-windows-msvc: MSVC name mangling, calling conventions, class layout, RTTI and exception model, the same UCRT, vcruntime140 and msvcp140, the same MS STL headers, CodeView debug information, and lld-link writes PDBs. Objects from clang-cl link against MSVC-built libraries and the result debugs in Visual Studio like any MSVC build (Chromium and Firefox ship Windows builds exactly so). What a Linux host needs is only the data side of the Microsoft toolchain: the MSVC headers and libraries and the Windows SDK. Everything executable (compiler, linker, librarian, resource compiler, manifest tool) comes from LLVM.

The sysroot package

toolchain_v143_sysroot.7z (285 MiB, 3.3 GB unpacked, 7,587 files) holds the headers and libraries in the Visual Studio layout that clang's /winsysroot option reads:

Path in the archive From the Windows machine
VC/Tools/MSVC/14.44.35207/include, atlmfc/include C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\14.44.35207\…
VC/Tools/MSVC/14.44.35207/lib/{x64,x86}, atlmfc/lib/{x64,x86} same
Windows Kits/10/Include/10.0.26100.0/{ucrt,um,shared,winrt,cppwinrt} C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0
Windows Kits/10/Lib/10.0.26100.0/{ucrt,um}/{x64,x86} C:\Program Files (x86)\Windows Kits\10\Lib\10.0.26100.0

The versions are the ones the v143 externals were built with (MSVC 14.44.35207, SDK 10.0.26100.0): the STL headers a consumer compiles against must match the ones the package's C++ libraries were built with, as between two MSVC builds. Building the archive is a robocopy /E of those directories into the layout above, then 7-Zip. Nothing is generated; a newer toolchain is a new archive.

Alternatives that fetch the same payload without a Windows machine: xwin (xwin splat, reads the Visual Studio installer manifest, downloads only the CRT/STL and SDK packages, unpacks with lowercase aliases) and msvc-wine (same download, also able to run the real cl.exe through Wine).

Filename case

The SDK is written for a case-insensitive filesystem and relies on it: windows.h includes "DriverSpecs.h" for a file named driverspecs.h, and MSVC-built objects carry /DEFAULTLIB:MSVCRT directives for msvcrt.lib. /winsysroot finds the right directories but file lookup on ext4 stays case-sensitive, and lowercase alias symlinks (the xwin approach, or ryzomcore's tool/wine_vs2008/lowercase_alias.py) only cover includes spelled in lowercase. The complete fix is a clang VFS overlay with case-sensitive: false that lists every directory and file of the sysroot with its real name; clang-cl takes it with -vfsoverlay <yaml>, lld-link with /vfsoverlay:<yaml>, and both then match any spelling. The overlay is generated once per sysroot (1.3 MB of JSON for this one):

"""gen_vfs.py <sysroot dir> <output yaml>: case-insensitive clang VFS overlay for a Windows sysroot."""
import os, sys, json

root = os.path.abspath(sys.argv[1])

def entry(path):
    items = []
    for name in sorted(os.listdir(path)):
        full = os.path.join(path, name)
        if os.path.islink(full):
            continue
        if os.path.isdir(full):
            items.append(entry(full))
        else:
            items.append({"type": "file", "name": name, "external-contents": full})
    return {"type": "directory", "name": os.path.basename(path) if path != root else root, "contents": items}

json.dump({"version": 0, "case-sensitive": "false", "use-external-names": True, "roots": [entry(root)]},
          open(sys.argv[2], "w"))

Note the spelling: the compiler-side option is clang's generic -vfsoverlay <file> (clang-cl has no /vfsoverlay: form), the linker-side one is lld-link's /vfsoverlay:<file>.

Toolchain file

# toolchain-clangcl-x64.cmake: Windows x64 from Linux with clang-cl + lld-link; WINSYSROOT in the environment
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_VERSION 10.0)
set(CMAKE_SYSTEM_PROCESSOR AMD64)
set(_sysroot "$ENV{WINSYSROOT}")
set(_vfs "${_sysroot}.yaml")
set(CMAKE_C_COMPILER clang-cl)
set(CMAKE_CXX_COMPILER clang-cl)
set(CMAKE_C_COMPILER_TARGET x86_64-pc-windows-msvc)
set(CMAKE_CXX_COMPILER_TARGET x86_64-pc-windows-msvc)
set(CMAKE_LINKER lld-link)
set(CMAKE_AR llvm-lib)
set(CMAKE_RC_COMPILER llvm-rc)
set(CMAKE_MT llvm-mt)
set(CMAKE_C_FLAGS_INIT "/winsysroot \"${_sysroot}\" -vfsoverlay \"${_vfs}\"")
set(CMAKE_CXX_FLAGS_INIT "/winsysroot \"${_sysroot}\" -vfsoverlay \"${_vfs}\"")
set(CMAKE_EXE_LINKER_FLAGS_INIT "/winsysroot:\"${_sysroot}\" /vfsoverlay:\"${_vfs}\"")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "/winsysroot:\"${_sysroot}\" /vfsoverlay:\"${_vfs}\"")
set(CMAKE_MODULE_LINKER_FLAGS_INIT "/winsysroot:\"${_sysroot}\" /vfsoverlay:\"${_vfs}\"")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE NEVER)

CMake 3.31 and 4.x detect MSVC 19.33-compatible clang-cl, pick the Ninja/MSVC flag set (/MD, /MDd, /Zi), and run their vs_link_exe wrapper with llvm-mt for manifests. The externals of the v143 package are found through CMAKE_PREFIX_PATH with their own CMake config files (ZLIBConfig.cmake, BoostConfig.cmake) or the standard Find modules; a FindLua.cmake that understands Lua 5.5's numeric version macros is needed (the one in the XP externals' cmake/ directory).

Container

FROM ubuntu:resolute
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
  clang lld llvm cmake ninja-build python3 p7zip-full ca-certificates \
  && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /mnt/nel /opt/sysroots

Ubuntu 26.04 ships clang/lld 21.1 and CMake 4.2; clang-cl, lld-link, llvm-lib, llvm-rc and llvm-mt are in those packages. The sysroot archive is extracted into a volume (/opt/sysroots/toolchain_v143_sysroot) and the overlay generated next to it (/opt/sysroots/toolchain_v143_sysroot.yaml); the build sets WINSYSROOT and uses the toolchain file. The same shape as ryzombuild_wine_amd64 with its _prefixes volume.

What the trial showed

  • hello.exe (MS STL, GetTickCount64) and the externals test (zlib 1.3.2 libpng 1.6.59, a luabind-registered function called from Lua 5.5) build on Linux and run on Windows against the package's DLLs.
  • CMake's default flags work unchanged; linking the MSVC-built static luabind into a clang-cl executable works, which is the C++ ABI interoperability question in one step.
  • A real luabind bug surfaced: scope.hpp chose the comma-operator overloads by __cplusplus >= 201103L. MSVC reports 199711L unless /Zc:__cplusplus is given, so the MSVC-built library exported the C++03 overload while clang-cl (real __cplusplus) called the C++11 one: an unresolved external. Fixed in ryzom/luabind (bb5e4fd) by keying on the Boost-derived LUABIND_NO_RVALUE_REFERENCES, which is the same on both sides of the ABI. Any header that keys ABI-relevant choices on __cplusplus is a risk for mixed MSVC/clang-cl builds; MSVC consumers built with /Zc:__cplusplus hit the same mismatch.

Open items

  1. Configure and build Ryzom Core itself with this toolchain against 2026q4_external_v143_x64: MSVC-only constructs to expect are MASM-syntax inline assembly in the x86 paths, a few pragmas, and precompiled-header behaviour. clang-cl defines _MSC_VER, so the existing #ifdef structure applies as is.
  2. The MFC tools and the Max plugins: atlmfc headers and libraries are in the sysroot and clang-cl compiles ATL/MFC (#import type libraries are the known gap); the 3ds Max SDK would be a second payload, as in the Wine prefix.
  3. Qt: the package's Qt DLLs link fine from clang-cl; only a clang-cl build of Qt itself would matter for the static variants.
  4. 32-bit (/machine:x86): the sysroot carries the x86 libraries too; untested.
  5. Quick start integration: a ryzombuild_clangcl_amd64 image and a toolchain_v143_sysroot volume filled by configure_docker.py from the CDN archive or from the host's own Visual Studio, a toolchain layer under tool/clangcl_v143/, and the v143/x64 externals detected the way dump_wine.py finds them in prefixes.

Clone this wiki locally