#!/bin/bash # # Downloads xwin and uses it to fetch the Windows SDK and MSVC CRT headers/libs # needed for cross-compiling Windows binaries from Linux using clang-cl. # # Usage: # ./get_win_sdk.sh [output_dir] # # Output defaults to ~/.xwin-sdk (override via $XWIN_SDK_DIR or first argument). set -euo pipefail SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) die() { echo "ERROR: $1" >&2; exit 1; } sdk_dir="${1:-${XWIN_SDK_DIR:-${HOME}/.xwin-sdk}}" if [[ "${sdk_dir}" == "--help" ]]; then echo "usage: $(basename "${BASH_SOURCE[0]}") [output_dir]" echo "" echo "Downloads the Windows SDK and MSVC CRT via xwin for cross-compilation." echo "Default output: ~/.xwin-sdk (override via \$XWIN_SDK_DIR or first argument)" exit 0 fi # If the directory already has SDK content, skip download if [ -d "${sdk_dir}/sdk/include/um" ] && [ -d "${sdk_dir}/crt/include" ]; then echo "SDK already present at '${sdk_dir}', skipping download." echo "Delete the directory to force re-download." # Still create the compat layout in case it's missing (e.g. script was updated) CREATE_COMPAT_ONLY=true else CREATE_COMPAT_ONLY=false fi if [ -e "${sdk_dir}" ]; then # Allow re-use of existing empty or partial directory if [ -d "${sdk_dir}" ]; then : else die "'${sdk_dir}' exists but is not a directory" fi fi mkdir -p "${sdk_dir}" # ------------------------------------------------------------------------- # Detect LLVM installation # ------------------------------------------------------------------------- LLVM_BIN="${LLVM_BIN_DIR:-}" if [ -z "${LLVM_BIN}" ]; then # Try common locations for candidate in /usr/lib/llvm-19/bin /usr/lib/llvm-18/bin /usr/lib/llvm-17/bin; do if [ -x "${candidate}/clang" ]; then LLVM_BIN="${candidate}" break fi done fi if [ -z "${LLVM_BIN}" ]; then # Fallback: try to find clang on PATH CLANG_PATH=$(command -v clang 2>/dev/null || true) if [ -n "${CLANG_PATH}" ]; then LLVM_BIN=$(dirname "$(readlink -f "${CLANG_PATH}")") fi fi if [ -z "${LLVM_BIN}" ]; then die "Could not find LLVM/clang installation. Set LLVM_BIN_DIR to the bin directory." fi echo "Using LLVM at: ${LLVM_BIN}" # ------------------------------------------------------------------------- # Download xwin binary and fetch SDK (skip if already present) # ------------------------------------------------------------------------- if [ "${CREATE_COMPAT_ONLY}" = false ]; then XWIN_VERSION="0.6.5" XWIN_ARCHIVE="xwin-${XWIN_VERSION}-x86_64-unknown-linux-musl.tar.gz" XWIN_URL="https://github.com/Jake-Shadle/xwin/releases/download/${XWIN_VERSION}/${XWIN_ARCHIVE}" TMPDIR=$(mktemp -d) trap 'rm -rf "${TMPDIR}"' EXIT echo "Downloading xwin ${XWIN_VERSION}..." if command -v wget &>/dev/null; then wget -q --show-progress -O "${TMPDIR}/${XWIN_ARCHIVE}" "${XWIN_URL}" elif command -v curl &>/dev/null; then curl -fSL --progress-bar -o "${TMPDIR}/${XWIN_ARCHIVE}" "${XWIN_URL}" else die "Neither wget nor curl found" fi echo "Extracting xwin..." tar -xzf "${TMPDIR}/${XWIN_ARCHIVE}" -C "${TMPDIR}" XWIN_BIN="${TMPDIR}/xwin-${XWIN_VERSION}-x86_64-unknown-linux-musl/xwin" if [ ! -x "${XWIN_BIN}" ]; then die "xwin binary not found after extraction" fi echo "Fetching Windows SDK and CRT (this may take a few minutes)..." "${XWIN_BIN}" --accept-license splat --output "${sdk_dir}" fi # ------------------------------------------------------------------------- # Create tool wrapper scripts in bin/ # ------------------------------------------------------------------------- BIN_DIR="${sdk_dir}/bin" mkdir -p "${BIN_DIR}" # clang-cl wrapper (since the host may not have a clang-cl symlink) cat > "${BIN_DIR}/clang-cl" << WRAPPER #!/bin/bash exec "${LLVM_BIN}/clang" --driver-mode=cl -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH "\$@" WRAPPER chmod +x "${BIN_DIR}/clang-cl" # clang wrapper for GNU assembly (.S files) cat > "${BIN_DIR}/clang" << WRAPPER #!/bin/bash exec "${LLVM_BIN}/clang" "\$@" WRAPPER chmod +x "${BIN_DIR}/clang" # ------------------------------------------------------------------------- # Create MSVC-compatible directory layout for xmake package builds. # # xmake's built-in msvc toolchain on Linux uses find_build_tools() which # expects the following structure: # /VC/Tools/MSVC//include → CRT headers # /VC/Tools/MSVC//lib/ → CRT libs # /Windows Kits/10/Include//{ucrt,um,shared} → SDK headers # /Windows Kits/10/Lib//{ucrt,um}/ → SDK libs # /bin// → tool wrappers # # We create this layout using symlinks back to the xwin flat layout. # ------------------------------------------------------------------------- echo "Creating MSVC-compatible directory layout..." FAKE_VC_VER="14.0.0" FAKE_SDK_VER="10.0.0.0" # --- VC Tools (CRT) --- VC_DIR="${sdk_dir}/VC/Tools/MSVC/${FAKE_VC_VER}" mkdir -p "${VC_DIR}" ln -sfn "${sdk_dir}/crt/include" "${VC_DIR}/include" mkdir -p "${VC_DIR}/lib" ln -sfn "${sdk_dir}/crt/lib/x86_64" "${VC_DIR}/lib/x64" # --- Windows Kits (SDK headers) --- WINSDK_INC="${sdk_dir}/Windows Kits/10/Include/${FAKE_SDK_VER}" mkdir -p "${WINSDK_INC}" ln -sfn "${sdk_dir}/sdk/include/ucrt" "${WINSDK_INC}/ucrt" ln -sfn "${sdk_dir}/sdk/include/um" "${WINSDK_INC}/um" ln -sfn "${sdk_dir}/sdk/include/shared" "${WINSDK_INC}/shared" # --- Windows Kits (SDK libs) --- WINSDK_LIB="${sdk_dir}/Windows Kits/10/Lib/${FAKE_SDK_VER}" mkdir -p "${WINSDK_LIB}/ucrt" "${WINSDK_LIB}/um" ln -sfn "${sdk_dir}/sdk/lib/ucrt/x86_64" "${WINSDK_LIB}/ucrt/x64" ln -sfn "${sdk_dir}/sdk/lib/um/x86_64" "${WINSDK_LIB}/um/x64" # --- Tool wrappers in bin// (for msvc toolchain PATH setup) --- ARCH_BIN="${sdk_dir}/bin/x64" mkdir -p "${ARCH_BIN}" # cl → clang-cl wrapper cat > "${ARCH_BIN}/cl" << WRAPPER #!/bin/bash exec "${LLVM_BIN}/clang" --driver-mode=cl -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH "\$@" WRAPPER chmod +x "${ARCH_BIN}/cl" cp "${ARCH_BIN}/cl" "${ARCH_BIN}/cl.exe" # link → lld-link (with /lib mode redirecting to llvm-lib for archiver use) # xmake sets ar=link.exe for non-LTO MSVC builds and may pass linker-only flags # like /opt:ref to the archiver. We detect /lib mode, filter those flags, and # redirect to llvm-lib. Also handles response files (@file) that xmake uses # when the argument list is too long. cat > "${ARCH_BIN}/link" << WRAPPER #!/bin/bash ALL_ARGS=() for arg in "\$@"; do if [[ "\$arg" == @* ]]; then rspfile="\${arg#@}" while IFS= read -r line; do [[ -n "\$line" ]] && ALL_ARGS+=("\$line") done < "\$rspfile" else ALL_ARGS+=("\$arg") fi done LIB_MODE=false HAS_OUT_LIB=false HAS_OBJ_ONLY=true ARGS=() for arg in "\${ALL_ARGS[@]}"; do lower="\${arg,,}" case "\$lower" in /lib|-lib) LIB_MODE=true ;; /out:*.lib|-out:*.lib) HAS_OUT_LIB=true; ARGS+=("\$arg") ;; /opt:*|-opt:*) ;; /subsystem:*|-subsystem:*) HAS_OBJ_ONLY=false; ARGS+=("\$arg") ;; *.exe) HAS_OBJ_ONLY=false; ARGS+=("\$arg") ;; *) ARGS+=("\$arg") ;; esac done if [ "\$LIB_MODE" = true ] || ([ "\$HAS_OUT_LIB" = true ] && [ "\$HAS_OBJ_ONLY" = true ]); then LIB_ARGS=() for arg in "\${ARGS[@]}"; do case "\${arg,,}" in -nodefaultlib:*|/nodefaultlib:*) ;; *) LIB_ARGS+=("\$arg") ;; esac done exec "${LLVM_BIN}/llvm-lib" "\${LIB_ARGS[@]}" else exec "${LLVM_BIN}/lld-link" "\$@" fi WRAPPER chmod +x "${ARCH_BIN}/link" cp "${ARCH_BIN}/link" "${ARCH_BIN}/link.exe" # lib → llvm-lib cat > "${ARCH_BIN}/lib" << WRAPPER #!/bin/bash exec "${LLVM_BIN}/llvm-lib" "\$@" WRAPPER chmod +x "${ARCH_BIN}/lib" cp "${ARCH_BIN}/lib" "${ARCH_BIN}/lib.exe" # rc → llvm-rc (with SDK include paths for winres.h etc.) cat > "${ARCH_BIN}/rc" << WRAPPER #!/bin/bash exec "${LLVM_BIN}/llvm-rc" /I "${sdk_dir}/crt/include" /I "${sdk_dir}/sdk/include/ucrt" /I "${sdk_dir}/sdk/include/um" /I "${sdk_dir}/sdk/include/shared" "\$@" WRAPPER chmod +x "${ARCH_BIN}/rc" cp "${ARCH_BIN}/rc" "${ARCH_BIN}/rc.exe" # ml64 → llvm-ml (MASM-compatible assembler) cat > "${ARCH_BIN}/ml64" << WRAPPER #!/bin/bash exec "${LLVM_BIN}/llvm-ml" -m64 "\$@" WRAPPER chmod +x "${ARCH_BIN}/ml64" cp "${ARCH_BIN}/ml64" "${ARCH_BIN}/ml64.exe" # clang-cl (for xmake's built-in clang-cl toolchain detection) cat > "${ARCH_BIN}/clang-cl" << WRAPPER #!/bin/bash exec "${LLVM_BIN}/clang" --driver-mode=cl -D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH "\$@" WRAPPER chmod +x "${ARCH_BIN}/clang-cl" # llvm-ar (cmake's clang-cl driver may use llvm-ar as archiver name but with # MSVC-style flags like /nologo /out: — redirect to llvm-lib which handles these) cat > "${ARCH_BIN}/llvm-ar" << WRAPPER #!/bin/bash exec "${LLVM_BIN}/llvm-lib" "\$@" WRAPPER chmod +x "${ARCH_BIN}/llvm-ar" # lld-link (for LTO builds where clang-cl toolchain uses lld-link) cat > "${ARCH_BIN}/lld-link" << WRAPPER #!/bin/bash exec "${LLVM_BIN}/lld-link" "\$@" WRAPPER chmod +x "${ARCH_BIN}/lld-link" # mt → llvm-mt (manifest tool) cat > "${ARCH_BIN}/mt" << WRAPPER #!/bin/bash exec "${LLVM_BIN}/llvm-mt" "\$@" WRAPPER chmod +x "${ARCH_BIN}/mt" cp "${ARCH_BIN}/mt" "${ARCH_BIN}/mt.exe" # ------------------------------------------------------------------------- # Create debug CRT lib symlinks (cmake's try_compile uses Debug config # by default, which links against msvcrtd.lib etc. -- these don't exist # in xwin since it only ships release libs. Symlink to release versions # so cmake compiler tests pass.) # ------------------------------------------------------------------------- CRT_LIB="${sdk_dir}/crt/lib/x86_64" for lib in msvcrt MSVCRT vcruntime msvcprt libcmt LIBCMT libcpmt libcpmt1 libconcrt libconcrt1 libvcruntime; do release="${CRT_LIB}/${lib}.lib" debug="${CRT_LIB}/${lib}d.lib" if [ -f "${release}" ] && [ ! -e "${debug}" ]; then ln -sfn "${lib}.lib" "${debug}" fi done echo "" echo "Windows SDK installed to: ${sdk_dir}" echo " SDK headers: ${sdk_dir}/sdk/include/um" echo " SDK libs: ${sdk_dir}/sdk/lib/um/x86_64" echo " CRT headers: ${sdk_dir}/crt/include" echo " CRT libs: ${sdk_dir}/crt/lib/x86_64" echo " Tool wrappers: ${BIN_DIR}/" echo " MSVC compat: ${sdk_dir}/VC/ and ${sdk_dir}/Windows Kits/" echo "" echo "Usage:" echo " xmake config -p windows -a x64 --toolchain=clang-cl --sdk=\${sdk_dir}" echo "" echo "Done"