From 211dde6b7bfdf98f25b5504ada4335c3ace4152d Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 29 Aug 2026 23:11:56 +0800 Subject: [PATCH 01/54] ci: qualify remaining Windows MinGW architectures --- .../activate-windows-target/action.yml | 46 +++- .github/actions/setup-demo-deps/action.yml | 16 +- .github/actions/setup-deps/action.yml | 42 +++- .github/actions/setup-llvm-mingw/action.yml | 205 ++++++++++++++++++ .github/workflows/benchmark.yml | 14 ++ .github/workflows/goroot.yml | 20 +- .github/workflows/llgo.yml | 34 ++- .../test_windows_toolchain_profile.ps1 | 33 ++- 8 files changed, 375 insertions(+), 35 deletions(-) create mode 100644 .github/actions/setup-llvm-mingw/action.yml diff --git a/.github/actions/activate-windows-target/action.yml b/.github/actions/activate-windows-target/action.yml index c2d7aa726c..43a0ca4d3d 100644 --- a/.github/actions/activate-windows-target/action.yml +++ b/.github/actions/activate-windows-target/action.yml @@ -52,7 +52,47 @@ 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" } + } + + 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" + $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") + } 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=clang" + Add-Content -Encoding utf8 $env:GITHUB_ENV "CXX=clang++" + 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" diff --git a/.github/actions/setup-demo-deps/action.yml b/.github/actions/setup-demo-deps/action.yml index a3bb9a1a55..0cf61b8262 100644 --- a/.github/actions/setup-demo-deps/action.yml +++ b/.github/actions/setup-demo-deps/action.yml @@ -23,7 +23,7 @@ runs: # stdlib path with that installation and cannot import encodings. Keep # the MinGW demo environment on the normal latest 3.12 patch. python-version: ${{ inputs.windows-abi == 'msvc' && '3.12.7' || '3.12' }} - architecture: x64 + architecture: ${{ inputs.windows-abi == 'mingw' && inputs.windows-arch == 'arm64' && 'arm64' || 'x64' }} # setup-python otherwise replaces PKG_CONFIG_PATH, hiding the native # vcpkg metadata installed by setup-deps from LLGo's runtime build. update-environment: false @@ -123,6 +123,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 +144,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..0fc03f3fb8 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 }}" @@ -500,7 +497,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 +522,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 +614,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 +627,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 +663,21 @@ 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" 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..bade654070 --- /dev/null +++ b/.github/actions/setup-llvm-mingw/action.yml @@ -0,0 +1,205 @@ +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 20241217 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: Restore llvm-mingw + if: runner.os == 'Windows' + id: llvm_mingw_cache + uses: actions/cache/restore@v4 + with: + path: ${{ runner.tool_cache }}\llvm-mingw\20241217\ucrt-x86_64 + key: llvm-mingw-20241217-ucrt-x86_64-f4f3ad8616c4183ce7b0d72df634400945b41ea9816145fc2430df6003455db7 + + - name: Install llvm-mingw + if: runner.os == 'Windows' && steps.llvm_mingw_cache.outputs.cache-hit != 'true' + shell: pwsh + env: + LLVM_MINGW_VERSION: "20241217" + LLVM_MINGW_SHA256: f4f3ad8616c4183ce7b0d72df634400945b41ea9816145fc2430df6003455db7 + 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\20241217\ucrt-x86_64 + key: llvm-mingw-20241217-ucrt-x86_64-f4f3ad8616c4183ce7b0d72df634400945b41ea9816145fc2430df6003455db7 + + - 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-20241217-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\20241217\ucrt-x86_64" + $llvmMingwBin = Join-Path $llvmMingwRoot "bin" + $llvmMingwRuntimeBin = Join-Path $llvmMingwRoot "i686-w64-mingw32\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-20241217-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\20241217\ucrt-x86_64" + $llvmMingwBin = Join-Path $llvmMingwRoot "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 (Test-Path $pkgconf) -or -not (Test-Path $hostPC)) { + throw "The CLANG64 host pkg-config profile is unavailable" + } + $targetLibPC = Join-Path $vcpkgRoot "lib\pkgconfig" + $targetSharePC = Join-Path $vcpkgRoot "share\pkgconfig" + $pkgConfigLibDir = "$hostPC;$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" diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 7525ba599e..6b2ba4a141 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: 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..4201b39978 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 @@ -210,7 +220,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" @@ -265,7 +277,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 @@ -347,6 +359,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 +377,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 diff --git a/.github/workflows/test_windows_toolchain_profile.ps1 b/.github/workflows/test_windows_toolchain_profile.ps1 index 3d0ee71090..98acfeee68 100644 --- a/.github/workflows/test_windows_toolchain_profile.ps1 +++ b/.github/workflows/test_windows_toolchain_profile.ps1 @@ -70,17 +70,26 @@ $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")) { +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 +105,20 @@ 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() +$compilerTarget = (& clang -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 +135,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" From 799ba63b74ab36621ce9098c668752eefa8b70a5 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 29 Aug 2026 23:40:49 +0800 Subject: [PATCH 02/54] ci: preserve the llvm-mingw runtime path --- .github/actions/setup-llvm-mingw/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/setup-llvm-mingw/action.yml b/.github/actions/setup-llvm-mingw/action.yml index bade654070..fc8da47c80 100644 --- a/.github/actions/setup-llvm-mingw/action.yml +++ b/.github/actions/setup-llvm-mingw/action.yml @@ -132,6 +132,7 @@ runs: $ErrorActionPreference = "Stop" $llvmMingwRoot = Join-Path $env:RUNNER_TOOL_CACHE "llvm-mingw\20241217\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" From 9859b822b3a0c22bdeb7fce5cc08102448a4fe1b Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 29 Aug 2026 23:47:23 +0800 Subject: [PATCH 03/54] ci: isolate llvm-mingw host and target metadata --- .github/actions/setup-llvm-mingw/action.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-llvm-mingw/action.yml b/.github/actions/setup-llvm-mingw/action.yml index fc8da47c80..7c8f750ca0 100644 --- a/.github/actions/setup-llvm-mingw/action.yml +++ b/.github/actions/setup-llvm-mingw/action.yml @@ -162,9 +162,15 @@ runs: if (-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" + New-Item -ItemType Directory -Force $hostLLVMPC | 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 = "$hostPC;$targetLibPC;$targetSharePC" + $pkgConfigLibDir = "$hostLLVMPC;$targetLibPC;$targetSharePC" @" @echo off setlocal From 5301f21c2e104da3c82de268049131c95b1efc88 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 00:40:13 +0800 Subject: [PATCH 04/54] build: support cross-host Windows GNU toolchains --- internal/build/build.go | 17 +++++++++-------- internal/build/native_toolchain_test.go | 9 ++++++--- internal/crosscompile/crosscompile.go | 16 +++++++++------- internal/crosscompile/crosscompile_test.go | 8 +++++--- internal/crosscompile/windows_toolchain.go | 10 +++++----- 5 files changed, 34 insertions(+), 26 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index abde7f16c6..3706692172 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -492,12 +492,12 @@ 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) { + if usesWindowsToolchainProfile(runtime.GOOS, conf) { nativeInput, err = parseNativeToolchainInput(commands, conf.LinkOptions) if err != nil { return nil, err } - nativeInput.ResolveCrossArch = conf.Mode != ModeGen + nativeInput.ResolveWindows = 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 { @@ -919,12 +919,13 @@ 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) { diff --git a/internal/build/native_toolchain_test.go b/internal/build/native_toolchain_test.go index cbe1e48167..94c633b1ee 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) } }) } 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 { From f04e9ba6bbfc5574730e91c35667d69361fc62cd Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 01:37:59 +0800 Subject: [PATCH 05/54] ssa: select the MinGW setjmp intrinsic --- ssa/eh.go | 7 +++++++ ssa/eh_patch_test.go | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) 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) { From 28f3317003a7c61187d038f1e757c09793b453ab Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 01:37:59 +0800 Subject: [PATCH 06/54] ci: qualify Windows GNU targets from Unix hosts --- .../activate-windows-target/action.yml | 39 +++ .github/actions/setup-llvm-mingw/action.yml | 222 +++++++++++++++++- .github/actions/test-windows-cross/action.yml | 101 ++++++++ .github/workflows/llgo.yml | 45 ++++ 4 files changed, 396 insertions(+), 11 deletions(-) create mode 100644 .github/actions/test-windows-cross/action.yml diff --git a/.github/actions/activate-windows-target/action.yml b/.github/actions/activate-windows-target/action.yml index 43a0ca4d3d..575490687a 100644 --- a/.github/actions/activate-windows-target/action.yml +++ b/.github/actions/activate-windows-target/action.yml @@ -96,3 +96,42 @@ runs: 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 "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-llvm-mingw/action.yml b/.github/actions/setup-llvm-mingw/action.yml index 7c8f750ca0..6ce79999fa 100644 --- a/.github/actions/setup-llvm-mingw/action.yml +++ b/.github/actions/setup-llvm-mingw/action.yml @@ -17,26 +17,43 @@ runs: shell: pwsh run: | if ("${{ inputs.llvm-version }}" -ne "19") { - throw "llvm-mingw 20241217 is paired with LLVM 19, not ${{ inputs.llvm-version }}" + 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\20241217\ucrt-x86_64 - key: llvm-mingw-20241217-ucrt-x86_64-f4f3ad8616c4183ce7b0d72df634400945b41ea9816145fc2430df6003455db7 + 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: "20241217" - LLVM_MINGW_SHA256: f4f3ad8616c4183ce7b0d72df634400945b41ea9816145fc2430df6003455db7 + LLVM_MINGW_VERSION: "20250114" + LLVM_MINGW_SHA256: 9395d95c089611e6cd728c9a07219ffc5ac706e0c719833a2f7e64a706a81541 run: | $ErrorActionPreference = "Stop" $asset = "llvm-mingw-$env:LLVM_MINGW_VERSION-ucrt-x86_64.zip" @@ -67,8 +84,8 @@ runs: if: runner.os == 'Windows' && steps.llvm_mingw_cache.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: - path: ${{ runner.tool_cache }}\llvm-mingw\20241217\ucrt-x86_64 - key: llvm-mingw-20241217-ucrt-x86_64-f4f3ad8616c4183ce7b0d72df634400945b41ea9816145fc2430df6003455db7 + 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' @@ -76,7 +93,7 @@ runs: uses: actions/cache/restore@v4 with: path: ${{ runner.tool_cache }}\llgo-mingw-vcpkg\${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }}\386 - key: llgo-llvm-mingw-20241217-ucrt-386-${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + 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' @@ -89,7 +106,7 @@ runs: $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\20241217\ucrt-x86_64" + $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" $env:PATH = "$llvmMingwBin;$env:PATH" @@ -121,7 +138,7 @@ runs: uses: actions/cache/save@v4 with: path: ${{ runner.tool_cache }}\llgo-mingw-vcpkg\${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }}\386 - key: llgo-llvm-mingw-20241217-ucrt-386-${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + key: llgo-llvm-mingw-20250114-ucrt-386-${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} - name: Configure llvm-mingw target if: runner.os == 'Windows' @@ -130,7 +147,7 @@ runs: VCPKG_MANIFEST_HASH: ${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} run: | $ErrorActionPreference = "Stop" - $llvmMingwRoot = Join-Path $env:RUNNER_TOOL_CACHE "llvm-mingw\20241217\ucrt-x86_64" + $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" @@ -210,3 +227,186 @@ runs: 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' + 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' && 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' && 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="$RUNNER_TOOL_CACHE/llvm-mingw/20250114/ucrt-${RUNNER_OS}-${RUNNER_ARCH}" + export PATH="$llvm_mingw_root/bin:$PATH" + + # The three architectures run sequentially in one job. Reuse the + # pinned vcpkg checkout and host tool instead of cloning/bootstrapping + # it once per target; target build and 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="$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 across sequential architecture + # qualifications; a previous target's wrapper may already be on 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_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..ce45d429fc --- /dev/null +++ b/.github/actions/test-windows-cross/action.yml @@ -0,0 +1,101 @@ +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 + # Make this invocation independent of any target selected by an + # earlier architecture step in the same cross-host job. Leaving the + # Go-compatible compiler overrides unset also exercises LLGo's normal + # compiler-target probing path. + export PATH="$LLGO_MINGW_TARGET_TOOLS:$PATH" + unset CC CXX PKG_CONFIG 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/llgo.yml b/.github/workflows/llgo.yml index 4201b39978..724f41b370 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -516,6 +516,51 @@ jobs: iwasm --stack-size=819200000 --heap-size=800000000 hello.wasm + windows-gnu-cross: + name: windows GNU cross (${{ matrix.os }}) + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-latest] + 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: | + GOWORK="$GITHUB_WORKSPACE/.github/plan9asm.work" \ + go build -p=2 -o "$RUNNER_TEMP/llgo-host" ./cmd/llgo + + - name: Test cross-host windows/amd64 + uses: ./.github/actions/test-windows-cross + with: + llgo: ${{ runner.temp }}/llgo-host + windows-arch: amd64 + + - name: Test cross-host windows/arm64 + uses: ./.github/actions/test-windows-cross + with: + llgo: ${{ runner.temp }}/llgo-host + windows-arch: arm64 + + - name: Test cross-host windows/386 + uses: ./.github/actions/test-windows-cross + with: + llgo: ${{ runner.temp }}/llgo-host + windows-arch: "386" + wasm-runtime: name: wasm-runtime (Go 1.24 and 1.27) timeout-minutes: 30 From 48df307f675a8e7c55902624d78928a48fc57d1d Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 01:45:32 +0800 Subject: [PATCH 07/54] ci: reuse cross-host llvm-mingw between targets --- .github/actions/setup-llvm-mingw/action.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/actions/setup-llvm-mingw/action.yml b/.github/actions/setup-llvm-mingw/action.yml index 6ce79999fa..fe86465f41 100644 --- a/.github/actions/setup-llvm-mingw/action.yml +++ b/.github/actions/setup-llvm-mingw/action.yml @@ -229,7 +229,7 @@ runs: Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_MINGW_TARGET_VCPKG_ROOT=$vcpkgRoot" - name: Restore cross-host llvm-mingw - if: runner.os != 'Windows' + if: runner.os != 'Windows' && env.LLGO_MINGW_CROSS_ROOT == '' id: cross_host_llvm_mingw_cache uses: actions/cache/restore@v4 with: @@ -237,7 +237,7 @@ runs: key: llvm-mingw-20250114-ucrt-${{ runner.os }}-${{ runner.arch }}-${{ runner.os == 'macOS' && '80b2e7ade71ba2dfe9e8d27fe47ae5738b1fb8d34e057faa2beef3070392f2d6' || 'a16f52dee819797248e6c7d63b8b1e50a92119f45767ecd8e9633d1733b896e2' }} - name: Install cross-host llvm-mingw - if: runner.os != 'Windows' && steps.cross_host_llvm_mingw_cache.outputs.cache-hit != 'true' + 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" @@ -281,7 +281,7 @@ runs: mv "$source_root" "$install_root" - name: Save cross-host llvm-mingw - if: runner.os != 'Windows' && steps.cross_host_llvm_mingw_cache.outputs.cache-hit != 'true' + 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 }} @@ -312,7 +312,7 @@ runs: 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="$RUNNER_TOOL_CACHE/llvm-mingw/20250114/ucrt-${RUNNER_OS}-${RUNNER_ARCH}" + 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" # The three architectures run sequentially in one job. Reuse the @@ -409,4 +409,5 @@ runs: 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" From 24bc70dd20e969ac4e9d1c9c3e8ec800a9120443 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 01:52:53 +0800 Subject: [PATCH 08/54] build: construct a complete Windows toolchain input --- internal/build/build.go | 11 +++++++---- internal/build/native_toolchain_test.go | 9 ++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index 3706692172..1d323bcf62 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -493,11 +493,10 @@ func Build(inv Invocation) (result []Package, resultErr error) { forceEspClang := conf.ForceEspClang || conf.Target != "" nativeInput := crosscompile.NativeToolchainInput{} if usesWindowsToolchainProfile(runtime.GOOS, conf) { - nativeInput, err = parseNativeToolchainInput(commands, conf.LinkOptions) + nativeInput, err = parseNativeToolchainInput(commands, conf.LinkOptions, conf.Mode != ModeGen) if err != nil { return nil, err } - nativeInput.ResolveWindows = 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 { @@ -928,8 +927,12 @@ func usesWindowsToolchainProfile(hostGOOS string, conf *Config) bool { (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 diff --git a/internal/build/native_toolchain_test.go b/internal/build/native_toolchain_test.go index 94c633b1ee..a6718d4792 100644 --- a/internal/build/native_toolchain_test.go +++ b/internal/build/native_toolchain_test.go @@ -51,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 @@ -91,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) } @@ -103,7 +106,7 @@ func TestParseNativeToolchainInputAcceptsEmptyLinkerFlags(t *testing.T) { got, err := parseNativeToolchainInput(commandEnv{}, LinkOptions{ ExternalLinker: " ", ExternalLinkerFlags: "\t", - }) + }, false) if err != nil { t.Fatal(err) } From 1d7e48b609cd3a5283d0f5cb280c36a44a6044c2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 02:58:35 +0800 Subject: [PATCH 09/54] ci: honor an existing llvm-mingw root --- .github/actions/setup-llvm-mingw/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-llvm-mingw/action.yml b/.github/actions/setup-llvm-mingw/action.yml index fe86465f41..9687aa89f0 100644 --- a/.github/actions/setup-llvm-mingw/action.yml +++ b/.github/actions/setup-llvm-mingw/action.yml @@ -363,7 +363,7 @@ runs: ;; esac - llvm_mingw_root="$RUNNER_TOOL_CACHE/llvm-mingw/20250114/ucrt-${RUNNER_OS}-${RUNNER_ARCH}" + 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" From 36876d137afe87c9cd9129047e0f90ee48da034b Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 02:58:35 +0800 Subject: [PATCH 10/54] ci: inspect the activated MSVC target --- .github/workflows/test_windows_toolchain_profile.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_windows_toolchain_profile.ps1 b/.github/workflows/test_windows_toolchain_profile.ps1 index 98acfeee68..9d81019734 100644 --- a/.github/workflows/test_windows_toolchain_profile.ps1 +++ b/.github/workflows/test_windows_toolchain_profile.ps1 @@ -105,7 +105,13 @@ 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 -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) { From 3638984601944dec08e31d6118f84a0b4d165658 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 03:19:48 +0800 Subject: [PATCH 11/54] ci: expose runtime patches to cross-host builds --- .github/workflows/llgo.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 724f41b370..3ee3a094cd 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -542,6 +542,10 @@ jobs: run: | GOWORK="$GITHUB_WORKSPACE/.github/plan9asm.work" \ 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/amd64 uses: ./.github/actions/test-windows-cross From 0022b3943ec8b821e25357ef91a3f1fbed0c8673 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 03:36:24 +0800 Subject: [PATCH 12/54] build: cover malformed native toolchain input --- internal/build/native_toolchain_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/build/native_toolchain_test.go b/internal/build/native_toolchain_test.go index a6718d4792..b55758f5b2 100644 --- a/internal/build/native_toolchain_test.go +++ b/internal/build/native_toolchain_test.go @@ -102,6 +102,19 @@ 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: " ", From e348fa2b9f5853e12d61f4f280997678b48c2126 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 03:36:24 +0800 Subject: [PATCH 13/54] ci: isolate 386 benchmark host runtime --- .github/workflows/benchmark.yml | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 6b2ba4a141..3b6f26bc5f 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -143,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 From 3e3a18b14876687442949c7a45a54a0d5b18d995 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 04:19:38 +0800 Subject: [PATCH 14/54] ci: bypass command wrappers for MinGW 386 links --- .../actions/activate-windows-target/action.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/actions/activate-windows-target/action.yml b/.github/actions/activate-windows-target/action.yml index 575490687a..35c729ebad 100644 --- a/.github/actions/activate-windows-target/action.yml +++ b/.github/actions/activate-windows-target/action.yml @@ -64,6 +64,8 @@ runs: "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")) { @@ -73,6 +75,12 @@ runs: } } $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" @@ -84,6 +92,11 @@ runs: 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 commands typed by users, but let LLGo invoke + # the real drivers directly. cmd.exe limits .cmd command lines to + # 8191 characters, which is too short for LLVM's static link flags. + $cc = $compiler + $cxx = $compilerXX } else { $target = (& clang.exe -dumpmachine).Trim() if ($target -notmatch "^$targetArch-.*-(windows-gnu|mingw32)$") { @@ -91,8 +104,8 @@ 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 "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" From a334a338f0e43aa5cc755b2f62657e2f11318654 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 05:25:45 +0800 Subject: [PATCH 15/54] ci: scope direct MinGW 386 drivers to benchmarks --- .../actions/activate-windows-target/action.yml | 17 ++--------------- .github/workflows/benchmark.yml | 10 ++++++++++ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/actions/activate-windows-target/action.yml b/.github/actions/activate-windows-target/action.yml index 35c729ebad..575490687a 100644 --- a/.github/actions/activate-windows-target/action.yml +++ b/.github/actions/activate-windows-target/action.yml @@ -64,8 +64,6 @@ runs: "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")) { @@ -75,12 +73,6 @@ runs: } } $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" @@ -92,11 +84,6 @@ runs: 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 commands typed by users, but let LLGo invoke - # the real drivers directly. cmd.exe limits .cmd command lines to - # 8191 characters, which is too short for LLVM's static link flags. - $cc = $compiler - $cxx = $compilerXX } else { $target = (& clang.exe -dumpmachine).Trim() if ($target -notmatch "^$targetArch-.*-(windows-gnu|mingw32)$") { @@ -104,8 +91,8 @@ runs: } } - 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 "CC=clang" + Add-Content -Encoding utf8 $env:GITHUB_ENV "CXX=clang++" 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" diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 3b6f26bc5f..cc4e95287c 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -193,6 +193,16 @@ jobs: windows-abi: ${{ matrix.windows_abi }} windows-arch: ${{ matrix.windows_arch }} + - name: Use direct MinGW 386 drivers for benchmark links + if: runner.os == 'Windows' && matrix.windows_abi == 'mingw' && matrix.windows_arch == '386' + shell: pwsh + run: | + # The general target profile keeps command wrappers so unqualified + # clang follows GOARCH. Benchmark links include LLVM's complete + # static library list, which exceeds cmd.exe's 8191-character limit. + Add-Content -Encoding utf8 $env:GITHUB_ENV "CC=$(Join-Path $env:LLGO_MINGW_TARGET_BIN 'i686-w64-mingw32-clang.exe')" + Add-Content -Encoding utf8 $env:GITHUB_ENV "CXX=$(Join-Path $env:LLGO_MINGW_TARGET_BIN 'i686-w64-mingw32-clang++.exe')" + - name: Measure pull request base if: github.event_name == 'pull_request' id: measure-base From bd399b85f9397c18b0e00b2258f05c7b596d3f65 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 17:20:17 +0800 Subject: [PATCH 16/54] ci: select cross-host Windows compilers explicitly --- .github/actions/activate-windows-target/action.yml | 3 +++ .github/actions/test-windows-cross/action.yml | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/actions/activate-windows-target/action.yml b/.github/actions/activate-windows-target/action.yml index 575490687a..2826267097 100644 --- a/.github/actions/activate-windows-target/action.yml +++ b/.github/actions/activate-windows-target/action.yml @@ -131,6 +131,9 @@ runs: # 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" diff --git a/.github/actions/test-windows-cross/action.yml b/.github/actions/test-windows-cross/action.yml index ce45d429fc..28d64a73ba 100644 --- a/.github/actions/test-windows-cross/action.yml +++ b/.github/actions/test-windows-cross/action.yml @@ -43,11 +43,14 @@ runs: exit 1 fi # Make this invocation independent of any target selected by an - # earlier architecture step in the same cross-host job. Leaving the - # Go-compatible compiler overrides unset also exercises LLGo's normal - # compiler-target probing path. + # earlier architecture step in the same cross-host job. The activation + # action exports these target-selecting wrappers so callers do not need + # to assemble compiler flags themselves. export PATH="$LLGO_MINGW_TARGET_TOOLS:$PATH" - unset CC CXX PKG_CONFIG PKG_CONFIG_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" From c0d7334e7452881ad2c8ae4a9f3651417453c0b3 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 17:29:52 +0800 Subject: [PATCH 17/54] ci: isolate cross-host vcpkg compilers --- .github/actions/setup-llvm-mingw/action.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/actions/setup-llvm-mingw/action.yml b/.github/actions/setup-llvm-mingw/action.yml index 9687aa89f0..b105626809 100644 --- a/.github/actions/setup-llvm-mingw/action.yml +++ b/.github/actions/setup-llvm-mingw/action.yml @@ -315,6 +315,13 @@ runs: 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" + # Each target activation persists its compiler wrappers through + # GITHUB_ENV. Do not let a preceding Windows target become vcpkg's + # macOS/Linux host compiler while the next target is 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 + # The three architectures run sequentially in one job. Reuse the # pinned vcpkg checkout and host tool instead of cloning/bootstrapping # it once per target; target build and install roots remain separate. From 277992beacfb8f7b926010dfca5fc57fd469a5e2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 17:32:39 +0800 Subject: [PATCH 18/54] ci: isolate cross-host Windows architectures --- .github/workflows/llgo.yml | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 3ee3a094cd..8bc56d2bd3 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -517,12 +517,13 @@ jobs: iwasm --stack-size=819200000 --heap-size=800000000 hello.wasm windows-gnu-cross: - name: windows GNU cross (${{ matrix.os }}) - timeout-minutes: 180 + 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 @@ -547,23 +548,11 @@ jobs: # available to the following cross-host builds. echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> "$GITHUB_ENV" - - name: Test cross-host windows/amd64 + - name: Test cross-host windows/${{ matrix.windows_arch }} uses: ./.github/actions/test-windows-cross with: llgo: ${{ runner.temp }}/llgo-host - windows-arch: amd64 - - - name: Test cross-host windows/arm64 - uses: ./.github/actions/test-windows-cross - with: - llgo: ${{ runner.temp }}/llgo-host - windows-arch: arm64 - - - name: Test cross-host windows/386 - uses: ./.github/actions/test-windows-cross - with: - llgo: ${{ runner.temp }}/llgo-host - windows-arch: "386" + windows-arch: ${{ matrix.windows_arch }} wasm-runtime: name: wasm-runtime (Go 1.24 and 1.27) From aa5c00f2fc774a19e2ca16792ebe6d3638d30fae Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 17:34:32 +0800 Subject: [PATCH 19/54] ci: clarify isolated cross-host setup --- .github/actions/setup-llvm-mingw/action.yml | 16 ++++++++-------- .github/actions/test-windows-cross/action.yml | 7 +++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/actions/setup-llvm-mingw/action.yml b/.github/actions/setup-llvm-mingw/action.yml index b105626809..537ade8e65 100644 --- a/.github/actions/setup-llvm-mingw/action.yml +++ b/.github/actions/setup-llvm-mingw/action.yml @@ -315,16 +315,16 @@ runs: 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" - # Each target activation persists its compiler wrappers through - # GITHUB_ENV. Do not let a preceding Windows target become vcpkg's - # macOS/Linux host compiler while the next target is configured. + # 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 - # The three architectures run sequentially in one job. Reuse the - # pinned vcpkg checkout and host tool instead of cloning/bootstrapping - # it once per target; target build and install roots remain separate. + # 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 @@ -395,8 +395,8 @@ runs: 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 across sequential architecture - # qualifications; a previous target's wrapper may already be on PATH. + # 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 diff --git a/.github/actions/test-windows-cross/action.yml b/.github/actions/test-windows-cross/action.yml index 28d64a73ba..ead139a597 100644 --- a/.github/actions/test-windows-cross/action.yml +++ b/.github/actions/test-windows-cross/action.yml @@ -42,10 +42,9 @@ runs: echo "LLGo host compiler is not executable: $LLGO_CROSS_COMPILER" >&2 exit 1 fi - # Make this invocation independent of any target selected by an - # earlier architecture step in the same cross-host job. The activation - # action exports these target-selecting wrappers so callers do not need - # to assemble compiler flags themselves. + # 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++" From 55971983c60db7881de09a58e9ac6b7e9ae023d5 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 18:37:13 +0800 Subject: [PATCH 20/54] ci: stabilize isolated Windows architecture lanes --- .../actions/activate-windows-target/action.yml | 18 ++++++++++++++++-- .github/actions/setup-demo-deps/action.yml | 7 +++++-- .github/workflows/benchmark.yml | 10 ---------- cl/_testgo/defer5/in.go | 2 +- cl/_testlibc/setjmp/in.go | 2 +- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.github/actions/activate-windows-target/action.yml b/.github/actions/activate-windows-target/action.yml index 2826267097..e9b0fb3039 100644 --- a/.github/actions/activate-windows-target/action.yml +++ b/.github/actions/activate-windows-target/action.yml @@ -64,6 +64,8 @@ runs: "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")) { @@ -73,6 +75,12 @@ runs: } } $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" @@ -84,6 +92,12 @@ runs: 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)$") { @@ -91,8 +105,8 @@ 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 "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" diff --git a/.github/actions/setup-demo-deps/action.yml b/.github/actions/setup-demo-deps/action.yml index 0cf61b8262..d2151feb09 100644 --- a/.github/actions/setup-demo-deps/action.yml +++ b/.github/actions/setup-demo-deps/action.yml @@ -21,9 +21,12 @@ 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: ${{ inputs.windows-abi == 'mingw' && inputs.windows-arch == 'arm64' && 'arm64' || 'x64' }} + architecture: x64 # setup-python otherwise replaces PKG_CONFIG_PATH, hiding the native # vcpkg metadata installed by setup-deps from LLGo's runtime build. update-environment: false diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index cc4e95287c..3b6f26bc5f 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -193,16 +193,6 @@ jobs: windows-abi: ${{ matrix.windows_abi }} windows-arch: ${{ matrix.windows_arch }} - - name: Use direct MinGW 386 drivers for benchmark links - if: runner.os == 'Windows' && matrix.windows_abi == 'mingw' && matrix.windows_arch == '386' - shell: pwsh - run: | - # The general target profile keeps command wrappers so unqualified - # clang follows GOARCH. Benchmark links include LLVM's complete - # static library list, which exceeds cmd.exe's 8191-character limit. - Add-Content -Encoding utf8 $env:GITHUB_ENV "CC=$(Join-Path $env:LLGO_MINGW_TARGET_BIN 'i686-w64-mingw32-clang.exe')" - Add-Content -Encoding utf8 $env:GITHUB_ENV "CXX=$(Join-Path $env:LLGO_MINGW_TARGET_BIN 'i686-w64-mingw32-clang++.exe')" - - name: Measure pull request base if: github.event_name == 'pull_request' id: measure-base 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 %{{[^ ]+}} From 1662914e32e63650eff8f7498638273ebc9c642e Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 19:19:53 +0800 Subject: [PATCH 21/54] ci: isolate Windows host and target architectures --- .../activate-windows-target/action.yml | 55 +++++++++++++++++-- .github/workflows/test_windows_runtime.ps1 | 40 +++++++------- 2 files changed, 68 insertions(+), 27 deletions(-) diff --git a/.github/actions/activate-windows-target/action.yml b/.github/actions/activate-windows-target/action.yml index e9b0fb3039..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 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" From f99453a5ffa09e4f3a7d248c609dfe2c458d19e1 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 20:23:01 +0800 Subject: [PATCH 22/54] ci: expose demo metadata to MinGW 386 --- .github/actions/setup-llvm-mingw/action.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup-llvm-mingw/action.yml b/.github/actions/setup-llvm-mingw/action.yml index 537ade8e65..9328cf50f9 100644 --- a/.github/actions/setup-llvm-mingw/action.yml +++ b/.github/actions/setup-llvm-mingw/action.yml @@ -183,11 +183,16 @@ runs: # 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" - New-Item -ItemType Directory -Force $hostLLVMPC | Out-Null + # 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 = "$hostLLVMPC;$targetLibPC;$targetSharePC" + $pkgConfigLibDir = "$profilePC;$hostLLVMPC;$targetLibPC;$targetSharePC" @" @echo off setlocal From 6b0dbba08387c37fc61ad83fd53daa2a327eef03 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sun, 30 Aug 2026 21:56:36 +0800 Subject: [PATCH 23/54] ci: use released plan9asm in cross-host builds --- .github/workflows/llgo.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 8bc56d2bd3..30fea1c16d 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -541,8 +541,7 @@ jobs: - name: Build Unix host compiler shell: bash run: | - GOWORK="$GITHUB_WORKSPACE/.github/plan9asm.work" \ - go build -p=2 -o "$RUNNER_TEMP/llgo-host" ./cmd/llgo + 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. From 9ce37c33895b11ca3d64d088aae1c681131d4c6f Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 01:34:07 +0800 Subject: [PATCH 24/54] ci: inspect embedded ELF with host LLVM tools --- .github/workflows/llgo.yml | 16 ++++++++++++++++ _demo/embed/test_esp32c3_startup.sh | 16 ++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 30fea1c16d..35c47445a6 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -186,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. 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 From 62562d9c25df51133a7eadcb77a0ee2c1938f3d1 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 15:54:03 +0800 Subject: [PATCH 25/54] ci: harden llvm-mingw host profile checks --- .github/actions/setup-llvm-mingw/action.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/actions/setup-llvm-mingw/action.yml b/.github/actions/setup-llvm-mingw/action.yml index 9328cf50f9..237701e12f 100644 --- a/.github/actions/setup-llvm-mingw/action.yml +++ b/.github/actions/setup-llvm-mingw/action.yml @@ -108,7 +108,6 @@ runs: $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" - $llvmMingwRuntimeBin = Join-Path $llvmMingwRoot "i686-w64-mingw32\bin" $env:PATH = "$llvmMingwBin;$env:PATH" & git clone --no-checkout https://github.com/microsoft/vcpkg.git $sourceRoot @@ -176,7 +175,7 @@ runs: $pkgconf = $env:LLGO_MINGW_HOST_PKGCONF $hostPC = $env:LLGO_MINGW_HOST_PC_DIR - if (-not (Test-Path $pkgconf) -or -not (Test-Path $hostPC)) { + 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 08cd7c6450ebf9912a783da5fc6a242a1e48c7b1 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 17:18:03 +0800 Subject: [PATCH 26/54] test: isolate Windows recoverable fault probe --- test/go/recover_fault_test.go | 5 +++ test/go/recover_fault_unix_check_test.go | 4 +++ test/go/recover_fault_windows_test.go | 45 +++++++++++++++++++++++- 3 files changed, 53 insertions(+), 1 deletion(-) 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..ed6fc42f6f 100644 --- a/test/go/recover_fault_windows_test.go +++ b/test/go/recover_fault_windows_test.go @@ -7,12 +7,16 @@ import ( "io" "os" "os/exec" + "runtime" "runtime/trace" "testing" "unsafe" ) -const nonNilFaultChildEnv = "LLGO_TEST_NON_NIL_FAULT" +const ( + nonNilFaultChildEnv = "LLGO_TEST_NON_NIL_FAULT" + recoverableFaultChildEnv = "LLGO_TEST_RECOVERABLE_FAULT" +) type traceFaultValue struct { a [16]int @@ -23,6 +27,45 @@ func copyTraceFaultValue(x, y *traceFaultValue) { *x = *y } +func enterRecoverableFaultTest(t *testing.T) bool { + t.Helper() + if os.Getenv(recoverableFaultChildEnv) == "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: Windows exception recovery can use more space below SP + // than Go 1.27 reserves on hosts with large XSTATE contexts. + // The child still has to pass the complete fault/panic/recover assertions; + // once the minimum supported Go release contains the upstream fix, revisit + // this isolation and the stack-headroom preparation below. + cmd := exec.Command(os.Args[0], "-test.run=^TestRecoverAfterFaultPreservesNamedResult$", "-test.v") + cmd.Env = append(os.Environ(), recoverableFaultChildEnv+"=1", "GOTRACEBACK=system") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("recoverable fault 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. + // The 64 one-KiB frames exceed the reported 8 KiB XSTATE-size difference. + // This preserves the real protected-page fault while avoiding the specific + // near-stack-boundary condition described by golang/go#81238. + growRecoverableFaultStack(64) +} + // Regression test matching GOROOT/test/fixedbugs/issue73748b.go. func TestRecoverFaultWhileTracing(t *testing.T) { if err := trace.Start(io.Discard); err != nil { From 76e4e255216f789e560e1e64d8a28e3c58296452 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 18:33:02 +0800 Subject: [PATCH 27/54] test: qualify Windows fault workaround rationale --- test/go/recover_fault_windows_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/go/recover_fault_windows_test.go b/test/go/recover_fault_windows_test.go index ed6fc42f6f..58a030941d 100644 --- a/test/go/recover_fault_windows_test.go +++ b/test/go/recover_fault_windows_test.go @@ -35,11 +35,11 @@ func enterRecoverableFaultTest(t *testing.T) bool { // 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: Windows exception recovery can use more space below SP - // than Go 1.27 reserves on hosts with large XSTATE contexts. + // 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; - // once the minimum supported Go release contains the upstream fix, revisit - // this isolation and the stack-headroom preparation below. + // 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=^TestRecoverAfterFaultPreservesNamedResult$", "-test.v") cmd.Env = append(os.Environ(), recoverableFaultChildEnv+"=1", "GOTRACEBACK=system") if output, err := cmd.CombinedOutput(); err != nil { @@ -60,9 +60,9 @@ func growRecoverableFaultStack(depth int) { func ensureRecoverableFaultStackHeadroom() { // Grow and then unwind the goroutine stack before raising the exception. - // The 64 one-KiB frames exceed the reported 8 KiB XSTATE-size difference. - // This preserves the real protected-page fault while avoiding the specific - // near-stack-boundary condition described by golang/go#81238. + // 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) } From 14aedcde5c1c208fe630553f5df1ae182313c425 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 19:06:30 +0800 Subject: [PATCH 28/54] test: isolate traced Windows fault probe --- test/go/recover_fault_windows_test.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/test/go/recover_fault_windows_test.go b/test/go/recover_fault_windows_test.go index 58a030941d..b26a3b19b7 100644 --- a/test/go/recover_fault_windows_test.go +++ b/test/go/recover_fault_windows_test.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "runtime" + "runtime/debug" "runtime/trace" "testing" "unsafe" @@ -16,6 +17,7 @@ import ( const ( nonNilFaultChildEnv = "LLGO_TEST_NON_NIL_FAULT" recoverableFaultChildEnv = "LLGO_TEST_RECOVERABLE_FAULT" + traceFaultChildEnv = "LLGO_TEST_TRACE_FAULT" ) type traceFaultValue struct { @@ -28,8 +30,15 @@ func copyTraceFaultValue(x, y *traceFaultValue) { } 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(recoverableFaultChildEnv) == "1" { + if os.Getenv(childEnv) == "1" { return true } @@ -40,10 +49,10 @@ func enterRecoverableFaultTest(t *testing.T) bool { // 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=^TestRecoverAfterFaultPreservesNamedResult$", "-test.v") - cmd.Env = append(os.Environ(), recoverableFaultChildEnv+"=1", "GOTRACEBACK=system") + 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("recoverable fault child failed: %v\n%s", err, output) + t.Fatalf("Windows fault test child failed: %v\n%s", err, output) } return false } @@ -68,10 +77,17 @@ func ensureRecoverableFaultStackHeadroom() { // 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() { From 4e7bd85d2f5b07ca80d74f1ccf6882635a978e59 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 04:59:08 +0800 Subject: [PATCH 29/54] ci: qualify Windows debuggers and host shells --- .github/actions/setup-demo-deps/action.yml | 12 +++ .github/actions/setup-deps/action.yml | 57 +++++++-------- .github/workflows/llgo.yml | 42 ++++++++--- .github/workflows/test_windows_host_shell.sh | 73 +++++++++++++++++++ .../test_windows_toolchain_profile.ps1 | 14 ++++ internal/build/dwarf_standard_test.go | 16 +++- 6 files changed, 173 insertions(+), 41 deletions(-) create mode 100755 .github/workflows/test_windows_host_shell.sh diff --git a/.github/actions/setup-demo-deps/action.yml b/.github/actions/setup-demo-deps/action.yml index d2151feb09..8e5f2e80fd 100644 --- a/.github/actions/setup-demo-deps/action.yml +++ b/.github/actions/setup-demo-deps/action.yml @@ -66,6 +66,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-abi == 'msvc' && inputs.windows-arch == '386' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $python = "${{ steps.windows_target_python.outputs.python-path }}" + $prefix = (& $python -c "import sys; print(sys.base_prefix)").Trim() + if (-not (Test-Path (Join-Path $prefix "python312.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 diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml index 0fc03f3fb8..6471055b0f 100644 --- a/.github/actions/setup-deps/action.yml +++ b/.github/actions/setup-deps/action.yml @@ -111,7 +111,7 @@ runs: 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' }} + 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-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' @@ -126,9 +126,10 @@ runs: # 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. + # matching official package. Use the target-native LLDB from the same + # package as well: 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" @@ -147,16 +148,14 @@ 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" @@ -168,19 +167,17 @@ 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" } } @@ -189,7 +186,7 @@ runs: 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' }} + 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-native-lldb-v1' }} - name: Cache Windows MSVC dependencies if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'msvc' @@ -444,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_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')" } diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 35c47445a6..6b45bb2003 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -118,6 +118,33 @@ jobs: -Profile "${{ matrix.windows_abi }}" -GoArch "${{ matrix.windows_arch || 'amd64' }}" + - name: Test from the MSYS2 host shell + if: ${{ matrix.windows_abi == 'mingw' }} + 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" + } + & $bash -c 'cd "$(cygpath -u "$GITHUB_WORKSPACE")" && bash .github/workflows/test_windows_host_shell.sh' + if ($LASTEXITCODE -ne 0) { + throw "Cygwin host-shell qualification failed with exit code $LASTEXITCODE" + } + - name: Test demo without RPATH (expect failure) if: runner.os != 'Windows' run: | @@ -265,17 +292,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" @@ -302,7 +326,7 @@ jobs: bash cmd/llgo/lldbtest/runtest.sh -v - name: DWARF standard tests - if: ${{ startsWith(matrix.os, 'macos') && matrix.os != 'macos-15-intel' }} + if: ${{ (startsWith(matrix.os, 'macos') && matrix.os != 'macos-15-intel') || matrix.windows_abi == 'msvc' }} run: go test -timeout 15m ./internal/build -run '^TestStandardDWARF$' -count=1 -v test: diff --git a/.github/workflows/test_windows_host_shell.sh b/.github/workflows/test_windows_host_shell.sh new file mode 100755 index 0000000000..24b5eee700 --- /dev/null +++ b/.github/workflows/test_windows_host_shell.sh @@ -0,0 +1,73 @@ +#!/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 + +unset CC CXX +executable="$source_dir/host-shell.exe" +trace=$(cd "$source_dir" && MSYS2_ARG_CONV_EXCL='*' "$llgo" build -x -o "$executable" . 2>&1) +if ! grep -Fq -- "$target" <<<"$trace"; then + echo "host shell did not retain target $target:" >&2 + echo "$trace" >&2 + exit 1 +fi + +output=$($executable 2>&1) +want="windows-${LLGO_WINDOWS_ABI}-${LLGO_WINDOWS_ARCH}-host-shell" +if [[ "$output" != "$want" ]]; then + echo "$executable printed '$output', want '$want'" >&2 + exit 1 +fi + +if [[ "$LLGO_WINDOWS_ABI" == msvc ]]; then + readobj=$(dirname "$(cygpath -u "$LLGO_LLDB")")/llvm-readobj.exe +else + readobj=$(cygpath -u "$LLGO_MINGW_HOST_ROOT")/bin/llvm-readobj.exe +fi +imports=$($readobj --coff-imports "$executable") +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_toolchain_profile.ps1 b/.github/workflows/test_windows_toolchain_profile.ps1 index 9d81019734..ac91b6a76e 100644 --- a/.github/workflows/test_windows_toolchain_profile.ps1 +++ b/.github/workflows/test_windows_toolchain_profile.ps1 @@ -161,6 +161,20 @@ 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" + $vsDevLine = 'call "' + $vsDevCmd + '" -no_logo -arch=' + $target.VisualStudio + + ' -host_arch=x64 && 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/internal/build/dwarf_standard_test.go b/internal/build/dwarf_standard_test.go index d29d3c7be5..5b28edfb41 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{ @@ -107,6 +108,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") } From 0fc3e2dd0fa442936d04370b7cde732ca2a89cc6 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 05:18:55 +0800 Subject: [PATCH 30/54] build: preserve explicit Windows PDB linker mode --- internal/build/link_options.go | 41 ++++++++++++ internal/build/link_options_test.go | 24 +++++++- internal/build/windows_pdb_test.go | 96 +++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 internal/build/windows_pdb_test.go diff --git a/internal/build/link_options.go b/internal/build/link_options.go index 857bef59c3..40f937a661 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 @@ -135,5 +137,44 @@ 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) } + +func hasCOFFDebugFlag(value string) 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 isCOFFDebugFlag(lower) { + return true + } + if strings.HasPrefix(lower, "-wl,") { + for _, linkerArg := range strings.Split(strings.TrimPrefix(lower, "-wl,"), ",") { + if isCOFFDebugFlag(linkerArg) { + return true + } + } + } + if lower == "-xlinker" && i+1 < len(args) && isCOFFDebugFlag(strings.ToLower(args[i+1])) { + return true + } + } + 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..d4da5eb4cb 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,25 @@ func TestDebugInfoLinkerArgs(t *testing.T) { } } +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 TestEffectiveOmitDWARF(t *testing.T) { tests := []struct { name string diff --git a/internal/build/windows_pdb_test.go b/internal/build/windows_pdb_test.go new file mode 100644 index 0000000000..2e516151f5 --- /dev/null +++ b/internal/build/windows_pdb_test.go @@ -0,0 +1,96 @@ +//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" + + 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 = 0 + 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; LLDB and + // runtime traceback consumers continue to use the embedded DWARF/PCLN data. + 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", 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"} { + 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) + } +} From 6e014ba25a49e260b620942e1cd14ea52ca4b572 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 05:18:55 +0800 Subject: [PATCH 31/54] ci: qualify remaining Windows debugger profiles --- .github/actions/setup-demo-deps/action.yml | 6 +- .github/actions/setup-deps/action.yml | 36 +++++---- .github/workflows/llgo.yml | 85 +++++++++++++------- .github/workflows/test_windows_host_shell.sh | 6 +- 4 files changed, 81 insertions(+), 52 deletions(-) diff --git a/.github/actions/setup-demo-deps/action.yml b/.github/actions/setup-demo-deps/action.yml index 8e5f2e80fd..1156d55237 100644 --- a/.github/actions/setup-demo-deps/action.yml +++ b/.github/actions/setup-demo-deps/action.yml @@ -44,7 +44,7 @@ runs: 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: @@ -55,7 +55,7 @@ runs: 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" @@ -67,7 +67,7 @@ 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-abi == 'msvc' && inputs.windows-arch == '386' + if: runner.os == 'Windows' && inputs.windows-arch == '386' shell: pwsh run: | $ErrorActionPreference = "Stop" diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml index 6471055b0f..c4d2cd3b13 100644 --- a/.github/actions/setup-deps/action.yml +++ b/.github/actions/setup-deps/action.yml @@ -105,16 +105,16 @@ 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-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" @@ -124,12 +124,11 @@ runs: 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. Use the target-native LLDB from the same - # package as well: the x64 debugger cannot reliably inspect ARM64 - # register variables or cross the WoW64 exception dispatcher into a - # 32-bit source frame. + # 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" @@ -158,7 +157,7 @@ runs: ) & $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)) { @@ -181,8 +180,8 @@ runs: } } - - 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 }} @@ -663,6 +662,15 @@ runs: 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.3\${{ 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. diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 6b45bb2003..875081b355 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -118,33 +118,6 @@ jobs: -Profile "${{ matrix.windows_abi }}" -GoArch "${{ matrix.windows_arch || 'amd64' }}" - - name: Test from the MSYS2 host shell - if: ${{ matrix.windows_abi == 'mingw' }} - 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" - } - & $bash -c 'cd "$(cygpath -u "$GITHUB_WORKSPACE")" && bash .github/workflows/test_windows_host_shell.sh' - if ($LASTEXITCODE -ne 0) { - throw "Cygwin host-shell qualification failed with exit code $LASTEXITCODE" - } - - name: Test demo without RPATH (expect failure) if: runner.os != 'Windows' run: | @@ -325,9 +298,61 @@ 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') || matrix.windows_abi == 'msvc' }} - 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 + if: ${{ (startsWith(matrix.os, 'macos') && matrix.os != 'macos-15-intel') || runner.os == 'Windows' }} + 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" + } + & $bash -c 'cd "$(cygpath -u "$GITHUB_WORKSPACE")" && 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 }}) diff --git a/.github/workflows/test_windows_host_shell.sh b/.github/workflows/test_windows_host_shell.sh index 24b5eee700..dd5765f1d8 100755 --- a/.github/workflows/test_windows_host_shell.sh +++ b/.github/workflows/test_windows_host_shell.sh @@ -56,11 +56,7 @@ if [[ "$output" != "$want" ]]; then exit 1 fi -if [[ "$LLGO_WINDOWS_ABI" == msvc ]]; then - readobj=$(dirname "$(cygpath -u "$LLGO_LLDB")")/llvm-readobj.exe -else - readobj=$(cygpath -u "$LLGO_MINGW_HOST_ROOT")/bin/llvm-readobj.exe -fi +readobj=$(command -v llvm-readobj.exe || command -v llvm-readobj) imports=$($readobj --coff-imports "$executable") forbidden='(msys-2\.0|cygwin1)\.dll' if [[ "$LLGO_WINDOWS_ABI" == msvc ]]; then From 81f5cc49663f1ff6403b03c5268dbcfc98a75d71 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 05:22:30 +0800 Subject: [PATCH 32/54] test: select O0 for Windows PDB integration --- internal/build/windows_pdb_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/build/windows_pdb_test.go b/internal/build/windows_pdb_test.go index 2e516151f5..993f48a05e 100644 --- a/internal/build/windows_pdb_test.go +++ b/internal/build/windows_pdb_test.go @@ -28,6 +28,7 @@ import ( "strings" "testing" + "github.com/xgo-dev/llgo/internal/optlevel" llvmenv "github.com/xgo-dev/llgo/xtool/env/llvm" ) @@ -41,7 +42,7 @@ func TestWindowsPDBLinkPath(t *testing.T) { pdb := filepath.Join(dir, "pdb-standard.pdb") conf := NewDefaultConf(ModeBuild) conf.OutFile = bin - conf.OptLevel = 0 + 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; LLDB and From 00d0372090d9304c48aacda07730ea092f7e4d1f Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 05:29:29 +0800 Subject: [PATCH 33/54] debug: verify mixed Go and C callback stacks --- cmd/llgo/lldbtest/_wrap/mixed.c | 19 +++++++++++++++++++ cmd/llgo/lldbtest/main.go | 17 +++++++++++++++++ cmd/llgo/lldbtest/test.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 cmd/llgo/lldbtest/_wrap/mixed.c diff --git a/cmd/llgo/lldbtest/_wrap/mixed.c b/cmd/llgo/lldbtest/_wrap/mixed.c new file mode 100644 index 0000000000..ebfba6fd97 --- /dev/null +++ b/cmd/llgo/lldbtest/_wrap/mixed.c @@ -0,0 +1,19 @@ +#if defined(_MSC_VER) +#define LLGO_NOINLINE __declspec(noinline) +#else +#define LLGO_NOINLINE __attribute__((noinline)) +#endif + +extern int llgo_lldb_go_callback(int value); + +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; +} diff --git a/cmd/llgo/lldbtest/main.go b/cmd/llgo/lldbtest/main.go index e0f1d0999a..5b7b01cc47 100644 --- a/cmd/llgo/lldbtest/main.go +++ b/cmd/llgo/lldbtest/main.go @@ -3,8 +3,22 @@ package main import ( "errors" "strings" + _ "unsafe" ) +const LLGoFiles = "_wrap/mixed.c" + +//go:linkname lldbMixedCall C.llgo_lldb_mixed_call +func lldbMixedCall(value int32) int32 + +//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 +} + type Base struct { name string } @@ -296,6 +310,9 @@ func ScopeSwitch(i int) { } func main() { + if got := lldbMixedCall(39); got != 45 { + panic("mixed Go/C callback returned the wrong value") + } FuncStructParams(TinyStruct{I: 1}, SmallStruct{I: 2, J: 3}, MidStruct{I: 4, J: 5, K: 6}, BigStruct{I: 7, J: 8, K: 9, L: 10, M: 11, N: 12, O: 13, P: 14, Q: 15, R: 16}) FuncStructPtrParams(&TinyStruct{I: 1}, &SmallStruct{I: 2, J: 3}, &MidStruct{I: 4, J: 5, K: 6}, &BigStruct{I: 7, J: 8, K: 9, L: 10, M: 11, N: 12, O: 13, P: 14, Q: 15, R: 16}) i := 100 diff --git a/cmd/llgo/lldbtest/test.py b/cmd/llgo/lldbtest/test.py index 274c79f25d..04d2941c35 100644 --- a/cmd/llgo/lldbtest/test.py +++ b/cmd/llgo/lldbtest/test.py @@ -211,6 +211,12 @@ 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"), ]), + test_case("mixed_go_c_callback", [ + ("callbackValue", "43"), + ("stack frames", + "llgo_lldb_go_callback llgo_lldb_c_inner llgo_lldb_mixed_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), @@ -383,6 +389,13 @@ def get_current_function_name(self) -> str: frame = self.process.GetSelectedThread().GetFrameAtIndex(0) return 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() llgo_plugin.print_go_expression( @@ -563,6 +576,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": From 5f29d2552acc32f62114a9bda88ca45721fa690a Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 05:34:21 +0800 Subject: [PATCH 34/54] build: preserve C source DWARF with Go debug info --- internal/build/build.go | 1 + internal/build/link_options.go | 11 +++++++++++ internal/build/link_options_test.go | 23 +++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/internal/build/build.go b/internal/build/build.go index 1d323bcf62..fc77d40ac7 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -3251,6 +3251,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/link_options.go b/internal/build/link_options.go index 40f937a661..5713509a6f 100644 --- a/internal/build/link_options.go +++ b/internal/build/link_options.go @@ -148,6 +148,17 @@ func debugInfoLinkerArgs(conf *Config, target *crosscompile.Export) []string { return slices.Clone(target.DebugInfo.PreserveLinkFlags) } +// debugInfoCompilerArgs keeps package C/C++ sources on the same typed DWARF +// policy as generated Go code. In particular, COFF builds deliberately use +// DWARF rather than Clang's MSVC-default CodeView so LLDB and Go traceback +// consumers see one source format in the linked image. +func debugInfoCompilerArgs(conf *Config, target *crosscompile.Export) []string { + if shouldEmitDebugInfo(conf, target) { + return []string{"-gdwarf-4"} + } + return nil +} + func hasCOFFDebugFlag(value string) bool { args, err := quoted.Split(value) if err != nil { diff --git a/internal/build/link_options_test.go b/internal/build/link_options_test.go index d4da5eb4cb..588afd3c75 100644 --- a/internal/build/link_options_test.go +++ b/internal/build/link_options_test.go @@ -66,6 +66,29 @@ 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: "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 From 9baf66be662add337e555b0579539e44d481ae73 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 05:34:21 +0800 Subject: [PATCH 35/54] debug: verify mixed callback fault stacks --- cmd/llgo/lldbtest/_wrap/mixed.c | 13 +++++++++++++ cmd/llgo/lldbtest/main.go | 16 ++++++++++++++++ cmd/llgo/lldbtest/runtest.sh | 3 ++- cmd/llgo/lldbtest/test.py | 12 +++++++++++- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/cmd/llgo/lldbtest/_wrap/mixed.c b/cmd/llgo/lldbtest/_wrap/mixed.c index ebfba6fd97..cd18bcb7d7 100644 --- a/cmd/llgo/lldbtest/_wrap/mixed.c +++ b/cmd/llgo/lldbtest/_wrap/mixed.c @@ -5,6 +5,9 @@ #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) { @@ -17,3 +20,13 @@ 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/main.go b/cmd/llgo/lldbtest/main.go index 5b7b01cc47..6f406ce4dc 100644 --- a/cmd/llgo/lldbtest/main.go +++ b/cmd/llgo/lldbtest/main.go @@ -2,6 +2,7 @@ package main import ( "errors" + "os" "strings" _ "unsafe" ) @@ -11,6 +12,12 @@ 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 { @@ -19,6 +26,12 @@ func llgo_lldb_go_callback(value int32) int32 { return callbackValue } +//go:noinline +//export llgo_lldb_go_fault_callback +func llgo_lldb_go_fault_callback() { + lldbCFault() +} + type Base struct { name string } @@ -388,6 +401,9 @@ func main() { println("") println(&s, &globalStruct, globalStructPtr.i16, globalStructPtr) globalStructPtr = nil + if os.Getenv("LLGO_LLDB_FAULT_TEST") != "" { + lldbMixedFaultCall() + } } var globalInt int = 301 diff --git a/cmd/llgo/lldbtest/runtest.sh b/cmd/llgo/lldbtest/runtest.sh index 3a6eef32fb..cb4664e4dd 100755 --- a/cmd/llgo/lldbtest/runtest.sh +++ b/cmd/llgo/lldbtest/runtest.sh @@ -51,7 +51,7 @@ 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')" + "script test.run_tests_with_result('./debug.out', ['main.go', '_wrap/mixed.c'], $verbose, $interactive, $plugin_path, '$result_file_for_lldb')" "quit" ) @@ -63,6 +63,7 @@ done cd "$package_path" # Run LLDB with the embedded LLGo plugin and the test script. +export LLGO_LLDB_FAULT_TEST=1 llgo lldb -lldb "$LLDB_PATH" -- "${lldb_args[@]}" # Read the exit code from the result file diff --git a/cmd/llgo/lldbtest/test.py b/cmd/llgo/lldbtest/test.py index 04d2941c35..0ad7db9bc3 100644 --- a/cmd/llgo/lldbtest/test.py +++ b/cmd/llgo/lldbtest/test.py @@ -217,6 +217,16 @@ def test_case(marker: str, expectations: List[tuple]) -> TestCase: "llgo_lldb_go_callback llgo_lldb_c_inner llgo_lldb_mixed_call main.main", "ordered_frames"), ]), + TestCase( + source_file="_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), @@ -527,7 +537,7 @@ def run_tests(executable_path: str, source_files: List[str], verbose: bool, inte selected_sources = {os.path.basename(path) for path in source_files} test_cases = [ test_case for test_case in TEST_CASES - if test_case.source_file in selected_sources + if os.path.basename(test_case.source_file) in selected_sources ] if not test_cases: raise LLDBTestException( From 9a7261ff889b875abc2ba91bcae5aee859028c81 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 05:40:40 +0800 Subject: [PATCH 36/54] ci: preserve native paths across Windows host shells --- .github/workflows/llgo.yml | 5 +++- .github/workflows/test_windows_host_shell.sh | 28 ++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 875081b355..10d897f690 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -349,7 +349,10 @@ jobs: if (-not (Test-Path $bash)) { throw "Cygwin bash was not found at $bash" } - & $bash -c 'cd "$(cygpath -u "$GITHUB_WORKSPACE")" && bash .github/workflows/test_windows_host_shell.sh' + # 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" } diff --git a/.github/workflows/test_windows_host_shell.sh b/.github/workflows/test_windows_host_shell.sh index dd5765f1d8..0c6e1138b3 100755 --- a/.github/workflows/test_windows_host_shell.sh +++ b/.github/workflows/test_windows_host_shell.sh @@ -42,14 +42,30 @@ esac unset CC CXX executable="$source_dir/host-shell.exe" -trace=$(cd "$source_dir" && MSYS2_ARG_CONV_EXCL='*' "$llgo" build -x -o "$executable" . 2>&1) +set +e +trace=$(cd "$source_dir" && "$llgo" build -x -o "$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 +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 + 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 @@ -57,7 +73,15 @@ if [[ "$output" != "$want" ]]; then fi readobj=$(command -v llvm-readobj.exe || command -v llvm-readobj) -imports=$($readobj --coff-imports "$executable") +set +e +imports=$($readobj --coff-imports "$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 forbidden='(msys-2\.0|cygwin1)\.dll' if [[ "$LLGO_WINDOWS_ABI" == msvc ]]; then forbidden='(msys-2\.0|cygwin1|libwinpthread)\.dll' From 66553d800505c0f8560aa682eb0285dd667fdb14 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 06:08:27 +0800 Subject: [PATCH 37/54] ci: fix Windows cross-target debug qualification --- .github/workflows/test_windows_toolchain_profile.ps1 | 8 +++++++- internal/build/dwarf_standard_test.go | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_windows_toolchain_profile.ps1 b/.github/workflows/test_windows_toolchain_profile.ps1 index ac91b6a76e..d6e4c7c3c2 100644 --- a/.github/workflows/test_windows_toolchain_profile.ps1 +++ b/.github/workflows/test_windows_toolchain_profile.ps1 @@ -70,6 +70,7 @@ $mainSource.Replace('$Profile', $Profile).Replace('$GoArch', $GoArch) | Set-Content -Encoding utf8 (Join-Path $sourceDir "main.go") $llgo = (Get-Command llgo.exe).Source +$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)[\\/]') { @@ -168,8 +169,13 @@ try { $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 "CC=" && set "CXX=" && cd /d "' + $sourceDir + + ' -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" diff --git a/internal/build/dwarf_standard_test.go b/internal/build/dwarf_standard_test.go index 5b28edfb41..58faa32d26 100644 --- a/internal/build/dwarf_standard_test.go +++ b/internal/build/dwarf_standard_test.go @@ -43,8 +43,8 @@ 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) + bin := filepath.Join(t.TempDir(), "dwarf-standard"+conf.AppExt) conf.OutFile = bin conf.OptLevel = level conf.LinkOptions.DWARF = DWARFPreserve From 0e9d1d9b043c879759b90155750853e99ddbedb8 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 06:13:32 +0800 Subject: [PATCH 38/54] debug: emit CodeView for explicit Windows PDBs --- internal/build/build.go | 2 + internal/build/collect.go | 1 + internal/build/collect_test.go | 38 +++++++++++++++++++ internal/build/fingerprint.go | 3 +- internal/build/link_options.go | 58 +++++++++++++++++++++++++---- internal/build/link_options_test.go | 54 ++++++++++++++++++++++++++- internal/build/windows_pdb_test.go | 10 ++++- internal/debuginfo/builder.go | 11 +++++- internal/debuginfo/builder_test.go | 25 +++++++++++++ ssa/backend_program_test.go | 4 +- ssa/di.go | 5 ++- ssa/package.go | 9 +++++ 12 files changed, 203 insertions(+), 17 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index fc77d40ac7..be4dbda5c2 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -558,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, @@ -587,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 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/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 5713509a6f..7547160f7b 100644 --- a/internal/build/link_options.go +++ b/internal/build/link_options.go @@ -100,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. @@ -148,18 +157,39 @@ func debugInfoLinkerArgs(conf *Config, target *crosscompile.Export) []string { return slices.Clone(target.DebugInfo.PreserveLinkFlags) } -// debugInfoCompilerArgs keeps package C/C++ sources on the same typed DWARF -// policy as generated Go code. In particular, COFF builds deliberately use -// DWARF rather than Clang's MSVC-default CodeView so LLDB and Go traceback -// consumers see one source format in the linked image. +// 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) { - return []string{"-gdwarf-4"} + 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) + } + 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 @@ -168,23 +198,35 @@ func hasCOFFDebugFlag(value string) bool { } for i, arg := range args { lower := strings.ToLower(arg) - if isCOFFDebugFlag(lower) { + if match(lower) { return true } if strings.HasPrefix(lower, "-wl,") { for _, linkerArg := range strings.Split(strings.TrimPrefix(lower, "-wl,"), ",") { - if isCOFFDebugFlag(linkerArg) { + if match(linkerArg) { return true } } } - if lower == "-xlinker" && i+1 < len(args) && isCOFFDebugFlag(strings.ToLower(args[i+1])) { + 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 588afd3c75..28ecdab4c4 100644 --- a/internal/build/link_options_test.go +++ b/internal/build/link_options_test.go @@ -76,6 +76,9 @@ func TestDebugInfoCompilerArgs(t *testing.T) { {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()}, @@ -108,6 +111,49 @@ func TestHasCOFFDebugFlag(t *testing.T) { } } +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 @@ -239,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/windows_pdb_test.go b/internal/build/windows_pdb_test.go index 993f48a05e..d4623b1c76 100644 --- a/internal/build/windows_pdb_test.go +++ b/internal/build/windows_pdb_test.go @@ -75,12 +75,18 @@ func TestWindowsPDBLinkPath(t *testing.T) { binDir := llvmenv.New("").BinDir() pdbutil := filepath.Join(binDir, "llvm-pdbutil") - out, err := exec.Command(pdbutil, "dump", "-summary", "-publics", pdb).CombinedOutput() + 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"} { + 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) } 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..72f1696802 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), 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{} From 66c730b345a42f435dae316777202b55620e73f2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 06:25:15 +0800 Subject: [PATCH 39/54] test: isolate mixed LLDB stack fixtures --- cmd/llgo/lldbtest/main.go | 33 ----------- cmd/llgo/lldbtest/{ => mixed}/_wrap/mixed.c | 0 cmd/llgo/lldbtest/mixed/mixed.go | 40 +++++++++++++ cmd/llgo/lldbtest/runtest.sh | 62 ++++++++++++--------- cmd/llgo/lldbtest/test.py | 25 ++++++--- 5 files changed, 93 insertions(+), 67 deletions(-) rename cmd/llgo/lldbtest/{ => mixed}/_wrap/mixed.c (100%) create mode 100644 cmd/llgo/lldbtest/mixed/mixed.go diff --git a/cmd/llgo/lldbtest/main.go b/cmd/llgo/lldbtest/main.go index 6f406ce4dc..e0f1d0999a 100644 --- a/cmd/llgo/lldbtest/main.go +++ b/cmd/llgo/lldbtest/main.go @@ -2,36 +2,9 @@ package main import ( "errors" - "os" "strings" - _ "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() -} - type Base struct { name string } @@ -323,9 +296,6 @@ func ScopeSwitch(i int) { } func main() { - if got := lldbMixedCall(39); got != 45 { - panic("mixed Go/C callback returned the wrong value") - } FuncStructParams(TinyStruct{I: 1}, SmallStruct{I: 2, J: 3}, MidStruct{I: 4, J: 5, K: 6}, BigStruct{I: 7, J: 8, K: 9, L: 10, M: 11, N: 12, O: 13, P: 14, Q: 15, R: 16}) FuncStructPtrParams(&TinyStruct{I: 1}, &SmallStruct{I: 2, J: 3}, &MidStruct{I: 4, J: 5, K: 6}, &BigStruct{I: 7, J: 8, K: 9, L: 10, M: 11, N: 12, O: 13, P: 14, Q: 15, R: 16}) i := 100 @@ -401,9 +371,6 @@ func main() { println("") println(&s, &globalStruct, globalStructPtr.i16, globalStructPtr) globalStructPtr = nil - if os.Getenv("LLGO_LLDB_FAULT_TEST") != "" { - lldbMixedFaultCall() - } } var globalInt int = 301 diff --git a/cmd/llgo/lldbtest/_wrap/mixed.c b/cmd/llgo/lldbtest/mixed/_wrap/mixed.c similarity index 100% rename from cmd/llgo/lldbtest/_wrap/mixed.c rename to cmd/llgo/lldbtest/mixed/_wrap/mixed.c 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 cb4664e4dd..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,36 +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', '_wrap/mixed.c'], $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. -export LLGO_LLDB_FAULT_TEST=1 -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 0ad7db9bc3..25831a3054 100644 --- a/cmd/llgo/lldbtest/test.py +++ b/cmd/llgo/lldbtest/test.py @@ -211,14 +211,21 @@ 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"), ]), - test_case("mixed_go_c_callback", [ - ("callbackValue", "43"), - ("stack frames", - "llgo_lldb_go_callback llgo_lldb_c_inner llgo_lldb_mixed_call main.main", - "ordered_frames"), - ]), TestCase( - source_file="_wrap/mixed.c", + 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", @@ -534,10 +541,10 @@ 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 os.path.basename(test_case.source_file) in selected_sources + if test_case.source_file in selected_sources ] if not test_cases: raise LLDBTestException( From 780ab6b4719f6fc3132b2b597d56b67054313b34 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 06:28:31 +0800 Subject: [PATCH 40/54] docs: clarify Windows PDB metadata --- internal/build/windows_pdb_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/build/windows_pdb_test.go b/internal/build/windows_pdb_test.go index d4623b1c76..f54de33a4f 100644 --- a/internal/build/windows_pdb_test.go +++ b/internal/build/windows_pdb_test.go @@ -45,8 +45,8 @@ func TestWindowsPDBLinkPath(t *testing.T) { 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; LLDB and - // runtime traceback consumers continue to use the embedded DWARF/PCLN data. + // 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 { From a0315ca237527adf351a6368aed6a9a2b1275b77 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 06:53:03 +0800 Subject: [PATCH 41/54] test: use the resolved Windows executable suffix --- internal/build/dwarf_standard_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/build/dwarf_standard_test.go b/internal/build/dwarf_standard_test.go index 58faa32d26..4e2b154141 100644 --- a/internal/build/dwarf_standard_test.go +++ b/internal/build/dwarf_standard_test.go @@ -44,7 +44,9 @@ func TestStandardDWARF(t *testing.T) { } { t.Run(level.String(), func(t *testing.T) { conf := NewDefaultConf(ModeBuild) - bin := filepath.Join(t.TempDir(), "dwarf-standard"+conf.AppExt) + // 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 From 8c70684ad64a7b86af7dd6dc70d84e761da3204d Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 07:26:14 +0800 Subject: [PATCH 42/54] test: retain MinGW DWARF verifier sections --- internal/build/dwarf_standard_test.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/build/dwarf_standard_test.go b/internal/build/dwarf_standard_test.go index 4e2b154141..c8614e76bf 100644 --- a/internal/build/dwarf_standard_test.go +++ b/internal/build/dwarf_standard_test.go @@ -42,9 +42,17 @@ func TestStandardDWARF(t *testing.T) { optlevel.Os, optlevel.Oz, } { - t.Run(level.String(), func(t *testing.T) { - conf := NewDefaultConf(ModeBuild) - // Do resolves the platform extension on an internal config clone, so + t.Run(level.String(), func(t *testing.T) { + conf := NewDefaultConf(ModeBuild) + if runtime.GOOS == "windows" && os.Getenv("LLGO_WINDOWS_ABI") == "mingw" { + // MinGW section GC leaves zero-address tombstones in linked COFF + // 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. + conf.LinkOptions.ExternalLinkerFlags = "-Wl,--no-gc-sections" + } + // 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 From c0abf1080df97f0307c19632414510fe2b17ca2b Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 07:26:27 +0800 Subject: [PATCH 43/54] test: format MinGW DWARF setup --- internal/build/dwarf_standard_test.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/build/dwarf_standard_test.go b/internal/build/dwarf_standard_test.go index c8614e76bf..caf8577b76 100644 --- a/internal/build/dwarf_standard_test.go +++ b/internal/build/dwarf_standard_test.go @@ -42,17 +42,17 @@ func TestStandardDWARF(t *testing.T) { optlevel.Os, optlevel.Oz, } { - t.Run(level.String(), func(t *testing.T) { - conf := NewDefaultConf(ModeBuild) - if runtime.GOOS == "windows" && os.Getenv("LLGO_WINDOWS_ABI") == "mingw" { - // MinGW section GC leaves zero-address tombstones in linked COFF - // 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. - conf.LinkOptions.ExternalLinkerFlags = "-Wl,--no-gc-sections" - } - // Do resolves the platform extension on an internal config clone, so + t.Run(level.String(), func(t *testing.T) { + conf := NewDefaultConf(ModeBuild) + if runtime.GOOS == "windows" && os.Getenv("LLGO_WINDOWS_ABI") == "mingw" { + // MinGW section GC leaves zero-address tombstones in linked COFF + // 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. + conf.LinkOptions.ExternalLinkerFlags = "-Wl,--no-gc-sections" + } + // 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 From 396219e4ae8427083a1eaa597df52b91eaa0a7d7 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 09:57:59 +0800 Subject: [PATCH 44/54] debug: preserve wide Windows 386 integer values --- ssa/di.go | 16 +++++++++++++++- ssa/di_debug_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/ssa/di.go b/ssa/di.go index 72f1696802..4c1b5b09ab 100644 --- a/ssa/di.go +++ b/ssa/di.go @@ -716,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 { @@ -726,6 +726,20 @@ func (b Builder) DIValue(variable *types.Var, v Expr, dv DIVar, scope DIScope, p } } +func (b Builder) needDebugAddr(t types.Type, typ Type) bool { + if needConstructAddr(t) { + 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 := t.(*types.Basic) + return ok && basic.Info()&types.IsInteger != 0 && + b.Prog.Target().effectiveGOOS() == "windows" && + b.Prog.PointerSize() == 4 && b.Prog.SizeOf(typ) > 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{}) From 7acf9aebca8da3621760e6cd44263bda9ea2eaec Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 09:58:18 +0800 Subject: [PATCH 45/54] test: select resolved LLDB source frames --- cmd/llgo/lldbtest/test.py | 51 +++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/cmd/llgo/lldbtest/test.py b/cmd/llgo/lldbtest/test.py index 25831a3054..fd6bf66e10 100644 --- a/cmd/llgo/lldbtest/test.py +++ b/cmd/llgo/lldbtest/test.py @@ -320,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', @@ -357,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: @@ -367,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(): @@ -374,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) @@ -399,12 +437,11 @@ 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() @@ -505,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") From 955f32ba26957063c94c92587466d452f222dd74 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 09:58:18 +0800 Subject: [PATCH 46/54] test: retain Windows DWARF verifier sections --- internal/build/dwarf_standard_test.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/internal/build/dwarf_standard_test.go b/internal/build/dwarf_standard_test.go index caf8577b76..6fbdf74c4c 100644 --- a/internal/build/dwarf_standard_test.go +++ b/internal/build/dwarf_standard_test.go @@ -44,13 +44,17 @@ func TestStandardDWARF(t *testing.T) { } { t.Run(level.String(), func(t *testing.T) { conf := NewDefaultConf(ModeBuild) - if runtime.GOOS == "windows" && os.Getenv("LLGO_WINDOWS_ABI") == "mingw" { - // MinGW section GC leaves zero-address tombstones in linked COFF - // 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. - conf.LinkOptions.ExternalLinkerFlags = "-Wl,--no-gc-sections" + 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") == "msvc" { + conf.LinkOptions.ExternalLinkerFlags = "-Xlinker /opt:noref" + } else { + conf.LinkOptions.ExternalLinkerFlags = "-Wl,--no-gc-sections" + } } // Do resolves the platform extension on an internal config clone, so // derive the explicit output path before the build as well. From 4930a00db77cced9bac7a929edd87c25d84cff4e Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 09:58:18 +0800 Subject: [PATCH 47/54] ci: use LLVM 19.1.7 target-native debuggers --- .github/actions/setup-demo-deps/action.yml | 17 ++++++++++++++--- .github/actions/setup-deps/action.yml | 19 ++++++++++--------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/.github/actions/setup-demo-deps/action.yml b/.github/actions/setup-demo-deps/action.yml index 1156d55237..1e040e9120 100644 --- a/.github/actions/setup-demo-deps/action.yml +++ b/.github/actions/setup-demo-deps/action.yml @@ -43,12 +43,23 @@ 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-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 @@ -71,9 +82,9 @@ runs: shell: pwsh run: | $ErrorActionPreference = "Stop" - $python = "${{ steps.windows_target_python.outputs.python-path }}" + $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 "python312.dll"))) { + 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" diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml index c4d2cd3b13..815ee267c9 100644 --- a/.github/actions/setup-deps/action.yml +++ b/.github/actions/setup-deps/action.yml @@ -110,16 +110,16 @@ runs: 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-native-lldb-v1' }} + 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 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" @@ -184,8 +184,8 @@ runs: 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-native-lldb-v1' }} + 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' @@ -247,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" @@ -441,7 +442,7 @@ runs: $lldb = Join-Path $llvmBin "lldb.exe" if ($target.GoArch -ne "amd64") { - $lldbRoot = Join-Path $env:RUNNER_TOOL_CACHE "llgo-compiler-rt\$env:LLVM_VERSION\$($target.GoArch)\lldb" + $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')" } @@ -469,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" } @@ -663,7 +664,7 @@ runs: 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.3\${{ inputs.windows-arch }}\lldb" + $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 }}" From 2e200c531a08d83fa419e8d3267ebfb5cf3d38a1 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 10:16:09 +0800 Subject: [PATCH 48/54] test: honor the default Windows ABI --- internal/build/dwarf_standard_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/build/dwarf_standard_test.go b/internal/build/dwarf_standard_test.go index 6fbdf74c4c..d1eeeb8e7f 100644 --- a/internal/build/dwarf_standard_test.go +++ b/internal/build/dwarf_standard_test.go @@ -50,10 +50,10 @@ func TestStandardDWARF(t *testing.T) { // 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") == "msvc" { - conf.LinkOptions.ExternalLinkerFlags = "-Xlinker /opt:noref" - } else { + 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 From 05f672fc32cfe22d67fd31d1007889945a405d97 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 10:32:14 +0800 Subject: [PATCH 49/54] ci: fix Windows debugger qualification --- .gitattributes | 1 + .github/workflows/llgo.yml | 5 ++++- cmd/llgo/lldbtest/common.sh | 8 ++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) 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/workflows/llgo.yml b/.github/workflows/llgo.yml index 10d897f690..a3157725f3 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -314,7 +314,10 @@ jobs: } - name: Native debug metadata tests - if: ${{ (startsWith(matrix.os, 'macos') && matrix.os != 'macos-15-intel') || runner.os == 'Windows' }} + # 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 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 From dfdf8c5f29b62db2532b85bb440193965802a5f4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 18:44:08 +0800 Subject: [PATCH 50/54] ci: preserve Windows host-shell target profiles --- .github/workflows/test_windows_host_shell.sh | 29 +++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_windows_host_shell.sh b/.github/workflows/test_windows_host_shell.sh index 0c6e1138b3..f9c452cd0e 100755 --- a/.github/workflows/test_windows_host_shell.sh +++ b/.github/workflows/test_windows_host_shell.sh @@ -40,7 +40,34 @@ case "$LLGO_WINDOWS_ABI" in *) echo "unsupported Windows ABI: $LLGO_WINDOWS_ABI" >&2; exit 1 ;; esac -unset CC CXX +prepend_runtime_dir() { + local native_dir=${1:-} + if [[ -n "$native_dir" ]]; then + PATH="$(cygpath -u "$native_dir"):$PATH" + fi +} +case "$LLGO_WINDOWS_ABI" in + mingw) + # Cygwin does not reliably preserve MSYS2's native DLL search entries. + # Reapply the activated MinGW runtime profile using this shell's path + # syntax; target-architecture DLLs must remain ahead of the host profile. + [[ -z "${LLGO_MINGW_HOST_ROOT:-}" ]] || prepend_runtime_dir "$LLGO_MINGW_HOST_ROOT/bin" + prepend_runtime_dir "${LLGO_MINGW_TARGET_RUNTIME_BIN:-}" + [[ -z "${LLGO_MINGW_TARGET_VCPKG_ROOT:-}" ]] || prepend_runtime_dir "$LLGO_MINGW_TARGET_VCPKG_ROOT/bin" + ;; + msvc) + [[ -z "${LLGO_WINDOWS_VCPKG_ROOT:-}" ]] || prepend_runtime_dir "$LLGO_WINDOWS_VCPKG_ROOT/bin" + ;; +esac +export PATH + +# 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" set +e trace=$(cd "$source_dir" && "$llgo" build -x -o "$executable" . 2>&1) From 0d58f7248d1ffdf75e31e588aeeb879be312d689 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 19:42:40 +0800 Subject: [PATCH 51/54] ci: diagnose Windows host-shell load failures --- .github/workflows/test_windows_host_shell.sh | 35 ++++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test_windows_host_shell.sh b/.github/workflows/test_windows_host_shell.sh index f9c452cd0e..af091e768b 100755 --- a/.github/workflows/test_windows_host_shell.sh +++ b/.github/workflows/test_windows_host_shell.sh @@ -43,9 +43,13 @@ esac prepend_runtime_dir() { local native_dir=${1:-} if [[ -n "$native_dir" ]]; then - PATH="$(cygpath -u "$native_dir"):$PATH" + local shell_dir + shell_dir=$(cygpath -u "$native_dir") + PATH="$shell_dir:$PATH" + runtime_search_dirs+=("$shell_dir") fi } +runtime_search_dirs=() case "$LLGO_WINDOWS_ABI" in mingw) # Cygwin does not reliably preserve MSYS2's native DLL search entries. @@ -84,6 +88,17 @@ if ! grep -Fq -- "$target" <<<"$trace"; then exit 1 fi +readobj=$(command -v llvm-readobj.exe || command -v llvm-readobj) +set +e +imports=$($readobj --coff-imports "$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=$? @@ -91,6 +106,14 @@ set -e if [[ $run_status -ne 0 ]]; then echo "$executable failed with exit code $run_status:" >&2 echo "$output" >&2 + echo "runtime search directories:" >&2 + printf ' %s\n' "${runtime_search_dirs[@]}" >&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" @@ -99,16 +122,6 @@ if [[ "$output" != "$want" ]]; then exit 1 fi -readobj=$(command -v llvm-readobj.exe || command -v llvm-readobj) -set +e -imports=$($readobj --coff-imports "$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 forbidden='(msys-2\.0|cygwin1)\.dll' if [[ "$LLGO_WINDOWS_ABI" == msvc ]]; then forbidden='(msys-2\.0|cygwin1|libwinpthread)\.dll' From 8ddbbd83fabe0f361220b79948fa36c860a3eef2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 20:12:19 +0800 Subject: [PATCH 52/54] ci: pass native paths to Windows inspection tools --- .github/workflows/test_windows_host_shell.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_windows_host_shell.sh b/.github/workflows/test_windows_host_shell.sh index af091e768b..cc17f501a8 100755 --- a/.github/workflows/test_windows_host_shell.sh +++ b/.github/workflows/test_windows_host_shell.sh @@ -89,8 +89,9 @@ if ! grep -Fq -- "$target" <<<"$trace"; then fi readobj=$(command -v llvm-readobj.exe || command -v llvm-readobj) +native_executable=$(cygpath -w "$executable") set +e -imports=$($readobj --coff-imports "$executable" 2>&1) +imports=$($readobj --coff-imports "$native_executable" 2>&1) readobj_status=$? set -e if [[ $readobj_status -ne 0 ]]; then From 5ce14445e7b8f5db32d534c08f24ad12fe7e38e9 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 20:15:27 +0800 Subject: [PATCH 53/54] ci: use native output paths from Windows host shells --- .github/workflows/test_windows_host_shell.sh | 34 +++----------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/.github/workflows/test_windows_host_shell.sh b/.github/workflows/test_windows_host_shell.sh index cc17f501a8..19069268b6 100755 --- a/.github/workflows/test_windows_host_shell.sh +++ b/.github/workflows/test_windows_host_shell.sh @@ -40,31 +40,6 @@ case "$LLGO_WINDOWS_ABI" in *) echo "unsupported Windows ABI: $LLGO_WINDOWS_ABI" >&2; exit 1 ;; esac -prepend_runtime_dir() { - local native_dir=${1:-} - if [[ -n "$native_dir" ]]; then - local shell_dir - shell_dir=$(cygpath -u "$native_dir") - PATH="$shell_dir:$PATH" - runtime_search_dirs+=("$shell_dir") - fi -} -runtime_search_dirs=() -case "$LLGO_WINDOWS_ABI" in - mingw) - # Cygwin does not reliably preserve MSYS2's native DLL search entries. - # Reapply the activated MinGW runtime profile using this shell's path - # syntax; target-architecture DLLs must remain ahead of the host profile. - [[ -z "${LLGO_MINGW_HOST_ROOT:-}" ]] || prepend_runtime_dir "$LLGO_MINGW_HOST_ROOT/bin" - prepend_runtime_dir "${LLGO_MINGW_TARGET_RUNTIME_BIN:-}" - [[ -z "${LLGO_MINGW_TARGET_VCPKG_ROOT:-}" ]] || prepend_runtime_dir "$LLGO_MINGW_TARGET_VCPKG_ROOT/bin" - ;; - msvc) - [[ -z "${LLGO_WINDOWS_VCPKG_ROOT:-}" ]] || prepend_runtime_dir "$LLGO_WINDOWS_VCPKG_ROOT/bin" - ;; -esac -export PATH - # 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 @@ -73,8 +48,12 @@ 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 "$executable" . 2>&1) +trace=$(cd "$source_dir" && "$llgo" build -x -o "$native_executable" . 2>&1) build_status=$? set -e if [[ $build_status -ne 0 ]]; then @@ -89,7 +68,6 @@ if ! grep -Fq -- "$target" <<<"$trace"; then fi readobj=$(command -v llvm-readobj.exe || command -v llvm-readobj) -native_executable=$(cygpath -w "$executable") set +e imports=$($readobj --coff-imports "$native_executable" 2>&1) readobj_status=$? @@ -107,8 +85,6 @@ set -e if [[ $run_status -ne 0 ]]; then echo "$executable failed with exit code $run_status:" >&2 echo "$output" >&2 - echo "runtime search directories:" >&2 - printf ' %s\n' "${runtime_search_dirs[@]}" >&2 echo "COFF imports:" >&2 echo "$imports" >&2 if command -v cygcheck >/dev/null 2>&1; then From 9f0a5af09580a2a7ac6019292501e3b4663677c4 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 31 Aug 2026 22:26:19 +0800 Subject: [PATCH 54/54] docs: clarify Windows debug option helpers --- internal/build/link_options.go | 1 + ssa/di.go | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/build/link_options.go b/internal/build/link_options.go index 7547160f7b..08bae2ff28 100644 --- a/internal/build/link_options.go +++ b/internal/build/link_options.go @@ -180,6 +180,7 @@ func hasCOFFPDBFlag(value string) bool { // "/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 diff --git a/ssa/di.go b/ssa/di.go index 4c1b5b09ab..6c0b32760f 100644 --- a/ssa/di.go +++ b/ssa/di.go @@ -726,18 +726,18 @@ func (b Builder) DIValue(variable *types.Var, v Expr, dv DIVar, scope DIScope, p } } -func (b Builder) needDebugAddr(t types.Type, typ Type) bool { - if needConstructAddr(t) { +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 := t.(*types.Basic) + basic, ok := underlying.(*types.Basic) return ok && basic.Info()&types.IsInteger != 0 && b.Prog.Target().effectiveGOOS() == "windows" && - b.Prog.PointerSize() == 4 && b.Prog.SizeOf(typ) > uint64(b.Prog.PointerSize()) + b.Prog.PointerSize() == 4 && b.Prog.SizeOf(ssaType) > uint64(b.Prog.PointerSize()) } const (