diff --git a/.gitattributes b/.gitattributes index cb844f225c..a8311aeabb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ **/expect.txt text eol=lf **/out.txt text eol=lf *.ll text eol=lf +*.sh text eol=lf _demo/go/export/libexport.h.want text eol=lf diff --git a/.github/actions/activate-windows-target/action.yml b/.github/actions/activate-windows-target/action.yml index c2d7aa726c..72da99e66a 100644 --- a/.github/actions/activate-windows-target/action.yml +++ b/.github/actions/activate-windows-target/action.yml @@ -13,17 +13,60 @@ inputs: runs: using: "composite" steps: - - name: Record the installed host compiler when available + - name: Record and isolate the host compiler when available if: runner.os == 'Windows' shell: pwsh run: | $ErrorActionPreference = "Stop" - # GOROOT and benchmark jobs pass their explicitly built host compiler - # by path. Shared test jobs install llgo on PATH and reuse it whenever - # a target-native test launches the compiler as a subprocess. + # GOROOT exposes its explicitly built compiler through LLGO_TEST_LLGO. + # Shared test jobs install llgo on PATH and reuse it whenever a + # target-native test launches the compiler as a subprocess. $hostCompiler = Get-Command llgo.exe -ErrorAction SilentlyContinue - if ($null -ne $hostCompiler) { - Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_TEST_COMPILER=$($hostCompiler.Source)" + $hostCompilerPath = if ($env:LLGO_TEST_LLGO -and (Test-Path $env:LLGO_TEST_LLGO)) { + (Resolve-Path $env:LLGO_TEST_LLGO).Path + } elseif ($null -ne $hostCompiler) { + $hostCompiler.Source + } + if ($hostCompilerPath) { + if ("${{ inputs.windows-abi }}" -eq "mingw" -and "${{ inputs.windows-arch }}" -eq "386") { + $hostBin = Join-Path $env:LLGO_MINGW_HOST_ROOT "bin" + $objdump = Join-Path $hostBin "llvm-objdump.exe" + if (-not (Test-Path $objdump)) { + throw "The MinGW host llvm-objdump is unavailable: $objdump" + } + + # The x86 target DLL directories must precede the x64 host on PATH + # when generated programs run. Put the host compiler's DLL closure + # beside llgo.exe so Windows resolves it before consulting PATH. + $compilerDir = Split-Path $hostCompilerPath + $queue = [Collections.Generic.Queue[string]]::new() + $queue.Enqueue($hostCompilerPath) + $copied = @{} + while ($queue.Count -ne 0) { + $binary = $queue.Dequeue() + foreach ($line in (& $objdump -p $binary)) { + if ($line -notmatch '^\s*DLL Name:\s*(\S+)\s*$') { + continue + } + $name = $Matches[1] + if ($copied.ContainsKey($name)) { + continue + } + $source = Join-Path $hostBin $name + if (-not (Test-Path $source)) { + continue + } + $destination = Join-Path $compilerDir $name + Copy-Item -LiteralPath $source -Destination $destination -Force + $copied[$name] = $true + $queue.Enqueue($destination) + } + } + if ($copied.Count -eq 0) { + throw "No MinGW host runtime DLLs were discovered for $hostCompilerPath" + } + } + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_TEST_COMPILER=$hostCompilerPath" } - name: Activate Windows MSVC target @@ -52,7 +95,103 @@ runs: Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_WINDOWS_TARGET_ACTIVE=1" Add-Content -Encoding utf8 $env:GITHUB_PATH (Join-Path $env:LLGO_WINDOWS_VCPKG_ROOT "bin") - - name: Reject unsupported Windows GNU target architecture - if: runner.os == 'Windows' && inputs.windows-abi == 'mingw' && inputs.windows-arch != 'amd64' + - name: Activate Windows GNU target + if: runner.os == 'Windows' && inputs.windows-abi == 'mingw' shell: pwsh - run: throw "Non-amd64 MinGW qualification belongs to R11" + run: | + $ErrorActionPreference = "Stop" + $goArch = "${{ inputs.windows-arch }}" + $targetArch = switch ($goArch) { + "386" { "i686" } + "amd64" { "x86_64|amd64" } + "arm64" { "aarch64" } + default { throw "Unsupported Windows GNU target architecture: $goArch" } + } + $cc = "clang" + $cxx = "clang++" + + if ($goArch -eq "386") { + foreach ($name in @("LLGO_MINGW_TARGET_BIN", "LLGO_MINGW_TARGET_RUNTIME_BIN", "LLGO_MINGW_TARGET_TOOLS", "LLGO_MINGW_TARGET_TRIPLE", "LLGO_MINGW_TARGET_VCPKG_ROOT")) { + $value = [Environment]::GetEnvironmentVariable($name) + if (-not $value) { + throw "The standalone llvm-mingw target did not export $name" + } + } + $compiler = Join-Path $env:LLGO_MINGW_TARGET_BIN "i686-w64-mingw32-clang.exe" + $compilerXX = Join-Path $env:LLGO_MINGW_TARGET_BIN "i686-w64-mingw32-clang++.exe" + foreach ($path in @($compiler, $compilerXX)) { + if (-not (Test-Path $path)) { + throw "The standalone llvm-mingw target compiler was not found: $path" + } + } + $target = (& $compiler -dumpmachine).Trim() + if ($target -notmatch "^$targetArch-.*-(windows-gnu|mingw32)$") { + throw "llvm-mingw reports $target, want windows/$goArch GNU" + } + # GITHUB_PATH prepends entries in reverse write order. Keep the + # target-selecting clang/pkg-config wrappers ahead of the bundle's + # unqualified x64 host commands for all subsequent steps. + Add-Content -Encoding utf8 $env:GITHUB_PATH $env:LLGO_MINGW_TARGET_BIN + Add-Content -Encoding utf8 $env:GITHUB_PATH $env:LLGO_MINGW_TARGET_RUNTIME_BIN + Add-Content -Encoding utf8 $env:GITHUB_PATH $env:LLGO_MINGW_TARGET_TOOLS + Add-Content -Encoding utf8 $env:GITHUB_PATH (Join-Path $env:LLGO_MINGW_TARGET_VCPKG_ROOT "bin") + # Keep the wrappers for interactive shells, but let LLGo invoke the + # drivers directly. A normal runtime link can exceed cmd.exe's 8191 + # character limit while remaining below LLGo's 30 KiB response-file + # threshold. + $cc = $compiler + $cxx = $compilerXX + } else { + $target = (& clang.exe -dumpmachine).Trim() + if ($target -notmatch "^$targetArch-.*-(windows-gnu|mingw32)$") { + throw "MSYS2 Clang reports $target, want windows/$goArch GNU" + } + } + + Add-Content -Encoding utf8 $env:GITHUB_ENV "CC=$cc" + Add-Content -Encoding utf8 $env:GITHUB_ENV "CXX=$cxx" + Add-Content -Encoding utf8 $env:GITHUB_ENV "GOOS=windows" + Add-Content -Encoding utf8 $env:GITHUB_ENV "GOARCH=$goArch" + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_WINDOWS_TARGET_ACTIVE=1" + + - name: Activate cross-host Windows GNU target + if: runner.os != 'Windows' && inputs.windows-abi == 'mingw' + shell: bash + run: | + set -euo pipefail + for name in \ + LLGO_MINGW_TARGET_BIN \ + LLGO_MINGW_TARGET_RUNTIME_BIN \ + LLGO_MINGW_TARGET_TOOLS \ + LLGO_MINGW_TARGET_TRIPLE \ + LLGO_MINGW_TARGET_VCPKG_ROOT; do + if [[ -z "${!name:-}" ]]; then + echo "the cross-host llvm-mingw target did not export $name" >&2 + exit 1 + fi + done + + target="$(${LLGO_MINGW_TARGET_TOOLS}/clang -dumpmachine)" + case "${{ inputs.windows-arch }}:$target" in + 386:i686-*-windows-gnu|386:i686-*-mingw32) ;; + amd64:x86_64-*-windows-gnu|amd64:x86_64-*-mingw32) ;; + arm64:aarch64-*-windows-gnu|arm64:aarch64-*-mingw32) ;; + *) + echo "llvm-mingw reports $target for windows/${{ inputs.windows-arch }}" >&2 + exit 1 + ;; + esac + + # Only expose the target-selecting wrappers. The compiler locates its + # own CRT by absolute path, target DLLs cannot run on this Unix host, + # and vcpkg contributes link flags through the pkg-config wrapper. + # This prevents llvm-mingw's unqualified host tools from shadowing the + # LLVM installation used to build the LLGo compiler itself. + echo "$LLGO_MINGW_TARGET_TOOLS" >> "$GITHUB_PATH" + echo "CC=$LLGO_MINGW_TARGET_TOOLS/clang" >> "$GITHUB_ENV" + echo "CXX=$LLGO_MINGW_TARGET_TOOLS/clang++" >> "$GITHUB_ENV" + echo "PKG_CONFIG=$LLGO_MINGW_TARGET_TOOLS/pkg-config" >> "$GITHUB_ENV" + echo "CGO_ENABLED=1" >> "$GITHUB_ENV" + echo "GOOS=windows" >> "$GITHUB_ENV" + echo "GOARCH=${{ inputs.windows-arch }}" >> "$GITHUB_ENV" + echo "LLGO_WINDOWS_TARGET_ACTIVE=1" >> "$GITHUB_ENV" diff --git a/.github/actions/setup-demo-deps/action.yml b/.github/actions/setup-demo-deps/action.yml index a3bb9a1a55..1e040e9120 100644 --- a/.github/actions/setup-demo-deps/action.yml +++ b/.github/actions/setup-demo-deps/action.yml @@ -21,7 +21,10 @@ runs: # LLVM 19.1.3's MSVC LLDB embeds the 3.12.7 hosted-tool-cache prefix. # A newer 3.12 patch is ABI-compatible, but LLDB combines its embedded # stdlib path with that installation and cannot import encodings. Keep - # the MinGW demo environment on the normal latest 3.12 patch. + # the MinGW demo environment on the normal latest 3.12 patch. This is + # the host-tools interpreter: use x64 on Windows ARM, where emulation + # retains access to wheels that upstream projects do not publish for + # native ARM64 Python. The target interpreter is installed below. python-version: ${{ inputs.windows-abi == 'msvc' && '3.12.7' || '3.12' }} architecture: x64 # setup-python otherwise replaces PKG_CONFIG_PATH, hiding the native @@ -40,19 +43,30 @@ runs: architecture: ${{ inputs.windows-arch == 'arm64' && 'arm64' || 'x86' }} update-environment: false + - name: Set up LLDB Python 3.10 on Windows 386 + if: runner.os == 'Windows' && inputs.windows-arch == '386' + id: windows_lldb_python_386 + uses: actions/setup-python@v6 + with: + # LLVM's official 19.1.7 Win32 LLDB links python310.dll. Keep this + # debugger runtime separate from the target Python 3.12 demo library. + python-version: "3.10" + architecture: x86 + update-environment: false + - name: Set up LLDB Python 3.11 on Windows ARM64 - if: runner.os == 'Windows' && inputs.windows-abi == 'msvc' && inputs.windows-arch == 'arm64' + if: runner.os == 'Windows' && inputs.windows-arch == 'arm64' id: windows_lldb_python uses: actions/setup-python@v6 with: - # LLVM's official 19.1.3 Windows-on-Arm LLDB links python311.dll. + # LLVM's official 19.1.7 Windows-on-Arm LLDB links python311.dll. # 3.11.9 is the final ARM64 artifact in actions/python-versions. python-version: "3.11.9" architecture: arm64 update-environment: false - name: Configure LLDB Python on Windows ARM64 - if: runner.os == 'Windows' && inputs.windows-abi == 'msvc' && inputs.windows-arch == 'arm64' + if: runner.os == 'Windows' && inputs.windows-arch == 'arm64' shell: pwsh run: | $ErrorActionPreference = "Stop" @@ -63,6 +77,18 @@ runs: } Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_WINDOWS_LLDB_PYTHON_HOME=$prefix" + - name: Configure LLDB Python on Windows 386 + if: runner.os == 'Windows' && inputs.windows-arch == '386' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $python = "${{ steps.windows_lldb_python_386.outputs.python-path }}" + $prefix = (& $python -c "import sys; print(sys.base_prefix)").Trim() + if (-not (Test-Path (Join-Path $prefix "python310.dll"))) { + throw "The native x86 LLDB Python runtime is incomplete: $prefix" + } + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_WINDOWS_LLDB_PYTHON_HOME=$prefix" + - name: Install cargs demo library on Unix if: runner.os != 'Windows' shell: bash @@ -123,6 +149,12 @@ runs: $null = Enter-LLGoVisualStudio2022 -GoArch "${{ inputs.windows-arch }}" } $compiler = (Get-Command clang.exe).Source + if ("${{ inputs.windows-abi }}" -eq "mingw" -and "${{ inputs.windows-arch }}" -eq "386") { + if (-not $env:LLGO_MINGW_TARGET_BIN) { + throw "The standalone llvm-mingw target was not configured" + } + $compiler = Join-Path $env:LLGO_MINGW_TARGET_BIN "i686-w64-mingw32-clang.exe" + } $compilerArgs = @() if ("${{ inputs.windows-abi }}" -eq "msvc") { $compilerArgs = @("--target=$env:LLGO_WINDOWS_TARGET_TRIPLE") @@ -138,6 +170,14 @@ runs: if ($compilerTarget -notmatch "-(windows-gnu|mingw32)$") { throw "The MinGW profile resolved Clang target $compilerTarget" } + $targetArch = switch ("${{ inputs.windows-arch }}") { + "386" { "i686" } + "amd64" { "x86_64|amd64" } + "arm64" { "aarch64" } + } + if ($compilerTarget -notmatch "^($targetArch)-") { + throw "The MinGW profile resolved $compilerTarget for windows/${{ inputs.windows-arch }}" + } $runtimeArgs = @() $archive = "libcargs.a" } diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml index 83184c642c..815ee267c9 100644 --- a/.github/actions/setup-deps/action.yml +++ b/.github/actions/setup-deps/action.yml @@ -31,9 +31,6 @@ runs: if ("${{ inputs.windows-arch }}" -notin @("386", "amd64", "arm64")) { throw "Unsupported Windows target architecture: ${{ inputs.windows-arch }}" } - if ("${{ inputs.windows-abi }}" -eq "mingw" -and "${{ inputs.windows-arch }}" -ne "amd64") { - throw "The MinGW profile currently supports windows/amd64; other GNU architectures are qualified in R11" - } Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_WINDOWS_ABI=${{ inputs.windows-abi }}" Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_WINDOWS_ARCH=${{ inputs.windows-arch }}" @@ -108,30 +105,30 @@ runs: path: ${{ runner.tool_cache }}\llgo-llvm\LLVM-19.1.3-Windows-X64 key: llgo-llvm-19.1.3-full-target-windows-x64-1077267ca353a1e236055ed4b57d6a404d09c40b01bd27dc882870395cdc1aae - - name: Cache Windows cross-target compiler-rt builtins - if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'msvc' && inputs.windows-arch != 'amd64' - id: windows_target_builtins_cache + - name: Cache target-native Windows LLVM support + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-arch != 'amd64' + id: windows_target_llvm_cache uses: actions/cache/restore@v4 with: - path: ${{ runner.tool_cache }}\llgo-compiler-rt\19.1.3\${{ inputs.windows-arch }} - key: ${{ inputs.windows-arch == 'arm64' && 'llgo-compiler-rt-19.1.3-windows-arm64-20a4a1094f813f7c548b2ff0a23bdc7f56ef0773c9aa1041d31ba56269b298a9-native-lldb-v1' || 'llgo-compiler-rt-19.1.3-windows-386-9bab022d175ff27e35db517b71d3e428027d75ffff0d0195ccc9f20ce5a20d4f' }} + path: ${{ runner.tool_cache }}\llgo-compiler-rt\19.1.7\${{ inputs.windows-arch }} + key: ${{ inputs.windows-arch == 'arm64' && 'llgo-compiler-rt-19.1.7-windows-arm64-08d871c9a20caf0987ede1a4e44d489e28854c9f2fff3da98b486891f4856e05-native-lldb-v1' || 'llgo-compiler-rt-19.1.7-windows-386-3d7044fa307d3183e4ee2a98e12dd0523ced58f1d1c95c5e057694fb75beb51a-native-lldb-v1' }} - - name: Install Windows cross-target compiler-rt builtins - if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'msvc' && inputs.windows-arch != 'amd64' && steps.windows_target_builtins_cache.outputs.cache-hit != 'true' + - name: Install target-native Windows LLVM support + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-arch != 'amd64' && steps.windows_target_llvm_cache.outputs.cache-hit != 'true' shell: pwsh env: - LLVM_VERSION: "19.1.3" + LLVM_VERSION: "19.1.7" LLVM_INSTALLER_ARCH: ${{ inputs.windows-arch == 'arm64' && 'woa64' || 'win32' }} - LLVM_INSTALLER_SHA256: ${{ inputs.windows-arch == 'arm64' && '20a4a1094f813f7c548b2ff0a23bdc7f56ef0773c9aa1041d31ba56269b298a9' || '9bab022d175ff27e35db517b71d3e428027d75ffff0d0195ccc9f20ce5a20d4f' }} + LLVM_INSTALLER_SHA256: ${{ inputs.windows-arch == 'arm64' && '08d871c9a20caf0987ede1a4e44d489e28854c9f2fff3da98b486891f4856e05' || '3d7044fa307d3183e4ee2a98e12dd0523ced58f1d1c95c5e057694fb75beb51a' }} LLVM_BUILTINS_ARCH: ${{ inputs.windows-arch == 'arm64' && 'aarch64' || 'i386' }} run: | $ErrorActionPreference = "Stop" - # The upstream x64 full-target archive supplies the host LLVM tools - # but only x64 compiler-rt. Extract the target library from the - # matching official package. Windows on ARM also needs the native - # LLDB from that package: x64 LLDB can launch an ARM64 process under - # emulation, but it cannot reliably read register-located variables. + # The upstream x64 full-target archive supplies the host LLVM tools but + # only x64 compiler-rt. The MSVC profile needs the target builtins from + # the matching official package. Both Windows ABIs use its target-native + # LLDB: the x64 debugger cannot reliably inspect ARM64 register variables + # or cross the WoW64 exception dispatcher into a 32-bit source frame. $installerName = "LLVM-$env:LLVM_VERSION-$env:LLVM_INSTALLER_ARCH.exe" $installer = Join-Path $env:RUNNER_TEMP $installerName $url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-$env:LLVM_VERSION/$installerName" @@ -150,19 +147,17 @@ runs: throw "7-Zip was not found at $sevenZip" } $builtinsRelative = "lib\clang\19\lib\windows\clang_rt.builtins-$env:LLVM_BUILTINS_ARCH.lib" - $extractPaths = @($builtinsRelative) - if ("${{ inputs.windows-arch }}" -eq "arm64") { - $extractPaths += @( - "bin\lldb.exe", - "bin\liblldb.dll", - "bin\lldb-argdumper.exe", - "bin\lldb-server.exe", - "lib\site-packages\lldb\*" - ) - } + $extractPaths = @( + $builtinsRelative, + "bin\lldb.exe", + "bin\liblldb.dll", + "bin\lldb-argdumper.exe", + "bin\lldb-server.exe", + "lib\site-packages\lldb\*" + ) & $sevenZip x -y "-o$extractRoot" $installer @extractPaths if ($LASTEXITCODE -ne 0) { - throw "Extracting the LLVM ${{ inputs.windows-arch }} builtins failed with exit code $LASTEXITCODE" + throw "Extracting LLVM ${{ inputs.windows-arch }} support failed with exit code $LASTEXITCODE" } $source = Join-Path $extractRoot $builtinsRelative if (-not (Test-Path $source)) { @@ -171,28 +166,26 @@ runs: $cacheRoot = Join-Path $env:RUNNER_TOOL_CACHE "llgo-compiler-rt\$env:LLVM_VERSION\${{ inputs.windows-arch }}" New-Item -ItemType Directory -Force $cacheRoot | Out-Null Copy-Item -LiteralPath $source -Destination $cacheRoot - if ("${{ inputs.windows-arch }}" -eq "arm64") { - $lldbRoot = Join-Path $cacheRoot "lldb" - New-Item -ItemType Directory -Force $lldbRoot | Out-Null - Copy-Item -LiteralPath (Join-Path $extractRoot "bin") -Destination $lldbRoot -Recurse - Copy-Item -LiteralPath (Join-Path $extractRoot "lib") -Destination $lldbRoot -Recurse - foreach ($required in @( - (Join-Path $lldbRoot "bin\lldb.exe"), - (Join-Path $lldbRoot "bin\liblldb.dll"), - (Join-Path $lldbRoot "lib\site-packages\lldb\__init__.py") - )) { - if (-not (Test-Path $required)) { - throw "The native Windows ARM64 LLDB installation is incomplete: $required" - } + $lldbRoot = Join-Path $cacheRoot "lldb" + New-Item -ItemType Directory -Force $lldbRoot | Out-Null + Copy-Item -LiteralPath (Join-Path $extractRoot "bin") -Destination $lldbRoot -Recurse + Copy-Item -LiteralPath (Join-Path $extractRoot "lib") -Destination $lldbRoot -Recurse + foreach ($required in @( + (Join-Path $lldbRoot "bin\lldb.exe"), + (Join-Path $lldbRoot "bin\liblldb.dll"), + (Join-Path $lldbRoot "lib\site-packages\lldb\__init__.py") + )) { + if (-not (Test-Path $required)) { + throw "The native Windows ${{ inputs.windows-arch }} LLDB installation is incomplete: $required" } } - - name: Save Windows cross-target compiler-rt builtins - if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'msvc' && inputs.windows-arch != 'amd64' && steps.windows_target_builtins_cache.outputs.cache-hit != 'true' + - name: Save target-native Windows LLVM support + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-arch != 'amd64' && steps.windows_target_llvm_cache.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: - path: ${{ runner.tool_cache }}\llgo-compiler-rt\19.1.3\${{ inputs.windows-arch }} - key: ${{ inputs.windows-arch == 'arm64' && 'llgo-compiler-rt-19.1.3-windows-arm64-20a4a1094f813f7c548b2ff0a23bdc7f56ef0773c9aa1041d31ba56269b298a9-native-lldb-v1' || 'llgo-compiler-rt-19.1.3-windows-386-9bab022d175ff27e35db517b71d3e428027d75ffff0d0195ccc9f20ce5a20d4f' }} + path: ${{ runner.tool_cache }}\llgo-compiler-rt\19.1.7\${{ inputs.windows-arch }} + key: ${{ inputs.windows-arch == 'arm64' && 'llgo-compiler-rt-19.1.7-windows-arm64-08d871c9a20caf0987ede1a4e44d489e28854c9f2fff3da98b486891f4856e05-native-lldb-v1' || 'llgo-compiler-rt-19.1.7-windows-386-3d7044fa307d3183e4ee2a98e12dd0523ced58f1d1c95c5e057694fb75beb51a-native-lldb-v1' }} - name: Cache Windows MSVC dependencies if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'msvc' @@ -254,6 +247,7 @@ runs: shell: pwsh env: LLVM_VERSION: "19.1.3" + LLVM_TARGET_VERSION: "19.1.7" VCPKG_MANIFEST_HASH: ${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} run: | $ErrorActionPreference = "Stop" @@ -447,8 +441,8 @@ runs: Add-Content -Encoding utf8 $env:GITHUB_PATH $nativeTools $lldb = Join-Path $llvmBin "lldb.exe" - if ($target.GoArch -eq "arm64") { - $lldbRoot = Join-Path $env:RUNNER_TOOL_CACHE "llgo-compiler-rt\$env:LLVM_VERSION\arm64\lldb" + if ($target.GoArch -ne "amd64") { + $lldbRoot = Join-Path $env:RUNNER_TOOL_CACHE "llgo-compiler-rt\$env:LLVM_TARGET_VERSION\$($target.GoArch)\lldb" $lldb = Join-Path $lldbRoot "bin\lldb.exe" Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_WINDOWS_LLDB_PYTHONPATH=$(Join-Path $lldbRoot 'lib\site-packages')" } @@ -476,7 +470,7 @@ runs: $targetBuiltins = (& (Join-Path $llvmBin "clang.exe") "--target=$($target.Triple)" --rtlib=compiler-rt -print-libgcc-file-name).Trim() if ($target.GoArch -ne "amd64" -and -not (Test-Path $targetBuiltins)) { $builtinsArch = if ($target.GoArch -eq "arm64") { "aarch64" } else { "i386" } - $cachedBuiltins = Join-Path $env:RUNNER_TOOL_CACHE "llgo-compiler-rt\$env:LLVM_VERSION\$($target.GoArch)\clang_rt.builtins-$builtinsArch.lib" + $cachedBuiltins = Join-Path $env:RUNNER_TOOL_CACHE "llgo-compiler-rt\$env:LLVM_TARGET_VERSION\$($target.GoArch)\clang_rt.builtins-$builtinsArch.lib" if (-not (Test-Path $cachedBuiltins)) { throw "The cached Windows $($target.GoArch) compiler-rt builtins were not found at $cachedBuiltins" } @@ -500,7 +494,10 @@ runs: id: windows_mingw uses: msys2/setup-msys2@v2 with: - msystem: CLANG64 + # CLANGARM64 supplies an architecture-native LLVM host and target on + # the ARM64 runner. The x64 runner keeps CLANG64 as the LLGo host even + # when R11 activates a separate llvm-mingw 386 target afterwards. + msystem: ${{ inputs.windows-arch == 'arm64' && 'CLANGARM64' || 'CLANG64' }} path-type: inherit update: true @@ -522,8 +519,20 @@ runs: ;; esac - repo=https://repo.msys2.org/mingw/clang64 - prefix=mingw-w64-clang-x86_64 + case "${{ inputs.windows-arch }}" in + arm64) + repo=https://repo.msys2.org/mingw/clangarm64 + prefix=mingw-w64-clang-aarch64 + ;; + amd64|386) + repo=https://repo.msys2.org/mingw/clang64 + prefix=mingw-w64-clang-x86_64 + ;; + *) + echo "unsupported Windows MinGW architecture: ${{ inputs.windows-arch }}" >&2 + exit 1 + ;; + esac packages=( "clang-$package_version" "clang-libs-$package_version" @@ -602,7 +611,8 @@ runs: run: | $ErrorActionPreference = "Stop" - $mingwRoot = Join-Path $env:MSYS2_LOCATION "clang64" + $mingwSubdir = if ("${{ inputs.windows-arch }}" -eq "arm64") { "clangarm64" } else { "clang64" } + $mingwRoot = Join-Path $env:MSYS2_LOCATION $mingwSubdir $mingwBin = Join-Path $mingwRoot "bin" $clang = Join-Path $mingwBin "clang.exe" $pkgconf = Join-Path $mingwBin "pkgconf.exe" @@ -614,8 +624,9 @@ runs: } $target = (& $clang -dumpmachine).Trim() - if ($target -notmatch "^(x86_64|amd64)-.*-(windows-gnu|mingw32)$") { - throw "MSYS2 Clang defaults to $target, want an x86_64 GNU/MinGW target" + $hostTargetArch = if ("${{ inputs.windows-arch }}" -eq "arm64") { "aarch64" } else { "x86_64|amd64" } + if ($target -notmatch "^($hostTargetArch)-.*-(windows-gnu|mingw32)$") { + throw "MSYS2 Clang defaults to $target, want a matching GNU/MinGW host target" } # Put one self-contained pkg-config command on PATH. Callers do not @@ -649,11 +660,30 @@ runs: Add-Content -Encoding utf8 $env:GITHUB_ENV "CC=clang" Add-Content -Encoding utf8 $env:GITHUB_ENV "CXX=clang++" Add-Content -Encoding utf8 $env:GITHUB_ENV "CGO_ENABLED=1" + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_MINGW_HOST_PC_DIR=$pcDir" + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_MINGW_HOST_PKGCONF=$pkgconf" + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_MINGW_HOST_ROOT=$mingwRoot" + if ("${{ inputs.windows-arch }}" -ne "amd64") { + $lldbRoot = Join-Path $env:RUNNER_TOOL_CACHE "llgo-compiler-rt\19.1.7\${{ inputs.windows-arch }}\lldb" + $lldb = Join-Path $lldbRoot "bin\lldb.exe" + if (-not (Test-Path $lldb)) { + throw "lldb.exe was not found for Windows ${{ inputs.windows-arch }}" + } + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_LLDB=$lldb" + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_WINDOWS_LLDB_PYTHONPATH=$(Join-Path $lldbRoot 'lib\site-packages')" + } Add-Content -Encoding utf8 $env:GITHUB_PATH $mingwBin # Keep the fixed-search-path wrapper ahead of CLANG64's pkg-config.exe; # setup-demo-deps adds profile-local metadata below RUNNER_TEMP. Add-Content -Encoding utf8 $env:GITHUB_PATH $nativeTools + - name: Set up standalone llvm-mingw target + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'mingw' && inputs.windows-arch == '386' + uses: ./.github/actions/setup-llvm-mingw + with: + llvm-version: ${{ inputs.llvm-version }} + windows-arch: ${{ inputs.windows-arch }} + - name: Install macOS dependencies if: runner.os == 'macOS' shell: bash diff --git a/.github/actions/setup-llvm-mingw/action.yml b/.github/actions/setup-llvm-mingw/action.yml new file mode 100644 index 0000000000..237701e12f --- /dev/null +++ b/.github/actions/setup-llvm-mingw/action.yml @@ -0,0 +1,424 @@ +name: "Set up llvm-mingw target" +description: "Install a pinned GNU/UCRT target toolchain and architecture-matched dependencies" +inputs: + windows-arch: + description: "Windows target GOARCH" + required: true + llvm-version: + description: "LLGo host LLVM major version" + required: false + default: "19" + +runs: + using: "composite" + steps: + - name: Validate llvm-mingw target + if: runner.os == 'Windows' + shell: pwsh + run: | + if ("${{ inputs.llvm-version }}" -ne "19") { + throw "llvm-mingw 20250114 is paired with LLVM 19, not ${{ inputs.llvm-version }}" + } + if ("${{ inputs.windows-arch }}" -ne "386") { + throw "The native llvm-mingw target currently qualifies windows/386, not windows/${{ inputs.windows-arch }}" + } + + - name: Validate cross-host llvm-mingw target + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + if [[ "${{ inputs.llvm-version }}" != 19 ]]; then + echo "llvm-mingw 20250114 is paired with LLVM 19, not ${{ inputs.llvm-version }}" >&2 + exit 1 + fi + case "${{ inputs.windows-arch }}" in + 386|amd64|arm64) ;; + *) + echo "unsupported llvm-mingw target: windows/${{ inputs.windows-arch }}" >&2 + exit 1 + ;; + esac + + - name: Restore llvm-mingw + if: runner.os == 'Windows' + id: llvm_mingw_cache + uses: actions/cache/restore@v4 + with: + path: ${{ runner.tool_cache }}\llvm-mingw\20250114\ucrt-x86_64 + key: llvm-mingw-20250114-ucrt-x86_64-9395d95c089611e6cd728c9a07219ffc5ac706e0c719833a2f7e64a706a81541 + + - name: Install llvm-mingw + if: runner.os == 'Windows' && steps.llvm_mingw_cache.outputs.cache-hit != 'true' + shell: pwsh + env: + LLVM_MINGW_VERSION: "20250114" + LLVM_MINGW_SHA256: 9395d95c089611e6cd728c9a07219ffc5ac706e0c719833a2f7e64a706a81541 + run: | + $ErrorActionPreference = "Stop" + $asset = "llvm-mingw-$env:LLVM_MINGW_VERSION-ucrt-x86_64.zip" + $archive = Join-Path $env:RUNNER_TEMP $asset + $url = "https://github.com/mstorsjo/llvm-mingw/releases/download/$env:LLVM_MINGW_VERSION/$asset" + & curl.exe --fail --location --retry 5 --retry-all-errors --output $archive $url + if ($LASTEXITCODE -ne 0) { + throw "Downloading llvm-mingw failed with exit code $LASTEXITCODE" + } + $actual = (Get-FileHash -Algorithm SHA256 $archive).Hash + if (-not $actual.Equals($env:LLVM_MINGW_SHA256, [StringComparison]::OrdinalIgnoreCase)) { + throw "llvm-mingw SHA-256 is $actual, want $env:LLVM_MINGW_SHA256" + } + + $extractRoot = Join-Path $env:RUNNER_TEMP "llgo-llvm-mingw-extract" + $installRoot = Join-Path $env:RUNNER_TOOL_CACHE "llvm-mingw\$env:LLVM_MINGW_VERSION\ucrt-x86_64" + New-Item -ItemType Directory -Force $extractRoot, (Split-Path $installRoot) | Out-Null + Expand-Archive -LiteralPath $archive -DestinationPath $extractRoot + $source = Get-ChildItem -LiteralPath $extractRoot -Directory | + Where-Object { Test-Path (Join-Path $_.FullName "bin\i686-w64-mingw32-clang.exe") } | + Select-Object -First 1 + if (-not $source) { + throw "The llvm-mingw archive does not contain the i686 target compiler" + } + Move-Item -LiteralPath $source.FullName -Destination $installRoot + + - name: Save llvm-mingw + if: runner.os == 'Windows' && steps.llvm_mingw_cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ runner.tool_cache }}\llvm-mingw\20250114\ucrt-x86_64 + key: llvm-mingw-20250114-ucrt-x86_64-9395d95c089611e6cd728c9a07219ffc5ac706e0c719833a2f7e64a706a81541 + + - name: Restore llvm-mingw target dependencies + if: runner.os == 'Windows' + id: llvm_mingw_vcpkg_cache + uses: actions/cache/restore@v4 + with: + path: ${{ runner.tool_cache }}\llgo-mingw-vcpkg\${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }}\386 + key: llgo-llvm-mingw-20250114-ucrt-386-${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + + - name: Build llvm-mingw target dependencies + if: runner.os == 'Windows' && steps.llvm_mingw_vcpkg_cache.outputs.cache-hit != 'true' + shell: pwsh + env: + VCPKG_BASELINE: ddd0023b0eee70986e42ed49d9d4afb8098f212e + VCPKG_MANIFEST_HASH: ${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + run: | + $ErrorActionPreference = "Stop" + $sourceRoot = Join-Path $env:RUNNER_TEMP "llgo-mingw-vcpkg-source" + $installRoot = Join-Path $env:RUNNER_TOOL_CACHE "llgo-mingw-vcpkg\$env:VCPKG_MANIFEST_HASH\386" + $manifestRoot = Join-Path $env:GITHUB_WORKSPACE ".github\windows\vcpkg" + $llvmMingwRoot = Join-Path $env:RUNNER_TOOL_CACHE "llvm-mingw\20250114\ucrt-x86_64" + $llvmMingwBin = Join-Path $llvmMingwRoot "bin" + $env:PATH = "$llvmMingwBin;$env:PATH" + + & git clone --no-checkout https://github.com/microsoft/vcpkg.git $sourceRoot + if ($LASTEXITCODE -ne 0) { + throw "Cloning vcpkg failed with exit code $LASTEXITCODE" + } + & git -C $sourceRoot checkout --detach $env:VCPKG_BASELINE + if ($LASTEXITCODE -ne 0) { + throw "Checking out vcpkg $env:VCPKG_BASELINE failed with exit code $LASTEXITCODE" + } + & (Join-Path $sourceRoot "bootstrap-vcpkg.bat") -disableMetrics + if ($LASTEXITCODE -ne 0) { + throw "Bootstrapping vcpkg failed with exit code $LASTEXITCODE" + } + & (Join-Path $sourceRoot "vcpkg.exe") install ` + --triplet=x86-mingw-dynamic ` + --host-triplet=x64-windows ` + "--x-manifest-root=$manifestRoot" ` + "--x-install-root=$installRoot" ` + --clean-after-build + if ($LASTEXITCODE -ne 0) { + throw "Installing llvm-mingw 386 dependencies failed with exit code $LASTEXITCODE" + } + + - name: Save llvm-mingw target dependencies + if: runner.os == 'Windows' && steps.llvm_mingw_vcpkg_cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ runner.tool_cache }}\llgo-mingw-vcpkg\${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }}\386 + key: llgo-llvm-mingw-20250114-ucrt-386-${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + + - name: Configure llvm-mingw target + if: runner.os == 'Windows' + shell: pwsh + env: + VCPKG_MANIFEST_HASH: ${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + run: | + $ErrorActionPreference = "Stop" + $llvmMingwRoot = Join-Path $env:RUNNER_TOOL_CACHE "llvm-mingw\20250114\ucrt-x86_64" + $llvmMingwBin = Join-Path $llvmMingwRoot "bin" + $llvmMingwRuntimeBin = Join-Path $llvmMingwRoot "i686-w64-mingw32\bin" + $compiler = Join-Path $llvmMingwBin "i686-w64-mingw32-clang.exe" + $compilerXX = Join-Path $llvmMingwBin "i686-w64-mingw32-clang++.exe" + $vcpkgRoot = Join-Path $env:RUNNER_TOOL_CACHE "llgo-mingw-vcpkg\$env:VCPKG_MANIFEST_HASH\386\x86-mingw-dynamic" + foreach ($path in @($compiler, $compilerXX, $llvmMingwRuntimeBin, (Join-Path $vcpkgRoot "include\gc\gc.h"))) { + if (-not (Test-Path $path)) { + throw "The llvm-mingw 386 target is incomplete: $path was not found" + } + } + $target = (& $compiler -dumpmachine).Trim() + if ($target -notmatch '^i686-.*-(windows-gnu|mingw32)$') { + throw "llvm-mingw reports $target, want an i686 GNU/MinGW target" + } + + $targetTools = Join-Path $env:RUNNER_TEMP "llgo-mingw-target-tools" + New-Item -ItemType Directory -Force $targetTools | Out-Null + @" + @echo off + "$compiler" %* + "@ | Set-Content -Encoding ascii (Join-Path $targetTools "clang.cmd") + @" + @echo off + "$compilerXX" %* + "@ | Set-Content -Encoding ascii (Join-Path $targetTools "clang++.cmd") + + $pkgconf = $env:LLGO_MINGW_HOST_PKGCONF + $hostPC = $env:LLGO_MINGW_HOST_PC_DIR + if (-not $pkgconf -or -not $hostPC -or -not (Test-Path $pkgconf) -or -not (Test-Path $hostPC)) { + throw "The CLANG64 host pkg-config profile is unavailable" + } + # Only LLVM belongs to the x64 host. Runtime dependencies must resolve + # from the x86 target tree; exposing CLANG64's complete pkg-config + # directory here would silently select x64 BDWGC/libffi/libuv again. + $hostLLVMPC = Join-Path $env:RUNNER_TEMP "llgo-mingw-host-pkgconfig" + # setup-demo-deps writes target-native cargs and Python metadata here + # after this action has configured the target wrapper. Include the + # stable directory now so those later files are visible without + # exposing any host dependency metadata. + $profilePC = Join-Path $env:RUNNER_TEMP "llgo-pkgconfig" + New-Item -ItemType Directory -Force $hostLLVMPC, $profilePC | Out-Null + Copy-Item -LiteralPath (Join-Path $hostPC "llvm-${{ inputs.llvm-version }}.pc") -Destination $hostLLVMPC + $targetLibPC = Join-Path $vcpkgRoot "lib\pkgconfig" + $targetSharePC = Join-Path $vcpkgRoot "share\pkgconfig" + $pkgConfigLibDir = "$profilePC;$hostLLVMPC;$targetLibPC;$targetSharePC" + @" + @echo off + setlocal + set "PKG_CONFIG_LIBDIR=$pkgConfigLibDir" + "$pkgconf" --dont-define-prefix %* + "@ | Set-Content -Encoding ascii (Join-Path $targetTools "pkg-config.cmd") + + $compilerUnix = $compiler.Replace('\', '/') + $compilerXXUnix = $compilerXX.Replace('\', '/') + $pkgconfUnix = $pkgconf.Replace('\', '/') + $pkgConfigLibDirUnix = $pkgConfigLibDir.Replace('\', '/') + @" + #!/usr/bin/env bash + exec "$compilerUnix" "`$@" + "@ | Set-Content -Encoding ascii (Join-Path $targetTools "clang") + @" + #!/usr/bin/env bash + exec "$compilerXXUnix" "`$@" + "@ | Set-Content -Encoding ascii (Join-Path $targetTools "clang++") + @" + #!/usr/bin/env bash + set -euo pipefail + PKG_CONFIG_LIBDIR='$pkgConfigLibDirUnix' exec "$pkgconfUnix" --dont-define-prefix "`$@" + "@ | Set-Content -Encoding ascii (Join-Path $targetTools "pkg-config") + + foreach ($package in @("llvm-${{ inputs.llvm-version }}", "bdw-gc", "libffi", "libuv", "openssl", "libcjson", "sqlite3", "zlib")) { + $env:PKG_CONFIG_LIBDIR = $pkgConfigLibDir + & $pkgconf --dont-define-prefix --modversion $package + if ($LASTEXITCODE -ne 0) { + throw "llvm-mingw pkg-config metadata for $package is unavailable" + } + } + Remove-Item Env:PKG_CONFIG_LIBDIR -ErrorAction SilentlyContinue + + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_MINGW_TARGET_BIN=$llvmMingwBin" + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_MINGW_TARGET_RUNTIME_BIN=$llvmMingwRuntimeBin" + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_MINGW_TARGET_TOOLS=$targetTools" + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_MINGW_TARGET_TRIPLE=i686-w64-windows-gnu" + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_MINGW_TARGET_VCPKG_ROOT=$vcpkgRoot" + + - name: Restore cross-host llvm-mingw + if: runner.os != 'Windows' && env.LLGO_MINGW_CROSS_ROOT == '' + id: cross_host_llvm_mingw_cache + uses: actions/cache/restore@v4 + with: + path: ${{ runner.tool_cache }}/llvm-mingw/20250114/ucrt-${{ runner.os }}-${{ runner.arch }} + key: llvm-mingw-20250114-ucrt-${{ runner.os }}-${{ runner.arch }}-${{ runner.os == 'macOS' && '80b2e7ade71ba2dfe9e8d27fe47ae5738b1fb8d34e057faa2beef3070392f2d6' || 'a16f52dee819797248e6c7d63b8b1e50a92119f45767ecd8e9633d1733b896e2' }} + + - name: Install cross-host llvm-mingw + if: runner.os != 'Windows' && env.LLGO_MINGW_CROSS_ROOT == '' && steps.cross_host_llvm_mingw_cache.outputs.cache-hit != 'true' + shell: bash + env: + LLVM_MINGW_VERSION: "20250114" + run: | + set -euo pipefail + case "$RUNNER_OS/$RUNNER_ARCH" in + Linux/X64) + asset="llvm-mingw-${LLVM_MINGW_VERSION}-ucrt-ubuntu-20.04-x86_64.tar.xz" + sha256=a16f52dee819797248e6c7d63b8b1e50a92119f45767ecd8e9633d1733b896e2 + ;; + macOS/ARM64|macOS/X64) + asset="llvm-mingw-${LLVM_MINGW_VERSION}-ucrt-macos-universal.tar.xz" + sha256=80b2e7ade71ba2dfe9e8d27fe47ae5738b1fb8d34e057faa2beef3070392f2d6 + ;; + *) + echo "llvm-mingw has no qualified $RUNNER_OS/$RUNNER_ARCH host archive" >&2 + exit 1 + ;; + esac + + archive="$RUNNER_TEMP/$asset" + curl --fail --location --retry 5 --retry-all-errors \ + --output "$archive" \ + "https://github.com/mstorsjo/llvm-mingw/releases/download/${LLVM_MINGW_VERSION}/${asset}" + actual="$(shasum -a 256 "$archive" | awk '{print $1}')" + if [[ "$actual" != "$sha256" ]]; then + echo "llvm-mingw SHA-256 is $actual, want $sha256" >&2 + exit 1 + fi + + extract_root="$RUNNER_TEMP/llgo-llvm-mingw-cross-extract" + install_root="$RUNNER_TOOL_CACHE/llvm-mingw/${LLVM_MINGW_VERSION}/ucrt-${RUNNER_OS}-${RUNNER_ARCH}" + mkdir -p "$extract_root" "$(dirname "$install_root")" + tar -xJf "$archive" -C "$extract_root" + source_root="$(find "$extract_root" -mindepth 1 -maxdepth 1 -type d \ + -exec test -x '{}/bin/i686-w64-mingw32-clang' \; -print -quit)" + if [[ -z "$source_root" ]]; then + echo "the llvm-mingw archive does not contain the i686 target compiler" >&2 + exit 1 + fi + mv "$source_root" "$install_root" + + - name: Save cross-host llvm-mingw + if: runner.os != 'Windows' && env.LLGO_MINGW_CROSS_ROOT == '' && steps.cross_host_llvm_mingw_cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ runner.tool_cache }}/llvm-mingw/20250114/ucrt-${{ runner.os }}-${{ runner.arch }} + key: llvm-mingw-20250114-ucrt-${{ runner.os }}-${{ runner.arch }}-${{ runner.os == 'macOS' && '80b2e7ade71ba2dfe9e8d27fe47ae5738b1fb8d34e057faa2beef3070392f2d6' || 'a16f52dee819797248e6c7d63b8b1e50a92119f45767ecd8e9633d1733b896e2' }} + + - name: Restore cross-host target dependencies + if: runner.os != 'Windows' + id: cross_host_vcpkg_cache + uses: actions/cache/restore@v4 + with: + path: ${{ runner.tool_cache }}/llgo-mingw-vcpkg/${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }}/${{ runner.os }}-${{ runner.arch }}/${{ inputs.windows-arch }} + key: llgo-llvm-mingw-20250114-ucrt-${{ runner.os }}-${{ runner.arch }}-${{ inputs.windows-arch }}-${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + + - name: Build cross-host target dependencies + if: runner.os != 'Windows' && steps.cross_host_vcpkg_cache.outputs.cache-hit != 'true' + shell: bash + env: + VCPKG_BASELINE: ddd0023b0eee70986e42ed49d9d4afb8098f212e + VCPKG_MANIFEST_HASH: ${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + run: | + set -euo pipefail + case "${{ inputs.windows-arch }}" in + 386) triplet=x86-mingw-dynamic ;; + amd64) triplet=x64-mingw-dynamic ;; + arm64) triplet=arm64-mingw-dynamic ;; + esac + + source_root="$RUNNER_TEMP/llgo-mingw-vcpkg-source" + install_root="$RUNNER_TOOL_CACHE/llgo-mingw-vcpkg/$VCPKG_MANIFEST_HASH/${RUNNER_OS}-${RUNNER_ARCH}/${{ inputs.windows-arch }}" + manifest_root="$GITHUB_WORKSPACE/.github/windows/vcpkg" + llvm_mingw_root="${LLGO_MINGW_CROSS_ROOT:-$RUNNER_TOOL_CACHE/llvm-mingw/20250114/ucrt-${RUNNER_OS}-${RUNNER_ARCH}}" + export PATH="$llvm_mingw_root/bin:$PATH" + + # Do not let target-specific compiler variables from the invoking job + # become vcpkg's macOS/Linux host compiler while dependencies are + # configured. + # The MinGW triplet still discovers its target-prefixed drivers from + # llvm-mingw on PATH. + unset CC CXX PKG_CONFIG PKG_CONFIG_PATH GOOS GOARCH + + # Keep the pinned checkout and host tool reusable if this composite is + # invoked more than once in a job; target build/install roots remain + # separate. + if [[ ! -d "$source_root/.git" ]]; then + git clone --no-checkout https://github.com/microsoft/vcpkg.git "$source_root" + fi + git -C "$source_root" checkout --detach "$VCPKG_BASELINE" + if [[ ! -x "$source_root/vcpkg" ]]; then + "$source_root/bootstrap-vcpkg.sh" -disableMetrics + fi + "$source_root/vcpkg" install \ + "--triplet=$triplet" \ + "--x-manifest-root=$manifest_root" \ + "--x-install-root=$install_root" \ + --clean-after-build + + - name: Save cross-host target dependencies + if: runner.os != 'Windows' && steps.cross_host_vcpkg_cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ runner.tool_cache }}/llgo-mingw-vcpkg/${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }}/${{ runner.os }}-${{ runner.arch }}/${{ inputs.windows-arch }} + key: llgo-llvm-mingw-20250114-ucrt-${{ runner.os }}-${{ runner.arch }}-${{ inputs.windows-arch }}-${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + + - name: Configure cross-host llvm-mingw target + if: runner.os != 'Windows' + shell: bash + env: + VCPKG_MANIFEST_HASH: ${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + run: | + set -euo pipefail + case "${{ inputs.windows-arch }}" in + 386) + prefix=i686-w64-mingw32 + canonical=i686-w64-windows-gnu + triplet=x86-mingw-dynamic + ;; + amd64) + prefix=x86_64-w64-mingw32 + canonical=x86_64-w64-windows-gnu + triplet=x64-mingw-dynamic + ;; + arm64) + prefix=aarch64-w64-mingw32 + canonical=aarch64-w64-windows-gnu + triplet=arm64-mingw-dynamic + ;; + esac + + llvm_mingw_root="${LLGO_MINGW_CROSS_ROOT:-$RUNNER_TOOL_CACHE/llvm-mingw/20250114/ucrt-${RUNNER_OS}-${RUNNER_ARCH}}" + target_bin="$llvm_mingw_root/bin" + runtime_bin="$llvm_mingw_root/$prefix/bin" + compiler="$target_bin/$prefix-clang" + compiler_xx="$target_bin/$prefix-clang++" + vcpkg_root="$RUNNER_TOOL_CACHE/llgo-mingw-vcpkg/$VCPKG_MANIFEST_HASH/${RUNNER_OS}-${RUNNER_ARCH}/${{ inputs.windows-arch }}/$triplet" + for path in "$compiler" "$compiler_xx" "$runtime_bin" "$vcpkg_root/include/gc/gc.h"; do + if [[ ! -e "$path" ]]; then + echo "the cross-host windows/${{ inputs.windows-arch }} target is incomplete: $path was not found" >&2 + exit 1 + fi + done + target="$($compiler -dumpmachine)" + case "$target" in + "$prefix"|"$canonical") ;; + *) + echo "llvm-mingw reports $target, want $canonical" >&2 + exit 1 + ;; + esac + + target_tools="$RUNNER_TEMP/llgo-mingw-target-tools-${{ inputs.windows-arch }}" + mkdir -p "$target_tools" + printf '#!/usr/bin/env bash\nexec %q "$@"\n' "$compiler" > "$target_tools/clang" + printf '#!/usr/bin/env bash\nexec %q "$@"\n' "$compiler_xx" > "$target_tools/clang++" + # Preserve the Unix host command if the composite is invoked again + # after a target-selecting wrapper has been added to PATH. + pkg_config="${LLGO_CROSS_HOST_PKG_CONFIG:-$(command -v pkg-config)}" + if [[ ! -x "$pkg_config" ]]; then + echo "the host pkg-config command is unavailable: $pkg_config" >&2 + exit 1 + fi + pkg_config_libdir="$vcpkg_root/lib/pkgconfig:$vcpkg_root/share/pkgconfig" + printf '#!/usr/bin/env bash\nPKG_CONFIG_LIBDIR=%q exec %q --dont-define-prefix "$@"\n' \ + "$pkg_config_libdir" "$pkg_config" > "$target_tools/pkg-config" + chmod +x "$target_tools/clang" "$target_tools/clang++" "$target_tools/pkg-config" + + for package in bdw-gc libffi libuv openssl libcjson sqlite3 zlib; do + "$target_tools/pkg-config" --modversion "$package" + done + + echo "LLGO_MINGW_TARGET_BIN=$target_bin" >> "$GITHUB_ENV" + echo "LLGO_MINGW_TARGET_RUNTIME_BIN=$runtime_bin" >> "$GITHUB_ENV" + echo "LLGO_MINGW_TARGET_TOOLS=$target_tools" >> "$GITHUB_ENV" + echo "LLGO_MINGW_TARGET_TRIPLE=$canonical" >> "$GITHUB_ENV" + echo "LLGO_MINGW_TARGET_VCPKG_ROOT=$vcpkg_root" >> "$GITHUB_ENV" + echo "LLGO_MINGW_CROSS_ROOT=$llvm_mingw_root" >> "$GITHUB_ENV" + echo "LLGO_CROSS_HOST_PKG_CONFIG=$pkg_config" >> "$GITHUB_ENV" diff --git a/.github/actions/test-windows-cross/action.yml b/.github/actions/test-windows-cross/action.yml new file mode 100644 index 0000000000..ead139a597 --- /dev/null +++ b/.github/actions/test-windows-cross/action.yml @@ -0,0 +1,103 @@ +name: "Test cross-host Windows build" +description: "Install one Windows GNU target and build representative artifacts without executing them" +inputs: + llgo: + description: "Unix-hosted LLGo compiler path" + required: true + windows-arch: + description: "Windows target GOARCH" + required: true + llvm-version: + description: "LLGo host LLVM major version" + required: false + default: "19" + +runs: + using: "composite" + steps: + - name: Install Windows GNU target + uses: ./.github/actions/setup-llvm-mingw + with: + llvm-version: ${{ inputs.llvm-version }} + windows-arch: ${{ inputs.windows-arch }} + + - name: Activate Windows GNU target + uses: ./.github/actions/activate-windows-target + with: + windows-abi: mingw + windows-arch: ${{ inputs.windows-arch }} + + - name: Build and inspect Windows GNU artifacts + shell: bash + env: + LLGO_CROSS_COMPILER: ${{ inputs.llgo }} + LLGO_CROSS_GOARCH: ${{ inputs.windows-arch }} + run: | + set -euo pipefail + if [[ "$GOOS/$GOARCH" != "windows/$LLGO_CROSS_GOARCH" ]]; then + echo "active target is $GOOS/$GOARCH, want windows/$LLGO_CROSS_GOARCH" >&2 + exit 1 + fi + if [[ ! -x "$LLGO_CROSS_COMPILER" ]]; then + echo "LLGo host compiler is not executable: $LLGO_CROSS_COMPILER" >&2 + exit 1 + fi + # Use the target-selecting wrappers exported by activation so callers + # do not need to assemble compiler flags and the host compiler cannot + # be selected accidentally through PATH. + export PATH="$LLGO_MINGW_TARGET_TOOLS:$PATH" + export CC="$LLGO_MINGW_TARGET_TOOLS/clang" + export CXX="$LLGO_MINGW_TARGET_TOOLS/clang++" + export PKG_CONFIG="$LLGO_MINGW_TARGET_TOOLS/pkg-config" + unset PKG_CONFIG_PATH + + output_dir="$RUNNER_TEMP/llgo-windows-cross-$LLGO_CROSS_GOARCH" + mkdir -p "$output_dir" + artifacts=() + for package in \ + windowsempty \ + windowsruntime \ + windowsffi \ + windowsstdlib \ + windowsnetwork; do + output="$output_dir/$package.exe" + ( + cd runtime + "$LLGO_CROSS_COMPILER" build -p=2 -o "$output" "./_test/$package" + ) + artifacts+=("$output") + done + + # Compile, but do not execute, both the general language suite and the + # Windows-specific OS/runtime suite. Native R11 jobs execute the same + # surfaces; this lane verifies that no host SDK or ABI leaks into them. + for package in go windows; do + output="$output_dir/test-$package.exe" + "$LLGO_CROSS_COMPILER" test -c -o "$output" "./test/$package" + artifacts+=("$output") + done + + case "$LLGO_CROSS_GOARCH" in + 386) machine='IMAGE_FILE_MACHINE_I386' ;; + amd64) machine='IMAGE_FILE_MACHINE_AMD64' ;; + arm64) machine='IMAGE_FILE_MACHINE_ARM64' ;; + *) + echo "unsupported Windows target architecture: $LLGO_CROSS_GOARCH" >&2 + exit 1 + ;; + esac + for artifact in "${artifacts[@]}"; do + file "$artifact" + headers="$(llvm-readobj --file-headers "$artifact")" + if ! grep -Fq "$machine" <<<"$headers"; then + echo "$artifact is not a windows/$LLGO_CROSS_GOARCH PE/COFF image:" >&2 + echo "$headers" >&2 + exit 1 + fi + imports="$(llvm-readobj --coff-imports "$artifact")" + if grep -Eqi '(msys-2\.0|cygwin1)\.dll' <<<"$imports"; then + echo "$artifact depends on an unsupported POSIX-emulation runtime:" >&2 + echo "$imports" >&2 + exit 1 + fi + done diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 7525ba599e..3b6f26bc5f 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -47,6 +47,13 @@ jobs: windows_arch: arm64 host_go_arch: x64 timeout: 120 + - os: windows-11-arm + id: windows-mingw-arm64 + display: Windows MinGW ARM64 + windows_abi: mingw + windows_arch: arm64 + host_go_arch: arm64 + timeout: 120 - os: windows-2022 id: windows-msvc-386 display: Windows MSVC 386 @@ -54,6 +61,13 @@ jobs: windows_arch: "386" host_go_arch: x64 timeout: 90 + - os: windows-2022 + id: windows-mingw-386 + display: Windows MinGW 386 + windows_abi: mingw + windows_arch: "386" + host_go_arch: x64 + timeout: 90 runs-on: ${{ matrix.os }} timeout-minutes: ${{ matrix.timeout }} env: @@ -129,6 +143,49 @@ jobs: build_host_compiler . .benchmark/llgo.exe fi + - name: Bundle MinGW host compiler runtime + if: runner.os == 'Windows' && matrix.windows_abi == 'mingw' && matrix.windows_arch == '386' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $hostBin = Join-Path $env:LLGO_MINGW_HOST_ROOT "bin" + $objdump = Join-Path $hostBin "llvm-objdump.exe" + if (-not (Test-Path $objdump)) { + throw "The MinGW host llvm-objdump is unavailable: $objdump" + } + + # Target DLL directories must remain first on PATH when benchmark + # programs run. Keep the host compiler architecture-safe without + # changing that target environment by placing its DLL closure beside + # the prebuilt compiler, which Windows searches before PATH. + $queue = [Collections.Generic.Queue[string]]::new() + Get-ChildItem "$env:GITHUB_WORKSPACE\.benchmark\*.exe" | + ForEach-Object { $queue.Enqueue($_.FullName) } + $copied = @{} + while ($queue.Count -ne 0) { + $binary = $queue.Dequeue() + foreach ($line in (& $objdump -p $binary)) { + if ($line -notmatch '^\s*DLL Name:\s*(\S+)\s*$') { + continue + } + $name = $Matches[1] + if ($copied.ContainsKey($name)) { + continue + } + $source = Join-Path $hostBin $name + if (-not (Test-Path $source)) { + continue + } + $destination = Join-Path "$env:GITHUB_WORKSPACE\.benchmark" $name + Copy-Item -LiteralPath $source -Destination $destination + $copied[$name] = $true + $queue.Enqueue($destination) + } + } + if ($copied.Count -eq 0) { + throw "No MinGW host runtime DLLs were discovered" + } + - name: Activate native Windows target if: runner.os == 'Windows' && matrix.windows_arch != 'amd64' uses: ./.github/actions/activate-windows-target diff --git a/.github/workflows/goroot.yml b/.github/workflows/goroot.yml index 9054e30aba..abcdf8dd14 100644 --- a/.github/workflows/goroot.yml +++ b/.github/workflows/goroot.yml @@ -45,16 +45,8 @@ jobs: windows_arch: arm64 - os: ubuntu-latest windows_arch: "386" - - os: windows-2022 - windows_abi: mingw - windows_arch: arm64 - - os: windows-2022 - windows_abi: mingw - windows_arch: "386" - os: windows-2022 windows_arch: arm64 - - os: windows-11-arm - windows_abi: mingw - os: windows-11-arm windows_arch: amd64 - os: windows-11-arm @@ -73,7 +65,7 @@ jobs: - name: Set up Go for building LLGo and the runner uses: ./.github/actions/setup-go with: - architecture: ${{ startsWith(matrix.os, 'windows') && 'x64' || '' }} + architecture: ${{ matrix.os == 'windows-11-arm' && matrix.windows_abi == 'mingw' && 'arm64' || startsWith(matrix.os, 'windows') && 'x64' || '' }} - name: Build LLGo and the GOROOT runner shell: bash @@ -269,14 +261,18 @@ jobs: { echo '## GOROOT run summary' echo - echo "Received $report_count/16 shard reports." + echo "Received $report_count/32 shard reports." echo echo '| Platform / toolchain | Shards | Selected | Observed | Passed | Failed | Skipped |' echo '|---|---:|---:|---:|---:|---:|---:|' write_row "Darwin · Go $primary_version" darwin/arm64 "$primary_version" 4 write_row "Linux · Go $primary_version" linux/amd64 "$primary_version" 4 write_row "Windows MSVC · Go $primary_version" windows-msvc/amd64 "$primary_version" 4 + write_row "Windows MSVC ARM64 · Go $primary_version" windows-msvc/arm64 "$primary_version" 4 + write_row "Windows MSVC 386 · Go $primary_version" windows-msvc/386 "$primary_version" 4 write_row "Windows MinGW · Go $primary_version" windows-mingw/amd64 "$primary_version" 4 + write_row "Windows MinGW ARM64 · Go $primary_version" windows-mingw/arm64 "$primary_version" 4 + write_row "Windows MinGW 386 · Go $primary_version" windows-mingw/386 "$primary_version" 4 echo echo '_Passed means the runner classification succeeded; expected xfail/not-applicable failures and classified flakes are counted as Passed._' echo @@ -291,8 +287,8 @@ jobs: fi } >>"$GITHUB_STEP_SUMMARY" - if [[ "$report_count" -ne 16 ]]; then - echo "error: expected 16 shard reports, got $report_count" >&2 + if [[ "$report_count" -ne 32 ]]; then + echo "error: expected 32 shard reports, got $report_count" >&2 exit 1 fi if [[ "$GOROOT_RESULT" != success ]]; then diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 590dcb1424..a3157725f3 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -43,11 +43,21 @@ jobs: windows_abi: msvc windows_arch: arm64 host_go_arch: x64 + - os: windows-11-arm + llvm: 19 + windows_abi: mingw + windows_arch: arm64 + host_go_arch: arm64 - os: windows-2022 llvm: 19 windows_abi: msvc windows_arch: "386" host_go_arch: x64 + - os: windows-2022 + llvm: 19 + windows_abi: mingw + windows_arch: "386" + host_go_arch: x64 runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -176,6 +186,22 @@ jobs: shell: bash run: | echo "Testing ESP32-C3 startup regressions..." + if [[ "$RUNNER_OS" == "Windows" && "${{ matrix.windows_abi }}" == "mingw" ]]; then + # Native target activation can put an architecture-limited + # llvm-mingw bundle first on PATH. Inspect the RISC-V firmware + # with the architecture-native host LLVM tools instead; this does + # not change the compiler/linker profile selected by LLGo. + host_llvm_bin="$(cygpath -u "$LLGO_MINGW_HOST_ROOT")/bin" + export LLVM_OBJDUMP="$host_llvm_bin/llvm-objdump.exe" + export LLVM_NM="$host_llvm_bin/llvm-nm.exe" + export LLVM_READELF="$host_llvm_bin/llvm-readelf.exe" + for llvm_tool in "$LLVM_OBJDUMP" "$LLVM_NM" "$LLVM_READELF"; do + if [[ ! -x "$llvm_tool" ]]; then + echo "required host LLVM inspection tool not found: $llvm_tool" >&2 + exit 1 + fi + done + fi # esptool inspects target firmware but runs on the CI host. For a # cross-architecture Windows lane, use the x64 host Python so pip # can install its native dependencies without cross-building them. @@ -210,7 +236,9 @@ jobs: run: sudo apt-get install -y lldb-${{matrix.llvm}} - name: Install MinGW LLDB for integration tests - if: ${{ matrix.os == 'windows-2022' && matrix.windows_abi == 'mingw' }} + # R12 qualifies debugger behavior for the new GNU architectures. Keep + # R11 focused on their language/runtime and C interoperability gates. + if: ${{ matrix.os == 'windows-2022' && matrix.windows_abi == 'mingw' && matrix.windows_arch == 'amd64' }} shell: msys2 {0} env: MSYS2_LLDB_PACKAGE_VERSION: "19.1.7-1" @@ -237,17 +265,14 @@ jobs: echo "Test lldb with llgo plugin on ${{matrix.os}} with LLVM ${{matrix.llvm}}" bash cmd/llgo/lldbtest/runtest.sh -v - # Windows/386 debugging is qualified in R12. The x64 LLDB follows the - # WoW64 exception dispatcher instead of the 32-bit source frame, so its - # variable results are not evidence for or against R10 runtime support. - name: LLDB integration tests (MSVC) - if: ${{ matrix.windows_abi == 'msvc' && matrix.windows_arch != '386' }} + if: ${{ matrix.windows_abi == 'msvc' }} shell: pwsh run: | - if ("${{ matrix.windows_arch }}" -eq "arm64") { - # Use the native debugger and Python runtime. The x64 debugger can - # start an ARM64 process through Windows emulation, but LLDB then - # reports register-located locals as unavailable. + if ("${{ matrix.windows_arch }}" -ne "amd64") { + # Use the target-native debugger and Python runtime. The x64 + # debugger cannot reliably inspect ARM64 register variables or + # cross the WoW64 exception dispatcher into a 32-bit source frame. $env:PYTHONHOME = $env:LLGO_WINDOWS_LLDB_PYTHON_HOME $env:PYTHONPATH = $env:LLGO_WINDOWS_LLDB_PYTHONPATH $env:PYTHONUTF8 = "1" @@ -265,7 +290,7 @@ jobs: } - name: LLDB integration tests (MinGW) - if: ${{ matrix.windows_abi == 'mingw' }} + if: ${{ matrix.windows_abi == 'mingw' && matrix.windows_arch == 'amd64' }} shell: msys2 {0} run: | # Git Bash prepends its own MinGW runtime DLLs to PATH. Use the @@ -273,9 +298,67 @@ jobs: echo "Test lldb with llgo plugin on ${{matrix.os}} with LLVM ${{matrix.llvm}}" bash cmd/llgo/lldbtest/runtest.sh -v - - name: DWARF standard tests - if: ${{ startsWith(matrix.os, 'macos') && matrix.os != 'macos-15-intel' }} - run: go test -timeout 15m ./internal/build -run '^TestStandardDWARF$' -count=1 -v + - name: LLDB integration tests (MinGW cross-architecture) + if: ${{ matrix.windows_abi == 'mingw' && matrix.windows_arch != 'amd64' }} + shell: pwsh + run: | + $env:PYTHONHOME = $env:LLGO_WINDOWS_LLDB_PYTHON_HOME + $env:PYTHONPATH = $env:LLGO_WINDOWS_LLDB_PYTHONPATH + $env:PYTHONUTF8 = "1" + $env:PYTHONIOENCODING = "utf-8" + $env:PATH = "$env:LLGO_WINDOWS_LLDB_PYTHON_HOME;$env:PATH" + Write-Host "Test lldb with llgo plugin on ${{matrix.os}} with LLVM ${{matrix.llvm}}" + bash cmd/llgo/lldbtest/runtest.sh -v + if ($LASTEXITCODE -ne 0) { + throw "LLDB integration tests failed with exit code $LASTEXITCODE" + } + + - name: Native debug metadata tests + # This Go test binary links the host LLVM libraries, so a Windows x64 + # host cannot cross-link it as 386 or ARM64. The target-native LLDB + # integration steps above cover debug metadata on those architectures. + if: ${{ (startsWith(matrix.os, 'macos') && matrix.os != 'macos-15-intel') || (runner.os == 'Windows' && matrix.windows_arch == 'amd64') }} + run: go test -timeout 15m ./internal/build -run '^(TestStandardDWARF|TestWindowsPDBLinkPath)$' -count=1 -v + + # Keep shell setup last so its POSIX compatibility tools cannot affect + # the native compiler, runtime, demo, or debugger qualification above. + - name: Set up the MSYS2 host shell for MSVC + if: ${{ matrix.windows_abi == 'msvc' }} + uses: msys2/setup-msys2@v2 + with: + msystem: MSYS + path-type: inherit + update: false + + - name: Test from the MSYS2 host shell + if: ${{ runner.os == 'Windows' }} + shell: msys2 {0} + run: bash .github/workflows/test_windows_host_shell.sh + + - name: Install Cygwin for host-shell qualification + if: ${{ matrix.os == 'windows-2022' && matrix.windows_arch == 'amd64' }} + id: cygwin + uses: cygwin/cygwin-install-action@v6 + with: + add-to-path: false + + - name: Test from the Cygwin host shell + if: ${{ matrix.os == 'windows-2022' && matrix.windows_arch == 'amd64' }} + shell: pwsh + env: + CYGWIN_ROOT: ${{ steps.cygwin.outputs.root }} + run: | + $bash = Join-Path $env:CYGWIN_ROOT "bin\bash.exe" + if (-not (Test-Path $bash)) { + throw "Cygwin bash was not found at $bash" + } + # The earlier MSYS2 setup remains on the inherited Windows PATH. Use + # Cygwin's own path converter and shell explicitly so an MSYS + # cygpath cannot turn D:\ into the invalid Cygwin path /d/. + & $bash --noprofile --norc -c 'export PATH=/usr/bin:/bin:$PATH; cd "$(/usr/bin/cygpath -u "$GITHUB_WORKSPACE")" && /usr/bin/bash .github/workflows/test_windows_host_shell.sh' + if ($LASTEXITCODE -ne 0) { + throw "Cygwin host-shell qualification failed with exit code $LASTEXITCODE" + } test: name: test (${{ matrix.lane }}, ${{ matrix.os }}${{ matrix.windows_abi && format(', {0}', matrix.windows_abi) || '' }}${{ matrix.windows_arch && format(', {0}', matrix.windows_arch) || '' }}, Go ${{ matrix.go_version }}, shard ${{ matrix.shard_index }}/${{ matrix.shard_total }}) @@ -347,6 +430,15 @@ jobs: windows_abi: msvc windows_arch: arm64 host_go_arch: x64 + - os: windows-11-arm + llvm: 19 + go_version: "1.27" + lane: full + shard_index: "0" + shard_total: "1" + windows_abi: mingw + windows_arch: arm64 + host_go_arch: arm64 - os: windows-2022 llvm: 19 go_version: "1.27" @@ -356,6 +448,15 @@ jobs: windows_abi: msvc windows_arch: "386" host_go_arch: x64 + - os: windows-2022 + llvm: 19 + go_version: "1.27" + lane: full + shard_index: "0" + shard_total: "1" + windows_abi: mingw + windows_arch: "386" + host_go_arch: x64 runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -486,6 +587,43 @@ jobs: iwasm --stack-size=819200000 --heap-size=800000000 hello.wasm + windows-gnu-cross: + name: windows GNU cross (${{ matrix.os }}, ${{ matrix.windows_arch }}) + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-latest] + windows_arch: [amd64, arm64, "386"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + + - name: Install host dependencies + uses: ./.github/actions/setup-deps + with: + llvm-version: 19 + + - name: Set up Go for building llgo + uses: ./.github/actions/setup-go + with: + go-version: "1.27.0" + + - name: Build Unix host compiler + shell: bash + run: | + go build -p=2 -o "$RUNNER_TEMP/llgo-host" ./cmd/llgo + # This development binary lives outside LLGO_ROOT/bin. Point it at + # the checkout explicitly so standard-library source patches are + # available to the following cross-host builds. + echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> "$GITHUB_ENV" + + - name: Test cross-host windows/${{ matrix.windows_arch }} + uses: ./.github/actions/test-windows-cross + with: + llgo: ${{ runner.temp }}/llgo-host + windows-arch: ${{ matrix.windows_arch }} + wasm-runtime: name: wasm-runtime (Go 1.24 and 1.27) timeout-minutes: 30 diff --git a/.github/workflows/test_windows_host_shell.sh b/.github/workflows/test_windows_host_shell.sh new file mode 100755 index 0000000000..19069268b6 --- /dev/null +++ b/.github/workflows/test_windows_host_shell.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${LLGO_WINDOWS_ABI:?LLGO_WINDOWS_ABI is required}" +: "${LLGO_WINDOWS_ARCH:?LLGO_WINDOWS_ARCH is required}" + +if [[ -n "${LLGO_TEST_COMPILER:-}" ]]; then + llgo=$(cygpath -u "$LLGO_TEST_COMPILER") +else + llgo=$(command -v llgo.exe || command -v llgo) +fi +temp_root=$(cygpath -u "$RUNNER_TEMP") +source_dir=$(mktemp -d "$temp_root/llgo-windows-host-shell.XXXXXX") +trap 'rm -rf "$source_dir"' EXIT + +cat >"$source_dir/go.mod" <<'EOF' +module example.com/llgo-windows-host-shell + +go 1.27 +EOF +cat >"$source_dir/main.go" <&2; exit 1 ;; +esac +case "$LLGO_WINDOWS_ABI" in + msvc) target="$target_arch-pc-windows-msvc" ;; + mingw) target="$target_arch-w64-windows-gnu" ;; + *) echo "unsupported Windows ABI: $LLGO_WINDOWS_ABI" >&2; exit 1 ;; +esac + +# An amd64 profile can be rediscovered from the shell's native Clang. Other +# architectures deliberately keep the compiler selected by target activation: +# the shell's native Clang may identify the right ABI but does not necessarily +# contain the requested architecture's CRT and libraries. +if [[ "$LLGO_WINDOWS_ARCH" == amd64 ]]; then + unset CC CXX +fi +executable="$source_dir/host-shell.exe" +# MSYS2 translates POSIX-looking arguments before invoking a native Windows +# program, while Cygwin deliberately does not. Use a native path for tool +# arguments and retain the shell path for execution and cleanup. +native_executable=$(cygpath -w "$executable") +set +e +trace=$(cd "$source_dir" && "$llgo" build -x -o "$native_executable" . 2>&1) +build_status=$? +set -e +if [[ $build_status -ne 0 ]]; then + echo "host-shell build failed with exit code $build_status:" >&2 + echo "$trace" >&2 + exit "$build_status" +fi +if ! grep -Fq -- "$target" <<<"$trace"; then + echo "host shell did not retain target $target:" >&2 + echo "$trace" >&2 + exit 1 +fi + +readobj=$(command -v llvm-readobj.exe || command -v llvm-readobj) +set +e +imports=$($readobj --coff-imports "$native_executable" 2>&1) +readobj_status=$? +set -e +if [[ $readobj_status -ne 0 ]]; then + echo "$readobj failed with exit code $readobj_status:" >&2 + echo "$imports" >&2 + exit "$readobj_status" +fi + +set +e +output=$($executable 2>&1) +run_status=$? +set -e +if [[ $run_status -ne 0 ]]; then + echo "$executable failed with exit code $run_status:" >&2 + echo "$output" >&2 + echo "COFF imports:" >&2 + echo "$imports" >&2 + if command -v cygcheck >/dev/null 2>&1; then + echo "Cygwin dependency resolution:" >&2 + cygcheck "$executable" >&2 || true + fi + exit "$run_status" +fi +want="windows-${LLGO_WINDOWS_ABI}-${LLGO_WINDOWS_ARCH}-host-shell" +if [[ "$output" != "$want" ]]; then + echo "$executable printed '$output', want '$want'" >&2 + exit 1 +fi + +forbidden='(msys-2\.0|cygwin1)\.dll' +if [[ "$LLGO_WINDOWS_ABI" == msvc ]]; then + forbidden='(msys-2\.0|cygwin1|libwinpthread)\.dll' +fi +if grep -Eqi "$forbidden" <<<"$imports"; then + echo "host-shell build has an unsupported POSIX-emulation dependency:" >&2 + echo "$imports" >&2 + exit 1 +fi diff --git a/.github/workflows/test_windows_runtime.ps1 b/.github/workflows/test_windows_runtime.ps1 index 931fc5cd25..b0e354ecf7 100644 --- a/.github/workflows/test_windows_runtime.ps1 +++ b/.github/workflows/test_windows_runtime.ps1 @@ -24,29 +24,27 @@ New-Item -ItemType Directory $out | Out-Null $env:LLGO_ROOT = $root $env:LLGO_BUILD_CACHE = "off" -# Each lane executes its selected architecture below. Also compile the raw -# SyscallN bridge for every Go-supported Windows architecture so a change in -# one lane cannot silently break assembly selected by another lane. +# Compile the raw SyscallN bridge with this lane's target compiler. The CI +# matrix gives every Go-supported Windows architecture its own job, so keeping +# this probe target-local avoids mixing architecture-specific toolchains. $syscallAsm = Join-Path $root "runtime\internal\lib\runtime\_wrap\syscall_windows.S" $targetSuffix = if ($Profile -eq "msvc") { "pc-windows-msvc" } else { "w64-windows-gnu" } -foreach ($syscallTarget in @( - @{ Triple = "i686-$targetSuffix"; Symbol = "_llgo_windows_syscall" }, - @{ Triple = "x86_64-$targetSuffix"; Symbol = "llgo_windows_syscall" }, - @{ Triple = "aarch64-$targetSuffix"; Symbol = "llgo_windows_syscall" } -)) { - $syscallObj = Join-Path $out ("syscall-{0}.obj" -f $syscallTarget.Triple) - & $clangExe "--target=$($syscallTarget.Triple)" -c $syscallAsm -o $syscallObj - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - $symbols = & $llvmNmExe --defined-only $syscallObj | Out-String - if (-not $symbols.Contains($syscallTarget.Symbol)) { - throw "$($syscallTarget.Triple) bridge is missing $($syscallTarget.Symbol)" - } - if ($syscallTarget.Triple.StartsWith("i686-") -and - $symbols -notmatch '(?m)^00000001 [aA] @feat\.00\r?$') { - throw "$($syscallTarget.Triple) bridge is not marked SafeSEH-compatible" - } +$syscallTarget = switch ($GoArch) { + "386" { @{ Triple = "i686-$targetSuffix"; Symbol = "_llgo_windows_syscall" } } + "amd64" { @{ Triple = "x86_64-$targetSuffix"; Symbol = "llgo_windows_syscall" } } + "arm64" { @{ Triple = "aarch64-$targetSuffix"; Symbol = "llgo_windows_syscall" } } +} +$syscallObj = Join-Path $out ("syscall-{0}.obj" -f $syscallTarget.Triple) +& $clangExe "--target=$($syscallTarget.Triple)" -c $syscallAsm -o $syscallObj +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} +$symbols = & $llvmNmExe --defined-only $syscallObj | Out-String +if (-not $symbols.Contains($syscallTarget.Symbol)) { + throw "$($syscallTarget.Triple) bridge is missing $($syscallTarget.Symbol)" +} +if ($GoArch -eq "386" -and $symbols -notmatch '(?m)^00000001 [aA] @feat\.00\r?$') { + throw "$($syscallTarget.Triple) bridge is not marked SafeSEH-compatible" } $runtime = Join-Path $out "windows-runtime-smoke.exe" diff --git a/.github/workflows/test_windows_toolchain_profile.ps1 b/.github/workflows/test_windows_toolchain_profile.ps1 index 3d0ee71090..d6e4c7c3c2 100644 --- a/.github/workflows/test_windows_toolchain_profile.ps1 +++ b/.github/workflows/test_windows_toolchain_profile.ps1 @@ -70,17 +70,27 @@ $mainSource.Replace('$Profile', $Profile).Replace('$GoArch', $GoArch) | Set-Content -Encoding utf8 (Join-Path $sourceDir "main.go") $llgo = (Get-Command llgo.exe).Source -foreach ($tool in @("clang.exe", "clang++.exe", "llvm-config.exe")) { +$profileClangDir = Split-Path (Get-Command clang.exe).Source +foreach ($tool in @("clang", "clang++", "llvm-config")) { $path = (Get-Command $tool).Source if ($Profile -eq "msvc" -and $path -match '(?i)[\\/](msys64|cygwin)[\\/]') { throw "$tool unexpectedly resolves through a POSIX environment: $path" } - if ($Profile -eq "mingw" -and $path -notmatch '(?i)[\\/]clang64[\\/]') { - throw "$tool does not belong to the independent CLANG64 profile: $path" + if ($Profile -eq "mingw") { + $profilePattern = switch ($GoArch) { + "386" { + if ($tool -eq "llvm-config") { '(?i)[\\/]clang64[\\/]' } else { '(?i)[\\/]llgo-mingw-target-tools[\\/]' } + } + "amd64" { '(?i)[\\/]clang64[\\/]' } + "arm64" { '(?i)[\\/]clangarm64[\\/]' } + } + if ($path -notmatch $profilePattern) { + throw "$tool does not belong to the windows/$GoArch MinGW profile: $path" + } } } $pkgConfig = (Get-Command pkg-config).Source -if ($pkgConfig -notmatch '(?i)[\\/]llgo-(msvc|mingw)-tools[\\/]pkg-config\.cmd$') { +if ($pkgConfig -notmatch '(?i)[\\/]llgo-(msvc|mingw)(-target)?-tools[\\/]pkg-config\.cmd$') { throw "pkg-config does not resolve to the profile-local command wrapper: $pkgConfig" } $pkgConfigShell = Join-Path (Split-Path $pkgConfig) "pkg-config" @@ -96,12 +106,26 @@ if ($Profile -eq "msvc" -and $env:LLGO_MSYS2_LOCATION) { & pkg-config --modversion llvm-19 | Out-Null Assert-Success "Reading LLVM metadata through the profile-local pkg-config" -$compilerTarget = (& clang.exe -dumpmachine).Trim() +$compilerArgs = @() +if ($Profile -eq "msvc") { + # Official LLVM is an x64 host compiler in every MSVC lane. Apply the + # activated target explicitly before checking cross-architecture output. + $compilerArgs = @("--target=$env:LLGO_WINDOWS_TARGET_TRIPLE") +} +$compilerTarget = (& clang @compilerArgs -dumpmachine).Trim() Assert-Success "Reading the Clang target" $targetPattern = if ($Profile -eq "msvc") { '-windows-msvc$' } else { '-(windows-gnu|mingw32)$' } if ($compilerTarget -notmatch $targetPattern) { throw "$Profile Clang reports incompatible target $compilerTarget" } +$targetArch = switch ($GoArch) { + "386" { "i686" } + "amd64" { "x86_64|amd64" } + "arm64" { "aarch64" } +} +if ($compilerTarget -notmatch "^($targetArch)-") { + throw "$Profile Clang reports $compilerTarget for windows/$GoArch" +} $savedCC = $env:CC $savedCXX = $env:CXX @@ -118,15 +142,15 @@ try { } finally { Pop-Location } - $targetArch = switch ($GoArch) { + $canonicalArch = switch ($GoArch) { "386" { "i686" } "amd64" { "x86_64" } "arm64" { "aarch64" } } $canonicalTarget = if ($Profile -eq "msvc") { - "$targetArch-pc-windows-msvc" + "$canonicalArch-pc-windows-msvc" } else { - "$targetArch-w64-windows-gnu" + "$canonicalArch-w64-windows-gnu" } if ($trace -notmatch [regex]::Escape($canonicalTarget)) { throw "Unset CC/CXX did not select the canonical $Profile target:`n$trace" @@ -138,6 +162,25 @@ try { & $env:ComSpec /d /s /c $cmdLine Assert-Success "Building with unset CC/CXX from cmd.exe" Assert-NativeOutput $cmdExe + + if ($Profile -eq "msvc") { + . (Join-Path $env:GITHUB_WORKSPACE ".github\windows\msvc-target.ps1") + $target = Get-LLGoWindowsMSVCTarget -GoArch $GoArch + $installPath = Find-LLGoVisualStudio2022 -GoArch $GoArch + $vsDevCmd = Join-Path $installPath "Common7\Tools\VsDevCmd.bat" + $vsDevExe = Join-Path $sourceDir "vsdev.exe" + # VsDevCmd prepends Visual Studio's optional bundled Clang. That compiler + # does not necessarily install compiler-rt for every selected target (the + # ARM64 lane is one example). Keep the standalone full-target LLVM profile + # selected by setup-deps ahead of it while retaining DevShell's SDK, CRT, + # and linker environment. + $vsDevLine = 'call "' + $vsDevCmd + '" -no_logo -arch=' + $target.VisualStudio + + ' -host_arch=x64 && set "PATH=' + $profileClangDir + ';%PATH%" && set "CC=" && set "CXX=" && cd /d "' + $sourceDir + + '" && "' + $llgo + '" build -o "' + $vsDevExe + '" .' + & $env:ComSpec /d /s /c $vsDevLine + Assert-Success "Building from a fresh Visual Studio Developer Shell" + Assert-NativeOutput $vsDevExe + } } finally { if ($null -eq $savedCC) { Remove-Item Env:CC -ErrorAction SilentlyContinue } else { $env:CC = $savedCC } if ($null -eq $savedCXX) { Remove-Item Env:CXX -ErrorAction SilentlyContinue } else { $env:CXX = $savedCXX } diff --git a/_demo/embed/test_esp32c3_startup.sh b/_demo/embed/test_esp32c3_startup.sh index c46160732b..e644161a98 100755 --- a/_demo/embed/test_esp32c3_startup.sh +++ b/_demo/embed/test_esp32c3_startup.sh @@ -38,6 +38,14 @@ TEST_GO="$TEMP_DIR/main.go" TEST_ELF="test.elf" TEST_BIN="test.bin" +# A native target profile may put architecture-limited LLVM utilities first on +# PATH. In particular, llvm-mingw's 386 bundle cannot inspect a RISC-V ELF. +# CI can select host LLVM utilities explicitly while leaving the compiler and +# linker profile used by LLGo unchanged. +LLVM_OBJDUMP_CMD="${LLVM_OBJDUMP:-llvm-objdump}" +LLVM_NM_CMD="${LLVM_NM:-llvm-nm}" +LLVM_READELF_CMD="${LLVM_READELF:-llvm-readelf}" + cleanup() { rm -rf "$TEMP_DIR" } @@ -148,7 +156,7 @@ echo "" echo "=== Test 1: Verify newlib startup (calls __libc_init_array) ===" # Disassemble _start and check for __libc_init_array call -if llvm-objdump -d "$TEST_ELF" | grep -A50 "<_start>:" | grep "__libc_init_array" > /dev/null; then +if "$LLVM_OBJDUMP_CMD" -d "$TEST_ELF" | grep -A50 "<_start>:" | grep "__libc_init_array" > /dev/null; then echo "✓ PASS: _start calls __libc_init_array" echo " ESP32-C3 uses newlib's standard startup" else @@ -157,7 +165,7 @@ else echo "" echo "Expected inheritance: esp32c3 → riscv32-nostart → riscv-nostart → riscv-basic" echo "Current _start disassembly:" - llvm-objdump -d "$TEST_ELF" | grep -A50 "<_start>:" || true + "$LLVM_OBJDUMP_CMD" -d "$TEST_ELF" | grep -A50 "<_start>:" || true exit 1 fi @@ -179,7 +187,7 @@ echo "=== Test 2: Verify __init_array_start symbol (ELF) ===" # $3 = __init_array_start (symbol name) # # We extract $1 and add "0x" prefix: 40380450 → 0x40380450 -INIT_ARRAY_START=$(llvm-nm "$TEST_ELF" | grep "__init_array_start" | awk '{print "0x"$1}') +INIT_ARRAY_START=$("$LLVM_NM_CMD" "$TEST_ELF" | grep "__init_array_start" | awk '{print "0x"$1}') if [ -z "$INIT_ARRAY_START" ]; then echo "✗ FAIL: __init_array_start symbol not found" exit 1 @@ -188,7 +196,7 @@ fi echo "✓ PASS: __init_array_start found at $INIT_ARRAY_START" INIT_ADDR_DEC=$((INIT_ARRAY_START)) -INIT_ARRAY_INFO=$(llvm-readelf -S "$TEST_ELF" | grep "\.init_array") +INIT_ARRAY_INFO=$("$LLVM_READELF_CMD" -S "$TEST_ELF" | grep "\.init_array") if [ -z "$INIT_ARRAY_INFO" ]; then echo "✗ FAIL: .init_array section not found" exit 1 diff --git a/cl/_testgo/defer5/in.go b/cl/_testgo/defer5/in.go index 69bf4b83b7..5f49d66462 100644 --- a/cl/_testgo/defer5/in.go +++ b/cl/_testgo/defer5/in.go @@ -32,7 +32,7 @@ func main() { // DARWIN-ARM64: [[SETJMP_RESULT:%[0-9]+]] = call i32 @sigsetjmp(ptr [[DEFER_JMPBUF]], i32 0) // LINUX-AMD64: [[SETJMP_RESULT:%[0-9]+]] = call i32 @__sigsetjmp(ptr [[DEFER_JMPBUF]], i32 0) // WINDOWS-ARM64: [[SETJMP_RESULT:%[0-9]+]] = call i32 @llgo_setjmp(ptr [[DEFER_JMPBUF]]) - // WINDOWS-AMD64: [[SETJMP_RESULT:%[0-9]+]] = call i32 @_setjmpex(ptr [[DEFER_JMPBUF]], ptr null) + // WINDOWS-AMD64: [[SETJMP_RESULT:%[0-9]+]] = call i32 @{{_setjmpex|__intrinsic_setjmpex}}(ptr [[DEFER_JMPBUF]], ptr null) // CHECK-NEXT: [[FIRST_ENTRY:%[0-9]+]] = icmp eq i32 [[SETJMP_RESULT]], 0 // CHECK-NEXT: br i1 [[FIRST_ENTRY]], label %{{.*}}, label %{{.*}} diff --git a/cl/_testlibc/setjmp/in.go b/cl/_testlibc/setjmp/in.go index 23b0251a07..86ae49afed 100644 --- a/cl/_testlibc/setjmp/in.go +++ b/cl/_testlibc/setjmp/in.go @@ -29,7 +29,7 @@ func fprintf(fp unsafe.Pointer, format *int8, __llgo_va_list ...any) int32 // DARWIN-ARM64-NEXT: [[RET:%[0-9]+]] = call i32 @sigsetjmp(ptr [[JMPBUF]], i32 0) // LINUX-AMD64-NEXT: [[RET:%[0-9]+]] = call i32 @__sigsetjmp(ptr [[JMPBUF]], i32 0) // WINDOWS-386-NEXT: [[RET:%[0-9]+]] = call i32 @_setjmp3(ptr [[JMPBUF]], i32 0) -// WINDOWS-AMD64-NEXT: [[RET:%[0-9]+]] = call i32 @_setjmpex(ptr [[JMPBUF]], ptr null) +// WINDOWS-AMD64-NEXT: [[RET:%[0-9]+]] = call i32 @{{_setjmpex|__intrinsic_setjmpex}}(ptr [[JMPBUF]], ptr null) // WINDOWS-ARM64-NEXT: [[RET:%[0-9]+]] = call i32 @llgo_setjmp(ptr [[JMPBUF]]) // CHECK-NEXT: [[FIRST:%[0-9]+]] = icmp eq i32 [[RET]], 0 // CHECK-NEXT: br i1 [[FIRST]], label %{{[^,]+}}, label %{{[^ ]+}} diff --git a/cmd/llgo/lldbtest/common.sh b/cmd/llgo/lldbtest/common.sh index abf8e12bc8..68358deb6d 100644 --- a/cmd/llgo/lldbtest/common.sh +++ b/cmd/llgo/lldbtest/common.sh @@ -4,7 +4,11 @@ LLDB_TEST_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # Function to find LLDB 18+ find_lldb() { - local lldb_paths=( + local lldb_paths=() + if [ -n "${LLGO_LLDB:-}" ]; then + lldb_paths+=("$LLGO_LLDB") + fi + lldb_paths+=( "/opt/homebrew/bin/lldb" "/usr/local/bin/lldb" "/usr/bin/lldb" @@ -29,7 +33,7 @@ find_lldb() { # Find LLDB 18+ LLDB_PATH=$(find_lldb) echo "LLDB_PATH: $LLDB_PATH" -$LLDB_PATH --version +"$LLDB_PATH" --version export LLDB_PATH # Default package path diff --git a/cmd/llgo/lldbtest/mixed/_wrap/mixed.c b/cmd/llgo/lldbtest/mixed/_wrap/mixed.c new file mode 100644 index 0000000000..cd18bcb7d7 --- /dev/null +++ b/cmd/llgo/lldbtest/mixed/_wrap/mixed.c @@ -0,0 +1,32 @@ +#if defined(_MSC_VER) +#define LLGO_NOINLINE __declspec(noinline) +#else +#define LLGO_NOINLINE __attribute__((noinline)) +#endif + +extern int llgo_lldb_go_callback(int value); +extern void llgo_lldb_go_fault_callback(void); + +static int *volatile llgo_lldb_null; + +static LLGO_NOINLINE int llgo_lldb_c_inner(int value) +{ + int result = llgo_lldb_go_callback(value + 1); + return result + 1; +} + +LLGO_NOINLINE int llgo_lldb_mixed_call(int value) +{ + int result = llgo_lldb_c_inner(value + 1); + return result + 1; +} + +LLGO_NOINLINE void llgo_lldb_c_fault(void) +{ + *llgo_lldb_null = 42; /* LLDB_BREAK: mixed_go_c_callback_fault */ +} + +LLGO_NOINLINE void llgo_lldb_mixed_fault_call(void) +{ + llgo_lldb_go_fault_callback(); +} diff --git a/cmd/llgo/lldbtest/mixed/mixed.go b/cmd/llgo/lldbtest/mixed/mixed.go new file mode 100644 index 0000000000..1f8cf947cc --- /dev/null +++ b/cmd/llgo/lldbtest/mixed/mixed.go @@ -0,0 +1,40 @@ +package main + +import ( + "os" + _ "unsafe" +) + +const LLGoFiles = "_wrap/mixed.c" + +//go:linkname lldbMixedCall C.llgo_lldb_mixed_call +func lldbMixedCall(value int32) int32 + +//go:linkname lldbMixedFaultCall C.llgo_lldb_mixed_fault_call +func lldbMixedFaultCall() + +//go:linkname lldbCFault C.llgo_lldb_c_fault +func lldbCFault() + +//go:noinline +//export llgo_lldb_go_callback +func llgo_lldb_go_callback(value int32) int32 { + callbackValue := value + 2 + println(callbackValue) // LLDB_BREAK: mixed_go_c_callback + return callbackValue +} + +//go:noinline +//export llgo_lldb_go_fault_callback +func llgo_lldb_go_fault_callback() { + lldbCFault() +} + +func main() { + if got := lldbMixedCall(39); got != 45 { + panic("mixed Go/C callback returned the wrong value") + } + if os.Getenv("LLGO_LLDB_FAULT_TEST") != "" { + lldbMixedFaultCall() + } +} diff --git a/cmd/llgo/lldbtest/runtest.sh b/cmd/llgo/lldbtest/runtest.sh index 3a6eef32fb..d175fcb08a 100755 --- a/cmd/llgo/lldbtest/runtest.sh +++ b/cmd/llgo/lldbtest/runtest.sh @@ -39,6 +39,11 @@ done # Build the project build_project "$package_path" || exit 1 +( + cd "$package_path" + llgo build "${LLDB_TEST_OPTLEVEL}" -ldflags=-w=false -o "debug-mixed.out" ./mixed +) || exit 1 + # Set up private paths for test results and auxiliary fixtures. test_tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/llgo-lldbtest.XXXXXX") trap 'rm -rf "$test_tmp_dir"' EXIT @@ -48,35 +53,43 @@ if command -v cygpath >/dev/null 2>&1; then result_file_for_lldb=$(cygpath -m "$result_file") fi -# Prepare LLDB commands -lldb_commands=( - "command script import ./test.py" - "script test.run_tests_with_result('./debug.out', ['main.go'], $verbose, $interactive, $plugin_path, '$result_file_for_lldb')" - "quit" -) - -# Prepare LLDB arguments without shell re-parsing. -lldb_args=() -for cmd in "${lldb_commands[@]}"; do - lldb_args+=("-o" "$cmd") -done - cd "$package_path" -# Run LLDB with the embedded LLGo plugin and the test script. -llgo lldb -lldb "$LLDB_PATH" -- "${lldb_args[@]}" -# Read the exit code from the result file -if [ -f "$result_file" ]; then +run_test_suite() { + local executable=$1 + local sources=$2 + local enable_fault=${3:-} + local lldb_commands=( + "command script import ./test.py" + "script test.run_tests_with_result('$executable', $sources, $verbose, $interactive, $plugin_path, '$result_file_for_lldb')" + "quit" + ) + local lldb_args=() + local cmd + for cmd in "${lldb_commands[@]}"; do + lldb_args+=("-o" "$cmd") + done + + rm -f "$result_file" + if [ -n "$enable_fault" ]; then + LLGO_LLDB_FAULT_TEST=1 llgo lldb -lldb "$LLDB_PATH" -- "${lldb_args[@]}" + else + llgo lldb -lldb "$LLDB_PATH" -- "${lldb_args[@]}" + fi + if [ ! -f "$result_file" ]; then + echo "Error: Could not find exit code file" + return 1 + fi + local exit_code exit_code=$(cat "$result_file") rm "$result_file" -else - echo "Error: Could not find exit code file" - exit 1 -fi + [ "$exit_code" -eq 0 ] +} -if [ "$exit_code" -ne 0 ]; then - exit "$exit_code" -fi +# Keep the mixed Go/C stack fixture separate: adding foreign calls to the large +# variable-formatting fixture can change otherwise unrelated DWARF locations. +run_test_suite './debug.out' "['main.go']" +run_test_suite './debug-mixed.out' "['mixed/mixed.go', 'mixed/_wrap/mixed.c']" fault llgo lldb -lldb "$LLDB_PATH" -- --batch "./debug.out" \ -o 'script import os; info = llgo_plugin.inspect_target(lldb.target); (info.schema_version == 1 and info.runtime_layout_version == 1 and info.pointer_size == lldb.target.GetAddressByteSize() and info.byte_order != "unknown") or os._exit(1)' \ diff --git a/cmd/llgo/lldbtest/test.py b/cmd/llgo/lldbtest/test.py index 274c79f25d..fd6bf66e10 100644 --- a/cmd/llgo/lldbtest/test.py +++ b/cmd/llgo/lldbtest/test.py @@ -211,6 +211,29 @@ def test_case(marker: str, expectations: List[tuple]) -> TestCase: ("namedInts", "[0]=11, [1]=12, [2]=13, [3]=14", "synthetic"), ("ints", "[]int{7, ... (1 more)}", "limited"), ]), + TestCase( + source_file="mixed/mixed.go", + marker="LLDB_BREAK: mixed_go_c_callback", + tests=[ + Test("callbackValue", "43"), + Test( + "stack frames", + "llgo_lldb_go_callback llgo_lldb_c_inner " + "llgo_lldb_mixed_call main.main", + "ordered_frames", + ), + ], + ), + TestCase( + source_file="mixed/_wrap/mixed.c", + marker="LLDB_BREAK: mixed_go_c_callback_fault", + tests=[Test( + "stack frames", + "llgo_lldb_c_fault llgo_lldb_go_fault_callback " + "llgo_lldb_mixed_fault_call main.main", + "ordered_frames", + )], + ), test_case("struct_values_initial", STRUCT_VALUES_INITIAL), test_case("struct_values_updated", STRUCT_VALUES_UPDATED), test_case("struct_ptrs_initial", STRUCT_VALUES_INITIAL), @@ -297,6 +320,8 @@ def __init__(self, executable_path: str, plugin_path: Optional[str] = None) -> N self.debugger.SetAsync(False) self.target: Optional[lldb.SBTarget] = None self.process: Optional[lldb.SBProcess] = None + self.frame: Optional[lldb.SBFrame] = None + self.breakpoint_line: Optional[int] = None self.type_mapping: Dict[str, str] = { 'long': 'int', 'unsigned long': 'uint', @@ -334,9 +359,13 @@ def set_breakpoint(self, file_spec: str, line_number: int) -> lldb.SBBreakpoint: raise LLDBTestException( f"Expected one breakpoint at {file_spec}:{line_number}, " f"found {bp.GetNumLocations()}") + location = bp.GetLocationAtIndex(0) + line_entry = location.GetAddress().GetLineEntry() + self.breakpoint_line = ( + line_entry.GetLine() if line_entry.IsValid() else line_number) return bp - def run_to_breakpoint(self) -> None: + def run_to_breakpoint(self, file_spec: str, line_number: int) -> None: if not self.process: self.process = self.target.LaunchSimple(None, None, os.getcwd()) else: @@ -344,6 +373,38 @@ def run_to_breakpoint(self) -> None: if self.process.GetState() != lldb.eStateStopped: raise LLDBTestException("Process didn't stop at breakpoint") + # Windows/386 reports the WoW64 exception dispatcher as frame zero + # while handling a software breakpoint. Select the source frame that + # owns the requested breakpoint before inspecting its variables. + thread = self.process.GetSelectedThread() + expected_file = os.path.normcase(os.path.basename(file_spec)) + expected_line = self.breakpoint_line or line_number + locations: List[str] = [] + for index in range(thread.GetNumFrames()): + frame = thread.GetFrameAtIndex(index) + line_entry = frame.GetLineEntry() + if not line_entry.IsValid(): + continue + source = line_entry.GetFileSpec().GetFilename() or "" + source_line = line_entry.GetLine() + locations.append( + f"{index}:{frame.GetFunctionName() or ''} " + f"at {source}:{source_line}") + if (os.path.normcase(source) == expected_file and + source_line == expected_line): + thread.SetSelectedFrame(index) + self.frame = frame + return + raise LLDBTestException( + f"No frame for breakpoint {file_spec}:{line_number} " + f"(resolved line {expected_line}); " + f"frames: {', '.join(locations)}") + + def get_selected_frame(self) -> lldb.SBFrame: + if not self.frame or not self.frame.IsValid(): + raise LLDBTestException("Breakpoint source frame was not selected") + return self.frame + def get_variable_value(self, var_expression: str) -> Optional[str]: value = self.get_variable(var_expression) if value and value.IsValid(): @@ -351,8 +412,8 @@ def get_variable_value(self, var_expression: str) -> Optional[str]: return None def get_variable(self, var_expression: str) -> Optional[lldb.SBValue]: - frame = self.process.GetSelectedThread().GetFrameAtIndex(0) - return llgo_plugin.evaluate_expression(frame, var_expression) + return llgo_plugin.evaluate_expression( + self.get_selected_frame(), var_expression) def get_variable_summary(self, var_expression: str) -> Optional[str]: value = self.get_variable(var_expression) @@ -376,12 +437,18 @@ def get_synthetic_children(self, var_expression: str) -> Optional[str]: return ", ".join(children) def get_all_variable_names(self) -> Set[str]: - frame = self.process.GetSelectedThread().GetFrameAtIndex(0) + frame = self.get_selected_frame() return set(var.GetName() for var in frame.GetVariables(True, True, False, True)) def get_current_function_name(self) -> str: - frame = self.process.GetSelectedThread().GetFrameAtIndex(0) - return frame.GetFunctionName() + return self.get_selected_frame().GetFunctionName() + + def get_frame_names(self) -> List[str]: + thread = self.process.GetSelectedThread() + return [ + thread.GetFrameAtIndex(index).GetFunctionName() or "" + for index in range(thread.GetNumFrames()) + ] def require_print_error(self, expression: str, expected: str) -> None: result = lldb.SBCommandReturnObject() @@ -475,7 +542,7 @@ def execute_tests(executable_path: str, test_cases: List[TestCase], verbose: boo f"{line_number} ({test_case.marker})") debugger.setup() debugger.set_breakpoint(test_case.source_file, line_number) - debugger.run_to_breakpoint() + debugger.run_to_breakpoint(test_case.source_file, line_number) if not results.case_results: debugger.require_print_error( "slice[bad]", "Unable to evaluate expression") @@ -511,7 +578,7 @@ def execute_tests(executable_path: str, test_cases: List[TestCase], verbose: boo def run_tests(executable_path: str, source_files: List[str], verbose: bool, interactive: bool, plugin_path: Optional[str]) -> int: - selected_sources = {os.path.basename(path) for path in source_files} + selected_sources = set(source_files) test_cases = [ test_case for test_case in TEST_CASES if test_case.source_file in selected_sources @@ -563,6 +630,26 @@ def execute_all_variables_test(test: Test, all_variable_names: Set[str]) -> Test def execute_single_variable_test(debugger: LLDBDebugger, test: Test) -> TestResult: + if test.mode == "ordered_frames": + frame_names = debugger.get_frame_names() + expected_names = test.expected_value.split() + next_frame = 0 + for expected_name in expected_names: + while (next_frame < len(frame_names) and + expected_name not in frame_names[next_frame]): + next_frame += 1 + if next_frame == len(frame_names): + return TestResult( + test=test, + status='fail', + actual=" -> ".join(frame_names) + ) + next_frame += 1 + return TestResult( + test=test, + status='pass', + actual=" -> ".join(frame_names) + ) if test.mode == "summary": actual_value = debugger.get_variable_summary(test.variable) elif test.mode == "synthetic": diff --git a/internal/build/build.go b/internal/build/build.go index abde7f16c6..be4dbda5c2 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -492,12 +492,11 @@ func Build(inv Invocation) (result []Package, resultErr error) { // Handle crosscompile configuration first to set correct GOOS/GOARCH forceEspClang := conf.ForceEspClang || conf.Target != "" nativeInput := crosscompile.NativeToolchainInput{} - if usesNativeWindowsToolchain(runtime.GOOS, conf) { - nativeInput, err = parseNativeToolchainInput(commands, conf.LinkOptions) + if usesWindowsToolchainProfile(runtime.GOOS, conf) { + nativeInput, err = parseNativeToolchainInput(commands, conf.LinkOptions, conf.Mode != ModeGen) if err != nil { return nil, err } - nativeInput.ResolveCrossArch = conf.Mode != ModeGen } export, err := crosscompile.UseWithGOARMAndToolchain(conf.Goos, conf.Goarch, conf.GOARM, conf.Target, IsWasiThreadsEnabled(), forceEspClang, conf.OptLevel, conf.ltoMode(), conf.goGlobalDCEEnabled(), nativeInput) if err != nil { @@ -559,6 +558,7 @@ func Build(inv Invocation) (result []Package, resultErr error) { cfg.Mode |= packages.NeedForTest } emitDebugInfo := shouldEmitDebugInfo(conf, &export) + emitCodeView := shouldEmitCodeView(conf, &export) frontendOptions := cl.Options{ Debug: emitDebugInfo, DebugSymbols: emitDebugInfo, @@ -588,6 +588,7 @@ func Build(inv Invocation) (result []Package, resultErr error) { prog.SetPthreadStackSize(uint64(conf.PthreadStackSize)) } prog.EnableLTOPluginMarkers(conf.LTOPlugin.Enabled()) + prog.EnableCodeViewDebugInfo(emitCodeView) funcInfo := conf.Mode != ModeGen && conf.PCLNMode != PCLNNone prog.EnableFuncInfoMetadata(funcInfo) // Site records are inline-asm fragments inside function bodies. Darwin @@ -919,16 +920,21 @@ func removeOutFmts(outFmts *OutFmtDetails) { } } -func usesNativeWindowsToolchain(hostGOOS string, conf *Config) bool { - // Windows can execute a host-architecture LLGo compiler while Clang targets - // another Windows architecture (for example, x64 LLGo producing ARM64 or - // 386 programs). The selected CC target, not the compiler process's GOARCH, - // defines the physical ABI that must be resolved below. - return conf.Target == "" && hostGOOS == "windows" && conf.Goos == hostGOOS +func usesWindowsToolchainProfile(hostGOOS string, conf *Config) bool { + // Linked Windows output always needs a coherent compiler, ABI, CRT, and + // linker profile. This also covers a Unix-hosted LLGo compiler using an + // llvm-mingw toolchain. IR-only generation remains host-independent unless + // it runs natively on Windows, preserving golden-test behavior. + return conf.Target == "" && conf.Goos == "windows" && + (hostGOOS == "windows" || conf.Mode != ModeGen) } -func parseNativeToolchainInput(commands commandEnv, options LinkOptions) (crosscompile.NativeToolchainInput, error) { - input := crosscompile.NativeToolchainInput{Dir: commands.dir, Environ: slices.Clone(commands.environ)} +func parseNativeToolchainInput(commands commandEnv, options LinkOptions, resolveWindows bool) (crosscompile.NativeToolchainInput, error) { + input := crosscompile.NativeToolchainInput{ + Dir: commands.dir, + Environ: slices.Clone(commands.environ), + ResolveWindows: resolveWindows, + } for _, setting := range []struct { name string value string @@ -3247,6 +3253,7 @@ func clFiles(ctx *context, files string, pkg *packages.Package, procFile func(li func clFile(ctx *context, args []string, cFile, expFile, pkgPath string, procFile func(linkFile string), verbose bool) { baseName := expFile + filepath.Base(cFile) ext := filepath.Ext(cFile) + args = append(slices.Clone(args), debugInfoCompilerArgs(ctx.buildConf, &ctx.crossCompile)...) // default clang++ will use c++ to compile c file,will cause symbol be mangled if ext == ".c" { diff --git a/internal/build/collect.go b/internal/build/collect.go index 16d70ba405..99cac04577 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -144,6 +144,7 @@ func (c *context) collectCommonInputs(m *manifestBuilder) { m.common.GoGlobalDCE = c.buildConf.goGlobalDCEEnabled() m.common.EnableLTOPlugin = c.buildConf.LTOPlugin.Enabled() m.common.EmitDWARF = shouldEmitDebugInfo(c.buildConf, &c.crossCompile) + m.common.EmitCodeView = shouldEmitCodeView(c.buildConf, &c.crossCompile) m.common.PCLNMode = effectivePCLNMode(c.buildConf).String() m.common.DisableBoundsChecks = c.buildConf.DisableBoundsChecks m.common.SaturatingFloatToUint32 = c.buildConf.SaturatingFloatToUint32 diff --git a/internal/build/collect_test.go b/internal/build/collect_test.go index 34cb59eae8..53b64b3872 100644 --- a/internal/build/collect_test.go +++ b/internal/build/collect_test.go @@ -335,6 +335,44 @@ func TestCollectFingerprintIncludesEmitDWARF(t *testing.T) { } } +func TestCollectFingerprintIncludesEmitCodeView(t *testing.T) { + dir := t.TempDir() + goFile := filepath.Join(dir, "main.go") + if err := os.WriteFile(goFile, []byte("package main"), 0o644); err != nil { + t.Fatal(err) + } + pkg := &aPackage{Package: &packages.Package{ + PkgPath: "test/pkg", + GoFiles: []string{goFile}, + }} + ctx := &context{ + conf: &packages.Config{}, + buildConf: &Config{ + Goos: "windows", + Goarch: "amd64", + Mode: ModeBuild, + BuildMode: BuildModeExe, + LinkOptions: LinkOptions{ + DWARF: DWARFPreserve, + ExternalLinkerFlags: "-Xlinker /debug:full", + }, + }, + crossCompile: crosscompile.Export{ + Toolchain: crosscompile.NativeToolchain{ABI: crosscompile.PlatformABIMsvc}, + }, + } + if err := ctx.collectFingerprint(pkg); err != nil { + t.Fatal(err) + } + data, err := decodeManifest(pkg.Manifest) + if err != nil { + t.Fatal(err) + } + if data.Common == nil || !data.Common.EmitDWARF || !data.Common.EmitCodeView { + t.Fatalf("PDB manifest does not record both debug formats:\n%s", pkg.Manifest) + } +} + func TestCollectFingerprintIncludesPCLNMode(t *testing.T) { t.Setenv(llgoFuncInfo, "") t.Setenv(llgoFuncInfoSites, "") diff --git a/internal/build/dwarf_standard_test.go b/internal/build/dwarf_standard_test.go index d29d3c7be5..d1eeeb8e7f 100644 --- a/internal/build/dwarf_standard_test.go +++ b/internal/build/dwarf_standard_test.go @@ -6,6 +6,7 @@ import ( "debug/dwarf" "debug/elf" "debug/macho" + "debug/pe" "fmt" "io" "os" @@ -29,8 +30,8 @@ type dwarfNode struct { } func TestStandardDWARF(t *testing.T) { - if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { - t.Skip("native DWARF integration test requires Mach-O or ELF") + if runtime.GOOS != "darwin" && runtime.GOOS != "linux" && runtime.GOOS != "windows" { + t.Skip("native DWARF integration test requires Mach-O, ELF, or PE") } for _, level := range []optlevel.Level{ @@ -42,8 +43,22 @@ func TestStandardDWARF(t *testing.T) { optlevel.Oz, } { t.Run(level.String(), func(t *testing.T) { - bin := filepath.Join(t.TempDir(), "dwarf-standard") conf := NewDefaultConf(ModeBuild) + if runtime.GOOS == "windows" { + // COFF section GC leaves zero-address tombstones in linked DWARF. + // llvm-dwarfdump does not recognize those tombstones and reports + // false overlapping ranges, while llvm-dwarfutil cannot normalize + // COFF. Retain this verifier fixture's sections; the LLDB integration + // tests exercise normal section-GC output. + if os.Getenv("LLGO_WINDOWS_ABI") == "mingw" { + conf.LinkOptions.ExternalLinkerFlags = "-Wl,--no-gc-sections" + } else { + conf.LinkOptions.ExternalLinkerFlags = "-Xlinker /opt:noref" + } + } + // Do resolves the platform extension on an internal config clone, so + // derive the explicit output path before the build as well. + bin := filepath.Join(t.TempDir(), "dwarf-standard"+defaultAppExt(conf)) conf.OutFile = bin conf.OptLevel = level conf.LinkOptions.DWARF = DWARFPreserve @@ -107,6 +122,17 @@ func openNativeDWARF(t *testing.T, bin string) (*dwarf.Data, string) { t.Fatal(err) } return data, dwarfPath + case "windows": + file, err := pe.Open(bin) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = file.Close() }) + data, err := file.DWARF() + if err != nil { + t.Fatal(err) + } + return data, bin default: panic("unreachable") } diff --git a/internal/build/fingerprint.go b/internal/build/fingerprint.go index 766a2463cf..a264ed99bc 100644 --- a/internal/build/fingerprint.go +++ b/internal/build/fingerprint.go @@ -133,6 +133,7 @@ type commonSection struct { GoGlobalDCE bool `yaml:"GO_GLOBAL_DCE,omitempty"` EnableLTOPlugin bool `yaml:"ENABLE_LTO_PLUGIN,omitempty"` EmitDWARF bool `yaml:"EMIT_DWARF,omitempty"` + EmitCodeView bool `yaml:"EMIT_CODEVIEW,omitempty"` PCLNMode string `yaml:"PCLN_MODE,omitempty"` DisableBoundsChecks bool `yaml:"DISABLE_BOUNDS_CHECKS,omitempty"` SaturatingFloatToUint32 bool `yaml:"SATURATING_FLOAT_TO_UINT32,omitempty"` @@ -157,7 +158,7 @@ func (s *commonSection) empty() bool { s.PlatformABI == "" && s.ObjectFormat == "" && s.DriverFlavor == "" && s.LinkerFlavor == "" && s.TargetTriple == "" && s.CRTFlavor == "" && s.CXXRuntime == "" && s.SDKVersion == "" && s.CRTVersion == "" && s.ToolsetVersion == "" && - !s.GoGlobalDCE && !s.EnableLTOPlugin && !s.EmitDWARF && s.PCLNMode == "" && + !s.GoGlobalDCE && !s.EnableLTOPlugin && !s.EmitDWARF && !s.EmitCodeView && s.PCLNMode == "" && !s.DisableBoundsChecks && !s.SaturatingFloatToUint32 && !s.LocalContext && s.CC == "" && len(s.CCArgs) == 0 && s.CCIdentity == "" && s.CXX == "" && len(s.CXXArgs) == 0 && s.CXXIdentity == "" && len(s.CCFlags) == 0 && diff --git a/internal/build/link_options.go b/internal/build/link_options.go index 857bef59c3..08bae2ff28 100644 --- a/internal/build/link_options.go +++ b/internal/build/link_options.go @@ -19,8 +19,10 @@ package build import ( "fmt" "slices" + "strings" "github.com/xgo-dev/llgo/internal/crosscompile" + "github.com/xgo-dev/llgo/internal/quoted" ) // DWARFMode records whether the command layer requested that the backend @@ -98,6 +100,15 @@ func shouldEmitDebugInfo(conf *Config, target *crosscompile.Export) bool { return conf.Mode != ModeGen || conf.LinkOptions.DWARF == DWARFPreserve } +// shouldEmitCodeView reports whether an MSVC COFF link explicitly requests a +// PDB. LLGo keeps its Go-compatible DWARF by default; CodeView is additional +// metadata for an explicitly requested native Windows debugger artifact. +func shouldEmitCodeView(conf *Config, target *crosscompile.Export) bool { + return shouldEmitDebugInfo(conf, target) && conf.Goos == "windows" && + target.Toolchain.ABI == crosscompile.PlatformABIMsvc && + hasCOFFPDBFlag(conf.LinkOptions.ExternalLinkerFlags) +} + // validateLinkOptions checks whether the typed linker intent can be honored // by the selected backend. User-facing Go flag syntax is parsed by // internal/goflags. @@ -135,5 +146,89 @@ func debugInfoLinkerArgs(conf *Config, target *crosscompile.Export) []string { if effectiveOmitDWARF(conf, target) { return slices.Clone(target.DebugInfo.OmitLinkFlags) } + // The default COFF policy keeps Go-compatible DWARF in the PE image. An + // explicit external-linker /debug option may instead request a PDB (for + // example /debug:full). Do not append /debug:dwarf after that user choice: + // lld-link applies the last /debug option and would silently suppress the + // requested PDB. DWARF omission above still wins when Go's -w is active. + if conf.Goos == "windows" && hasCOFFDebugFlag(conf.LinkOptions.ExternalLinkerFlags) { + return nil + } return slices.Clone(target.DebugInfo.PreserveLinkFlags) } + +// debugInfoCompilerArgs keeps package C/C++ sources on the same typed debug +// policy as generated Go code. COFF PDB builds add CodeView while retaining +// DWARF, so LLDB, Go tracebacks, and native Windows debuggers share the same +// source inputs. +func debugInfoCompilerArgs(conf *Config, target *crosscompile.Export) []string { + if shouldEmitDebugInfo(conf, target) { + args := []string{"-gdwarf-4"} + if shouldEmitCodeView(conf, target) { + args = append(args, "-gcodeview") + } + return args + } + return nil +} + +func hasCOFFPDBFlag(value string) bool { + pdb := false + hasCOFFDebugFlagMatching(value, func(arg string) bool { + if isCOFFDebugFlag(arg) { + // lld-link applies the last /debug option. Preserve that ordering so + // "/debug:full /debug:none" does not add unused CodeView records. + pdb = isCOFFPDBFlag(arg) + } + // Visit every argument so the last /debug option determines the result. + return false + }) + return pdb +} + +func hasCOFFDebugFlag(value string) bool { + return hasCOFFDebugFlagMatching(value, isCOFFDebugFlag) +} + +func hasCOFFDebugFlagMatching(value string, match func(string) bool) bool { + args, err := quoted.Split(value) + if err != nil { + // Native toolchain input validation reports the malformed value before a + // real link. Keep this helper side-effect-free for configuration tests. + return false + } + for i, arg := range args { + lower := strings.ToLower(arg) + if match(lower) { + return true + } + if strings.HasPrefix(lower, "-wl,") { + for _, linkerArg := range strings.Split(strings.TrimPrefix(lower, "-wl,"), ",") { + if match(linkerArg) { + return true + } + } + } + if lower == "-xlinker" && i+1 < len(args) && match(strings.ToLower(args[i+1])) { + return true + } + } + return false +} + +func isCOFFPDBFlag(arg string) bool { + if arg == "/debug" || arg == "-debug" { + return true + } + for _, prefix := range []string{"/debug:", "-debug:"} { + if value, ok := strings.CutPrefix(arg, prefix); ok { + return value != "none" && value != "dwarf" + } + } + return false +} + +func isCOFFDebugFlag(arg string) bool { + return arg == "/debug" || strings.HasPrefix(arg, "/debug:") || + arg == "-debug" || strings.HasPrefix(arg, "-debug:") +} diff --git a/internal/build/link_options_test.go b/internal/build/link_options_test.go index f040f296e2..28ecdab4c4 100644 --- a/internal/build/link_options_test.go +++ b/internal/build/link_options_test.go @@ -50,7 +50,10 @@ func TestDebugInfoLinkerArgs(t *testing.T) { {name: "explicit w false", conf: Config{LinkOptions: LinkOptions{OmitSymbolTable: true, DWARF: DWARFPreserve}}}, {name: "windows safe default", conf: Config{BuildMode: BuildModeExe, OmitDWARFByDefault: true}, target: windowsDebugInfo(), want: []string{"-Wl,/debug:none"}}, {name: "windows explicit w", conf: Config{BuildMode: BuildModeExe, LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: windowsDebugInfo(), want: []string{"-Wl,/debug:none"}}, - {name: "windows explicit w false", conf: Config{BuildMode: BuildModeExe, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: windowsDebugInfo(), want: []string{"-Wl,/debug:dwarf"}}, + {name: "windows explicit w false", conf: Config{Goos: "windows", BuildMode: BuildModeExe, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: windowsDebugInfo(), want: []string{"-Wl,/debug:dwarf"}}, + {name: "windows explicit PDB", conf: Config{Goos: "windows", BuildMode: BuildModeExe, LinkOptions: LinkOptions{DWARF: DWARFPreserve, ExternalLinkerFlags: "-Wl,/debug:full,/pdb:app.pdb"}}, target: windowsDebugInfo()}, + {name: "windows explicit PDB via Xlinker", conf: Config{Goos: "windows", BuildMode: BuildModeExe, LinkOptions: LinkOptions{DWARF: DWARFPreserve, ExternalLinkerFlags: "-Xlinker /debug:full"}}, target: windowsDebugInfo()}, + {name: "windows w overrides external PDB", conf: Config{Goos: "windows", BuildMode: BuildModeExe, LinkOptions: LinkOptions{DWARF: DWARFOmit, ExternalLinkerFlags: "/debug:full"}}, target: windowsDebugInfo(), want: []string{"-Wl,/debug:none"}}, {name: "windows c-archive", conf: Config{BuildMode: BuildModeCArchive, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: windowsDebugInfo()}, {name: "target linker already suppresses DWARF", conf: Config{Target: "rp2040", LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: alwaysOmitDebugInfo()}, } @@ -63,6 +66,94 @@ func TestDebugInfoLinkerArgs(t *testing.T) { } } +func TestDebugInfoCompilerArgs(t *testing.T) { + tests := []struct { + name string + conf Config + target crosscompile.Export + want []string + }{ + {name: "linked safe default", conf: Config{Mode: ModeBuild, OmitDWARFByDefault: true}}, + {name: "linked explicit omit", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFOmit}}}, + {name: "linked explicit preserve", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, want: []string{"-gdwarf-4"}}, + {name: "Windows PDB", conf: Config{Goos: "windows", Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFPreserve, ExternalLinkerFlags: "-Xlinker /debug:full"}}, target: windowsDebugInfo(), want: []string{"-gdwarf-4", "-gcodeview"}}, + {name: "Windows DWARF", conf: Config{Goos: "windows", Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFPreserve, ExternalLinkerFlags: "-Xlinker /debug:dwarf"}}, target: windowsDebugInfo(), want: []string{"-gdwarf-4"}}, + {name: "MinGW PDB flag", conf: Config{Goos: "windows", Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFPreserve, ExternalLinkerFlags: "-Xlinker /debug:full"}}, target: windowsGNUToolchainDebugInfo(), want: []string{"-gdwarf-4"}}, + {name: "generation default", conf: Config{Mode: ModeGen}}, + {name: "generation explicit preserve", conf: Config{Mode: ModeGen, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, want: []string{"-gdwarf-4"}}, + {name: "target always omits", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: alwaysOmitDebugInfo()}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := debugInfoCompilerArgs(&tt.conf, &tt.target); !reflect.DeepEqual(got, tt.want) { + t.Fatalf("debugInfoCompilerArgs() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestHasCOFFDebugFlag(t *testing.T) { + for _, test := range []struct { + value string + want bool + }{ + {value: "/debug", want: true}, + {value: "/DEBUG:FULL", want: true}, + {value: "-Wl,/debug:ghash,/pdb:app.pdb", want: true}, + {value: "-Xlinker /debug:dwarf", want: true}, + {value: "/pdb:app.pdb"}, + {value: "-Wl,/incremental:no"}, + {value: "'/debug"}, + } { + if got := hasCOFFDebugFlag(test.value); got != test.want { + t.Errorf("hasCOFFDebugFlag(%q) = %v, want %v", test.value, got, test.want) + } + } +} + +func TestHasCOFFPDBFlag(t *testing.T) { + for _, test := range []struct { + value string + want bool + }{ + {value: "/debug", want: true}, + {value: "/DEBUG:FULL", want: true}, + {value: "-Wl,/debug:ghash,/pdb:app.pdb", want: true}, + {value: "/debug:none /debug:full", want: true}, + {value: "/debug:full /debug:none"}, + {value: "-Xlinker /debug:full -Xlinker /debug:dwarf"}, + {value: "-Xlinker /debug:dwarf"}, + {value: "/debug:none"}, + {value: "/pdb:app.pdb"}, + } { + if got := hasCOFFPDBFlag(test.value); got != test.want { + t.Errorf("hasCOFFPDBFlag(%q) = %v, want %v", test.value, got, test.want) + } + } +} + +func TestShouldEmitCodeView(t *testing.T) { + pdb := LinkOptions{DWARF: DWARFPreserve, ExternalLinkerFlags: "-Xlinker /debug:full"} + for _, test := range []struct { + name string + conf Config + target crosscompile.Export + want bool + }{ + {name: "MSVC PDB", conf: Config{Goos: "windows", Mode: ModeBuild, LinkOptions: pdb}, target: windowsDebugInfo(), want: true}, + {name: "default Windows DWARF", conf: Config{Goos: "windows", Mode: ModeBuild}, target: windowsDebugInfo()}, + {name: "omitted debug", conf: Config{Goos: "windows", Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFOmit, ExternalLinkerFlags: "/debug:full"}}, target: windowsDebugInfo()}, + {name: "MinGW", conf: Config{Goos: "windows", Mode: ModeBuild, LinkOptions: pdb}, target: windowsGNUToolchainDebugInfo()}, + {name: "non-Windows", conf: Config{Goos: "linux", Mode: ModeBuild, LinkOptions: pdb}, target: windowsDebugInfo()}, + } { + t.Run(test.name, func(t *testing.T) { + if got := shouldEmitCodeView(&test.conf, &test.target); got != test.want { + t.Fatalf("shouldEmitCodeView() = %v, want %v", got, test.want) + } + }) + } +} + func TestEffectiveOmitDWARF(t *testing.T) { tests := []struct { name string @@ -194,12 +285,18 @@ func configurableDebugInfo() crosscompile.Export { } func windowsDebugInfo() crosscompile.Export { - return crosscompile.Export{DebugInfo: crosscompile.DebugInfoPolicy{ + return crosscompile.Export{Toolchain: crosscompile.NativeToolchain{ABI: crosscompile.PlatformABIMsvc}, DebugInfo: crosscompile.DebugInfoPolicy{ OmitLinkFlags: []string{"-Wl,/debug:none"}, PreserveLinkFlags: []string{"-Wl,/debug:dwarf"}, }} } +func windowsGNUToolchainDebugInfo() crosscompile.Export { + target := windowsDebugInfo() + target.Toolchain.ABI = crosscompile.PlatformABIGNU + return target +} + func alwaysOmitDebugInfo() crosscompile.Export { return crosscompile.Export{DebugInfo: crosscompile.DebugInfoPolicy{AlwaysOmit: true}} } diff --git a/internal/build/native_toolchain_test.go b/internal/build/native_toolchain_test.go index cbe1e48167..b55758f5b2 100644 --- a/internal/build/native_toolchain_test.go +++ b/internal/build/native_toolchain_test.go @@ -14,7 +14,7 @@ import ( "github.com/xgo-dev/llgo/internal/crosscompile" ) -func TestUsesNativeWindowsToolchain(t *testing.T) { +func TestUsesWindowsToolchainProfile(t *testing.T) { for _, test := range []struct { name string hostOS string @@ -23,13 +23,16 @@ func TestUsesNativeWindowsToolchain(t *testing.T) { }{ {name: "native Windows", hostOS: "windows", conf: Config{Goos: "windows", Goarch: "amd64"}, want: true}, {name: "cross-architecture Windows", hostOS: "windows", conf: Config{Goos: "windows", Goarch: "arm64"}, want: true}, + {name: "cross-host Windows build", hostOS: "darwin", conf: Config{Goos: "windows", Goarch: "arm64", Mode: ModeBuild}, want: true}, + {name: "cross-host Windows test", hostOS: "linux", conf: Config{Goos: "windows", Goarch: "386", Mode: ModeTest}, want: true}, + {name: "cross-host Windows IR", hostOS: "darwin", conf: Config{Goos: "windows", Goarch: "arm64", Mode: ModeGen}}, {name: "named target", hostOS: "windows", conf: Config{Goos: "windows", Goarch: "amd64", Target: "esp32c3"}}, {name: "cross OS", hostOS: "windows", conf: Config{Goos: "linux", Goarch: "amd64"}}, {name: "non-Windows host", hostOS: "darwin", conf: Config{Goos: "darwin", Goarch: "arm64"}}, } { t.Run(test.name, func(t *testing.T) { - if got := usesNativeWindowsToolchain(test.hostOS, &test.conf); got != test.want { - t.Fatalf("usesNativeWindowsToolchain() = %v, want %v", got, test.want) + if got := usesWindowsToolchainProfile(test.hostOS, &test.conf); got != test.want { + t.Fatalf("usesWindowsToolchainProfile() = %v, want %v", got, test.want) } }) } @@ -48,13 +51,16 @@ func TestParseNativeToolchainInput(t *testing.T) { ExternalLinker: `clang "--target=x86_64-pc-windows-msvc"`, ExternalLinkerFlags: `'/debug:dwarf' /incremental:no`, } - got, err := parseNativeToolchainInput(commands, options) + got, err := parseNativeToolchainInput(commands, options, true) if err != nil { t.Fatal(err) } if got.Dir != commands.dir || !reflect.DeepEqual(got.Environ, commands.environ) { t.Fatalf("invocation state = dir %q env %q", got.Dir, got.Environ) } + if !got.ResolveWindows { + t.Fatal("ResolveWindows = false, want true") + } for name, values := range map[string]struct { got []string want []string @@ -88,7 +94,7 @@ func TestParseNativeToolchainInputErrors(t *testing.T) { {name: "unterminated extldflags", options: LinkOptions{ExternalLinkerFlags: `'/debug`}, wantErr: "could not parse -extldflags"}, } { t.Run(test.name, func(t *testing.T) { - _, err := parseNativeToolchainInput(commandEnv{environ: test.environ}, test.options) + _, err := parseNativeToolchainInput(commandEnv{environ: test.environ}, test.options, false) if err == nil || !strings.Contains(err.Error(), test.wantErr) { t.Fatalf("error = %v, want substring %q", err, test.wantErr) } @@ -96,11 +102,24 @@ func TestParseNativeToolchainInputErrors(t *testing.T) { } } +func TestBuildRejectsMalformedNativeToolchainInput(t *testing.T) { + t.Setenv("CC", "'clang") + conf := NewDefaultConf(ModeBuild) + conf.Goos = "windows" + conf.Goarch = "amd64" + conf.GOAMD64 = "v1" + + _, err := Build(Invocation{Config: conf, Dir: t.TempDir()}) + if err == nil || !strings.Contains(err.Error(), "could not parse CC") { + t.Fatalf("Build() error = %v, want malformed CC error", err) + } +} + func TestParseNativeToolchainInputAcceptsEmptyLinkerFlags(t *testing.T) { got, err := parseNativeToolchainInput(commandEnv{}, LinkOptions{ ExternalLinker: " ", ExternalLinkerFlags: "\t", - }) + }, false) if err != nil { t.Fatal(err) } diff --git a/internal/build/windows_pdb_test.go b/internal/build/windows_pdb_test.go new file mode 100644 index 0000000000..f54de33a4f --- /dev/null +++ b/internal/build/windows_pdb_test.go @@ -0,0 +1,103 @@ +//go:build !llgo + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package build + +import ( + "debug/pe" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/xgo-dev/llgo/internal/optlevel" + llvmenv "github.com/xgo-dev/llgo/xtool/env/llvm" +) + +func TestWindowsPDBLinkPath(t *testing.T) { + if runtime.GOOS != "windows" || os.Getenv("LLGO_WINDOWS_ABI") != "msvc" { + t.Skip("native MSVC linker integration test") + } + + dir := t.TempDir() + bin := filepath.Join(dir, "pdb-standard.exe") + pdb := filepath.Join(dir, "pdb-standard.pdb") + conf := NewDefaultConf(ModeBuild) + conf.OutFile = bin + conf.OptLevel = optlevel.O0 + conf.LinkOptions.DWARF = DWARFPreserve + // Keep the default Go-compatible DWARF while asking lld-link to publish a + // companion PDB. The PDB supplies native Windows public symbols and source + // lines; LLDB and runtime tracebacks continue to use embedded DWARF/PCLN. + conf.LinkOptions.ExternalLinkerFlags = fmt.Sprintf( + `-Xlinker /debug:full -Xlinker "/pdb:%s"`, filepath.ToSlash(pdb)) + if _, err := Do([]string{"./testdata/dwarf"}, conf); err != nil { + t.Fatalf("build Windows PDB fixture: %v", err) + } + if got := runBinary(t, bin); got != "dwarf-ok\n" { + t.Fatalf("PDB fixture output = %q", got) + } + + info, err := os.Stat(pdb) + if err != nil { + t.Fatalf("stat PDB: %v", err) + } + if info.Size() == 0 { + t.Fatal("PDB is empty") + } + + file, err := pe.Open(bin) + if err != nil { + t.Fatal(err) + } + defer file.Close() + if _, err := file.DWARF(); err != nil { + t.Fatalf("PDB link stripped embedded DWARF: %v", err) + } + + binDir := llvmenv.New("").BinDir() + pdbutil := filepath.Join(binDir, "llvm-pdbutil") + out, err := exec.Command(pdbutil, "dump", "-summary", "-publics", "-files", "-l", pdb).CombinedOutput() + if err != nil { + t.Fatalf("llvm-pdbutil: %v\n%s", err, out) + } + text := string(out) + for _, want := range []string{ + "Has Publics: true", + "main.main", + "main.go", + "helper.go", + "line/addr entries", + } { + if !strings.Contains(text, want) { + t.Fatalf("PDB output does not contain %q:\n%s", want, out) + } + } + + readobj := filepath.Join(binDir, "llvm-readobj") + out, err = exec.Command(readobj, "--coff-debug-directory", bin).CombinedOutput() + if err != nil { + t.Fatalf("llvm-readobj: %v\n%s", err, out) + } + if !strings.Contains(strings.ToLower(string(out)), strings.ToLower(filepath.Base(pdb))) { + t.Fatalf("PE debug directory does not reference %s:\n%s", filepath.Base(pdb), out) + } +} diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index 9c111e2c5c..50c75656d5 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -457,12 +457,12 @@ func useWithGOARMAndToolchain(goos, goarch, goarm string, wasiThreads, forceEspC targetTriple := llvm.GetTargetTripleWithGOARM(goos, goarch, goarm) llgoRoot := env.LLGoROOT() nativePlatformToolchain := usesNativePlatformToolchain( - runtime.GOOS, runtime.GOARCH, goos, goarch, nativeInput.ResolveCrossArch, + runtime.GOOS, runtime.GOARCH, goos, goarch, nativeInput.ResolveWindows, ) - // Native Windows resolves its compiler and dependencies as one coherent - // profile below. Do not download or reject an unrelated embedded Clang - // installation before that profile has a chance to resolve. + // Linked Windows output resolves its compiler and dependencies as one + // coherent profile below, including when the LLGo host is macOS or Linux. + // Do not inspect an unrelated embedded Clang before that profile resolves. var clangRoot string if !(nativePlatformToolchain && goos == "windows") { clangRoot, err = getESPClangRoot(forceEspClang) @@ -692,9 +692,11 @@ func useWithGOARMAndToolchain(goos, goarch, goarm string, wasiThreads, forceEspC return } -func usesNativePlatformToolchain(hostGOOS, hostGOARCH, targetGOOS, targetGOARCH string, resolveCrossArch bool) bool { - return hostGOOS == targetGOOS && - (hostGOARCH == targetGOARCH || targetGOOS == "windows" && resolveCrossArch) +func usesNativePlatformToolchain(hostGOOS, hostGOARCH, targetGOOS, targetGOARCH string, resolveWindows bool) bool { + if targetGOOS == "windows" && resolveWindows { + return true + } + return hostGOOS == targetGOOS && hostGOARCH == targetGOARCH } // UseTarget loads configuration from a target name (e.g., "rp2040", "wasi") diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go index 8e1471f70e..7055e35f63 100644 --- a/internal/crosscompile/crosscompile_test.go +++ b/internal/crosscompile/crosscompile_test.go @@ -722,16 +722,18 @@ func TestUsesNativePlatformToolchain(t *testing.T) { for _, test := range []struct { name string hostOS, hostArch, targetOS, targetArch string - resolveCrossArch, want bool + resolveWindows, want bool }{ {name: "same platform", hostOS: "linux", hostArch: "amd64", targetOS: "linux", targetArch: "amd64", want: true}, - {name: "Windows linked cross architecture", hostOS: "windows", hostArch: "amd64", targetOS: "windows", targetArch: "arm64", resolveCrossArch: true, want: true}, + {name: "Windows linked cross architecture", hostOS: "windows", hostArch: "amd64", targetOS: "windows", targetArch: "arm64", resolveWindows: true, want: true}, {name: "Windows IR-only cross architecture", hostOS: "windows", hostArch: "amd64", targetOS: "windows", targetArch: "arm64"}, + {name: "Windows linked cross host", hostOS: "darwin", hostArch: "arm64", targetOS: "windows", targetArch: "386", resolveWindows: true, want: true}, + {name: "Windows IR-only cross host", hostOS: "linux", hostArch: "amd64", targetOS: "windows", targetArch: "amd64"}, {name: "non-Windows cross architecture", hostOS: "darwin", hostArch: "arm64", targetOS: "darwin", targetArch: "amd64"}, {name: "cross OS", hostOS: "windows", hostArch: "amd64", targetOS: "linux", targetArch: "amd64"}, } { t.Run(test.name, func(t *testing.T) { - if got := usesNativePlatformToolchain(test.hostOS, test.hostArch, test.targetOS, test.targetArch, test.resolveCrossArch); got != test.want { + if got := usesNativePlatformToolchain(test.hostOS, test.hostArch, test.targetOS, test.targetArch, test.resolveWindows); got != test.want { t.Fatalf("usesNativePlatformToolchain() = %v, want %v", got, test.want) } }) diff --git a/internal/crosscompile/windows_toolchain.go b/internal/crosscompile/windows_toolchain.go index e88c365a7e..16d92e41d1 100644 --- a/internal/crosscompile/windows_toolchain.go +++ b/internal/crosscompile/windows_toolchain.go @@ -21,11 +21,11 @@ type NativeToolchainInput struct { ExternalFlags []string Dir string Environ []string - // ResolveCrossArch selects the host's native Windows toolchain when a - // Windows process produces linked output for another Windows architecture. + // ResolveWindows selects a Windows native-format toolchain when linked + // output is produced for another architecture or from a non-Windows host. // Pure IR generation leaves it false so multi-target golden tests do not - // depend on the host compiler's architecture or ABI profile. - ResolveCrossArch bool + // depend on an installed compiler's architecture or ABI profile. + ResolveWindows bool } type toolIdentity struct { @@ -186,7 +186,7 @@ func requireClangGNUDriver(setting string, command []string, identity toolIdenti if strings.Contains(strings.ToLower(identity.version), "clang") { return nil } - return fmt.Errorf("%s %q uses an unsupported driver (%s); native Windows currently requires Clang's GNU-compatible driver", setting, command[0], identity.version) + return fmt.Errorf("%s %q uses an unsupported driver (%s); Windows builds currently require Clang's GNU-compatible driver", setting, command[0], identity.version) } func compatibleWindowsToolchains(a, b NativeToolchain) error { diff --git a/internal/debuginfo/builder.go b/internal/debuginfo/builder.go index 353c8c0b8e..68d6ceb822 100644 --- a/internal/debuginfo/builder.go +++ b/internal/debuginfo/builder.go @@ -23,8 +23,9 @@ const ( // Config describes properties of the generated debug information. Optimized // reports what the compilation pipeline does; it does not select any pass. type Config struct { - Producer string - Optimized bool + Producer string + Optimized bool + EmitCodeView bool } // Builder is a package-local owner of an LLVM DIBuilder. Finalize must be @@ -51,6 +52,12 @@ func New(module llvm.Module, config Config) *Builder { } b.addModuleFlag(2, "Debug Info Version", defaultDebugInfoVersion) b.addModuleFlag(7, "Dwarf Version", defaultDwarfVersion) + if config.EmitCodeView { + // Keep DWARF enabled as well. LLVM can lower the same source metadata to + // both formats: DWARF remains available to LLDB and Go-compatible + // traceback consumers, while lld-link can merge CodeView into a PDB. + b.addModuleFlag(2, "CodeView", 1) + } b.addModuleFlag(1, "wchar_size", 4) b.addModuleFlag(8, "PIC Level", 2) b.addModuleFlag(7, "uwtable", 1) diff --git a/internal/debuginfo/builder_test.go b/internal/debuginfo/builder_test.go index 7485aaa4d1..27fc275e60 100644 --- a/internal/debuginfo/builder_test.go +++ b/internal/debuginfo/builder_test.go @@ -95,6 +95,31 @@ func TestBuilderWindowsDebuggerMarkerUsesComdat(t *testing.T) { } } +func TestBuilderCodeViewRetainsDWARF(t *testing.T) { + ctx := llvm.NewContext() + defer ctx.Dispose() + module := ctx.NewModule("windows-codeview-test") + defer module.Dispose() + module.SetTarget("x86_64-pc-windows-msvc") + + builder := New(module, Config{EmitCodeView: true}) + builder.CompileUnit("main.go", "/src/example") + builder.Finalize() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("Windows CodeView module is invalid: %v\n%s", err, module.String()) + } + ir := module.String() + for _, want := range []string{ + `!{i32 2, !"CodeView", i32 1}`, + `!{i32 7, !"Dwarf Version", i32 4}`, + } { + if !strings.Contains(ir, want) { + t.Fatalf("Windows debug module is missing %q:\n%s", want, ir) + } + } +} + func TestBuilderMetadataOperations(t *testing.T) { ctx := llvm.NewContext() defer ctx.Dispose() diff --git a/ssa/backend_program_test.go b/ssa/backend_program_test.go index 8b4a27fad9..c958332a86 100644 --- a/ssa/backend_program_test.go +++ b/ssa/backend_program_test.go @@ -33,6 +33,7 @@ func TestNewBackendProgramSharesPreparedGoState(t *testing.T) { coordinator.EnableFuncInfoMetadata(true) coordinator.EnableFuncInfoSites(true) coordinator.SetDebugInfoOptimized(false) + coordinator.EnableCodeViewDebugInfo(true) pkg := types.NewPackage("example.com/p", "p") fset := token.NewFileSet() @@ -82,7 +83,8 @@ func TestNewBackendProgramSharesPreparedGoState(t *testing.T) { } if !backend.disableBoundsChecks || !backend.enableGoGlobalDCE || !backend.enableDeadcodeDrop || backend.pthreadStackSize != 4096 || !backend.enableLTOPluginMarker || - !backend.enableFuncInfoMetadata || !backend.enableFuncInfoSites || backend.debugInfoOptimized { + !backend.enableFuncInfoMetadata || !backend.enableFuncInfoSites || backend.debugInfoOptimized || + !backend.emitCodeViewDebugInfo { t.Fatal("backend Program did not preserve coordinator configuration") } } diff --git a/ssa/di.go b/ssa/di.go index bf8a228a86..6c0b32760f 100644 --- a/ssa/di.go +++ b/ssa/di.go @@ -26,8 +26,9 @@ type diBuilder = *aDIBuilder func newDIBuilder(prog Program, pkg Package, positioner Positioner) diBuilder { return &aDIBuilder{ di: debuginfo.New(pkg.mod, debuginfo.Config{ - Producer: "LLGo", - Optimized: prog.debugInfoOptimized, + Producer: "LLGo", + Optimized: prog.debugInfoOptimized, + EmitCodeView: prog.emitCodeViewDebugInfo, }), prog: prog, types: make(map[*aType]DIType), @@ -715,7 +716,7 @@ func (b Builder) DIDeclare(variable *types.Var, v Expr, dv DIVar, scope DIScope, func (b Builder) DIValue(variable *types.Var, v Expr, dv DIVar, scope DIScope, pos token.Position, blk BasicBlock) { ty := v.Type.RawType().Underlying() - if !needConstructAddr(ty) { + if !b.needDebugAddr(ty, v.Type) { expr := b.di().createExpression(nil) b.di().dbgValue(v, dv, scope, pos, expr, blk) } else { @@ -725,6 +726,20 @@ func (b Builder) DIValue(variable *types.Var, v Expr, dv DIVar, scope DIScope, p } } +func (b Builder) needDebugAddr(underlying types.Type, ssaType Type) bool { + if needConstructAddr(underlying) { + return true + } + // On 32-bit Windows, LLVM can lower a wide integer constant to a single + // address-sized DWARF stack value. That loses its upper half (notably for + // unsigned values), so debuggers report the variable as unavailable. Keep + // the complete value in addressable storage and describe it by dereference. + basic, ok := underlying.(*types.Basic) + return ok && basic.Info()&types.IsInteger != 0 && + b.Prog.Target().effectiveGOOS() == "windows" && + b.Prog.PointerSize() == 4 && b.Prog.SizeOf(ssaType) > uint64(b.Prog.PointerSize()) +} + const ( opDeref = 0x06 ) diff --git a/ssa/di_debug_test.go b/ssa/di_debug_test.go index 04fe53cae6..b197c87da1 100644 --- a/ssa/di_debug_test.go +++ b/ssa/di_debug_test.go @@ -227,6 +227,41 @@ func TestWindowsDebugPointerParameter(t *testing.T) { } } +func TestWindows386WideIntegerDebugValue(t *testing.T) { + const goarch = "386" + fset := token.NewFileSet() + file := fset.AddFile("wide.go", -1, 100) + pkgTypes := types.NewPackage("example.com/p", "p") + variable := types.NewVar(file.Pos(20), pkgTypes, "value", types.Typ[types.Uint64]) + sig := types.NewSignatureType(nil, nil, nil, nil, nil, false) + object := types.NewFunc(file.Pos(10), pkgTypes, "f", sig) + + prog := NewProgram(&Target{GOOS: "windows", GOARCH: goarch, OptLevel: optlevel.O0}) + defer prog.Dispose() + prog.TypeSizes(types.SizesFor("gc", goarch)) + pkg := prog.NewPackage("p", "example.com/p") + pkg.InitDebug("p", "example.com/p", fset) + fn := pkg.NewFunc("example.com/p.f", sig, InGo) + builder := fn.MakeBody(1) + defer builder.Dispose() + pos := fset.Position(variable.Pos()) + builder.DebugFunction(fn, object.Scope(), fset.Position(object.Pos()), pos) + debugVar := builder.DIVarAuto(fn, pos, variable.Name(), prog.Uint64()) + builder.DIValue(variable, prog.IntVal(1<<32|17, prog.Uint64()), debugVar, fn, pos, fn.Block(0)) + builder.Return() + pkg.FinalizeDebug() + + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatalf("debug metadata is invalid: %v\n%s", err, pkg.Module().String()) + } + ir := pkg.Module().String() + for _, want := range []string{"alloca i64", "store i64 4294967313", "#dbg_value(ptr", "DW_OP_deref"} { + if !strings.Contains(ir, want) { + t.Errorf("Windows/386 wide integer debug value is missing %q:\n%s", want, ir) + } + } +} + func TestDIGlobalIgnoresStorageLessFrontendVariable(t *testing.T) { var builder Builder builder.DIGlobal(pyVarExpr(Nil, "attribute"), "module.attribute", token.Position{}) diff --git a/ssa/eh.go b/ssa/eh.go index 94730c33e4..39182ad6bc 100644 --- a/ssa/eh.go +++ b/ssa/eh.go @@ -19,6 +19,7 @@ package ssa import ( "go/token" "go/types" + "strings" "unsafe" "github.com/xgo-dev/llvm" @@ -159,6 +160,12 @@ func (b Builder) windowsSetjmp(jb Expr) Expr { // LLGo owns Go defer unwinding, and RtlUnwind cannot leave a vectored // exception handler that interrupted generated Go code reliably. name = "_setjmpex" + triple := strings.ToLower(prog.target.Spec().Triple) + if strings.Contains(triple, "windows-gnu") || strings.Contains(triple, "mingw") { + // MinGW exposes the same intrinsic entry point under its GNU CRT + // spelling; unlike the setjmp macro this lets LLGo pass a nil frame. + name = "__intrinsic_setjmpex" + } params = types.NewTuple( ptrParam, types.NewParam(token.NoPos, nil, "", prog.VoidPtr().raw.Type), diff --git a/ssa/eh_patch_test.go b/ssa/eh_patch_test.go index 76e4707bcb..b9de505334 100644 --- a/ssa/eh_patch_test.go +++ b/ssa/eh_patch_test.go @@ -91,7 +91,7 @@ func TestWindowsSetjmpABI(t *testing.T) { }{ {name: "386", arch: "386", setjmp: "@_setjmp3", longjmp: "@longjmp"}, {name: "amd64-msvc", arch: "amd64", setjmp: "@_setjmpex", longjmp: "@llgo_longjmp"}, - {name: "amd64-gnu", arch: "amd64", llvmTarget: "x86_64-w64-windows-gnu", setjmp: "@_setjmpex", longjmp: "@llgo_longjmp"}, + {name: "amd64-gnu", arch: "amd64", llvmTarget: "x86_64-w64-windows-gnu", setjmp: "@__intrinsic_setjmpex", longjmp: "@llgo_longjmp"}, {name: "arm64", arch: "arm64", setjmp: "@llgo_setjmp", longjmp: "@llgo_longjmp"}, } for _, test := range tests { @@ -142,7 +142,7 @@ func TestWindowsDirectSetjmpABI(t *testing.T) { }{ {name: "386", arch: "386", setjmp: "@_setjmp3", longjmp: "@longjmp"}, {name: "amd64-msvc", arch: "amd64", setjmp: "@_setjmpex", longjmp: "@llgo_longjmp"}, - {name: "amd64-gnu", arch: "amd64", llvmTarget: "x86_64-w64-windows-gnu", setjmp: "@_setjmpex", longjmp: "@llgo_longjmp"}, + {name: "amd64-gnu", arch: "amd64", llvmTarget: "x86_64-w64-windows-gnu", setjmp: "@__intrinsic_setjmpex", longjmp: "@llgo_longjmp"}, {name: "arm64", arch: "arm64", setjmp: "@llgo_setjmp", longjmp: "@llgo_longjmp"}, } { t.Run(test.name, func(t *testing.T) { diff --git a/ssa/package.go b/ssa/package.go index 10e1bdae63..49678a512c 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -246,6 +246,7 @@ type aProgram struct { enableFuncInfoMetadata bool enableFuncInfoSites bool debugInfoOptimized bool + emitCodeViewDebugInfo bool } type AbiSymbol struct { @@ -358,6 +359,7 @@ func (p Program) NewBackendProgram() Program { backend.enableFuncInfoMetadata = p.enableFuncInfoMetadata backend.enableFuncInfoSites = p.enableFuncInfoSites backend.debugInfoOptimized = p.debugInfoOptimized + backend.emitCodeViewDebugInfo = p.emitCodeViewDebugInfo return backend } @@ -425,6 +427,13 @@ func (p Program) SetDebugInfoOptimized(enable bool) { p.debugInfoOptimized = enable } +// EnableCodeViewDebugInfo asks LLVM to lower the existing source metadata to +// CodeView in addition to DWARF. The final COFF link may then merge CodeView +// records into a PDB without removing the DWARF used by LLDB and tracebacks. +func (p Program) EnableCodeViewDebugInfo(enable bool) { + p.emitCodeViewDebugInfo = enable +} + func (p Program) SetNoInterfaceMethod(fullName string) { p.packageSyntax.mu.Lock() p.packageSyntax.noInterface[fullName] = none{} diff --git a/test/go/recover_fault_test.go b/test/go/recover_fault_test.go index da87de0524..00fc34b969 100644 --- a/test/go/recover_fault_test.go +++ b/test/go/recover_fault_test.go @@ -23,12 +23,17 @@ func faultCopy(dst, src []byte) (n int, err error) { } func TestRecoverAfterFaultPreservesNamedResult(t *testing.T) { + if !enterRecoverableFaultTest(t) { + return + } + // Automatic GC is orthogonal to this test and can perturb the host's // signal/exception recovery path while the fault is being converted into // a panic. Disable it only around the intentional fault so Go and LLGo run // the same deterministic recovery check. oldGCPercent := debug.SetGCPercent(-1) defer debug.SetGCPercent(oldGCPercent) + ensureRecoverableFaultStackHeadroom() old := debug.SetPanicOnFault(true) defer debug.SetPanicOnFault(old) diff --git a/test/go/recover_fault_unix_check_test.go b/test/go/recover_fault_unix_check_test.go index 6fdc0cfde3..440673c6da 100644 --- a/test/go/recover_fault_unix_check_test.go +++ b/test/go/recover_fault_unix_check_test.go @@ -4,4 +4,8 @@ package gotest import "testing" +func enterRecoverableFaultTest(t *testing.T) bool { return true } + +func ensureRecoverableFaultStackHeadroom() {} + func checkRecoveredFaultAddress(t *testing.T, err error, address *byte) {} diff --git a/test/go/recover_fault_windows_test.go b/test/go/recover_fault_windows_test.go index 570f676e36..b26a3b19b7 100644 --- a/test/go/recover_fault_windows_test.go +++ b/test/go/recover_fault_windows_test.go @@ -7,12 +7,18 @@ import ( "io" "os" "os/exec" + "runtime" + "runtime/debug" "runtime/trace" "testing" "unsafe" ) -const nonNilFaultChildEnv = "LLGO_TEST_NON_NIL_FAULT" +const ( + nonNilFaultChildEnv = "LLGO_TEST_NON_NIL_FAULT" + recoverableFaultChildEnv = "LLGO_TEST_RECOVERABLE_FAULT" + traceFaultChildEnv = "LLGO_TEST_TRACE_FAULT" +) type traceFaultValue struct { a [16]int @@ -23,12 +29,65 @@ func copyTraceFaultValue(x, y *traceFaultValue) { *x = *y } +func enterRecoverableFaultTest(t *testing.T) bool { + return enterWindowsFaultTest( + t, recoverableFaultChildEnv, + "^TestRecoverAfterFaultPreservesNamedResult$", + ) +} + +func enterWindowsFaultTest(t *testing.T, childEnv, testPattern string) bool { + t.Helper() + if os.Getenv(childEnv) == "1" { + return true + } + + // Keep a possible host Go runtime defect from corrupting the parent test + // process. The process-level 0xc0000005 observed in CI may be an instance of + // golang/go#81238, where Windows exception recovery can write below a + // goroutine stack and corrupt the adjacent heap on some hosts. + // The child still has to pass the complete fault/panic/recover assertions; + // if and when the upstream runtime issue is resolved, revisit this isolation + // and the stack-headroom preparation below. + cmd := exec.Command(os.Args[0], "-test.run="+testPattern, "-test.v") + cmd.Env = append(os.Environ(), childEnv+"=1", "GOTRACEBACK=system") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("Windows fault test child failed: %v\n%s", err, output) + } + return false +} + +//go:noinline +func growRecoverableFaultStack(depth int) { + var frame [1024]byte + frame[0] = byte(depth) + if depth != 0 { + growRecoverableFaultStack(depth - 1) + } + runtime.KeepAlive(&frame) +} + +func ensureRecoverableFaultStackHeadroom() { + // Grow and then unwind the goroutine stack before raising the exception. + // About 64 one-KiB frames provide headroom for the host-dependent Windows + // exception context suspected in golang/go#81238. This preserves the real + // protected-page fault while avoiding the issue's near-stack-boundary setup. + growRecoverableFaultStack(64) +} + // Regression test matching GOROOT/test/fixedbugs/issue73748b.go. func TestRecoverFaultWhileTracing(t *testing.T) { + if !enterWindowsFaultTest(t, traceFaultChildEnv, "^TestRecoverFaultWhileTracing$") { + return + } + + oldGCPercent := debug.SetGCPercent(-1) + defer debug.SetGCPercent(oldGCPercent) if err := trace.Start(io.Discard); err != nil { t.Fatal(err) } defer trace.Stop() + ensureRecoverableFaultStackHeadroom() var recovered bool func() {