diff --git a/.github/actions/setup-demo-deps/action.yml b/.github/actions/setup-demo-deps/action.yml index 714e44ae0a..1d2abdeea1 100644 --- a/.github/actions/setup-demo-deps/action.yml +++ b/.github/actions/setup-demo-deps/action.yml @@ -1,14 +1,27 @@ name: "Setup Demo Dependencies" description: "Install dependencies required by the LLGo demo suite" +inputs: + windows-abi: + description: "Native Windows toolchain profile: msvc or mingw" + required: false + default: "msvc" runs: using: "composite" steps: - name: Set up Python 3.12 on Windows if: runner.os == 'Windows' + id: windows_python uses: actions/setup-python@v6 with: - python-version: "3.12" + # 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. + python-version: ${{ inputs.windows-abi == 'msvc' && '3.12.7' || '3.12' }} + # setup-python otherwise replaces PKG_CONFIG_PATH, hiding the native + # vcpkg metadata installed by setup-deps from LLGo's runtime build. + update-environment: false - name: Install cargs demo library on Unix if: runner.os != 'Windows' @@ -58,11 +71,23 @@ runs: run: | $ErrorActionPreference = "Stop" - $architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture - switch ($architecture) { - "X64" { $targetTriple = "x86_64-pc-windows-msvc" } - "Arm64" { $targetTriple = "aarch64-pc-windows-msvc" } - default { throw "Unsupported Windows architecture: $architecture" } + if ("${{ inputs.windows-abi }}" -notin @("msvc", "mingw")) { + throw "Unsupported Windows ABI profile: ${{ inputs.windows-abi }}" + } + $compiler = (Get-Command clang.exe).Source + $target = (& $compiler -dumpmachine).Trim() + if ("${{ inputs.windows-abi }}" -eq "msvc") { + if ($target -notmatch "-windows-msvc$") { + throw "The MSVC profile resolved Clang target $target" + } + $runtimeArgs = @("-fms-runtime-lib=dll") + $archive = "cargs.lib" + } else { + if ($target -notmatch "-(windows-gnu|mingw32)$") { + throw "The MinGW profile resolved Clang target $target" + } + $runtimeArgs = @() + $archive = "libcargs.a" } $libsDir = Join-Path $env:GITHUB_WORKSPACE "_demo\c\cargs\libs" @@ -77,18 +102,22 @@ runs: Invoke-WebRequest "$baseUrl/src/cargs.c" -OutFile $source $object = Join-Path $env:RUNNER_TEMP "cargs.obj" - & $env:CC "--target=$targetTriple" -O2 -std=c11 ` + & $compiler @runtimeArgs -O2 -std=c11 ` "-I$includeDir" -c $source -o $object if ($LASTEXITCODE -ne 0) { throw "Compiling cargs failed with exit code $LASTEXITCODE" } - & llvm-lib "/out:$(Join-Path $libDir 'cargs.lib')" $object + if ("${{ inputs.windows-abi }}" -eq "msvc") { + & llvm-lib "/out:$(Join-Path $libDir $archive)" $object + } else { + & llvm-ar rcs (Join-Path $libDir $archive) $object + } if ($LASTEXITCODE -ne 0) { throw "Archiving cargs failed with exit code $LASTEXITCODE" } - # A static MSVC library avoids a demo-only DLL search path. pkgconf's - # logical -lcargs entry is resolved by LLGo's Windows dependency path. + # A static target-native library avoids a demo-only DLL search path. + # Each profile's pkg-config resolves -lcargs to its own archive format. # Windows pkgconf relocates a variable named "prefix" relative to the # .pc file by default. Keep these installation paths absolute because # the metadata intentionally lives under RUNNER_TEMP, not the library. @@ -105,8 +134,6 @@ runs: Cflags: -I"`${includedir}" "@ | Set-Content -Encoding ascii (Join-Path $pcDir "cargs.pc") - Add-Content -Encoding utf8 $env:GITHUB_ENV "PKG_CONFIG_PATH=$pcDir;$env:PKG_CONFIG_PATH" - - name: Install Python demo dependencies on Unix if: runner.os != 'Windows' shell: bash @@ -129,19 +156,26 @@ runs: run: | $ErrorActionPreference = "Stop" - python -m pip install numpy torch + $python = "${{ steps.windows_python.outputs.python-path }}" + & $python -m pip install numpy torch if ($LASTEXITCODE -ne 0) { throw "Installing Python demo dependencies failed with exit code $LASTEXITCODE" } - $prefix = (& python -c "import sys; print(sys.base_prefix)").Trim() - $includeDir = (& python -c "import sysconfig; print(sysconfig.get_path('include'))").Trim() - $version = (& python -c "import sys; print(f'{sys.version_info.major}{sys.version_info.minor}')").Trim() + $prefix = (& $python -c "import sys; print(sys.base_prefix)").Trim() + $includeDir = (& $python -c "import sysconfig; print(sysconfig.get_path('include'))").Trim() + $version = (& $python -c "import sys; print(f'{sys.version_info.major}{sys.version_info.minor}')").Trim() $libDir = Join-Path $prefix "libs" $importLibrary = Join-Path $libDir "python$version.lib" if (-not (Test-Path $importLibrary)) { throw "Python import library was not found at $importLibrary" } + if ("${{ inputs.windows-abi }}" -eq "mingw") { + # LLVM's MinGW linker accepts the same COFF import-library contents + # but its -l search uses the GNU filename. Keep the official Python + # artifact untouched and expose only a profile-local spelling. + Copy-Item -Force $importLibrary (Join-Path $libDir "libpython$version.a") + } $pcDir = Join-Path $env:RUNNER_TEMP "llgo-pkgconfig" New-Item -ItemType Directory -Force $pcDir | Out-Null @@ -157,3 +191,6 @@ runs: Libs: -L"`${libdir}" -lpython$version Cflags: -I"`${includedir}" "@ | Set-Content -Encoding ascii (Join-Path $pcDir "python3-embed.pc") + + Add-Content -Encoding utf8 $env:GITHUB_PATH $prefix + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_WINDOWS_PYTHON_HOME=$prefix" diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml index 379d5c6939..0cc3c8c36a 100644 --- a/.github/actions/setup-deps/action.yml +++ b/.github/actions/setup-deps/action.yml @@ -9,24 +9,403 @@ inputs: description: "Whether to install LLVM" required: false default: "true" + windows-abi: + description: "Native Windows toolchain profile: msvc or mingw" + required: false + default: "msvc" runs: using: "composite" steps: - - name: Set up Windows MSYS2 - if: runner.os == 'Windows' && inputs.install-llvm == 'true' - id: msys2 - # CLANG64 provides a native Windows LLVM build. It is only the host - # library used by github.com/xgo-dev/llvm; LLGo's target configuration - # independently selects the requested Go target ABI. + - name: Validate Windows toolchain profile + if: runner.os == 'Windows' + shell: pwsh + run: | + if ("${{ inputs.windows-abi }}" -notin @("msvc", "mingw")) { + throw "Unsupported Windows ABI profile: ${{ inputs.windows-abi }}" + } + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_WINDOWS_ABI=${{ inputs.windows-abi }}" + + - name: Cache full-target Windows LLVM + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'msvc' + id: windows_llvm_cache + uses: actions/cache/restore@v4 + with: + path: ${{ runner.tool_cache }}\llgo-llvm\LLVM-19.1.3-Windows-X64 + key: llgo-llvm-19.1.3-full-target-windows-x64-1077267ca353a1e236055ed4b57d6a404d09c40b01bd27dc882870395cdc1aae + + - name: Install full-target Windows LLVM + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'msvc' && steps.windows_llvm_cache.outputs.cache-hit != 'true' + shell: pwsh + env: + # 19.1.3 is the last LLVM 19 release with the upstream full-target + # MSVC archive. Later Windows archives omit WebAssembly and therefore + # cannot host LLGo's existing WASI cross-compilation path. + LLVM_VERSION: "19.1.3" + LLVM_ARCHIVE_SHA256: 1077267ca353a1e236055ed4b57d6a404d09c40b01bd27dc882870395cdc1aae + run: | + $ErrorActionPreference = "Stop" + if ("${{ inputs.llvm-version }}" -ne "19") { + throw "Unsupported Windows LLVM version: ${{ inputs.llvm-version }}" + } + + $archiveName = "LLVM-$env:LLVM_VERSION-Windows-X64.tar.xz" + $archive = Join-Path $env:RUNNER_TEMP $archiveName + $url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-$env:LLVM_VERSION/$archiveName" + & curl.exe --fail --location --retry 5 --retry-all-errors --output $archive $url + if ($LASTEXITCODE -ne 0) { + throw "Downloading LLVM failed with exit code $LASTEXITCODE" + } + $actual = (Get-FileHash -Algorithm SHA256 $archive).Hash + if (-not $actual.Equals($env:LLVM_ARCHIVE_SHA256, [StringComparison]::OrdinalIgnoreCase)) { + throw "LLVM archive SHA-256 is $actual, want $env:LLVM_ARCHIVE_SHA256" + } + + $extractParent = Join-Path $env:RUNNER_TEMP "llgo-llvm-extract" + $installParent = Join-Path $env:RUNNER_TOOL_CACHE "llgo-llvm" + $installRoot = Join-Path $installParent "LLVM-$env:LLVM_VERSION-Windows-X64" + $sevenZip = Join-Path $env:ProgramFiles "7-Zip\7z.exe" + if (-not (Test-Path $sevenZip)) { + throw "7-Zip was not found at $sevenZip" + } + New-Item -ItemType Directory -Force $extractParent, $installParent | Out-Null + & $sevenZip x -y "-o$extractParent" $archive + if ($LASTEXITCODE -ne 0) { + throw "Decompressing the LLVM xz archive failed with exit code $LASTEXITCODE" + } + $tar = Get-ChildItem -LiteralPath $extractParent -Filter "*.tar" | Select-Object -First 1 + if (-not $tar) { + throw "The LLVM xz archive did not contain a tar stream" + } + & $sevenZip x -y "-o$extractParent" $tar.FullName + if ($LASTEXITCODE -ne 0) { + throw "Extracting the LLVM tar archive failed with exit code $LASTEXITCODE" + } + $extractedRoot = Get-ChildItem -LiteralPath $extractParent -Directory | + Where-Object { Test-Path (Join-Path $_.FullName "bin\llvm-config.exe") } | + Select-Object -First 1 + if (-not $extractedRoot) { + throw "llvm-config.exe was not found in the full-target LLVM archive" + } + Move-Item -LiteralPath $extractedRoot.FullName -Destination $installRoot + Remove-Item -LiteralPath $archive, $tar.FullName + + - name: Save full-target Windows LLVM + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'msvc' && steps.windows_llvm_cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + 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 MSVC dependencies + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'msvc' + id: windows_vcpkg_cache + uses: actions/cache/restore@v4 + with: + path: ${{ runner.tool_cache }}\llgo-vcpkg\${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + key: llgo-vcpkg-x64-windows-${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + + - name: Install Windows MSVC dependencies + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'msvc' && steps.windows_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-vcpkg-source" + $installRoot = Join-Path $env:RUNNER_TOOL_CACHE "llgo-vcpkg\$env:VCPKG_MANIFEST_HASH" + $manifestRoot = Join-Path $env:GITHUB_WORKSPACE ".github\windows\vcpkg" + + # Version overrides refer to historical Git tree objects. Keep the + # complete Git object database rather than a source ZIP or partial + # clone so vcpkg does not need a second network request while it + # materializes the pinned libffi port. + & 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 x64-windows ` + "--x-manifest-root=$manifestRoot" ` + "--x-install-root=$installRoot" ` + --clean-after-build + if ($LASTEXITCODE -ne 0) { + throw "Installing vcpkg dependencies failed with exit code $LASTEXITCODE" + } + + - name: Save Windows MSVC dependencies + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'msvc' && steps.windows_vcpkg_cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ runner.tool_cache }}\llgo-vcpkg\${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + key: llgo-vcpkg-x64-windows-${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + + - name: Configure standalone Windows LLVM, SDK, and dependencies + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'msvc' + shell: pwsh + env: + LLVM_VERSION: "19.1.3" + VCPKG_MANIFEST_HASH: ${{ hashFiles('.github/windows/vcpkg/vcpkg.json') }} + run: | + $ErrorActionPreference = "Stop" + + $llvmRoot = Join-Path $env:RUNNER_TOOL_CACHE "llgo-llvm\LLVM-$env:LLVM_VERSION-Windows-X64" + if (-not (Test-Path (Join-Path $llvmRoot "bin\llvm-config.exe"))) { + throw "The cached full-target LLVM installation is incomplete: $llvmRoot" + } + $llvmBin = Join-Path $llvmRoot "bin" + $llvmConfig = Join-Path $llvmBin "llvm-config.exe" + $vcpkgRoot = Join-Path $env:RUNNER_TOOL_CACHE "llgo-vcpkg\$env:VCPKG_MANIFEST_HASH" + $vcpkgTripletRoot = Join-Path $vcpkgRoot "x64-windows" + $pkgconf = Join-Path $vcpkgTripletRoot "tools\pkgconf\pkgconf.exe" + if (-not (Test-Path (Join-Path $vcpkgTripletRoot "include\gc\gc.h"))) { + throw "The cached x64-windows dependency installation is incomplete" + } + if (-not (Test-Path $pkgconf)) { + throw "The native pkgconf executable was not installed at $pkgconf" + } + + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + # LLVM 19 predates the Visual Studio 18 STL compatibility floor. Keep + # this pinned lane on VS 2022 instead of selecting a newer toolset + # merely because the runner image also happens to contain one. + $installPath = & $vswhere -latest -products * -version '[17.0,18.0)' ` + -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` + -property installationPath + if (-not $installPath) { + throw "Visual Studio 2022 C++ tools required by LLVM 19 were not found" + } + Import-Module "$installPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll" + Enter-VsDevShell -VsInstallPath $installPath ` + -SkipAutomaticLocation ` + -DevCmdArguments "-arch=x64 -host_arch=x64" + + # Clang emits the MSVC ABI but uses the SDK/UCRT selected by the + # Visual Studio developer environment. Persist only the target search + # variables and native tools needed by later composite-action callers. + foreach ($name in @( + "INCLUDE", + "LIB", + "LIBPATH", + "UCRTVersion", + "UniversalCRTSdkDir", + "VCINSTALLDIR", + "VCToolsInstallDir", + "VCToolsVersion", + "WindowsSdkDir", + "WindowsSDKVersion" + )) { + $value = [Environment]::GetEnvironmentVariable($name) + if ($value) { + Add-Content -Encoding utf8 $env:GITHUB_ENV "$name=$value" + } + } + # Enter-VsDevShell only updates this action process. Export the native + # SDK tools needed by later composite actions without persisting its + # complete PATH (which can include unrelated runner-image toolchains). + @("cl.exe", "lib.exe", "link.exe", "dumpbin.exe", "nmake.exe", "rc.exe", "mt.exe") | + ForEach-Object { Split-Path (Get-Command $_).Source } | + Select-Object -Unique | + ForEach-Object { Add-Content -Encoding utf8 $env:GITHUB_PATH $_ } + + $pcDir = Join-Path $env:RUNNER_TEMP "llgo-pkgconfig" + New-Item -ItemType Directory -Force $pcDir | Out-Null + $cflags = ((& $llvmConfig --cflags) -join " ").Trim().Replace('\', '/') + if ($LASTEXITCODE -ne 0) { + throw "llvm-config failed with exit code $LASTEXITCODE" + } + $llvmLibDir = ((& $llvmConfig --libdir) -join " ").Trim().Replace('\', '/') + if ($LASTEXITCODE -ne 0 -or -not (Test-Path $llvmLibDir)) { + throw "llvm-config reported an invalid library directory: $llvmLibDir" + } + $llvmLibOutput = ((& $llvmConfig --link-static --libnames all) -join " ").Trim() + if ($LASTEXITCODE -ne 0) { + throw "llvm-config failed to report the static LLVM libraries" + } + $llvmLibNames = ($llvmLibOutput -split '\s+') | Where-Object { $_ } + if ($llvmLibNames.Count -eq 0) { + throw "llvm-config did not report the static LLVM libraries" + } + $systemLibOutput = ((& $llvmConfig --link-static --system-libs) -join " ").Trim() + if ($LASTEXITCODE -ne 0) { + throw "llvm-config failed to report the Windows system libraries" + } + $systemLibNames = ($systemLibOutput -split '\s+') | + Where-Object { $_ -and $_ -ne "libxml2s.lib" } + # The official archive reports libxml2s for optional XML-backed + # Windows-manifest support but does not ship that library. The LLVM Go + # bindings do not use that component; retaining it would make every + # host compiler depend on an unrelated third-party archive. + $libraryFlags = (@($llvmLibNames) + @($systemLibNames)) | ForEach-Object { + if (-not $_.EndsWith(".lib", [StringComparison]::OrdinalIgnoreCase)) { + throw "llvm-config reported an unsupported Windows library argument: $_" + } + "-l$([IO.Path]::GetFileNameWithoutExtension($_))" + } + # llvm-config prints full .lib paths on Windows. Express the same + # ordered static link as standard -L/-l flags so pkgconf does not treat + # backslashes as escapes and Go's CGO flag validation remains intact. + $ldflags = (@("-L$llvmLibDir") + @($libraryFlags)) -join " " + @" + Name: LLVM ${{ inputs.llvm-version }} + Description: LLVM ${{ inputs.llvm-version }} host compiler and linker flags + Version: $env:LLVM_VERSION + Cflags: $cflags + Libs: $ldflags + "@ | Set-Content -Encoding ascii (Join-Path $pcDir "llvm-${{ inputs.llvm-version }}.pc") + + # Upstream Windows LLVM release archives contain LLVMFileCheck.lib but + # omit the FileCheck executable used by LLGo's IR regression tests. + # Build that utility from its matching, hash-pinned upstream source so + # the native CI lane does not depend on an MSYS2 LLVM tools package. + $fileCheckRoot = Join-Path $env:RUNNER_TEMP "llgo-filecheck" + $fileCheckSource = Join-Path $fileCheckRoot "FileCheck.cpp" + $fileCheck = Join-Path $fileCheckRoot "FileCheck.exe" + $fileCheckSourceSHA256 = "7a0d8a2989d497b679efdc93244300ba4c7cad531ba90ba82c0af106cf864466" + New-Item -ItemType Directory -Force $fileCheckRoot | Out-Null + $fileCheckURL = "https://raw.githubusercontent.com/llvm/llvm-project/llvmorg-$env:LLVM_VERSION/llvm/utils/FileCheck/FileCheck.cpp" + & curl.exe --fail --location --retry 5 --retry-all-errors --output $fileCheckSource $fileCheckURL + if ($LASTEXITCODE -ne 0) { + throw "Downloading FileCheck source failed with exit code $LASTEXITCODE" + } + $fileCheckSourceActual = (Get-FileHash -Algorithm SHA256 $fileCheckSource).Hash + if (-not $fileCheckSourceActual.Equals($fileCheckSourceSHA256, [StringComparison]::OrdinalIgnoreCase)) { + throw "FileCheck source SHA-256 is $fileCheckSourceActual, want $fileCheckSourceSHA256" + } + $fileCheckLibraries = @("LLVMFileCheck.lib", "LLVMSupport.lib", "LLVMDemangle.lib") | + ForEach-Object { Join-Path $llvmLibDir $_ } + $fileCheckSystemLibraries = $systemLibNames | ForEach-Object { + "-l$([IO.Path]::GetFileNameWithoutExtension($_))" + } + foreach ($library in $fileCheckLibraries) { + if (-not (Test-Path $library)) { + throw "FileCheck dependency was not found: $library" + } + } + $llvmIncludeDir = Join-Path $llvmRoot "include" + & (Join-Path $llvmBin "clang++.exe") ` + --target=x86_64-pc-windows-msvc ` + -fuse-ld=lld ` + -fms-runtime-lib=dll ` + -std=c++17 ` + "-I$llvmIncludeDir" ` + $fileCheckSource ` + $fileCheckLibraries ` + $fileCheckSystemLibraries ` + -o $fileCheck + if ($LASTEXITCODE -ne 0 -or -not (Test-Path $fileCheck)) { + throw "Building the native FileCheck utility failed with exit code $LASTEXITCODE" + } + $fileCheckInput = Join-Path $fileCheckRoot "smoke.txt" + "CHECK: llgo-filecheck" | Set-Content -Encoding ascii $fileCheckInput + "llgo-filecheck" | & $fileCheck $fileCheckInput + if ($LASTEXITCODE -ne 0) { + throw "The native FileCheck smoke test failed with exit code $LASTEXITCODE" + } + + # Native pkgconf treats a variable literally named `prefix` as a + # relocatable installation marker by default. vcpkg's .pc files are + # already relocatable. Put a self-contained conventional pkg-config + # command on PATH so callers need neither PKG_CONFIG nor + # PKG_CONFIG_PATH, and so the MSVC profile cannot see MinGW metadata. + $nativeTools = Join-Path $env:RUNNER_TEMP "llgo-msvc-tools" + New-Item -ItemType Directory -Force $nativeTools | Out-Null + $pkgconfWrapper = Join-Path $nativeTools "pkg-config.cmd" + $vcpkgLibPC = Join-Path $vcpkgTripletRoot "lib\pkgconfig" + $vcpkgSharePC = Join-Path $vcpkgTripletRoot "share\pkgconfig" + @" + @echo off + "$pkgconf" --dont-define-prefix "--with-path=$pcDir" "--with-path=$vcpkgLibPC" "--with-path=$vcpkgSharePC" %* + "@ | Set-Content -Encoding ascii $pkgconfWrapper + $pkgconfShell = Join-Path $nativeTools "pkg-config" + $pkgconfUnix = $pkgconf.Replace('\', '/') + $pcDirUnix = $pcDir.Replace('\', '/') + $vcpkgLibPCUnix = $vcpkgLibPC.Replace('\', '/') + $vcpkgSharePCUnix = $vcpkgSharePC.Replace('\', '/') + @" + #!/usr/bin/env bash + set -euo pipefail + MSYS2_ARG_CONV_EXCL='*' exec "$pkgconfUnix" --dont-define-prefix "--with-path=$pcDirUnix" "--with-path=$vcpkgLibPCUnix" "--with-path=$vcpkgSharePCUnix" "`$@" + "@ | Set-Content -Encoding ascii $pkgconfShell + + $gcLibFlags = ((& $pkgconfWrapper --libs bdw-gc) -join " ").Trim() -split '\s+' + if ($LASTEXITCODE -ne 0) { + throw "pkgconf failed to resolve bdw-gc libraries" + } + $gcLibDirFlag = $gcLibFlags | Where-Object { $_.StartsWith("-L") } | Select-Object -First 1 + if (-not $gcLibDirFlag) { + throw "pkgconf did not report the bdw-gc library directory: $gcLibFlags" + } + $gcLibDir = [IO.Path]::GetFullPath($gcLibDirFlag.Substring(2)) + $expectedVcpkgLibDir = [IO.Path]::GetFullPath((Join-Path $vcpkgTripletRoot "lib")) + if (-not $gcLibDir.Equals($expectedVcpkgLibDir, [StringComparison]::OrdinalIgnoreCase)) { + throw "pkgconf resolved bdw-gc to $gcLibDir, want $expectedVcpkgLibDir" + } + # Go's Windows external linker detects LLD through the compiler command + # prefix. Without this option it emits a GNU ld linker script that the + # native MSVC linker dialect cannot consume. + # The upstream full-target LLVM archive and the x64-windows vcpkg + # triplet use the dynamic MSVC runtime. Make cgo objects match them so + # lld-link does not reject mixed /MT and /MD RuntimeLibrary directives. + Add-Content -Encoding utf8 $env:GITHUB_ENV "CC=clang --target=x86_64-pc-windows-msvc -fuse-ld=lld -fms-runtime-lib=dll" + Add-Content -Encoding utf8 $env:GITHUB_ENV "CXX=clang++ --target=x86_64-pc-windows-msvc -fuse-ld=lld -fms-runtime-lib=dll -std=c++17" + Add-Content -Encoding utf8 $env:GITHUB_ENV "CGO_ENABLED=1" + Add-Content -Encoding utf8 $env:GITHUB_PATH $fileCheckRoot + Add-Content -Encoding utf8 $env:GITHUB_PATH $llvmBin + Add-Content -Encoding utf8 $env:GITHUB_PATH (Join-Path $vcpkgTripletRoot "bin") + # GITHUB_PATH is prepended in reverse write order. Add the wrapper + # last so plain `pkg-config` always selects the profile-local command. + Add-Content -Encoding utf8 $env:GITHUB_PATH $nativeTools + + $lldb = Join-Path $llvmBin "lldb.exe" + if (-not (Test-Path $lldb)) { + throw "lldb.exe was not found in the standalone LLVM archive" + } + Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_LLDB=$lldb" + + $version = (& $llvmConfig --version).Trim() + if ($version -ne $env:LLVM_VERSION) { + throw "Expected LLVM $env:LLVM_VERSION, got $version" + } + $targetsBuilt = @((& $llvmConfig --targets-built) -split '\s+' | Where-Object { $_ }) + if ($targetsBuilt -notcontains "WebAssembly") { + throw "LLVM lacks the WebAssembly backend required by Windows-host cross-compilation: $targetsBuilt" + } + $target = (& (Join-Path $llvmBin "clang.exe") -dumpmachine).Trim() + if ($target -ne "x86_64-pc-windows-msvc") { + throw "Standalone Clang defaults to $target, want x86_64-pc-windows-msvc" + } + $builtins = (& (Join-Path $llvmBin "clang.exe") --target=x86_64-pc-windows-msvc --rtlib=compiler-rt -print-libgcc-file-name).Trim() + if (-not (Test-Path $builtins)) { + throw "Clang's MSVC compiler-rt builtins were not found at $builtins" + } + foreach ($package in @("llvm-${{ inputs.llvm-version }}", "bdw-gc", "libffi", "libuv", "openssl", "libcjson", "sqlite3", "zlib")) { + & $pkgconfWrapper --modversion $package + if ($LASTEXITCODE -ne 0) { + throw "pkg-config metadata for $package is unavailable" + } + } + + - name: Set up Windows MinGW + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'mingw' + id: windows_mingw uses: msys2/setup-msys2@v2 with: msystem: CLANG64 path-type: inherit update: true - - name: Install Windows dependencies - if: runner.os == 'Windows' && inputs.install-llvm == 'true' + - name: Install Windows MinGW dependencies + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'mingw' shell: msys2 {0} run: | set -euo pipefail @@ -54,8 +433,8 @@ runs: "lld-$package_version" "libc++-$package_version" "libunwind-$package_version" - # LLVM 19 imports libxml2-2.dll. Current MSYS2 libxml2 releases - # provide libxml2-16.dll after an ABI version bump. + # LLVM 19 imports libxml2-2.dll. Current MSYS2 libxml2 releases use + # a newer DLL ABI, so keep the matching archived runtime package. "libxml2-2.12.9-2" ) urls=() @@ -63,13 +442,9 @@ runs: urls+=("$repo/$prefix-$package-any.pkg.tar.zst") done - # Keep only the mutually versioned LLVM split packages on archived - # URLs. Their ordinary runtime dependencies have no version constraint - # in the package metadata, so let pacman resolve current versions - # instead of relying on old package files that MSYS2 eventually prunes. - # LLVM 19 predates MSYS2's compiler-runtime dependency rename from - # gcc-libs to cc-libs. libc++ 19 already supplies the runtime, so - # satisfy only the renamed package metadata. + # LLVM 19 predates the compiler-runtime dependency rename from + # gcc-libs to cc-libs. libc++ supplies the required runtime; satisfy + # only the renamed package metadata while installing the pinned set. assumed_cc_runtime="$prefix-cc-libs=$llvm_version" pacman --noconfirm -U \ --assume-installed "$assumed_cc_runtime" \ @@ -81,7 +456,11 @@ runs: "$prefix-libuv" \ "$prefix-gc" \ "$prefix-libatomic_ops" \ + "$prefix-libffi" \ + "$prefix-openssl" \ + "$prefix-zlib" \ "$prefix-cjson" \ + "$prefix-make" \ make for package in clang clang-libs compiler-rt llvm llvm-libs lld libc++ libunwind; do @@ -96,24 +475,15 @@ runs: echo "expected libxml2 2.12.9-2, got $installed_libxml2" >&2 exit 1 fi - for package in libuv gc libatomic_ops cjson; do - pacman -Q "$prefix-$package" - done - # The released LLVM binding discovers its host flags through - # versioned pkg-config metadata. MSYS2 does not ship llvm.pc, so derive - # it from the authoritative llvm-config installation instead of - # leaking global CGO flags into every package in the build. + # MSYS2 does not ship llvm.pc. Generate the versioned metadata from + # its authoritative llvm-config in CLANG64's normal package root. cflags="$(llvm-config --cflags)" ldflags="$(llvm-config --ldflags --libs all --system-libs)" cflags="${cflags//$'\r'/}" cflags="${cflags//$'\n'/ }" ldflags="${ldflags//$'\r'/}" ldflags="${ldflags//$'\n'/ }" - # Install the metadata in CLANG64's standard pkg-config tree. This - # keeps discovery stable for both MSYS2 shells and native Go/CGO - # subprocesses without passing shell-specific temporary paths between - # workflow steps. pc_dir="$MINGW_PREFIX/lib/pkgconfig" mkdir -p "$pc_dir" printf '%s\n' \ @@ -124,97 +494,65 @@ runs: "Libs: $ldflags" \ > "$pc_dir/llvm-$llvm_major.pc" - # setup-msys2 configures its dedicated bash shell, but a later native - # PowerShell step would otherwise fall back to Strawberry Perl's - # pkg-config and miss the CLANG64 metadata. Export native path - # spellings so every caller of this composite action resolves the same - # compiler and package database, independent of its shell. - mingw_bin="$(cygpath -w "$MINGW_PREFIX/bin")" - pc_dir_windows="$(cygpath -w "$pc_dir")" - printf '%s\n' "$mingw_bin" >> "$GITHUB_PATH" - - { - echo "CC=clang" - echo "CXX=clang++" - echo "CGO_ENABLED=1" - echo "PKG_CONFIG_PATH=$pc_dir_windows" - } >> "$GITHUB_ENV" - - if [[ "$(llvm-config --version)" != "$llvm_version" ]]; then - echo "expected LLVM $llvm_version, got $(llvm-config --version)" >&2 - exit 1 - fi - pkg-config --modversion "llvm-$llvm_major" - - - name: Configure Windows SDK and MSVC tools - if: runner.os == 'Windows' && inputs.install-llvm == 'true' + - name: Configure independent Windows MinGW profile + if: runner.os == 'Windows' && inputs.install-llvm == 'true' && inputs.windows-abi == 'mingw' shell: pwsh env: - MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }} + MSYS2_LOCATION: ${{ steps.windows_mingw.outputs.msys2-location }} run: | $ErrorActionPreference = "Stop" - $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - # LLVM 19 predates the Visual Studio 18 STL compatibility floor. Keep - # this pinned lane on VS 2022 instead of selecting a newer toolset - # merely because the runner image also happens to contain one. - $installPath = & $vswhere -latest -products * -version '[17.0,18.0)' ` - -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` - -property installationPath - if (-not $installPath) { - throw "Visual Studio 2022 C++ tools required by LLVM 19 were not found" + $mingwRoot = Join-Path $env:MSYS2_LOCATION "clang64" + $mingwBin = Join-Path $mingwRoot "bin" + $clang = Join-Path $mingwBin "clang.exe" + $pkgconf = Join-Path $mingwBin "pkgconf.exe" + $pcDir = Join-Path $mingwRoot "lib\pkgconfig" + foreach ($path in @($clang, $pkgconf, (Join-Path $pcDir "llvm-${{ inputs.llvm-version }}.pc"))) { + if (-not (Test-Path $path)) { + throw "The MinGW profile is incomplete: $path was not found" + } } - Import-Module "$installPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll" - Enter-VsDevShell -VsInstallPath $installPath ` - -SkipAutomaticLocation ` - -DevCmdArguments "-arch=x64 -host_arch=x64" - # Clang emits the MSVC ABI but uses the SDK/UCRT selected by the - # Visual Studio developer environment. Persist only the target search - # variables and native tools needed by later composite-action callers. - foreach ($name in @( - "INCLUDE", - "LIB", - "LIBPATH", - "UCRTVersion", - "UniversalCRTSdkDir", - "VCINSTALLDIR", - "VCToolsInstallDir", - "WindowsSdkDir", - "WindowsSDKVersion" - )) { - $value = [Environment]::GetEnvironmentVariable($name) - if ($value) { - Add-Content -Encoding utf8 $env:GITHUB_ENV "$name=$value" + $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" + } + + # Put one self-contained pkg-config command on PATH. Callers do not + # need PKG_CONFIG or PKG_CONFIG_PATH, and this profile never sees the + # MSVC vcpkg tree. + $nativeTools = Join-Path $env:RUNNER_TEMP "llgo-mingw-tools" + $profilePC = Join-Path $env:RUNNER_TEMP "llgo-pkgconfig" + New-Item -ItemType Directory -Force $nativeTools, $profilePC | Out-Null + $pkgConfig = Join-Path $nativeTools "pkg-config.cmd" + @" + @echo off + "$pkgconf" --dont-define-prefix "--with-path=$profilePC" "--with-path=$pcDir" %* + "@ | Set-Content -Encoding ascii $pkgConfig + $pkgConfigShell = Join-Path $nativeTools "pkg-config" + $pkgconfUnix = $pkgconf.Replace('\', '/') + $profilePCUnix = $profilePC.Replace('\', '/') + $pcDirUnix = $pcDir.Replace('\', '/') + @" + #!/usr/bin/env bash + set -euo pipefail + MSYS2_ARG_CONV_EXCL='*' exec "$pkgconfUnix" --dont-define-prefix "--with-path=$profilePCUnix" "--with-path=$pcDirUnix" "`$@" + "@ | Set-Content -Encoding ascii $pkgConfigShell + + foreach ($package in @("llvm-${{ inputs.llvm-version }}", "bdw-gc", "libffi", "libuv", "openssl", "libcjson", "sqlite3", "zlib")) { + & $pkgConfig --modversion $package + if ($LASTEXITCODE -ne 0) { + throw "MinGW pkg-config metadata for $package is unavailable" } } - # CMake's NMake generator used by WAMR needs nmake in later workflow - # steps; Enter-VsDevShell only updates this action process. - @("cl.exe", "lib.exe", "link.exe", "nmake.exe", "rc.exe", "mt.exe") | - ForEach-Object { Split-Path (Get-Command $_).Source } | - Select-Object -Unique | - ForEach-Object { Add-Content -Encoding utf8 $env:GITHUB_PATH $_ } - # LLGo uses MSYS2's GNU tar+xz to unpack the large ESP toolchain in - # about a minute; Windows' bundled bsdtar can exceed 25 minutes on a - # hosted runner. Keep the location explicit for native Go processes. - Add-Content -Encoding utf8 $env:GITHUB_ENV "LLGO_MSYS2_LOCATION=$env:MSYS2_LOCATION" - - # MSYS2 stores compiler-rt under its GNU archive name. Clang's MSVC - # target searches the equivalent target-specific COFF layout, so make - # the same archive visible there without changing its ABI or contents. - $clang = Join-Path $env:MSYS2_LOCATION "clang64\bin\clang.exe" - if (-not (Test-Path $clang)) { - throw "LLVM 19 clang was not found at $clang" - } - $resourceDir = (& $clang -print-resource-dir).Trim() - $gnuBuiltins = Join-Path $resourceDir "lib\windows\libclang_rt.builtins-x86_64.a" - if (-not (Test-Path $gnuBuiltins)) { - throw "LLVM 19 compiler-rt archive was not found at $gnuBuiltins" - } - $msvcRuntimeDir = Join-Path $resourceDir "lib\x86_64-pc-windows-msvc" - New-Item -ItemType Directory -Force $msvcRuntimeDir | Out-Null - Copy-Item -Force $gnuBuiltins (Join-Path $msvcRuntimeDir "clang_rt.builtins.lib") + 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_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: Install macOS dependencies if: runner.os == 'macOS' diff --git a/.github/windows/vcpkg/vcpkg.json b/.github/windows/vcpkg/vcpkg.json new file mode 100644 index 0000000000..ba25c3a65a --- /dev/null +++ b/.github/windows/vcpkg/vcpkg.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "name": "llgo-ci-windows-dependencies", + "version-string": "1", + "builtin-baseline": "ddd0023b0eee70986e42ed49d9d4afb8098f212e", + "dependencies": [ + "bdwgc", + "cjson", + "libffi", + "libuv", + "openssl", + { + "name": "pkgconf", + "host": true + }, + "sqlite3", + "zlib" + ], + "overrides": [ + { + "name": "libffi", + "version": "3.4.6" + } + ] +} diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 2f7910226b..fad9c6be40 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -27,8 +27,14 @@ jobs: display: macOS timeout: 20 - os: windows-2022 - id: windows - display: Windows + id: windows-msvc + display: Windows MSVC + windows_abi: msvc + timeout: 30 + - os: windows-2022 + id: windows-mingw + display: Windows MinGW + windows_abi: mingw timeout: 30 runs-on: ${{ matrix.os }} timeout-minutes: ${{ matrix.timeout }} @@ -63,6 +69,7 @@ jobs: uses: ./.github/actions/setup-deps with: llvm-version: 19 + windows-abi: ${{ matrix.windows_abi || 'msvc' }} - name: Set up Go uses: ./.github/actions/setup-go @@ -73,7 +80,7 @@ jobs: # The first integrated Windows revision may have a base that cannot # build on Windows. Still collect the head; comparisons start once the # base contains Windows support. - continue-on-error: ${{ matrix.id == 'windows' }} + continue-on-error: ${{ startsWith(matrix.id, 'windows-') }} shell: bash run: | benchmark/baseline/run.sh \ @@ -84,7 +91,7 @@ jobs: - name: Note unavailable Windows baseline if: >- github.event_name == 'pull_request' && - matrix.id == 'windows' && + startsWith(matrix.id, 'windows-') && steps.measure-base.outcome == 'failure' shell: bash run: | diff --git a/.github/workflows/build-cache.yml b/.github/workflows/build-cache.yml index 3850b47063..d27b0eddb7 100644 --- a/.github/workflows/build-cache.yml +++ b/.github/workflows/build-cache.yml @@ -16,11 +16,21 @@ concurrency: jobs: build-cache: + name: build-cache (${{ matrix.os }}${{ matrix.windows_abi && format(', {0}', matrix.windows_abi) || '' }}, ${{ matrix.llvm }}) timeout-minutes: 30 strategy: matrix: - os: [macos-latest, ubuntu-latest, windows-2022] - llvm: [19] + include: + - os: macos-latest + llvm: 19 + - os: ubuntu-latest + llvm: 19 + - os: windows-2022 + llvm: 19 + windows_abi: msvc + - os: windows-2022 + llvm: 19 + windows_abi: mingw runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -29,11 +39,12 @@ jobs: uses: ./.github/actions/setup-deps with: llvm-version: ${{matrix.llvm}} + windows-abi: ${{ matrix.windows_abi || 'msvc' }} - name: Set up Go uses: ./.github/actions/setup-go - - name: Install wamr (for wasm tests) + - name: Install WAMR (for wasm tests) shell: bash run: bash dev/build_iwasm.sh diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 0cb6b1be86..2d6ad6c51b 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -47,13 +47,17 @@ jobs: source doc/_readme/scripts/run.sh local_install: + name: local install (${{ matrix.os }}${{ matrix.windows_abi && format(', {0}', matrix.windows_abi) || '' }}) timeout-minutes: 30 strategy: matrix: - os: - - macos-latest - - ubuntu-latest - - windows-2022 + include: + - os: macos-latest + - os: ubuntu-latest + - os: windows-2022 + windows_abi: msvc + - os: windows-2022 + windows_abi: mingw runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -84,6 +88,8 @@ jobs: - name: Install dependencies on Windows if: startsWith(matrix.os, 'windows') uses: ./.github/actions/setup-deps + with: + windows-abi: ${{ matrix.windows_abi || 'msvc' }} - name: Install llgo with tools shell: bash @@ -116,13 +122,17 @@ jobs: source doc/_readme/scripts/run.sh local_install_full: + name: local full install (${{ matrix.os }}${{ matrix.windows_abi && format(', {0}', matrix.windows_abi) || '' }}) timeout-minutes: 30 strategy: matrix: - os: - - macos-latest - - ubuntu-latest - - windows-2022 + include: + - os: macos-latest + - os: ubuntu-latest + - os: windows-2022 + windows_abi: msvc + - os: windows-2022 + windows_abi: mingw runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -154,6 +164,8 @@ jobs: - name: Install dependencies on Windows if: startsWith(matrix.os, 'windows') uses: ./.github/actions/setup-deps + with: + windows-abi: ${{ matrix.windows_abi || 'msvc' }} - name: Install llgo with tools shell: bash diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 2af2df91c3..55c4db24fe 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,6 +16,7 @@ concurrency: jobs: test: + name: test (${{ matrix.os }}${{ matrix.windows_abi && format(', {0}', matrix.windows_abi) || '' }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}) # The known-good #172 Windows coverage run took about 56 minutes. Leave # room for runner variance and the additional shared-matrix checks without # changing the established Linux/macOS budget. @@ -39,9 +40,19 @@ jobs: - os: windows-2022 llvm: 19 go: "1.26.7" + windows_abi: msvc + - os: windows-2022 + llvm: 19 + go: "1.26.7" + windows_abi: mingw - os: windows-2022 llvm: 19 go: "1.27.0" + windows_abi: msvc + - os: windows-2022 + llvm: 19 + go: "1.27.0" + windows_abi: mingw runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -49,6 +60,7 @@ jobs: uses: ./.github/actions/setup-deps with: llvm-version: ${{matrix.llvm}} + windows-abi: ${{ matrix.windows_abi || 'msvc' }} - name: Install embedded dependencies uses: ./.github/actions/setup-embed-deps @@ -67,6 +79,8 @@ jobs: - name: Install further optional dependencies for demos uses: ./.github/actions/setup-demo-deps + with: + windows-abi: ${{ matrix.windows_abi || 'msvc' }} - name: Set LLGO_ROOT shell: bash @@ -101,7 +115,7 @@ jobs: ) ffi_include="$(pkg-config --variable=includedir libffi)" - clang -target x86_64-pc-windows-msvc -I"$ffi_include" -c \ + clang -I"$ffi_include" -c \ runtime/internal/clite/ffi/_wrap/libffi.c \ -o "$RUNNER_TEMP/llgo-libffi-amd64.obj" llvm-readobj --file-headers "$RUNNER_TEMP/llgo-libffi-amd64.obj" @@ -122,7 +136,9 @@ jobs: # Clang driver for the host test binaries instead of excluding # those packages from the shared suite. go_test+=("-ldflags=-linkmode=external -extldflags=-lsynchronization") - export LLGO_REQUIRE_MSVC=1 + if [[ "${{ matrix.windows_abi }}" == msvc ]]; then + export LLGO_REQUIRE_MSVC=1 + fi export LLGO_STDIO_NOBUF=1 fi diff --git a/.github/workflows/goroot.yml b/.github/workflows/goroot.yml index 71056c5250..15a423a9fd 100644 --- a/.github/workflows/goroot.yml +++ b/.github/workflows/goroot.yml @@ -15,7 +15,7 @@ concurrency: jobs: goroot: - name: GOROOT (${{ matrix.lane }}, ${{ matrix.os }}, Go ${{ matrix.go-version }}, shard ${{ matrix.shard-index }}/4) + name: GOROOT (${{ matrix.lane }}, ${{ matrix.os }}${{ startsWith(matrix.os, 'windows') && format(', {0}', matrix.windows_abi) || '' }}, Go ${{ matrix.go-version }}, shard ${{ matrix.shard-index }}/4) timeout-minutes: 180 env: GOPROXY: https://proxy.golang.org,direct @@ -28,11 +28,17 @@ jobs: # line. Old xfail/not-applicable classifications are retired with the # corpus. os: [macos-latest, ubuntu-latest, windows-2022] + windows_abi: [msvc, mingw] go-version: ["1.26.7"] shard-index: ["0", "1", "2", "3"] include: - go-version: "1.26.7" lane: primary + exclude: + - os: macos-latest + windows_abi: mingw + - os: ubuntu-latest + windows_abi: mingw runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v7 @@ -41,6 +47,7 @@ jobs: uses: ./.github/actions/setup-deps with: llvm-version: 19 + windows-abi: ${{ matrix.windows_abi }} - name: Set up Go uses: ./.github/actions/setup-go @@ -87,6 +94,7 @@ jobs: shell: bash env: MATRIX_OS: ${{ matrix.os }} + WINDOWS_ABI: ${{ matrix.windows_abi }} GO_VERSION: ${{ matrix.go-version }} SHARD_INDEX: ${{ matrix.shard-index }} run: | @@ -99,7 +107,7 @@ jobs: if [[ "$MATRIX_OS" == "macos-latest" ]]; then platform=darwin/arm64 elif [[ "$MATRIX_OS" == "windows-2022" ]]; then - platform=windows/amd64 + platform="windows-${WINDOWS_ABI}/amd64" fi selected=$(sed -nE 's/.* shard=[^ ]+ cases=([0-9]+) directive_mode=.*/\1/p' "$log" | tail -1) @@ -145,7 +153,7 @@ jobs: if: always() uses: actions/upload-artifact@v7 with: - name: goroot-stat-${{ matrix.os }}-${{ matrix.go-version }}-${{ matrix.shard-index }} + name: goroot-stat-${{ matrix.os }}-${{ matrix.windows_abi }}-${{ matrix.go-version }}-${{ matrix.shard-index }} path: ${{ runner.temp }}/goroot-report if-no-files-found: error retention-days: 7 @@ -206,7 +214,7 @@ jobs: { echo '## GOROOT run summary' echo - echo "Received $report_count/12 shard reports." + echo "Received $report_count/16 shard reports." echo echo '| Platform / toolchain | Shards | Selected | Observed | Passed | Failed | Skipped |' echo '|---|---:|---:|---:|---:|---:|---:|' @@ -214,7 +222,8 @@ jobs: write_row '**Darwin total**' darwin/arm64 '' 4 write_row 'Linux · Go 1.26.7' linux/amd64 1.26.7 4 write_row '**Linux total**' linux/amd64 '' 4 - write_row 'Windows · Go 1.26.7' windows/amd64 1.26.7 4 + write_row 'Windows MSVC · Go 1.26.7' windows-msvc/amd64 1.26.7 4 + write_row 'Windows MinGW · Go 1.26.7' windows-mingw/amd64 1.26.7 4 echo echo '_Passed means the runner classification succeeded; expected xfail/not-applicable failures and classified flakes are counted as Passed._' echo @@ -229,8 +238,8 @@ jobs: fi } >>"$GITHUB_STEP_SUMMARY" - if [[ "$report_count" -ne 12 ]]; then - echo "error: expected 12 shard reports, got $report_count" >&2 + if [[ "$report_count" -ne 16 ]]; then + echo "error: expected 16 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 c588740c81..04246aabb6 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -16,7 +16,7 @@ concurrency: jobs: llgo: - name: llgo (${{ matrix.lane }}, ${{ matrix.os }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}) + name: llgo (${{ matrix.lane }}, ${{ matrix.os }}${{ matrix.windows_abi && format(', {0}', matrix.windows_abi) || '' }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}) continue-on-error: ${{ matrix.lane == 'compatibility' }} timeout-minutes: 60 strategy: @@ -47,6 +47,12 @@ jobs: llvm: 19 go: "1.26.7" lane: primary + windows_abi: msvc + - os: windows-2022 + llvm: 19 + go: "1.26.7" + lane: primary + windows_abi: mingw runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -54,10 +60,13 @@ jobs: uses: ./.github/actions/setup-deps with: llvm-version: ${{matrix.llvm}} + windows-abi: ${{ matrix.windows_abi || 'msvc' }} - name: Install embedded dependencies uses: ./.github/actions/setup-embed-deps - name: Setup demo dependencies uses: ./.github/actions/setup-demo-deps + with: + windows-abi: ${{ matrix.windows_abi || 'msvc' }} - name: Set up Go for build uses: ./.github/actions/setup-go @@ -77,6 +86,11 @@ jobs: with: go-version: ${{matrix.go}} + - name: Validate the native Windows toolchain profile + if: ${{ matrix.os == 'windows-2022' && matrix.lane == 'primary' }} + shell: pwsh + run: .\.github\workflows\test_windows_toolchain_profile.ps1 -Profile "${{ matrix.windows_abi }}" + - name: Test demo without RPATH (expect failure) if: runner.os != 'Windows' run: | @@ -150,8 +164,8 @@ jobs: if: ${{ matrix.os == 'ubuntu-latest' && matrix.lane == 'primary' }} run: sudo apt-get install -y lldb-${{matrix.llvm}} - - name: Install Windows LLDB for integration tests - if: ${{ matrix.os == 'windows-2022' && matrix.lane == 'primary' }} + - name: Install MinGW LLDB for integration tests + if: ${{ matrix.os == 'windows-2022' && matrix.windows_abi == 'mingw' && matrix.lane == 'primary' }} shell: msys2 {0} env: MSYS2_LLDB_PACKAGE_VERSION: "19.1.7-1" @@ -173,8 +187,27 @@ jobs: echo "LLGO_LLDB=$(cygpath -m "$lldb_path")" >> "$GITHUB_ENV" - name: LLDB integration tests - if: ${{ matrix.lane == 'primary' }} + if: ${{ matrix.lane == 'primary' && matrix.os != 'windows-2022' }} + run: | + echo "Test lldb with llgo plugin on ${{matrix.os}} with LLVM ${{matrix.llvm}}" + bash cmd/llgo/lldbtest/runtest.sh -v + + - name: LLDB integration tests (MSVC) + if: ${{ matrix.lane == 'primary' && matrix.windows_abi == 'msvc' }} + shell: pwsh + run: | + # The upstream LLVM archive records its build runner's Python home. + # setup-demo-deps installs that exact Python patch release. + $env:PYTHONHOME = $env:LLGO_WINDOWS_PYTHON_HOME + Write-Host "Test lldb with llgo plugin on ${{matrix.os}} with LLVM ${{matrix.llvm}}" + bash cmd/llgo/lldbtest/runtest.sh -v + + - name: LLDB integration tests (MinGW) + if: ${{ matrix.lane == 'primary' && matrix.windows_abi == 'mingw' }} + shell: msys2 {0} run: | + # Git Bash prepends its own MinGW runtime DLLs to PATH. Use the + # CLANG64 shell so the debuggee loads its ABI-matching dependencies. echo "Test lldb with llgo plugin on ${{matrix.os}} with LLVM ${{matrix.llvm}}" bash cmd/llgo/lldbtest/runtest.sh -v @@ -183,45 +216,82 @@ jobs: run: go test -timeout 15m ./internal/build -run '^TestStandardDWARF$' -count=1 -v test: - name: test (${{ matrix.lane }}, ${{ matrix.os }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}, shard ${{ matrix.shard }}) + name: test (${{ matrix.lane }}, ${{ matrix.os }}${{ matrix.windows_abi && format(', {0}', matrix.windows_abi) || '' }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}, shard ${{ matrix.shard }}) continue-on-error: ${{ matrix.lane == 'compatibility' }} timeout-minutes: ${{ startsWith(matrix.os, 'windows') && 60 || startsWith(matrix.os, 'macos') && 45 || 30 }} strategy: fail-fast: false matrix: - # Keep compatibility and primary toolchains pinned to exact patches. - os: - - macos-latest - - ubuntu-latest - - windows-2022 - llvm: [19] - go: ["1.25.0", "1.26.7", "1.27.0"] - # In-command package parallelism lets Ubuntu use two shards while - # retaining headroom for the serial std build-mode checks. - shard: ["0", "1"] include: - - go: "1.25.0" + # In-command package parallelism lets Ubuntu use two shards while + # retaining headroom for the serial std build-mode checks. + - os: ubuntu-latest + llvm: 19 + go: "1.25.0" + shard: "0" + lane: compatibility + - os: ubuntu-latest + llvm: 19 + go: "1.25.0" + shard: "1" lane: compatibility - - go: "1.26.7" + - os: ubuntu-latest + llvm: 19 + go: "1.26.7" + shard: "0" + lane: primary + - os: ubuntu-latest + llvm: 19 + go: "1.26.7" + shard: "1" lane: primary - - go: "1.27.0" + - os: ubuntu-latest + llvm: 19 + go: "1.27.0" + shard: "0" + lane: primary + - os: ubuntu-latest + llvm: 19 + go: "1.27.0" + shard: "1" lane: primary - exclude: # The full demo lane above exercises Go 1.25 user-project/runtime - # compatibility on macOS. Keep the much larger per-package - # compatibility matrix on Ubuntu and use one parallel primary shard - # on macOS. + # compatibility on macOS. Use one parallel current-Go shard there. - os: macos-latest - go: "1.25.0" + llvm: 19 + go: "1.26.7" + shard: "0" + lane: primary - os: macos-latest - shard: "1" - # Windows is a primary current-Go lane. One process-level shard and - # -p=2 keep both the hosted runner and the 8 GB development VM below - # their memory limits without excluding any test package. + llvm: 19 + go: "1.27.0" + shard: "0" + lane: primary + # Each Windows profile runs the complete package set with -p=2. - os: windows-2022 - go: "1.25.0" + llvm: 19 + go: "1.26.7" + shard: "0" + lane: primary + windows_abi: msvc - os: windows-2022 - shard: "1" + llvm: 19 + go: "1.26.7" + shard: "0" + lane: primary + windows_abi: mingw + - os: windows-2022 + llvm: 19 + go: "1.27.0" + shard: "0" + lane: primary + windows_abi: msvc + - os: windows-2022 + llvm: 19 + go: "1.27.0" + shard: "0" + lane: primary + windows_abi: mingw runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -229,8 +299,11 @@ jobs: uses: ./.github/actions/setup-deps with: llvm-version: ${{matrix.llvm}} + windows-abi: ${{ matrix.windows_abi || 'msvc' }} - name: Install further optional dependencies for demos uses: ./.github/actions/setup-demo-deps + with: + windows-abi: ${{ matrix.windows_abi || 'msvc' }} - name: Set up Go for build and testing uses: ./.github/actions/setup-go @@ -256,7 +329,9 @@ jobs: shell: pwsh run: | $llgo = (Get-Command llgo.exe).Source - .\.github\workflows\test_windows_runtime.ps1 -LLGo $llgo + .\.github\workflows\test_windows_runtime.ps1 ` + -LLGo $llgo ` + -Profile "${{ matrix.windows_abi }}" - name: run llgo test env: @@ -310,7 +385,7 @@ jobs: fi hello: - name: hello (${{ matrix.lane }}, ${{ matrix.os }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}) + name: hello (${{ matrix.lane }}, ${{ matrix.os }}${{ matrix.windows_abi && format(', {0}', matrix.windows_abi) || '' }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}) continue-on-error: ${{ matrix.lane == 'compatibility' }} timeout-minutes: 30 strategy: @@ -334,6 +409,12 @@ jobs: llvm: 19 go: "1.26.7" lane: primary + windows_abi: msvc + - os: windows-2022 + llvm: 19 + go: "1.26.7" + lane: primary + windows_abi: mingw - os: ubuntu-latest llvm: 19 go: "1.27.0" @@ -346,6 +427,12 @@ jobs: llvm: 19 go: "1.27.0" lane: primary + windows_abi: msvc + - os: windows-2022 + llvm: 19 + go: "1.27.0" + lane: primary + windows_abi: mingw runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -353,6 +440,7 @@ jobs: uses: ./.github/actions/setup-deps with: llvm-version: ${{matrix.llvm}} + windows-abi: ${{ matrix.windows_abi || 'msvc' }} - name: Set up Go for building llgo uses: ./.github/actions/setup-go @@ -411,9 +499,9 @@ jobs: timeout-minutes: 30 strategy: matrix: - # WASI output is host-independent; keep this expensive WAMR build on - # Ubuntu. Native Darwin coverage remains in the primary LLGo lanes and - # release artifact smoke tests. + # WASI output is host-independent; keep this source build on Ubuntu. + # Native Darwin coverage remains in the primary LLGo lanes and release + # artifact smoke tests. os: [ubuntu-latest] llvm: [19] runs-on: ${{matrix.os}} @@ -427,14 +515,9 @@ jobs: - name: Set up Go for building llgo uses: ./.github/actions/setup-go - - name: Install wamr - run: | - git clone --branch WAMR-2.4.4 --depth 1 https://github.com/bytecodealliance/wasm-micro-runtime.git - mkdir wasm-micro-runtime/product-mini/platforms/linux/build - cd wasm-micro-runtime/product-mini/platforms/linux/build - cmake -D WAMR_BUILD_EXCE_HANDLING=1 -D WAMR_BUILD_FAST_INTERP=0 -DWAMR_BUILD_SHARED_MEMORY=1 -DWAMR_BUILD_LIB_WASI_THREADS=1 -DWAMR_BUILD_LIB_PTHREAD=1 -DCMAKE_BUILD_TYPE=Debug -DWAMR_BUILD_DEBUG_INTERP=1 .. - make -j8 - echo "$PWD" >> $GITHUB_PATH + - name: Install WAMR + shell: bash + run: bash dev/build_iwasm.sh - name: Install llgo run: | @@ -453,7 +536,6 @@ jobs: # Check file type file hello.wasm - # Run the wasm binary using llgo_wasm iwasm --stack-size=819200000 --heap-size=800000000 hello.wasm wasm-runtime: diff --git a/.github/workflows/test_windows_runtime.ps1 b/.github/workflows/test_windows_runtime.ps1 index 26c4182e99..bd21f3df70 100644 --- a/.github/workflows/test_windows_runtime.ps1 +++ b/.github/workflows/test_windows_runtime.ps1 @@ -1,6 +1,8 @@ param( [Parameter(Mandatory = $true)] - [string]$LLGo + [string]$LLGo, + [ValidateSet("msvc", "mingw")] + [string]$Profile = "msvc" ) $ErrorActionPreference = "Stop" @@ -23,10 +25,11 @@ $env:LLGO_BUILD_CACHE = "off" # bridge for every Go-supported Windows architecture so target-ABI regressions # do not wait for native 386 or ARM64 runners. $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-pc-windows-msvc"; Symbol = "_llgo_windows_syscall" }, - @{ Triple = "x86_64-pc-windows-msvc"; Symbol = "llgo_windows_syscall" }, - @{ Triple = "aarch64-pc-windows-msvc"; Symbol = "llgo_windows_syscall" } + @{ 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 diff --git a/.github/workflows/test_windows_toolchain_profile.ps1 b/.github/workflows/test_windows_toolchain_profile.ps1 new file mode 100644 index 0000000000..bad7ff1cfa --- /dev/null +++ b/.github/workflows/test_windows_toolchain_profile.ps1 @@ -0,0 +1,131 @@ +param( + [ValidateSet("msvc", "mingw")] + [string]$Profile = "msvc" +) + +$ErrorActionPreference = "Stop" + +function Assert-Success([string]$Operation, [int]$ExitCode = $LASTEXITCODE) { + if ($ExitCode -ne 0) { + throw "$Operation failed with exit code $ExitCode" + } +} + +function Invoke-NativeCapture([string]$Executable, [object[]]$ArgumentList = @()) { + # Windows PowerShell 5.1 promotes redirected native stderr to ErrorRecord. + # Capture each record as its original text under Continue so LLGo's normal + # -x trace and println stderr remain data; pwsh 7 follows the same path. + $savedErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = "Continue" + $output = (& $Executable @ArgumentList 2>&1 | + ForEach-Object { $_.ToString() }) -join [Environment]::NewLine + $exitCode = $LASTEXITCODE + } finally { + $ErrorActionPreference = $savedErrorActionPreference + } + return [PSCustomObject]@{ Output = $output; ExitCode = $exitCode } +} + +function Assert-NativeOutput([string]$Executable) { + # Go's implementation-defined println builtin may write to stderr. Capture + # both streams because this smoke test validates execution, not stream choice. + $result = Invoke-NativeCapture $Executable + Assert-Success "Running $Executable" $result.ExitCode + $output = $result.Output.Trim() + $want = "windows-$Profile-profile" + if ($output -ne $want) { + throw "$Executable printed '$output', want '$want'" + } + $dependents = (& llvm-readobj.exe --coff-imports $Executable | Out-String) + Assert-Success "Inspecting $Executable" + $forbidden = if ($Profile -eq "msvc") { + '(?i)(msys-2\.0|cygwin1|libwinpthread)\.dll' + } else { + '(?i)(msys-2\.0|cygwin1)\.dll' + } + if ($dependents -match $forbidden) { + throw "$Executable has an unsupported POSIX-emulation dependency:`n$dependents" + } +} + +$sourceDir = Join-Path $env:RUNNER_TEMP "llgo-windows-toolchain-profile" +New-Item -ItemType Directory -Force $sourceDir | Out-Null +@' +module example.com/llgo-windows-toolchain-profile + +go 1.26 +'@ | Set-Content -Encoding ascii (Join-Path $sourceDir "go.mod") +$mainSource = @' +package main + +func main() { + println("windows-$Profile-profile") +} +'@ +$mainSource.Replace('$Profile', $Profile) | 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")) { + $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" + } +} +$pkgConfig = (Get-Command pkg-config).Source +if ($pkgConfig -notmatch '(?i)[\\/]llgo-(msvc|mingw)-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" +if (-not (Test-Path $pkgConfigShell)) { + throw "The shell-compatible pkg-config wrapper was not found: $pkgConfigShell" +} +if ($env:PKG_CONFIG -or $env:PKG_CONFIG_PATH) { + throw "The $Profile profile unexpectedly requires PKG_CONFIG or PKG_CONFIG_PATH" +} +if ($Profile -eq "msvc" -and $env:LLGO_MSYS2_LOCATION) { + throw "The MSVC lane still exports 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() +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" +} + +$savedCC = $env:CC +$savedCXX = $env:CXX +try { + Remove-Item Env:CC -ErrorAction SilentlyContinue + Remove-Item Env:CXX -ErrorAction SilentlyContinue + + $powershellExe = Join-Path $sourceDir "powershell.exe" + Push-Location $sourceDir + try { + $result = Invoke-NativeCapture $llgo @("build", "-x", "-o", $powershellExe, ".") + Assert-Success "Building with unset CC/CXX from PowerShell" $result.ExitCode + $trace = $result.Output + } finally { + Pop-Location + } + $canonicalTarget = if ($Profile -eq "msvc") { 'x86_64-pc-windows-msvc' } else { 'x86_64-w64-windows-gnu' } + if ($trace -notmatch [regex]::Escape($canonicalTarget)) { + throw "Unset CC/CXX did not select the canonical $Profile target:`n$trace" + } + Assert-NativeOutput $powershellExe + + $cmdExe = Join-Path $sourceDir "cmd.exe" + $cmdLine = 'cd /d "' + $sourceDir + '" && "' + $llgo + '" build -o "' + $cmdExe + '" .' + & $env:ComSpec /d /s /c $cmdLine + Assert-Success "Building with unset CC/CXX from cmd.exe" + Assert-NativeOutput $cmdExe +} finally { + if ($null -eq $savedCC) { Remove-Item Env:CC -ErrorAction SilentlyContinue } else { $env:CC = $savedCC } + if ($null -eq $savedCXX) { Remove-Item Env:CXX -ErrorAction SilentlyContinue } else { $env:CXX = $savedCXX } +} diff --git a/_demo/c/go.mod b/_demo/c/go.mod index 16f0a75aca..0fb349c055 100644 --- a/_demo/c/go.mod +++ b/_demo/c/go.mod @@ -2,6 +2,4 @@ module github.com/xgo-dev/llgo/_demo/c go 1.20 -require github.com/goplus/lib v0.3.0 - -replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60 +require github.com/goplus/lib v0.5.0 diff --git a/_demo/c/go.sum b/_demo/c/go.sum index 6de27d79a8..ff80135f7b 100644 --- a/_demo/c/go.sum +++ b/_demo/c/go.sum @@ -1,2 +1,2 @@ -github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60 h1:z31CbbdpuQYcM7R0vgOmhKi5KltVzXZx9lEDyRTLoxc= -github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/goplus/lib v0.5.0 h1:zM8LAPatB4m8D/RflgDCSk6bJU1dEf7vzY6wqetO1WM= +github.com/goplus/lib v0.5.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/_demo/embed/go.mod b/_demo/embed/go.mod index 8bf4df54e6..d943bbd968 100644 --- a/_demo/embed/go.mod +++ b/_demo/embed/go.mod @@ -2,6 +2,4 @@ module github.com/xgo-dev/llgo/_demo/embed go 1.20 -require github.com/goplus/lib v0.3.0 - -replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60 +require github.com/goplus/lib v0.5.0 diff --git a/_demo/embed/go.sum b/_demo/embed/go.sum index 6de27d79a8..ff80135f7b 100644 --- a/_demo/embed/go.sum +++ b/_demo/embed/go.sum @@ -1,2 +1,2 @@ -github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60 h1:z31CbbdpuQYcM7R0vgOmhKi5KltVzXZx9lEDyRTLoxc= -github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/goplus/lib v0.5.0 h1:zM8LAPatB4m8D/RflgDCSk6bJU1dEf7vzY6wqetO1WM= +github.com/goplus/lib v0.5.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/_demo/go/export/use/Makefile b/_demo/go/export/use/Makefile index 44dda629a4..ffa5b5b5a9 100644 --- a/_demo/go/export/use/Makefile +++ b/_demo/go/export/use/Makefile @@ -3,13 +3,17 @@ # LINK_TYPE=static - Link with static library (default) # LINK_TYPE=shared - Link with shared library +CC_WAS_DEFAULT := $(filter default undefined,$(origin CC)) +ifneq ($(CC_WAS_DEFAULT),) CC = clang +endif CFLAGS = -Wall -Wextra -std=c99 INCLUDES = -I.. TARGET = main.out SOURCES = main.c HEADER = ../libexport.h -RUNTIME_LIBS = -lpthread -lm $(shell pkg-config --libs bdw-gc || echo -lgc) $(shell pkg-config --libs libuv || echo -luv) +PKG_CONFIG ?= pkg-config +RUNTIME_LIBS = -lpthread -lm $(shell "$(PKG_CONFIG)" --libs bdw-gc || echo -lgc) $(shell "$(PKG_CONFIG)" --libs libuv || echo -luv) # Default to static linking LINK_TYPE ?= static @@ -28,21 +32,33 @@ else ifneq ($(WINDOWS),) MSVC_TARGET_386 = i686-pc-windows-msvc MSVC_TARGET_amd64 = x86_64-pc-windows-msvc MSVC_TARGET_arm64 = aarch64-pc-windows-msvc - CFLAGS += --target=$(MSVC_TARGET_$(GOARCH)) -Wl,/debug:dwarf + ifneq ($(CC_WAS_DEFAULT),) + CFLAGS += --target=$(MSVC_TARGET_$(GOARCH)) + WINDOWS_CC_TARGET = $(MSVC_TARGET_$(GOARCH)) + else + WINDOWS_CC_TARGET = $(shell $(CC) -dumpmachine) + endif + ifneq ($(findstring windows-msvc,$(WINDOWS_CC_TARGET)),) + CFLAGS += -Wl,/debug:dwarf + # A c-archive has no final link step in which LLGo can carry native + # library options. Its runtime objects reference the ISO stdio entry + # points directly, while the MSVC UCRT headers normally provide inline + # adapters. Link the SDK's compatibility import library in the native + # consumer; MinGW exports these entry points without this bridge. + WINDOWS_STATIC_STDLIB_LIBS = -llegacy_stdio_definitions + endif SHARED_LINK_FLAGS = ../libexport.lib PLATFORM_LIBS = -lsynchronization -lws2_32 - MSYS2_LIB_DIR = $(shell pkg-config --variable=libdir bdw-gc) - LIBUV_SYSTEM_LIBS = $(filter-out -L% -luv,$(shell pkg-config --libs libuv)) - # Keep the C consumer on LLGo's MSVC ABI. lld-link accepts these controlled - # MSYS2 import archives by path; -l would search for incompatible names. - RUNTIME_LIBS = "$(MSYS2_LIB_DIR)/libgc.dll.a" "$(MSYS2_LIB_DIR)/libuv.dll.a" $(LIBUV_SYSTEM_LIBS) - STATIC_STDLIB_LIBS = "$(MSYS2_LIB_DIR)/libffi.dll.a" + # Keep dependency discovery inside the selected target profile. The MSVC + # and GNU/MinGW lanes provide separate pkg-config roots and library names. + RUNTIME_LIBS = $(shell "$(PKG_CONFIG)" --libs bdw-gc) $(shell "$(PKG_CONFIG)" --libs libuv) + STATIC_STDLIB_LIBS = $(shell "$(PKG_CONFIG)" --libs libffi) $(WINDOWS_STATIC_STDLIB_LIBS) else SHARED_EXT = so - PLATFORM_LIBS = $(shell pkg-config --libs libunwind 2>/dev/null || echo -lunwind) + PLATFORM_LIBS = $(shell "$(PKG_CONFIG)" --libs libunwind 2>/dev/null || echo -lunwind) endif ifeq ($(WINDOWS),) - STATIC_STDLIB_LIBS = $(shell pkg-config --libs libffi 2>/dev/null || echo -lffi) -lresolv + STATIC_STDLIB_LIBS = $(shell "$(PKG_CONFIG)" --libs libffi 2>/dev/null || echo -lffi) -lresolv endif # Library and flags based on link type diff --git a/_demo/go/go.mod b/_demo/go/go.mod index 8d6cfc33f9..c6effd3a41 100644 --- a/_demo/go/go.mod +++ b/_demo/go/go.mod @@ -2,6 +2,4 @@ module github.com/xgo-dev/llgo/_demo/go go 1.20 -require github.com/goplus/lib v0.3.1 - -replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60 +require github.com/goplus/lib v0.5.0 diff --git a/_demo/go/go.sum b/_demo/go/go.sum index 6de27d79a8..ff80135f7b 100644 --- a/_demo/go/go.sum +++ b/_demo/go/go.sum @@ -1,2 +1,2 @@ -github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60 h1:z31CbbdpuQYcM7R0vgOmhKi5KltVzXZx9lEDyRTLoxc= -github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/goplus/lib v0.5.0 h1:zM8LAPatB4m8D/RflgDCSk6bJU1dEf7vzY6wqetO1WM= +github.com/goplus/lib v0.5.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/_demo/py/go.mod b/_demo/py/go.mod index 5029db9d51..4377b5fd17 100644 --- a/_demo/py/go.mod +++ b/_demo/py/go.mod @@ -2,6 +2,4 @@ module github.com/xgo-dev/llgo/_demo/py go 1.20 -require github.com/goplus/lib v0.2.0 - -replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60 +require github.com/goplus/lib v0.5.0 diff --git a/_demo/py/go.sum b/_demo/py/go.sum index 6de27d79a8..ff80135f7b 100644 --- a/_demo/py/go.sum +++ b/_demo/py/go.sum @@ -1,2 +1,2 @@ -github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60 h1:z31CbbdpuQYcM7R0vgOmhKi5KltVzXZx9lEDyRTLoxc= -github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/goplus/lib v0.5.0 h1:zM8LAPatB4m8D/RflgDCSk6bJU1dEf7vzY6wqetO1WM= +github.com/goplus/lib v0.5.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/_xtool/go.mod b/_xtool/go.mod index 4f371aaa84..ab2441221b 100644 --- a/_xtool/go.mod +++ b/_xtool/go.mod @@ -2,6 +2,4 @@ module github.com/xgo-dev/llgo/_xtool go 1.20 -require github.com/goplus/lib v0.2.0 - -replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60 +require github.com/goplus/lib v0.5.0 diff --git a/_xtool/go.sum b/_xtool/go.sum index 6de27d79a8..ff80135f7b 100644 --- a/_xtool/go.sum +++ b/_xtool/go.sum @@ -1,2 +1,2 @@ -github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60 h1:z31CbbdpuQYcM7R0vgOmhKi5KltVzXZx9lEDyRTLoxc= -github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/goplus/lib v0.5.0 h1:zM8LAPatB4m8D/RflgDCSk6bJU1dEf7vzY6wqetO1WM= +github.com/goplus/lib v0.5.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/benchmark/baseline/main.go b/benchmark/baseline/main.go index 672300f6ba..a8c18ba2d9 100644 --- a/benchmark/baseline/main.go +++ b/benchmark/baseline/main.go @@ -228,13 +228,13 @@ func collect(ctx context.Context, root, llgo, out string, buildRuns, runRuns int buildArgs = append(buildArgs, "-o", binary, filepath.Join(root, item.source)) // Keep first-use toolchain and filesystem caches out of the measured // median so the first revision is not systematically disadvantaged. - if err := runQuiet(ctx, env, llgo, buildArgs...); err != nil { + if err := runQuietInDir(ctx, root, env, llgo, buildArgs...); err != nil { return fmt.Errorf("warm build %s: %w", item.name, err) } buildDurations := make([]time.Duration, 0, buildRuns) for range buildRuns { start := time.Now() - if err := runMeasured(ctx, env, llgo, buildArgs...); err != nil { + if err := runMeasuredInDir(ctx, root, env, llgo, buildArgs...); err != nil { return fmt.Errorf("build %s: %w", item.name, err) } buildDurations = append(buildDurations, time.Since(start)) @@ -291,8 +291,14 @@ func benchmarkEnv(root string) []string { } func runQuiet(ctx context.Context, env []string, name string, args ...string) error { + return runQuietInDir(ctx, "", env, name, args...) +} + +func runQuietInDir( + ctx context.Context, dir string, env []string, name string, args ...string, +) error { var output bytes.Buffer - if err := run(ctx, env, &output, name, args...); err != nil { + if err := runInDir(ctx, dir, env, &output, name, args...); err != nil { if detail := strings.TrimSpace(output.String()); detail != "" { return fmt.Errorf("%w\n%s", err, detail) } @@ -305,18 +311,31 @@ func runQuiet(ctx context.Context, env []string, name string, args ...string) er // work. If the command fails, the failed sample is discarded anyway, so rerun // it once with capture enabled to provide the actionable compiler output. func runMeasured(ctx context.Context, env []string, name string, args ...string) error { - err := run(ctx, env, io.Discard, name, args...) + return runMeasuredInDir(ctx, "", env, name, args...) +} + +func runMeasuredInDir( + ctx context.Context, dir string, env []string, name string, args ...string, +) error { + err := runInDir(ctx, dir, env, io.Discard, name, args...) if err == nil { return nil } - if detailErr := runQuiet(ctx, env, name, args...); detailErr != nil { + if detailErr := runQuietInDir(ctx, dir, env, name, args...); detailErr != nil { return detailErr } return err } func run(ctx context.Context, env []string, output io.Writer, name string, args ...string) error { + return runInDir(ctx, "", env, output, name, args...) +} + +func runInDir( + ctx context.Context, dir string, env []string, output io.Writer, name string, args ...string, +) error { cmd := exec.CommandContext(ctx, name, args...) + cmd.Dir = dir cmd.Env = env cmd.Stdout = output cmd.Stderr = output diff --git a/benchmark/baseline/main_test.go b/benchmark/baseline/main_test.go index 459e04ae86..3553b20154 100644 --- a/benchmark/baseline/main_test.go +++ b/benchmark/baseline/main_test.go @@ -350,6 +350,58 @@ func TestCollect(t *testing.T) { } } +func TestCollectBuildsFromSourceRoot(t *testing.T) { + if os.PathSeparator != '/' { + t.Skip("fake compiler uses a POSIX shell") + } + root := t.TempDir() + cwdFile := filepath.Join(root, "compiler.cwd") + t.Setenv("COMPILER_CWD_FILE", cwdFile) + compiler := writeScript(t, filepath.Join(root, "llgo"), `#!/bin/sh +set -eu +pwd -P > "$COMPILER_CWD_FILE" +out= +while [ "$#" -gt 0 ]; do + if [ "$1" = -o ]; then + out=$2 + shift 2 + continue + fi + shift +done +cat > "$out" <<'EOF' +#!/bin/sh +printf 'Hello, world\n' +EOF +chmod +x "$out" +`) + + oldInspect := inspectExecutable + inspectExecutable = func(string) (footprint, error) { + return footprint{file: 1, text: 1}, nil + } + t.Cleanup(func() { inspectExecutable = oldInspect }) + + if err := collect(context.Background(), root, compiler, filepath.Join(root, "out"), 1, 1); err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(cwdFile) + if err != nil { + t.Fatal(err) + } + rootInfo, err := os.Stat(root) + if err != nil { + t.Fatal(err) + } + cwdInfo, err := os.Stat(strings.TrimSpace(string(data))) + if err != nil { + t.Fatal(err) + } + if !os.SameFile(rootInfo, cwdInfo) { + t.Fatalf("compiler directory = %q, want %q", strings.TrimSpace(string(data)), root) + } +} + func TestCollectRejectsInvalidRuns(t *testing.T) { err := collect(context.Background(), ".", "llgo", t.TempDir(), 0, 1) if err == nil || !strings.Contains(err.Error(), "must be positive") { diff --git a/cl/_testlibc/setjmp/in.go b/cl/_testlibc/setjmp/in.go index 8936700831..23b0251a07 100644 --- a/cl/_testlibc/setjmp/in.go +++ b/cl/_testlibc/setjmp/in.go @@ -41,7 +41,7 @@ func fprintf(fp unsafe.Pointer, format *int8, __llgo_va_list ...any) int32 // DARWIN-NEXT: call void @siglongjmp(ptr [[JMPBUF]], i32 1) // LINUX-NEXT: call void @siglongjmp(ptr [[JMPBUF]], i32 1) // WINDOWS-386-NEXT: call void @longjmp(ptr [[JMPBUF]], i32 1) -// WINDOWS-AMD64-NEXT: call void @longjmp(ptr [[JMPBUF]], i32 1) +// WINDOWS-AMD64-NEXT: call void @llgo_longjmp(ptr [[JMPBUF]], i32 1) // WINDOWS-ARM64-NEXT: call void @llgo_longjmp(ptr [[JMPBUF]], i32 1) // CHECK: {{^_llgo_[0-9]+:}} // CHECK: [[PRINT_RET:%[0-9]+]] = sext i32 [[RET]] to i64 diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index 5fe28fd07b..f6f8817054 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -88,7 +88,7 @@ func use(env *JmpBuf) int32 { ` ir := compileWithRewritesTarget(t, src, nil, &llssa.Target{GOOS: "windows", GOARCH: "amd64"}) if !strings.Contains(ir, `call i32 @_setjmpex(ptr %0, ptr null)`) || - !strings.Contains(ir, `call void @longjmp(ptr %0, i32 1)`) { + !strings.Contains(ir, `call void @llgo_longjmp(ptr %0, i32 1)`) { t.Fatalf("Windows setjmp intrinsics were not lowered in the caller:\n%s", ir) } if strings.Contains(ir, `define i32 @setjmpintrinsic.setjmp`) || diff --git a/cmd/llgo/lldbtest/runtest.sh b/cmd/llgo/lldbtest/runtest.sh index 04c518af9b..738a8e73af 100755 --- a/cmd/llgo/lldbtest/runtest.sh +++ b/cmd/llgo/lldbtest/runtest.sh @@ -88,8 +88,12 @@ llgo lldb -lldb "$LLDB_PATH" -- --batch "./debug.out" \ non_llgo_dir="$test_tmp_dir/non-llgo" host_exe_ext=$(go env GOEXE) mkdir -p "$non_llgo_dir" +# CC may include target and runtime-selection flags (for example, the MSVC +# profile uses "clang --target=... -fms-runtime-lib=dll"). Preserve those as +# separate arguments instead of treating the complete value as a file name. +read -r -a cc_command <<< "${CC:-cc}" printf 'typedef struct { const char *data; unsigned long len; } string; string cstring = {"raw", 3}; int main(void) { return 0; }\n' | \ - "${CC:-cc}" -x c -g -o "$non_llgo_dir/non-llgo$host_exe_ext" - + "${cc_command[@]}" -x c -g -o "$non_llgo_dir/non-llgo$host_exe_ext" - llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/non-llgo$host_exe_ext" \ -o 'script import os; info = llgo_plugin.inspect_target(lldb.target); (not info.marker_versions and not info.supported) or os._exit(1)' \ -o 'script import os; value = lldb.target.FindFirstGlobalVariable("cstring"); (value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2) or os._exit(1)' \ @@ -98,7 +102,7 @@ llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/non-llgo$host_exe_ext" \ # An unknown marker must disable only LLGo-specific presentation. printf 'typedef struct { const char *data; unsigned long len; } string; string cstring = {"raw", 3}; __attribute__((used)) int __llgo_debugger_marker_v2 = 2; int main(void) { return 0; }\n' | \ - "${CC:-cc}" -x c -g -o "$non_llgo_dir/unsupported-llgo$host_exe_ext" - + "${cc_command[@]}" -x c -g -o "$non_llgo_dir/unsupported-llgo$host_exe_ext" - llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/unsupported-llgo$host_exe_ext" \ -o 'script import os; info = llgo_plugin.inspect_target(lldb.target); (info.marker_versions == (2,) and not info.supported) or os._exit(1)' \ -o 'script import os; value = lldb.target.FindFirstGlobalVariable("cstring"); (value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2) or os._exit(1)' \ @@ -108,7 +112,7 @@ llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/unsupported-llgo$host_exe # Multiple marker versions are ambiguous even when one version is supported. printf 'typedef struct { const char *data; unsigned long len; } string; string cstring = {"raw", 3}; __attribute__((used)) int __llgo_debugger_marker_v1 = 1; __attribute__((used)) int __llgo_debugger_marker_v2 = 2; int main(void) { return 0; }\n' | \ - "${CC:-cc}" -x c -g -o "$non_llgo_dir/ambiguous-llgo$host_exe_ext" - + "${cc_command[@]}" -x c -g -o "$non_llgo_dir/ambiguous-llgo$host_exe_ext" - llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/ambiguous-llgo$host_exe_ext" \ -o 'script import os; info = llgo_plugin.inspect_target(lldb.target); (info.marker_versions == (1, 2) and not info.supported) or os._exit(1)' \ -o 'script import os; value = lldb.target.FindFirstGlobalVariable("cstring"); (value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2) or os._exit(1)' \ diff --git a/dev/build_iwasm.sh b/dev/build_iwasm.sh index 86219eda54..f39ba5b1f4 100755 --- a/dev/build_iwasm.sh +++ b/dev/build_iwasm.sh @@ -2,7 +2,9 @@ # Script to build iwasm with correct options for llgo WASM testing # This ensures local testing uses the same iwasm configuration as CI -set -e +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) # Determine cache directory based on platform if [ "$(uname -s)" = "Darwin" ]; then @@ -12,7 +14,7 @@ else fi IWASM_BIN_DIR="${LLGO_CACHE_DIR}/bin" -WAMR_VERSION="WAMR-2.4.4" +WAMR_VERSION="WAMR-2.4.5" echo "Building iwasm for llgo WASM testing..." echo "Target directory: ${IWASM_BIN_DIR}" @@ -22,13 +24,12 @@ mkdir -p "${IWASM_BIN_DIR}" # Create temp directory for building TEMP_DIR=$(mktemp -d) +trap 'rm -rf "${TEMP_DIR}"' EXIT cd "${TEMP_DIR}" echo "Cloning wasm-micro-runtime ${WAMR_VERSION}..." -git clone --branch ${WAMR_VERSION} --depth 1 https://github.com/bytecodealliance/wasm-micro-runtime.git +git clone --branch "${WAMR_VERSION}" --depth 1 https://github.com/wasm-micro-runtime/wasm-micro-runtime.git -# WAMR's Windows platform sources expect MSVC preprocessing. This compiler -# choice is only for the host-side iwasm test helper. IWASM_NAME="iwasm" CMAKE_GENERATOR_ARGS=() case "$(uname -s)" in @@ -41,11 +42,54 @@ case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) PLATFORM="windows" IWASM_NAME="iwasm.exe" - CMAKE_GENERATOR_ARGS=( - -G "NMake Makefiles" - -D CMAKE_C_COMPILER=cl - -D CMAKE_CXX_COMPILER=cl - ) + WINDOWS_ABI="${LLGO_WINDOWS_ABI:-}" + if [ -z "${WINDOWS_ABI}" ]; then + if [ -n "${MINGW_PREFIX:-}" ]; then + WINDOWS_ABI="mingw" + else + WINDOWS_ABI="msvc" + fi + fi + if [ "${WINDOWS_ABI}" = "mingw" ]; then + # Keep MinGW on its own GNU ABI toolchain and derive WAMR's target + # from the active compiler instead of assuming an x86-64 host. + read -r -a WAMR_CC <<< "${CC:-clang}" + COMPILER_TARGET=$("${WAMR_CC[@]}" -dumpmachine) + case "${COMPILER_TARGET%%-*}" in + x86_64|amd64) + WAMR_BUILD_TARGET=X86_64 + ;; + i386|i486|i586|i686|x86) + WAMR_BUILD_TARGET=X86_32 + ;; + aarch64|arm64) + WAMR_BUILD_TARGET=AARCH64 + ;; + *) + echo "Unsupported MinGW WAMR compiler target: ${COMPILER_TARGET}" >&2 + exit 1 + ;; + esac + CMAKE_GENERATOR_ARGS=( + -G "MinGW Makefiles" + -D "CMAKE_C_COMPILER=${CC:-clang}" + -D "CMAKE_CXX_COMPILER=${CXX:-clang++}" + -D "CMAKE_C_FLAGS=-fms-extensions -pthread" + -D "CMAKE_CXX_FLAGS=-fms-extensions -pthread" + -D "WAMR_BUILD_TARGET=${WAMR_BUILD_TARGET}" + ) + elif [ "${WINDOWS_ABI}" = "msvc" ]; then + # The standalone MSVC profile exports nmake and the matching SDK + # environment without requiring an MSYS2 installation. + CMAKE_GENERATOR_ARGS=( + -G "NMake Makefiles" + -D CMAKE_C_COMPILER=cl + -D CMAKE_CXX_COMPILER=cl + ) + else + echo "Unsupported Windows ABI profile: ${WINDOWS_ABI}" >&2 + exit 1 + fi ;; *) echo "Unsupported platform: $(uname -s)" @@ -53,6 +97,13 @@ case "$(uname -s)" in ;; esac +if [ "${PLATFORM}" = "windows" ] && [ "${WINDOWS_ABI:-}" = "mingw" ]; then + # WAMR 2.4.5 predates its upstream MinGW source fix. Keep the pinned + # release and apply that exact backport instead of carrying a local fork. + git -C wasm-micro-runtime apply \ + "${SCRIPT_DIR}/patches/wamr-2.4.5-mingw.patch" +fi + echo "Building for platform: ${PLATFORM}" mkdir -p wasm-micro-runtime/product-mini/platforms/${PLATFORM}/build @@ -79,9 +130,9 @@ cmake --build . --parallel "$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null echo "Installing iwasm to ${IWASM_BIN_DIR}..." cp "${IWASM_NAME}" "${IWASM_BIN_DIR}/" -# Cleanup -cd / -rm -rf "${TEMP_DIR}" +if [ -n "${GITHUB_PATH:-}" ]; then + printf '%s\n' "${IWASM_BIN_DIR}" >> "${GITHUB_PATH}" +fi echo "" echo "✓ iwasm successfully built and installed to ${IWASM_BIN_DIR}/${IWASM_NAME}" diff --git a/dev/patches/wamr-2.4.5-mingw.patch b/dev/patches/wamr-2.4.5-mingw.patch new file mode 100644 index 0000000000..474bb33d2b --- /dev/null +++ b/dev/patches/wamr-2.4.5-mingw.patch @@ -0,0 +1,69 @@ +From 0b9db98aea32f5d634de8268a57b442f41fffc45 Mon Sep 17 00:00:00 2001 +From: Jiawei Huang +Date: Wed, 15 Jul 2026 11:10:33 +0800 +Subject: [PATCH] windows: fix MinGW-w64 build of win_file.c + +Backport of bytecodealliance/wasm-micro-runtime@0b9db98aea32f5d634de8268a57b442f41fffc45. +--- + core/shared/platform/windows/win_file.c | 35 +++++++++++++++++-------- + 1 file changed, 17 insertions(+), 18 deletions(-) + +diff --git a/core/shared/platform/windows/win_file.c b/core/shared/platform/windows/win_file.c +index 34a1260b..8626351b 100644 +--- a/core/shared/platform/windows/win_file.c ++++ b/core/shared/platform/windows/win_file.c +@@ -7,7 +7,7 @@ + #include "libc_errno.h" + #include "win_util.h" + +-#include "PathCch.h" ++#include "pathcch.h" + + #pragma comment(lib, "Pathcch.lib") + +@@ -1295,26 +1295,25 @@ os_readlinkat(os_file_handle handle, const char *path, char *buf, + + if (wbufsize >= 4 && wbuf[0] == L'\\' && wbuf[1] == L'?' + && wbuf[2] == L'?' && wbuf[3] == L'\\') { +- // Starts with \??\ ++ // Starts with \??\ prefix + if (wbufsize >= 6 + && ((wbuf[4] >= L'A' && wbuf[4] <= L'Z') + || (wbuf[4] >= L'a' && wbuf[4] <= L'z')) +- && wbuf[5] == L':' && (wbufsize == 6 || wbuf[6] == L'\\')) +- { +- // \??\:\ +- wbuf += 4; +- wbufsize -= 4; +- } +- else if (wbufsize >= 8 && (wbuf[4] == L'U' || wbuf[4] == L'u') +- && (wbuf[5] == L'N' || wbuf[5] == L'n') +- && (wbuf[6] == L'C' || wbuf[6] == L'c') +- && wbuf[7] == L'\\') +- { +- // \??\UNC\\\ - make sure the final path looks like \\\\ +- wbuf += 6; +- wbuf[0] = L'\\'; +- wbufsize -= 6; +- } ++ && wbuf[5] == L':' && (wbufsize == 6 || wbuf[6] == L'\\')) { ++ // \??\:\ form ++ wbuf += 4; ++ wbufsize -= 4; ++ } ++ else if (wbufsize >= 8 && (wbuf[4] == L'U' || wbuf[4] == L'u') ++ && (wbuf[5] == L'N' || wbuf[5] == L'n') ++ && (wbuf[6] == L'C' || wbuf[6] == L'c') ++ && wbuf[7] == L'\\') { ++ // \??\UNC\\\ - make sure the final path looks ++ // like \\\\ afterwards ++ wbuf += 6; ++ wbuf[0] = L'\\'; ++ wbufsize -= 6; ++ } + } + } + else if (reparse_data->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) { +-- +2.50.1 + diff --git a/doc/_readme/go.mod b/doc/_readme/go.mod index 11c3640562..9901394aec 100644 --- a/doc/_readme/go.mod +++ b/doc/_readme/go.mod @@ -2,6 +2,4 @@ module github.com/xgo-dev/llgo/doc/_readme go 1.20 -require github.com/goplus/lib v0.2.0 - -replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60 +require github.com/goplus/lib v0.5.0 diff --git a/doc/_readme/go.sum b/doc/_readme/go.sum index 6de27d79a8..ff80135f7b 100644 --- a/doc/_readme/go.sum +++ b/doc/_readme/go.sum @@ -1,2 +1,2 @@ -github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60 h1:z31CbbdpuQYcM7R0vgOmhKi5KltVzXZx9lEDyRTLoxc= -github.com/cpunion/lib v0.0.0-20260827173821-b993779acb60/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/goplus/lib v0.5.0 h1:zM8LAPatB4m8D/RflgDCSk6bJU1dEf7vzY6wqetO1WM= +github.com/goplus/lib v0.5.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/internal/build/build.go b/internal/build/build.go index 2fccfe4412..f5b87e4111 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -60,6 +60,7 @@ import ( "github.com/xgo-dev/llgo/internal/packages" "github.com/xgo-dev/llgo/internal/pclnmap" "github.com/xgo-dev/llgo/internal/pclnpost" + "github.com/xgo-dev/llgo/internal/quoted" "github.com/xgo-dev/llgo/internal/typepatch" "github.com/xgo-dev/llgo/ssa/abi" xenv "github.com/xgo-dev/llgo/xtool/env" @@ -490,7 +491,14 @@ func Build(inv Invocation) (result []Package, resultErr error) { }() // Handle crosscompile configuration first to set correct GOOS/GOARCH forceEspClang := conf.ForceEspClang || conf.Target != "" - export, err := crosscompile.UseWithGOARM(conf.Goos, conf.Goarch, conf.GOARM, conf.Target, IsWasiThreadsEnabled(), forceEspClang, conf.OptLevel, conf.ltoMode(), conf.goGlobalDCEEnabled()) + nativeInput := crosscompile.NativeToolchainInput{} + if usesNativeWindowsToolchain(runtime.GOOS, runtime.GOARCH, conf) { + nativeInput, err = parseNativeToolchainInput(commands, conf.LinkOptions) + if err != nil { + return nil, err + } + } + export, err := crosscompile.UseWithGOARMAndToolchain(conf.Goos, conf.Goarch, conf.GOARM, conf.Target, IsWasiThreadsEnabled(), forceEspClang, conf.OptLevel, conf.ltoMode(), conf.goGlobalDCEEnabled(), nativeInput) if err != nil { return nil, fmt.Errorf("failed to setup crosscompile: %w", err) } @@ -910,6 +918,42 @@ func removeOutFmts(outFmts *OutFmtDetails) { } } +func usesNativeWindowsToolchain(hostGOOS, hostGOARCH string, conf *Config) bool { + return conf.Target == "" && hostGOOS == "windows" && + conf.Goos == hostGOOS && conf.Goarch == hostGOARCH +} + +func parseNativeToolchainInput(commands commandEnv, options LinkOptions) (crosscompile.NativeToolchainInput, error) { + input := crosscompile.NativeToolchainInput{Dir: commands.dir, Environ: slices.Clone(commands.environ)} + for _, setting := range []struct { + name string + value string + out *[]string + allowEmpty bool + }{ + {name: "CC", value: commands.lookup("CC"), out: &input.CC}, + {name: "CXX", value: commands.lookup("CXX"), out: &input.CXX}, + {name: "-extld", value: options.ExternalLinker, out: &input.ExternalLinker, allowEmpty: true}, + {name: "-extldflags", value: options.ExternalLinkerFlags, out: &input.ExternalFlags, allowEmpty: true}, + } { + if setting.value == "" { + continue + } + args, err := quoted.Split(setting.value) + if err != nil { + return crosscompile.NativeToolchainInput{}, fmt.Errorf("could not parse %s value %q: %w", setting.name, setting.value, err) + } + if len(args) == 0 { + if setting.allowEmpty { + continue + } + return crosscompile.NativeToolchainInput{}, fmt.Errorf("%s requires a non-empty command or argument list", setting.name) + } + *setting.out = args + } + return input, nil +} + // cHeaderPackages excludes the patched standard runtime implementation. Its // //export callbacks are linker implementation details and may use internal C // types that are deliberately not representable in a public generated header. @@ -954,6 +998,17 @@ func cSharedLinkArgs(toolchain crosscompile.NativeToolchain) []string { return args } +func cSharedImportLibraryArgs(toolchain crosscompile.NativeToolchain, output string) []string { + if toolchain.ObjectFormat != crosscompile.ObjectFormatCOFF || toolchain.ABI != crosscompile.PlatformABIGNU { + return nil + } + // lld-link creates a sibling .lib automatically for MSVC links. MinGW's + // GNU-flavor lld needs an explicit output path for the equivalent COFF + // import library. + imports := strings.TrimSuffix(output, filepath.Ext(output)) + ".lib" + return []string{"-Xlinker", "--out-implib", "-Xlinker", imports} +} + // DefaultBuildTags returns the build tags LLGo always enables for a target. func DefaultBuildTags(goarch, target string) string { return defaultBuildTags(goarch, target) @@ -1187,21 +1242,22 @@ func preloadPatchedPackageSyntax(prog llssa.Program, patches cl.Patches, dedup p } func (c *context) compiler() *clang.Cmd { - config := clang.NewConfig( - c.crossCompile.CC, - c.crossCompile.CCFLAGS, - c.crossCompile.CFLAGS, - c.crossCompile.LDFLAGS, - c.crossCompile.Linker, - ) - cmd := clang.NewCompiler(config) + cmd := clang.NewCompiler(c.clangConfig()) cmd.Dir = c.commands.dir cmd.Env = slices.Clone(c.commands.environ) cmd.Verbose = c.shouldPrintCommands(false) return cmd } -func (c *context) linker() *clang.Cmd { +func (c *context) cxxCompiler() *clang.Cmd { + cmd := clang.NewCXXCompiler(c.clangConfig()) + cmd.Dir = c.commands.dir + cmd.Env = slices.Clone(c.commands.environ) + cmd.Verbose = c.shouldPrintCommands(false) + return cmd +} + +func (c *context) clangConfig() clang.Config { config := clang.NewConfig( c.crossCompile.CC, c.crossCompile.CCFLAGS, @@ -1209,7 +1265,22 @@ func (c *context) linker() *clang.Cmd { c.crossCompile.LDFLAGS, c.crossCompile.Linker, ) + config.CCArgs = slices.Clone(c.crossCompile.CCArgs) + config.CXX = c.crossCompile.CXX + config.CXXArgs = slices.Clone(c.crossCompile.CXXArgs) + config.LinkerArgs = slices.Clone(c.crossCompile.LinkerArgs) + return config +} + +func (c *context) linker() *clang.Cmd { + config := c.clangConfig() cmd := clang.NewLinker(config) + if config.Linker == "" && config.CXX != "" { + // Native LLGo historically linked through clang++. Preserve that C++ + // runtime behavior while allowing CC and CXX to be selected and probed + // independently. An explicit -extld keeps Go's precedence. + cmd = clang.NewCXXCompiler(config) + } cmd.Dir = c.commands.dir cmd.Env = slices.Clone(c.commands.environ) cmd.Verbose = c.shouldPrintCommands(false) @@ -1865,6 +1936,7 @@ func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose switch ctx.buildConf.BuildMode { case BuildModeCShared: buildArgs = append(buildArgs, cSharedLinkArgs(ctx.crossCompile.Toolchain)...) + buildArgs = append(buildArgs, cSharedImportLibraryArgs(ctx.crossCompile.Toolchain, linkOutput)...) case BuildModeExe: if needsLinuxNoPIE(ctx, linkArgs) { buildArgs = append(buildArgs, "-no-pie") @@ -3182,7 +3254,7 @@ func clFile(ctx *context, args []string, cFile, expFile, pkgPath string, procFil fmt.Fprintf(os.Stderr, "# compiling %s for pkg: %s\n", llFile, pkgPath) fmt.Fprintln(os.Stderr, "clang", llArgs) } - cmd := ctx.compiler() + cmd := ctx.compilerForSource(cFile) err := cmd.Compile(llArgs...) check(err) } @@ -3194,12 +3266,21 @@ func clFile(ctx *context, args []string, cFile, expFile, pkgPath string, procFil fmt.Fprintf(os.Stderr, "# compiling %s for pkg: %s\n", objFile, pkgPath) fmt.Fprintln(os.Stderr, "clang", objArgs) } - cmd := ctx.compiler() + cmd := ctx.compilerForSource(cFile) err := cmd.Compile(objArgs...) check(err) procFile(objFile) } +func (c *context) compilerForSource(path string) *clang.Cmd { + switch strings.ToLower(filepath.Ext(path)) { + case ".cc", ".cpp", ".cxx": + return c.cxxCompiler() + default: + return c.compiler() + } +} + func pkgExists(initial []*packages.Package, pkg *packages.Package) bool { for _, v := range initial { if v == pkg { diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 9bd2a5e22a..2399846dca 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -37,6 +37,14 @@ import ( ) func TestMain(m *testing.M) { + if os.Getenv("LLGO_TEST_PKG_CONFIG_HELPER") == "1" { + if len(os.Args) > 1 && os.Args[1] == "--libs" { + fmt.Print("-L/request/lib -lrequest") + } else { + fmt.Print(`-I/request/include -DREQUEST="request value"`) + } + os.Exit(0) + } if os.Getenv("LLGO_TEST_FAILING_ARCHIVER") == "1" { _, _ = io.Copy(io.Discard, os.Stdin) fmt.Fprintln(os.Stderr, "merge failed") @@ -1607,6 +1615,26 @@ func TestCSharedLinkArgs(t *testing.T) { } } +func TestCSharedImportLibraryArgs(t *testing.T) { + output := filepath.Join("tmp", "native-shared.dll") + gnu := crosscompile.NativeToolchain{ + ObjectFormat: crosscompile.ObjectFormatCOFF, + ABI: crosscompile.PlatformABIGNU, + } + want := strings.Join([]string{"-Xlinker", "--out-implib", "-Xlinker", filepath.Join("tmp", "native-shared.lib")}, " ") + if got := strings.Join(cSharedImportLibraryArgs(gnu, output), " "); got != want { + t.Fatalf("GNU COFF import-library arguments = %q, want %q", got, want) + } + for _, toolchain := range []crosscompile.NativeToolchain{ + {ObjectFormat: crosscompile.ObjectFormatCOFF, ABI: crosscompile.PlatformABIMsvc}, + {ObjectFormat: crosscompile.ObjectFormatELF, ABI: crosscompile.PlatformABIGNU}, + } { + if got := cSharedImportLibraryArgs(toolchain, output); got != nil { + t.Fatalf("toolchain %+v import-library arguments = %q, want none", toolchain, got) + } + } +} + func TestFullRpathArgs(t *testing.T) { linkArgs := []string{"-L/first", "-lfoo", "-L/second", "-L/first"} coff := crosscompile.NativeToolchain{ObjectFormat: crosscompile.ObjectFormatCOFF} diff --git a/internal/build/cgo.go b/internal/build/cgo.go index cd26bf01eb..a9fb9390f0 100644 --- a/internal/build/cgo.go +++ b/internal/build/cgo.go @@ -34,6 +34,7 @@ import ( "github.com/xgo-dev/llgo/internal/buildtags" llclang "github.com/xgo-dev/llgo/internal/clang" llssa "github.com/xgo-dev/llgo/ssa" + xenv "github.com/xgo-dev/llgo/xtool/env" "github.com/xgo-dev/llgo/xtool/safesplit" ) @@ -479,14 +480,15 @@ func parseCgoDeclWithCommandEnv(commands commandEnv, line string) (cgoDecls []cg switch flag { case "pkg-config": - libsCmd := exec.Command("pkg-config", "--libs", arg) + pkgConfig := xenv.PkgConfigCommand(commands.dir, commands.environ) + libsCmd := exec.Command(pkgConfig, "--libs", arg) commands.configure(libsCmd) ldflags, e := libsCmd.Output() if e != nil { err = fmt.Errorf("pkg-config: %v", e) return } - flagsCmd := exec.Command("pkg-config", "--cflags", arg) + flagsCmd := exec.Command(pkgConfig, "--cflags", arg) commands.configure(flagsCmd) cflags, e := flagsCmd.Output() if e != nil { diff --git a/internal/build/cgo_test.go b/internal/build/cgo_test.go index 13e82da628..684a603dba 100644 --- a/internal/build/cgo_test.go +++ b/internal/build/cgo_test.go @@ -163,6 +163,29 @@ printf '%s\n' '-I/request/include -DREQUEST="request value"' } } +func TestParseCgoDeclWithCommandEnvUsesPkgConfigSetting(t *testing.T) { + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + commands := commandEnv{dir: t.TempDir(), environ: append(os.Environ(), + `PKG_CONFIG='`+executable+`' --ignored-like-go`, + "LLGO_TEST_PKG_CONFIG_HELPER=1", + )} + got, err := parseCgoDeclWithCommandEnv(commands, "#cgo windows pkg-config: request") + if err != nil { + t.Fatal(err) + } + want := []cgoDecl{{ + tag: "windows", + cflags: []string{"-I/request/include", `-DREQUEST="request value"`}, + ldflags: []string{"-L/request/lib", "-lrequest"}, + }} + if !reflect.DeepEqual(got, want) { + t.Fatalf("parseCgoDeclWithCommandEnv(PKG_CONFIG) = %#v, want %#v", got, want) + } +} + func TestParseCgoPreambleDelegatesToCommandEnvParser(t *testing.T) { preamble, decls, err := parseCgoPreamble(token.Position{Filename: "request.go", Line: 7}, "#cgo CFLAGS: -I/request/include") if err != nil { diff --git a/internal/build/collect.go b/internal/build/collect.go index c336c1cca7..16d70ba405 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -23,6 +23,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "strconv" "strings" @@ -134,6 +135,12 @@ func (c *context) collectCommonInputs(m *manifestBuilder) { m.common.ObjectFormat = string(c.crossCompile.Toolchain.ObjectFormat) m.common.DriverFlavor = string(c.crossCompile.Toolchain.Driver) m.common.LinkerFlavor = string(c.crossCompile.Toolchain.Linker) + m.common.TargetTriple = c.crossCompile.Toolchain.TargetTriple + m.common.CRTFlavor = string(c.crossCompile.Toolchain.CRT) + m.common.CXXRuntime = string(c.crossCompile.Toolchain.CXXRuntime) + m.common.SDKVersion = c.crossCompile.Toolchain.SDKVersion + m.common.CRTVersion = c.crossCompile.Toolchain.CRTVersion + m.common.ToolsetVersion = c.crossCompile.Toolchain.ToolsetVersion m.common.GoGlobalDCE = c.buildConf.goGlobalDCEEnabled() m.common.EnableLTOPlugin = c.buildConf.LTOPlugin.Enabled() m.common.EmitDWARF = shouldEmitDebugInfo(c.buildConf, &c.crossCompile) @@ -146,6 +153,17 @@ func (c *context) collectCommonInputs(m *manifestBuilder) { if c.crossCompile.CC != "" { m.common.CC = c.crossCompile.CC } + if len(c.crossCompile.CCArgs) > 0 { + m.common.CCArgs = slices.Clone(c.crossCompile.CCArgs) + } + m.common.CCIdentity = c.crossCompile.CCIdentity + if c.crossCompile.CXX != "" { + m.common.CXX = c.crossCompile.CXX + } + if len(c.crossCompile.CXXArgs) > 0 { + m.common.CXXArgs = slices.Clone(c.crossCompile.CXXArgs) + } + m.common.CXXIdentity = c.crossCompile.CXXIdentity if len(c.crossCompile.CCFLAGS) > 0 { m.common.CCFlags = append([]string(nil), c.crossCompile.CCFLAGS...) } @@ -158,6 +176,10 @@ func (c *context) collectCommonInputs(m *manifestBuilder) { if c.crossCompile.Linker != "" { m.common.Linker = c.crossCompile.Linker } + if len(c.crossCompile.LinkerArgs) > 0 { + m.common.LinkerArgs = slices.Clone(c.crossCompile.LinkerArgs) + } + m.common.LinkerIdentity = c.crossCompile.LinkerIdentity // Extra files from target configuration if len(c.crossCompile.ExtraFiles) > 0 { diff --git a/internal/build/collect_test.go b/internal/build/collect_test.go index a9018c75e5..34cb59eae8 100644 --- a/internal/build/collect_test.go +++ b/internal/build/collect_test.go @@ -102,6 +102,83 @@ func TestCollectFingerprint(t *testing.T) { } } +func TestNativeToolchainIdentityParticipatesInFingerprint(t *testing.T) { + base := crosscompile.Export{ + CC: "clang", + CCArgs: []string{"--target=x86_64-pc-windows-msvc"}, + CXX: "clang++", + CXXArgs: []string{"--target=x86_64-pc-windows-msvc"}, + CCIdentity: "clang version 19.1.7", + CXXIdentity: "clang version 19.1.7", + CCFLAGS: []string{"-O2"}, + CFLAGS: []string{"-Iinclude"}, + LDFLAGS: []string{"/incremental:no"}, + Linker: "clang", + LinkerArgs: []string{"--target=x86_64-pc-windows-msvc"}, + LinkerIdentity: "clang version 19.1.7", + Toolchain: crosscompile.NativeToolchain{ + ABI: crosscompile.PlatformABIMsvc, + ObjectFormat: crosscompile.ObjectFormatCOFF, + Driver: crosscompile.DriverFlavorClangGNU, + Linker: crosscompile.LinkerFlavorCOFFLLD, + TargetTriple: "x86_64-pc-windows-msvc", + CRT: crosscompile.CRTFlavorUCRT, + CXXRuntime: crosscompile.CXXRuntimeMSVC, + SDKVersion: "10.0.26100.0", + CRTVersion: "10.0.26100.0", + ToolsetVersion: "14.44.35207", + }, + } + fingerprint := func(export crosscompile.Export) (string, *commonSection) { + ctx := &context{buildConf: &Config{}, crossCompile: export} + manifest := newManifestBuilder() + ctx.collectCommonInputs(manifest) + return manifest.Fingerprint(), &manifest.common + } + + baseFingerprint, common := fingerprint(base) + if common.TargetTriple != base.Toolchain.TargetTriple || + common.CRTFlavor != string(base.Toolchain.CRT) || + common.CXXRuntime != string(base.Toolchain.CXXRuntime) || + common.SDKVersion != base.Toolchain.SDKVersion || + common.CRTVersion != base.Toolchain.CRTVersion || + common.ToolsetVersion != base.Toolchain.ToolsetVersion || + common.CCIdentity != base.CCIdentity || common.CXXIdentity != base.CXXIdentity || + common.LinkerIdentity != base.LinkerIdentity { + t.Fatalf("toolchain manifest = %+v", common) + } + + for _, test := range []struct { + name string + mutate func(*crosscompile.Export) + }{ + {"target triple", func(e *crosscompile.Export) { e.Toolchain.TargetTriple = "i686-pc-windows-msvc" }}, + {"CRT", func(e *crosscompile.Export) { e.Toolchain.CRT = crosscompile.CRTFlavorUnknown }}, + {"C++ runtime", func(e *crosscompile.Export) { e.Toolchain.CXXRuntime = crosscompile.CXXRuntimeUnknown }}, + {"SDK version", func(e *crosscompile.Export) { e.Toolchain.SDKVersion = "10.0.22621.0" }}, + {"CRT version", func(e *crosscompile.Export) { e.Toolchain.CRTVersion = "10.0.22621.0" }}, + {"toolset", func(e *crosscompile.Export) { e.Toolchain.ToolsetVersion = "14.40.33807" }}, + {"CC", func(e *crosscompile.Export) { e.CC = "other-clang" }}, + {"CC args", func(e *crosscompile.Export) { e.CCArgs = []string{"--target=i686-pc-windows-msvc"} }}, + {"CC identity", func(e *crosscompile.Export) { e.CCIdentity = "clang version 20.1.0" }}, + {"CXX", func(e *crosscompile.Export) { e.CXX = "other-clang++" }}, + {"CXX args", func(e *crosscompile.Export) { e.CXXArgs = []string{"--target=i686-pc-windows-msvc"} }}, + {"CXX identity", func(e *crosscompile.Export) { e.CXXIdentity = "clang version 20.1.0" }}, + {"linker", func(e *crosscompile.Export) { e.Linker = "other-clang" }}, + {"linker args", func(e *crosscompile.Export) { e.LinkerArgs = []string{"--target=i686-pc-windows-msvc"} }}, + {"linker identity", func(e *crosscompile.Export) { e.LinkerIdentity = "clang version 20.1.0" }}, + } { + t.Run(test.name, func(t *testing.T) { + changed := base + test.mutate(&changed) + got, _ := fingerprint(changed) + if got == baseFingerprint { + t.Fatalf("fingerprint did not change after mutating %s", test.name) + } + }) + } +} + func TestCollectFingerprintDeterminism(t *testing.T) { td := t.TempDir() diff --git a/internal/build/fingerprint.go b/internal/build/fingerprint.go index d3c4d58100..766a2463cf 100644 --- a/internal/build/fingerprint.go +++ b/internal/build/fingerprint.go @@ -124,6 +124,12 @@ type commonSection struct { ObjectFormat string `yaml:"OBJECT_FORMAT,omitempty"` DriverFlavor string `yaml:"DRIVER_FLAVOR,omitempty"` LinkerFlavor string `yaml:"LINKER_FLAVOR,omitempty"` + TargetTriple string `yaml:"TARGET_TRIPLE,omitempty"` + CRTFlavor string `yaml:"CRT_FLAVOR,omitempty"` + CXXRuntime string `yaml:"CXX_RUNTIME,omitempty"` + SDKVersion string `yaml:"SDK_VERSION,omitempty"` + CRTVersion string `yaml:"CRT_VERSION,omitempty"` + ToolsetVersion string `yaml:"TOOLSET_VERSION,omitempty"` GoGlobalDCE bool `yaml:"GO_GLOBAL_DCE,omitempty"` EnableLTOPlugin bool `yaml:"ENABLE_LTO_PLUGIN,omitempty"` EmitDWARF bool `yaml:"EMIT_DWARF,omitempty"` @@ -132,20 +138,31 @@ type commonSection struct { SaturatingFloatToUint32 bool `yaml:"SATURATING_FLOAT_TO_UINT32,omitempty"` LocalContext bool `yaml:"LOCAL_CONTEXT,omitempty"` CC string `yaml:"CC,omitempty"` + CCArgs []string `yaml:"CC_ARGS,omitempty"` + CCIdentity string `yaml:"CC_IDENTITY,omitempty"` + CXX string `yaml:"CXX,omitempty"` + CXXArgs []string `yaml:"CXX_ARGS,omitempty"` + CXXIdentity string `yaml:"CXX_IDENTITY,omitempty"` CCFlags []string `yaml:"CCFLAGS,omitempty"` CFlags []string `yaml:"CFLAGS,omitempty"` LDFlags []string `yaml:"LDFLAGS,omitempty"` Linker string `yaml:"LINKER,omitempty"` + LinkerArgs []string `yaml:"LINKER_ARGS,omitempty"` + LinkerIdentity string `yaml:"LINKER_IDENTITY,omitempty"` ExtraFiles []fileDigest `yaml:"EXTRA_FILES,omitempty"` } func (s *commonSection) empty() bool { return s.AbiMode == "" && len(s.BuildTags) == 0 && s.Target == "" && s.TargetABI == "" && 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.DisableBoundsChecks && !s.SaturatingFloatToUint32 && !s.LocalContext && - s.CC == "" && len(s.CCFlags) == 0 && len(s.CFlags) == 0 && len(s.LDFlags) == 0 && - s.Linker == "" && len(s.ExtraFiles) == 0 + s.CC == "" && len(s.CCArgs) == 0 && s.CCIdentity == "" && s.CXX == "" && + len(s.CXXArgs) == 0 && s.CXXIdentity == "" && len(s.CCFlags) == 0 && + len(s.CFlags) == 0 && len(s.LDFlags) == 0 && s.Linker == "" && + len(s.LinkerArgs) == 0 && s.LinkerIdentity == "" && len(s.ExtraFiles) == 0 } type packageSection struct { diff --git a/internal/build/invocation.go b/internal/build/invocation.go index 6eb2e3c20b..9b41427e01 100644 --- a/internal/build/invocation.go +++ b/internal/build/invocation.go @@ -4,6 +4,7 @@ import ( "go/version" "os/exec" "path/filepath" + "runtime" "slices" "strings" ) @@ -37,6 +38,19 @@ func (e commandEnv) configure(cmd *exec.Cmd) *exec.Cmd { return cmd } +func (e commandEnv) lookup(name string) string { + for i := len(e.environ) - 1; i >= 0; i-- { + key, value, ok := strings.Cut(e.environ[i], "=") + if !ok { + continue + } + if key == name || runtime.GOOS == "windows" && strings.EqualFold(key, name) { + return value + } + } + return "" +} + func resolveOutputs(dir string, out *OutFmtDetails) { out.Out = resolvePath(dir, out.Out) out.PCLN = resolvePath(dir, out.PCLN) diff --git a/internal/build/link_options.go b/internal/build/link_options.go index 4ed4265978..857bef59c3 100644 --- a/internal/build/link_options.go +++ b/internal/build/link_options.go @@ -42,6 +42,11 @@ type LinkOptions struct { // Go's implied -w rule; native symbol-table omission is implemented later. OmitSymbolTable bool DWARF DWARFMode + // ExternalLinker and ExternalLinkerFlags retain the Go linker's -extld + // command and -extldflags argument-list spelling. The native toolchain + // resolver applies Go's quoted command parsing after target selection. + ExternalLinker string + ExternalLinkerFlags string } func (o LinkOptions) validate() error { diff --git a/internal/build/native_toolchain_test.go b/internal/build/native_toolchain_test.go new file mode 100644 index 0000000000..1bb6eca518 --- /dev/null +++ b/internal/build/native_toolchain_test.go @@ -0,0 +1,191 @@ +//go:build !llgo + +package build + +import ( + "bytes" + "fmt" + "os" + "reflect" + "slices" + "strings" + "testing" + + "github.com/xgo-dev/llgo/internal/crosscompile" +) + +func TestUsesNativeWindowsToolchain(t *testing.T) { + for _, test := range []struct { + name string + hostOS string + hostArch string + conf Config + want bool + }{ + {name: "native Windows", hostOS: "windows", hostArch: "amd64", conf: Config{Goos: "windows", Goarch: "amd64"}, want: true}, + {name: "named target", hostOS: "windows", hostArch: "amd64", conf: Config{Goos: "windows", Goarch: "amd64", Target: "esp32c3"}}, + {name: "cross OS", hostOS: "windows", hostArch: "amd64", conf: Config{Goos: "linux", Goarch: "amd64"}}, + {name: "cross architecture", hostOS: "windows", hostArch: "arm64", conf: Config{Goos: "windows", Goarch: "amd64"}}, + {name: "non-Windows host", hostOS: "darwin", hostArch: "arm64", conf: Config{Goos: "darwin", Goarch: "arm64"}}, + } { + t.Run(test.name, func(t *testing.T) { + if got := usesNativeWindowsToolchain(test.hostOS, test.hostArch, &test.conf); got != test.want { + t.Fatalf("usesNativeWindowsToolchain() = %v, want %v", got, test.want) + } + }) + } +} + +func TestParseNativeToolchainInput(t *testing.T) { + commands := commandEnv{ + dir: "/work", + environ: []string{ + "CC=old-clang", + `CC=clang '--target=x86_64-pc-windows-msvc'`, + `CXX="C:/Program Files/LLVM/bin/clang++.exe" --target=x86_64-pc-windows-msvc`, + }, + } + options := LinkOptions{ + ExternalLinker: `clang "--target=x86_64-pc-windows-msvc"`, + ExternalLinkerFlags: `'/debug:dwarf' /incremental:no`, + } + got, err := parseNativeToolchainInput(commands, options) + 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) + } + for name, values := range map[string]struct { + got []string + want []string + }{ + "CC": {got.CC, []string{"clang", "--target=x86_64-pc-windows-msvc"}}, + "CXX": {got.CXX, []string{"C:/Program Files/LLVM/bin/clang++.exe", "--target=x86_64-pc-windows-msvc"}}, + "-extld": {got.ExternalLinker, []string{"clang", "--target=x86_64-pc-windows-msvc"}}, + "-extldflags": {got.ExternalFlags, []string{"/debug:dwarf", "/incremental:no"}}, + } { + if !reflect.DeepEqual(values.got, values.want) { + t.Errorf("%s = %q, want %q", name, values.got, values.want) + } + } + + got.Environ[0] = "changed" + if commands.environ[0] == "changed" { + t.Fatal("parseNativeToolchainInput aliased the invocation environment") + } +} + +func TestParseNativeToolchainInputErrors(t *testing.T) { + for _, test := range []struct { + name string + environ []string + options LinkOptions + wantErr string + }{ + {name: "unterminated CC", environ: []string{"CC='clang"}, wantErr: "could not parse CC"}, + {name: "empty CXX", environ: []string{"CXX= "}, wantErr: "CXX requires a non-empty command"}, + {name: "unterminated extld", options: LinkOptions{ExternalLinker: `'clang`}, wantErr: "could not parse -extld"}, + {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) + if err == nil || !strings.Contains(err.Error(), test.wantErr) { + t.Fatalf("error = %v, want substring %q", err, test.wantErr) + } + }) + } +} + +func TestParseNativeToolchainInputAcceptsEmptyLinkerFlags(t *testing.T) { + got, err := parseNativeToolchainInput(commandEnv{}, LinkOptions{ + ExternalLinker: " ", + ExternalLinkerFlags: "\t", + }) + if err != nil { + t.Fatal(err) + } + if len(got.ExternalLinker) != 0 || len(got.ExternalFlags) != 0 { + t.Fatalf("external inputs = %q, %q; want empty", got.ExternalLinker, got.ExternalFlags) + } +} + +func TestCommandEnvLookupUsesLastValue(t *testing.T) { + environ := []string{"CC=first", "malformed", "CXX=clang++", "CC=last"} + commands := commandEnv{environ: environ} + if got := commands.lookup("CC"); got != "last" { + t.Fatalf("lookup(CC) = %q, want last", got) + } + if got := commands.lookup("CXX"); got != "clang++" { + t.Fatalf("lookup(CXX) = %q, want clang++", got) + } + if got := commands.lookup("MISSING"); got != "" { + t.Fatalf("lookup(MISSING) = %q", got) + } +} + +func TestNativeToolchainCommands(t *testing.T) { + if os.Getenv("LLGO_NATIVE_COMMAND_HELPER") == "1" { + separator := slices.Index(os.Args, "--") + if separator < 0 { + os.Exit(2) + } + _, _ = fmt.Fprint(os.Stdout, strings.Join(os.Args[separator+1:], " ")) + os.Exit(0) + } + + helper := func(kind string) []string { + return []string{os.Args[0], "-test.run=^TestNativeToolchainCommands$", "--", kind} + } + cc := helper("cc") + cxx := helper("cxx") + ctx := &context{ + buildConf: &Config{}, + crossCompile: crosscompile.Export{ + CC: cc[0], + CCArgs: cc[1:], + CXX: cxx[0], + CXXArgs: cxx[1:], + }, + commands: commandEnv{ + dir: t.TempDir(), + environ: append(os.Environ(), "LLGO_NATIVE_COMMAND_HELPER=1"), + }, + } + run := func(name, want string, command func(*bytes.Buffer) error) { + t.Helper() + t.Run(name, func(t *testing.T) { + var output bytes.Buffer + if err := command(&output); err != nil { + t.Fatal(err) + } + if got := output.String(); got != want { + t.Fatalf("command arguments = %q, want %q", got, want) + } + }) + } + run("C source", "cc input", func(output *bytes.Buffer) error { + cmd := ctx.compilerForSource("source.c") + cmd.Stdout, cmd.Stderr = output, output + return cmd.Compile("input") + }) + run("C++ source", "cxx input", func(output *bytes.Buffer) error { + cmd := ctx.compilerForSource("source.cpp") + cmd.Stdout, cmd.Stderr = output, output + return cmd.Compile("input") + }) + run("default linker", "cxx input", func(output *bytes.Buffer) error { + cmd := ctx.linker() + cmd.Stdout, cmd.Stderr = output, output + return cmd.Link("input") + }) + + extld := helper("extld") + ctx.crossCompile.Linker = extld[0] + ctx.crossCompile.LinkerArgs = extld[1:] + run("explicit external linker", "extld input", func(output *bytes.Buffer) error { + cmd := ctx.linker() + cmd.Stdout, cmd.Stderr = output, output + return cmd.Link("input") + }) +} diff --git a/internal/build/windows_artifacts_test.go b/internal/build/windows_artifacts_test.go index f8b23b1637..f25ecdfc5a 100644 --- a/internal/build/windows_artifacts_test.go +++ b/internal/build/windows_artifacts_test.go @@ -219,6 +219,9 @@ func testWindowsCSharedArtifact(t *testing.T) { ctx := newWindowsArtifactContext(t, BuildModeCShared) object := compileWindowsArtifact(t, ctx, dir, "shared.c", ` int answer_from_dll(void) { return 42; } +#ifdef __MINGW32__ +int DllMainCRTStartup(void *module, unsigned reason, void *reserved) { return 1; } +#endif `) dll := filepath.Join(dir, "native-shared.dll") prog := llssa.NewProgram(nil) @@ -226,7 +229,11 @@ int answer_from_dll(void) { return 42; } lpkg := prog.NewPackage("example.com/windowsfixture", "example.com/windowsfixture") lpkg.SetExport("example.com/windowsfixture.answer", "answer_from_dll") exportArgs := cSharedExportArgs(ctx, []*aPackage{{LPkg: lpkg}}) - linkArgs := append([]string{"-nostdlib", "-Wl,/noentry"}, exportArgs...) + linkArgs := []string{"-nostdlib"} + if ctx.crossCompile.Toolchain.ABI != crosscompile.PlatformABIGNU { + linkArgs = append(linkArgs, "-Wl,/noentry") + } + linkArgs = append(linkArgs, exportArgs...) if err := linkObjFiles(ctx, dll, []string{object}, linkArgs, false); err != nil { t.Fatal(err) } @@ -239,10 +246,20 @@ int answer_from_dll(void) { return 42; } consumerCtx := newWindowsArtifactContext(t, BuildModeExe) consumer := compileWindowsArtifact(t, consumerCtx, dir, "shared-consumer.c", ` __declspec(dllimport) int answer_from_dll(void); +#ifdef __MINGW32__ +int main(void) { return answer_from_dll() == 42 ? 0 : 23; } +#else int mainCRTStartup(void) { return answer_from_dll() == 42 ? 0 : 23; } +#endif `) executable := filepath.Join(dir, "shared-consumer.exe") - linkWindowsExecutable(t, consumerCtx, executable, consumer, imports) + if consumerCtx.crossCompile.Toolchain.ABI == crosscompile.PlatformABIGNU { + if err := linkObjFiles(consumerCtx, executable, []string{consumer, imports}, nil, false); err != nil { + t.Fatal(err) + } + } else { + linkWindowsExecutable(t, consumerCtx, executable, consumer, imports) + } checkPEArtifact(t, executable, false) runTool(t, executable) } diff --git a/internal/cabi/cabi.go b/internal/cabi/cabi.go index 486b61d9bf..ddb7225a62 100644 --- a/internal/cabi/cabi.go +++ b/internal/cabi/cabi.go @@ -35,31 +35,25 @@ func targetArch(llvmTarget string) string { return llvmTarget } -// isMSVCTarget treats an absent or unknown Windows environment as MSVC. This -// matches the default target convention used for GOOS=windows; explicit GNU, -// MinGW, and Cygwin environments retain their non-MSVC ABI. -func isMSVCTarget(target *ssa.Target, llvmTarget string) bool { - if llvmTarget == "" { +// usesWindowsCABI reports whether the target uses the native Windows C ABI. +// MinGW uses that ABI too; its GNU environment selects a toolchain and CRT, +// not the SysV calling convention. MSYS and Cygwin are POSIX-emulation ABIs +// and are deliberately excluded. +func usesWindowsCABI(target *ssa.Target, llvmTarget string) bool { + if llvmTarget == "" || !strings.Contains(llvmTarget, "-") { return target != nil && target.GOOS == "windows" } parts := strings.Split(strings.ToLower(llvmTarget), "-") - if len(parts) == 1 { - return target != nil && target.GOOS == "windows" - } windows := false - msvc := false - gnu := false for _, part := range parts[1:] { switch { - case part == "windows" || part == "win32": + case strings.Contains(part, "cygwin"), part == "cygnus", strings.Contains(part, "msys"): + return false + case part == "windows", part == "win32", strings.Contains(part, "mingw"): windows = true - case strings.HasPrefix(part, "msvc"): - msvc = true - case strings.Contains(part, "mingw") || strings.HasPrefix(part, "gnu") || strings.Contains(part, "cygwin") || part == "cygnus": - gnu = true } } - return windows && (msvc || !gnu) + return windows } func isCOFFTarget(target *ssa.Target, llvmTarget string) bool { @@ -88,7 +82,7 @@ func NewTransformer(prog ssa.Program, llvmTarget string, targetAbi string, mode mode: mode, optimize: optimize, } - if isMSVCTarget(target, llvmTarget) { + if usesWindowsCABI(target, llvmTarget) { switch arch { case "amd64": tr.sys = &TypeInfoWindowsAmd64{tr} diff --git a/internal/cabi/cabi_patch_test.go b/internal/cabi/cabi_patch_test.go index 79dac17b51..5e35ff5468 100644 --- a/internal/cabi/cabi_patch_test.go +++ b/internal/cabi/cabi_patch_test.go @@ -63,6 +63,9 @@ func TestTargetArchAndNewTransformerArchSelection(t *testing.T) { {"x86_64-pc-windows-msvc", "", "amd64", func(sys TypeInfoSys) bool { _, ok := sys.(*TypeInfoWindowsAmd64); return ok }}, {"aarch64-pc-windows-msvc", "", "arm64", func(sys TypeInfoSys) bool { _, ok := sys.(*TypeInfoWindowsArm64); return ok }}, {"i686-pc-windows-msvc", "", "386", func(sys TypeInfoSys) bool { _, ok := sys.(*TypeInfoWindows386); return ok }}, + {"x86_64-w64-windows-gnu", "", "amd64", func(sys TypeInfoSys) bool { _, ok := sys.(*TypeInfoWindowsAmd64); return ok }}, + {"aarch64-w64-windows-gnu", "", "arm64", func(sys TypeInfoSys) bool { _, ok := sys.(*TypeInfoWindowsArm64); return ok }}, + {"i686-w64-windows-gnu", "", "386", func(sys TypeInfoSys) bool { _, ok := sys.(*TypeInfoWindows386); return ok }}, } for _, tc := range tests { tr := NewTransformer(prog, tc.target, tc.abi, ModeCFunc, true) @@ -257,7 +260,7 @@ entry: } } -func TestMSVCTargetDetection(t *testing.T) { +func TestWindowsCABITargetDetection(t *testing.T) { tests := []struct { name string target *llssa.Target @@ -267,9 +270,11 @@ func TestMSVCTargetDetection(t *testing.T) { {"explicit msvc", nil, "x86_64-pc-windows-msvc", true}, {"versioned msvc", nil, "x86_64-pc-windows-msvc19.40", true}, {"windows default environment", nil, "x86_64-pc-windows", true}, - {"mingw", nil, "x86_64-w64-windows-gnu", false}, - {"mingw short triple", nil, "x86_64-w64-mingw32", false}, + {"mingw", nil, "x86_64-w64-windows-gnu", true}, + {"mingw short triple", nil, "x86_64-w64-mingw32", true}, {"cygwin", nil, "x86_64-pc-windows-cygnus", false}, + {"cygwin short triple", nil, "x86_64-pc-cygwin", false}, + {"msys", nil, "x86_64-pc-msys", false}, {"linux", nil, "x86_64-unknown-linux-gnu", false}, {"implicit windows", &llssa.Target{GOOS: "windows"}, "", true}, {"arch-only windows", &llssa.Target{GOOS: "windows"}, "x86_64", true}, @@ -277,8 +282,8 @@ func TestMSVCTargetDetection(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - if got := isMSVCTarget(test.target, test.triple); got != test.want { - t.Fatalf("isMSVCTarget(%q) = %v, want %v", test.triple, got, test.want) + if got := usesWindowsCABI(test.target, test.triple); got != test.want { + t.Fatalf("usesWindowsCABI(%q) = %v, want %v", test.triple, got, test.want) } }) } diff --git a/internal/clang/clang.go b/internal/clang/clang.go index b57253d4f0..2f6e669456 100644 --- a/internal/clang/clang.go +++ b/internal/clang/clang.go @@ -32,10 +32,14 @@ import ( // Config represents clang configuration parameters. type Config struct { CC string // Compiler to use (e.g., "clang", "clang++") + CCArgs []string // Arguments that are part of the CC command + CXX string // C++ compiler to use + CXXArgs []string // Arguments that are part of the CXX command CCFLAGS []string // Compiler flags for C/C++ compilation CFLAGS []string // C-specific flags LDFLAGS []string // Linker flags Linker string // Linker to use (e.g., "ld.lld", "avr-ld") + LinkerArgs []string // Arguments that are part of the linker command ResponseFileStyle ResponseFileStyle } @@ -63,29 +67,35 @@ func NewConfig(cc string, ccflags, cflags, ldflags []string, linker string) Conf // Cmd represents a clang command with environment and configuration support. type Cmd struct { - app string - config Config - Dir string - Env []string - Verbose bool - Stdin io.Reader - Stdout io.Writer - Stderr io.Writer + app string + prefixArgs []string + config Config + Dir string + Env []string + Verbose bool + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer } // New creates a new clang command with configuration. func New(app string, config Config) *Cmd { + return newWithArgs(app, nil, config) +} + +func newWithArgs(app string, args []string, config Config) *Cmd { if app == "" { app = "clang" } return &Cmd{ - app: app, - config: config, - Env: nil, - Verbose: false, - Stdin: nil, - Stdout: os.Stdout, - Stderr: os.Stderr, + app: app, + prefixArgs: slices.Clone(args), + config: config, + Env: nil, + Verbose: false, + Stdin: nil, + Stdout: os.Stdout, + Stderr: os.Stderr, } } @@ -95,18 +105,23 @@ func NewCompiler(config Config) *Cmd { if config.CC != "" { app = config.CC } - return New(app, config) + return newWithArgs(app, config.CCArgs, config) +} + +// NewCXXCompiler creates a C++ compiler command with proper flag merging. +func NewCXXCompiler(config Config) *Cmd { + if config.CXX != "" { + return newWithArgs(config.CXX, config.CXXArgs, config) + } + return NewCompiler(config) } // NewLinker creates a linker command with proper flag merging. func NewLinker(config Config) *Cmd { - app := "clang" if config.Linker != "" { - app = config.Linker - } else if config.CC != "" { - app = config.CC + return newWithArgs(config.Linker, config.LinkerArgs, config) } - return New(app, config) + return NewCompiler(config) } // Compile executes a compilation command with merged flags. @@ -128,12 +143,12 @@ func (c *Cmd) Link(args ...string) error { return c.exec(allArgs...) } -// resolveMSVCImportLibraries lets clang's MSVC driver consume GNU-named COFF -// import archives installed by environments such as MSYS2. The driver lowers -// -lname to name.lib and lets the linker search every -L directory for that -// exact spelling; it does not probe libname.dll.a per directory. Preserve that -// behavior by preferring name.lib anywhere on the explicit search path, and -// only replace -lname when libname.dll.a is the sole available spelling. +// resolveMSVCImportLibraries lets clang's MSVC driver consume library names +// emitted for Unix-compatible -l flags. The driver lowers -lname to name.lib +// and lets the linker search every -L directory for that exact spelling; it +// does not probe libname.lib or libname.dll.a per directory. Preserve that +// behavior by preferring name.lib anywhere on the explicit search path, then +// replace -lname with the first alternative spelling that exists. func resolveMSVCImportLibraries(baseDir string, args []string) []string { if !slices.ContainsFunc(args, func(arg string) bool { return strings.Contains(arg, "-windows-msvc") @@ -159,7 +174,11 @@ func resolveMSVCImportLibraries(baseDir string, args []string) []string { if findLibrary(baseDir, dirs, name+".lib") != "" { continue } - if archive := findLibrary(baseDir, dirs, "lib"+name+".dll.a"); archive != "" { + archive := findLibrary(baseDir, dirs, "lib"+name+".lib") + if archive == "" { + archive = findLibrary(baseDir, dirs, "lib"+name+".dll.a") + } + if archive != "" { if !changed { resolved = slices.Clone(args) changed = true @@ -228,6 +247,11 @@ func (c *Cmd) mergeLinkerFlags() []string { // exec executes the clang command with given arguments. func (c *Cmd) exec(args ...string) error { + if len(c.prefixArgs) != 0 { + allArgs := make([]string, 0, len(c.prefixArgs)+len(args)) + allArgs = append(allArgs, c.prefixArgs...) + args = append(allArgs, args...) + } responseFile := "" if useResponseFile(c.app, args) { var err error diff --git a/internal/clang/clang_test.go b/internal/clang/clang_test.go index 08105bd484..23c37ffddf 100644 --- a/internal/clang/clang_test.go +++ b/internal/clang/clang_test.go @@ -54,6 +54,55 @@ func newTestCmd(config Config) *Cmd { return cmd } +func TestConfiguredCommandPrefixes(t *testing.T) { + config := Config{ + CC: os.Args[0], + CCArgs: []string{"--cc-prefix"}, + CXX: os.Args[0], + CXXArgs: []string{"--cxx-prefix"}, + Linker: os.Args[0], + LinkerArgs: []string{"--link-prefix"}, + CCFLAGS: []string{"-O2"}, + LDFLAGS: []string{"-Wl,test"}, + } + for _, test := range []struct { + name string + cmd *Cmd + run func(*Cmd) error + want string + }{ + {name: "CC", cmd: NewCompiler(config), run: func(cmd *Cmd) error { return cmd.Compile("-c", "file.c") }, want: "--cc-prefix -O2 -c file.c"}, + {name: "CXX", cmd: NewCXXCompiler(config), run: func(cmd *Cmd) error { return cmd.Compile("-c", "file.cpp") }, want: "--cxx-prefix -O2 -c file.cpp"}, + {name: "linker", cmd: NewLinker(config), run: func(cmd *Cmd) error { return cmd.Link("file.o") }, want: "--link-prefix -Wl,test file.o"}, + } { + t.Run(test.name, func(t *testing.T) { + var output bytes.Buffer + test.cmd.Env = append(os.Environ(), clangTestHelperEnv+"=1") + test.cmd.Stdout = &output + if err := test.run(test.cmd); err != nil { + t.Fatal(err) + } + if got := strings.TrimSpace(output.String()); got != test.want { + t.Fatalf("command = %q, want %q", got, test.want) + } + }) + } +} + +func TestCXXCompilerFallsBackToCCCommand(t *testing.T) { + config := Config{CC: os.Args[0], CCArgs: []string{"--cc-prefix"}} + cmd := NewCXXCompiler(config) + var output bytes.Buffer + cmd.Env = append(os.Environ(), clangTestHelperEnv+"=1") + cmd.Stdout = &output + if err := cmd.Compile("-c", "file.cpp"); err != nil { + t.Fatal(err) + } + if got, want := strings.TrimSpace(output.String()), "--cc-prefix -c file.cpp"; got != want { + t.Fatalf("command = %q, want %q", got, want) + } +} + func TestWriteWindowsResponseArg(t *testing.T) { tests := []struct { arg string @@ -105,9 +154,10 @@ func TestResolveMSVCImportLibraries(t *testing.T) { t.Fatal(err) } } - clangImport := filepath.Join(root, "first", "libclang.dll.a") + clangImport := filepath.Join(root, "first", "libclang.lib") for _, file := range []string{ clangImport, + filepath.Join(root, "first", "libgnu.dll.a"), filepath.Join(root, "first", "libnative.dll.a"), filepath.Join(root, "second", "native.lib"), } { @@ -115,11 +165,11 @@ func TestResolveMSVCImportLibraries(t *testing.T) { t.Fatal(err) } } - args := []string{"-target", "x86_64-pc-windows-msvc", "-Lfirst", "-L", "second", "-lclang", "-lnative", "-lmissing", "-l:exact.a"} + args := []string{"-target", "x86_64-pc-windows-msvc", "-Lfirst", "-L", "second", "-lclang", "-lgnu", "-lnative", "-lmissing", "-l:exact.a"} // Keep -lnative because the MSVC driver resolves it as native.lib across // the complete search path, even though an earlier directory contains the // GNU-only libnative.dll.a spelling. - want := []string{"-target", "x86_64-pc-windows-msvc", "-Lfirst", "-L", "second", clangImport, "-lnative", "-lmissing", "-l:exact.a"} + want := []string{"-target", "x86_64-pc-windows-msvc", "-Lfirst", "-L", "second", clangImport, filepath.Join(root, "first", "libgnu.dll.a"), "-lnative", "-lmissing", "-l:exact.a"} if got := resolveMSVCImportLibraries(root, args); !slices.Equal(got, want) { t.Fatalf("resolved libraries = %q, want %q", got, want) } diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index e0a2490752..f84131bfc0 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "strings" "github.com/xgo-dev/llgo/internal/crosscompile/compile" @@ -21,24 +22,33 @@ import ( ) type Export struct { - CC string // Compiler to use + CC string // C compiler executable + CCArgs []string + CXX string // C++ compiler executable + CXXArgs []string CCFLAGS []string CFLAGS []string LDFLAGS []string + // CompilerIdentity values are stable version strings used to keep cached + // packages separate when an explicit native toolchain changes. + CCIdentity string + CXXIdentity string // Toolchain records the ABI and flag dialect selected for native-format // outputs. It is independent of the host shell and remains empty for named // embedded/WebAssembly targets that drive their linker directly. Toolchain NativeToolchain // Additional fields from target configuration - BuildTags []string - GOOS string - GOARCH string - Libc string - Linker string // Linker to use (e.g., "ld.lld", "avr-ld") - ExtraFiles []string // Extra files to compile and link (e.g., .s, .c files) - ClangRoot string // Root directory of custom clang installation - ClangBinPath string // Path to clang binary directory + BuildTags []string + GOOS string + GOARCH string + Libc string + Linker string // Linker to use (e.g., "ld.lld", "avr-ld") + LinkerArgs []string + LinkerIdentity string + ExtraFiles []string // Extra files to compile and link (e.g., .s, .c files) + ClangRoot string // Root directory of custom clang installation + ClangBinPath string // Path to clang binary directory LLVMTarget string // LLVM Target TargetABI string // RISC-V Target ABI (e.g., "lp64", "lp64d") @@ -55,10 +65,16 @@ type Export struct { // produce a native object. Keep these choices explicit: in particular, a // Windows host does not imply either the MSVC or MinGW ABI. type NativeToolchain struct { - ABI PlatformABI - ObjectFormat ObjectFormat - Driver DriverFlavor - Linker LinkerFlavor + ABI PlatformABI + ObjectFormat ObjectFormat + Driver DriverFlavor + Linker LinkerFlavor + TargetTriple string + CRT CRTFlavor + CXXRuntime CXXRuntimeFlavor + SDKVersion string + CRTVersion string + ToolsetVersion string } type PlatformABI string @@ -89,10 +105,25 @@ const ( type LinkerFlavor string const ( - LinkerFlavorUnknown LinkerFlavor = "" - LinkerFlavorELFLLD LinkerFlavor = "ld.lld" - LinkerFlavorMachO LinkerFlavor = "ld64.lld" - LinkerFlavorCOFFLLD LinkerFlavor = "lld-link" + LinkerFlavorUnknown LinkerFlavor = "" + LinkerFlavorELFLLD LinkerFlavor = "ld.lld" + LinkerFlavorMachO LinkerFlavor = "ld64.lld" + LinkerFlavorCOFFLLD LinkerFlavor = "lld-link" + LinkerFlavorMinGWLLD LinkerFlavor = "mingw-lld" +) + +type CRTFlavor string + +const ( + CRTFlavorUnknown CRTFlavor = "" + CRTFlavorUCRT CRTFlavor = "ucrt" +) + +type CXXRuntimeFlavor string + +const ( + CXXRuntimeUnknown CXXRuntimeFlavor = "" + CXXRuntimeMSVC CXXRuntimeFlavor = "msvc" ) // DebugInfoPolicy describes how a selected linker handles debug information. @@ -134,7 +165,7 @@ func nativeToolchain(goos string) NativeToolchain { func nativeDebugInfoPolicy(toolchain NativeToolchain) DebugInfoPolicy { switch toolchain.Linker { - case LinkerFlavorMachO, LinkerFlavorELFLLD: + case LinkerFlavorMachO, LinkerFlavorELFLLD, LinkerFlavorMinGWLLD: return DebugInfoPolicy{OmitLinkFlags: []string{"-Wl,-S"}} case LinkerFlavorCOFFLLD: return DebugInfoPolicy{ @@ -382,9 +413,17 @@ func nativeSectionFlags(toolchain NativeToolchain) (ccflags, ldflags []string) { case ObjectFormatMachO: return nil, []string{"-Xlinker", "-dead_strip"} case ObjectFormatCOFF: + if toolchain.ABI == PlatformABIGNU { + return []string{"-fdata-sections", "-ffunction-sections"}, []string{ + "-fdata-sections", + "-ffunction-sections", + "-Wl,--gc-sections", + } + } return []string{"-fdata-sections", "-ffunction-sections"}, []string{ "-fdata-sections", "-ffunction-sections", + "--rtlib=compiler-rt", "-Wl,/opt:ref", // UCRT defines printf-family entry points inline in its headers. // LLGo C linknames use the traditional external symbols supplied by @@ -411,13 +450,23 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Le } func useWithGOARM(goos, goarch, goarm string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool) (export Export, err error) { + return useWithGOARMAndToolchain(goos, goarch, goarm, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, NativeToolchainInput{}) +} + +func useWithGOARMAndToolchain(goos, goarch, goarm string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool, nativeInput NativeToolchainInput) (export Export, err error) { targetTriple := llvm.GetTargetTripleWithGOARM(goos, goarch, goarm) llgoRoot := env.LLGoROOT() + nativePlatformToolchain := usesNativePlatformToolchain(runtime.GOOS, runtime.GOARCH, goos, goarch) - // Check for ESP Clang support for target-based builds - clangRoot, err := getESPClangRoot(forceEspClang) - if err != nil { - return + // 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. + var clangRoot string + if !(nativePlatformToolchain && goos == "windows") { + clangRoot, err = getESPClangRoot(forceEspClang) + if err != nil { + return + } } // Set ClangRoot and CC if clang is available @@ -428,11 +477,25 @@ func useWithGOARM(goos, goarch, goarm string, wasiThreads, forceEspClang bool, l export.CC = "clang++" } - if runtime.GOOS == goos && runtime.GOARCH == goarch { - export.Toolchain = nativeToolchain(goos) + if nativePlatformToolchain { + if goos == "windows" { + export, err = resolveWindowsToolchain(goarch, nativeInput, probeNativeTool) + if err != nil { + return + } + targetTriple = export.Toolchain.TargetTriple + // Native Windows tool selection is authoritative. An embedded + // toolchain cached below LLGO_ROOT must not leak include or library + // paths into this profile. + clangRoot = "" + export.ClangRoot = "" + } else { + export.Toolchain = nativeToolchain(goos) + } export.DebugInfo = nativeDebugInfoPolicy(export.Toolchain) // not cross compile // Set up basic flags for non-cross-compile + externalLDFlags := slices.Clone(export.LDFLAGS) export.LDFLAGS = []string{ "-target", targetTriple, "-Qunused-arguments", @@ -489,6 +552,7 @@ func useWithGOARM(goos, goarch, goarm string, wasiThreads, forceEspClang bool, l ccflags, ldflags := nativeSectionFlags(export.Toolchain) export.CCFLAGS = append(export.CCFLAGS, ccflags...) export.LDFLAGS = append(export.LDFLAGS, ldflags...) + export.LDFLAGS = append(export.LDFLAGS, externalLDFlags...) return } if goarch != "wasm" { @@ -626,6 +690,10 @@ func useWithGOARM(goos, goarch, goarm string, wasiThreads, forceEspClang bool, l return } +func usesNativePlatformToolchain(hostGOOS, hostGOARCH, targetGOOS, targetGOARCH string) bool { + return hostGOOS == targetGOOS && hostGOARCH == targetGOARCH +} + // UseTarget loads configuration from a target name (e.g., "rp2040", "wasi") func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (export Export, err error) { resolver := targets.NewDefaultResolver() @@ -927,8 +995,15 @@ func Use(goos, goarch, targetName string, wasiThreads, forceEspClang bool, level // setting affects native GOARCH=arm clang and linker triples; named targets // retain their target configuration's LLVM triple. func UseWithGOARM(goos, goarch, goarm, targetName string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool) (export Export, err error) { + return UseWithGOARMAndToolchain(goos, goarch, goarm, targetName, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, NativeToolchainInput{}) +} + +// UseWithGOARMAndToolchain is UseWithGOARM with explicit Go-compatible native +// compiler commands. Named -target configurations intentionally ignore these +// host commands and preserve their existing toolchain selection. +func UseWithGOARMAndToolchain(goos, goarch, goarm, targetName string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool, nativeInput NativeToolchainInput) (export Export, err error) { if targetName != "" && !strings.HasPrefix(targetName, "wasm") && !strings.HasPrefix(targetName, "wasi") { return UseTarget(targetName, level, ltoMode) } - return useWithGOARM(goos, goarch, goarm, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE) + return useWithGOARMAndToolchain(goos, goarch, goarm, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, nativeInput) } diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go index 78f368c882..340155a7d9 100644 --- a/internal/crosscompile/crosscompile_test.go +++ b/internal/crosscompile/crosscompile_test.go @@ -482,7 +482,7 @@ func TestUseWithTarget(t *testing.T) { if runtime.GOOS == "darwin" && len(export.LDFLAGS) == 0 { t.Error("Expected LDFLAGS to be set for native build") } - wantDebugInfo := nativeDebugInfoPolicy(nativeToolchain(runtime.GOOS)) + wantDebugInfo := nativeDebugInfoPolicy(export.Toolchain) if export.DebugInfo.AlwaysOmit != wantDebugInfo.AlwaysOmit || !slices.Equal(export.DebugInfo.OmitLinkFlags, wantDebugInfo.OmitLinkFlags) || !slices.Equal(export.DebugInfo.PreserveLinkFlags, wantDebugInfo.PreserveLinkFlags) { @@ -546,10 +546,14 @@ func TestOptimizationFlagPlacement(t *testing.T) { if !slices.Contains(export.CCFLAGS, "-O3") { t.Fatalf("host CCFLAGS = %v, want -O3", export.CCFLAGS) } - if !hasFlagValue(export.CCFLAGS, "-target", llvm.GetTargetTriple(runtime.GOOS, runtime.GOARCH)) { + wantTarget := llvm.GetTargetTriple(runtime.GOOS, runtime.GOARCH) + if export.Toolchain.TargetTriple != "" { + wantTarget = export.Toolchain.TargetTriple + } + if !hasFlagValue(export.CCFLAGS, "-target", wantTarget) { t.Fatalf("host CCFLAGS = %v, want native -target", export.CCFLAGS) } - if !hasFlagValue(export.LDFLAGS, "-target", llvm.GetTargetTriple(runtime.GOOS, runtime.GOARCH)) { + if !hasFlagValue(export.LDFLAGS, "-target", wantTarget) { t.Fatalf("host LDFLAGS = %v, want native -target", export.LDFLAGS) } } @@ -630,7 +634,7 @@ func TestNativeWindowsSectionFlags(t *testing.T) { t.Errorf("native Windows CCFLAGS = %v, want %q", ccflags, want) } } - for _, want := range []string{"-fdata-sections", "-ffunction-sections", "-Wl,/opt:ref", "-llegacy_stdio_definitions"} { + for _, want := range []string{"-fdata-sections", "-ffunction-sections", "--rtlib=compiler-rt", "-Wl,/opt:ref", "-llegacy_stdio_definitions"} { if !slices.Contains(ldflags, want) { t.Errorf("native Windows LDFLAGS = %v, want %q", ldflags, want) } @@ -651,34 +655,87 @@ func TestNativeWindowsExportFlags(t *testing.T) { if err != nil { t.Fatal(err) } - if export.Toolchain != nativeToolchain("windows") { + wantTriple, err := windowsTargetTriple(runtime.GOARCH, export.Toolchain.ABI) + if err != nil { + t.Fatal(err) + } + if export.Toolchain.ObjectFormat != ObjectFormatCOFF || + export.Toolchain.Driver != DriverFlavorClangGNU || + export.Toolchain.TargetTriple != wantTriple { t.Fatalf("native Windows toolchain = %+v", export.Toolchain) } - for _, want := range []string{ - "-Wl,/errorlimit:0", - "-Wl,/opt:noicf", - "-Wl,/opt:ref", - "-Wl,/opt:lldlto=2", - "-llegacy_stdio_definitions", - } { + var wanted, unwanted []string + switch export.Toolchain.ABI { + case PlatformABIMsvc: + if export.Toolchain.Linker != LinkerFlavorCOFFLLD || + export.Toolchain.CRT != CRTFlavorUCRT || + export.Toolchain.CXXRuntime != CXXRuntimeMSVC { + t.Fatalf("native MSVC toolchain = %+v", export.Toolchain) + } + wanted = []string{ + "-Wl,/errorlimit:0", + "-Wl,/opt:noicf", + "-Wl,/opt:ref", + "-Wl,/opt:lldlto=2", + "-llegacy_stdio_definitions", + } + unwanted = []string{ + "-Wl,--error-limit=0", + "-Wl,--icf=none", + "--gc-sections", + } + case PlatformABIGNU: + if export.Toolchain.Linker != LinkerFlavorMinGWLLD || + export.Toolchain.CRT != CRTFlavorUnknown || + export.Toolchain.CXXRuntime != CXXRuntimeUnknown { + t.Fatalf("native GNU/MinGW toolchain = %+v", export.Toolchain) + } + wanted = []string{ + "-Wl,--error-limit=0", + "-Wl,--icf=none", + "-Wl,--gc-sections", + "-Wl,--lto-O2", + } + unwanted = []string{ + "-Wl,/errorlimit:0", + "-Wl,/opt:noicf", + "-Wl,/opt:ref", + "-Wl,/opt:lldlto=2", + "-llegacy_stdio_definitions", + } + default: + t.Fatalf("unsupported native Windows ABI: %+v", export.Toolchain) + } + for _, want := range wanted { if !slices.Contains(export.LDFLAGS, want) { t.Errorf("native Windows LDFLAGS = %v, want %q", export.LDFLAGS, want) } } - for _, unwanted := range []string{ - "-Wl,--error-limit=0", - "-Wl,--icf=none", - "--gc-sections", - "-latomic", - "-lpthread", - "-ldl", - } { - if slices.Contains(export.LDFLAGS, unwanted) { - t.Errorf("native Windows LDFLAGS = %v, do not want %q", export.LDFLAGS, unwanted) + for _, flag := range append(unwanted, "-latomic", "-lpthread", "-ldl") { + if slices.Contains(export.LDFLAGS, flag) { + t.Errorf("native Windows LDFLAGS = %v, do not want %q", export.LDFLAGS, flag) } } } +func TestUsesNativePlatformToolchain(t *testing.T) { + for _, test := range []struct { + name string + hostOS, hostArch, targetOS, targetArch string + want bool + }{ + {name: "same platform", hostOS: "windows", hostArch: "amd64", targetOS: "windows", targetArch: "amd64", want: true}, + {name: "cross architecture", hostOS: "windows", hostArch: "amd64", targetOS: "windows", targetArch: "arm64"}, + {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); got != test.want { + t.Fatalf("usesNativePlatformToolchain() = %v, want %v", got, test.want) + } + }) + } +} + func TestDevLTOGlobalDCEUseLTOFlagsControlledByOption(t *testing.T) { export, err := use(runtime.GOOS, runtime.GOARCH, false, false, optlevel.O2, lto.Off, false) if err != nil { diff --git a/internal/crosscompile/fetch.go b/internal/crosscompile/fetch.go index 566380b705..4277527a3c 100644 --- a/internal/crosscompile/fetch.go +++ b/internal/crosscompile/fetch.go @@ -3,6 +3,7 @@ package crosscompile import ( "archive/tar" "archive/zip" + "bytes" "compress/gzip" "fmt" "io" @@ -303,24 +304,17 @@ func extractTarGz(tarGzFile, dest string) error { } func extractTarXz(tarXzFile, dest string) error { + return extractTarXzForGOOS(runtime.GOOS, os.Getenv("ProgramFiles"), os.Getenv("SystemRoot"), tarXzFile, dest) +} + +func extractTarXzForGOOS(goos, programFiles, systemRoot, tarXzFile, dest string) error { tarCommand := "tar" tarArgs := []string{"-xf", tarXzFile, "-C", dest} - if runtime.GOOS == "windows" { - var xzCommand string - tarCommand, xzCommand = windowsTarXzTools( - os.Getenv("LLGO_MSYS2_LOCATION"), os.Getenv("SystemRoot"), - ) - if xzCommand != "" { - // Windows' bundled bsdtar takes more than 25 minutes to unpack the - // 2.7 GiB ESP toolchain on hosted runners. MSYS2 GNU tar does it in - // about a minute, but needs --force-local for native drive paths. - tarArgs = []string{ - "--force-local", - "--use-compress-program=" + filepath.ToSlash(xzCommand), - "-xf", filepath.ToSlash(tarXzFile), - "-C", filepath.ToSlash(dest), - } + if goos == "windows" { + if sevenZip := windowsSevenZip(programFiles); sevenZip != "" { + return extractTarXzWith7Zip(sevenZip, tarXzFile, dest) } + tarCommand = windowsTarCommand(systemRoot) } cmd := exec.Command(tarCommand, tarArgs...) if output, err := cmd.CombinedOutput(); err != nil { @@ -329,22 +323,66 @@ func extractTarXz(tarXzFile, dest string) error { return nil } -func windowsTarXzTools(msysRoot, systemRoot string) (tarCommand, xzCommand string) { - if msysRoot != "" { - binDir := filepath.Join(msysRoot, "usr", "bin") - msysTar := filepath.Join(binDir, "tar.exe") - msysXz := filepath.Join(binDir, "xz.exe") - if fileExists(msysTar) && fileExists(msysXz) { - return msysTar, msysXz +func extractTarXzWith7Zip(sevenZip, tarXzFile, dest string) error { + return extractTarXzWith7ZipCommand(sevenZip, tarXzFile, dest, exec.Command) +} + +func extractTarXzWith7ZipCommand(sevenZip, tarXzFile, dest string, command func(string, ...string) *exec.Cmd) error { + decompress := command(sevenZip, "x", "-so", tarXzFile) + extract := command(sevenZip, "x", "-si", "-ttar", "-y", "-o"+dest) + pipe, err := decompress.StdoutPipe() + if err != nil { + return fmt.Errorf("7-Zip xz pipe: %w", err) + } + extract.Stdin = pipe + var decompressStderr, extractStderr bytes.Buffer + decompress.Stderr = &decompressStderr + extract.Stdout = io.Discard + extract.Stderr = &extractStderr + + // Start the consumer first so the decompressor can stream a multi-gigabyte + // tar without writing a second archive to the comparatively small Windows + // hosted-runner disk. + if err := extract.Start(); err != nil { + return fmt.Errorf("start 7-Zip tar extraction: %w", err) + } + if err := decompress.Start(); err != nil { + _ = extract.Process.Kill() + _ = extract.Wait() + return fmt.Errorf("start 7-Zip xz decompression: %w", err) + } + extractErr := extract.Wait() + decompressErr := decompress.Wait() + if decompressErr != nil { + return fmt.Errorf("7-Zip xz decompression: %w: %s", decompressErr, strings.TrimSpace(decompressStderr.String())) + } + if extractErr != nil { + return fmt.Errorf("7-Zip tar extraction: %w: %s", extractErr, strings.TrimSpace(extractStderr.String())) + } + return nil +} + +func windowsSevenZip(programFiles string) string { + if programFiles != "" { + sevenZip := filepath.Join(programFiles, "7-Zip", "7z.exe") + if fileExists(sevenZip) { + return sevenZip } } + if sevenZip, err := exec.LookPath("7z.exe"); err == nil { + return sevenZip + } + return "" +} + +func windowsTarCommand(systemRoot string) string { if systemRoot != "" { nativeTar := filepath.Join(systemRoot, "System32", "tar.exe") if fileExists(nativeTar) { - return nativeTar, "" + return nativeTar } } - return "tar", "" + return "tar" } func fileExists(name string) bool { diff --git a/internal/crosscompile/fetch_test.go b/internal/crosscompile/fetch_test.go index 5fd547d732..e9bdf5eaea 100644 --- a/internal/crosscompile/fetch_test.go +++ b/internal/crosscompile/fetch_test.go @@ -9,12 +9,14 @@ import ( "compress/gzip" "errors" "fmt" + "io" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "runtime" + "slices" "strings" "sync" "sync/atomic" @@ -111,7 +113,7 @@ func createTestTarXz(t *testing.T, files map[string]string) string { return xzFile } -func TestWindowsTarXzTools(t *testing.T) { +func TestWindowsArchiveTools(t *testing.T) { touch := func(t *testing.T, name string) { t.Helper() if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil { @@ -122,40 +124,167 @@ func TestWindowsTarXzTools(t *testing.T) { } } - t.Run("MSYS2", func(t *testing.T) { + t.Run("7-Zip", func(t *testing.T) { root := t.TempDir() - tarPath := filepath.Join(root, "usr", "bin", "tar.exe") - xzPath := filepath.Join(root, "usr", "bin", "xz.exe") - touch(t, tarPath) - touch(t, xzPath) - - gotTar, gotXz := windowsTarXzTools(root, "") - if gotTar != tarPath || gotXz != xzPath { - t.Fatalf("windowsTarXzTools() = (%q, %q), want (%q, %q)", gotTar, gotXz, tarPath, xzPath) + sevenZip := filepath.Join(root, "7-Zip", "7z.exe") + touch(t, sevenZip) + if got := windowsSevenZip(root); got != sevenZip { + t.Fatalf("windowsSevenZip() = %q, want %q", got, sevenZip) + } + }) + + t.Run("7-ZipFromPath", func(t *testing.T) { + binDir := t.TempDir() + sevenZip := filepath.Join(binDir, "7z.exe") + touch(t, sevenZip) + t.Setenv("PATH", binDir) + if got := windowsSevenZip(""); got != sevenZip { + t.Fatalf("windowsSevenZip() = %q, want %q", got, sevenZip) } }) t.Run("NativeFallback", func(t *testing.T) { - msysRoot := t.TempDir() - touch(t, filepath.Join(msysRoot, "usr", "bin", "tar.exe")) systemRoot := t.TempDir() nativeTar := filepath.Join(systemRoot, "System32", "tar.exe") touch(t, nativeTar) - gotTar, gotXz := windowsTarXzTools(msysRoot, systemRoot) - if gotTar != nativeTar || gotXz != "" { - t.Fatalf("windowsTarXzTools() = (%q, %q), want (%q, empty)", gotTar, gotXz, nativeTar) + if got := windowsTarCommand(systemRoot); got != nativeTar { + t.Fatalf("windowsTarCommand() = %q, want %q", got, nativeTar) } }) t.Run("PathFallback", func(t *testing.T) { - gotTar, gotXz := windowsTarXzTools("", "") - if gotTar != "tar" || gotXz != "" { - t.Fatalf("windowsTarXzTools() = (%q, %q), want (tar, empty)", gotTar, gotXz) + if got := windowsTarCommand(""); got != "tar" { + t.Fatalf("windowsTarCommand() = %q, want tar", got) } }) } +func TestExtractTarXzWith7ZipCommand(t *testing.T) { + if mode := os.Getenv("LLGO_7ZIP_HELPER"); mode != "" { + switch mode { + case "decompress": + if os.Getenv("LLGO_7ZIP_FAIL") == mode { + os.Exit(2) + } + _, _ = os.Stdout.WriteString("streamed tar payload") + case "extract": + payload, err := io.ReadAll(os.Stdin) + if err != nil { + os.Exit(3) + } + if err := os.WriteFile(os.Getenv("LLGO_7ZIP_OUTPUT"), payload, 0o644); err != nil { + os.Exit(4) + } + if os.Getenv("LLGO_7ZIP_FAIL") == mode { + os.Exit(5) + } + default: + os.Exit(6) + } + os.Exit(0) + } + + output := filepath.Join(t.TempDir(), "payload") + t.Setenv("LLGO_7ZIP_OUTPUT", output) + command := func(_ string, args ...string) *exec.Cmd { + mode := "extract" + if slices.Contains(args, "-so") { + mode = "decompress" + } + cmd := exec.Command(os.Args[0], "-test.run=^TestExtractTarXzWith7ZipCommand$") + cmd.Env = append(os.Environ(), "LLGO_7ZIP_HELPER="+mode) + return cmd + } + + if err := extractTarXzWith7ZipCommand("7z", "input.tar.xz", "output", command); err != nil { + t.Fatal(err) + } + payload, err := os.ReadFile(output) + if err != nil { + t.Fatal(err) + } + if got, want := string(payload), "streamed tar payload"; got != want { + t.Fatalf("streamed payload = %q, want %q", got, want) + } + + for _, test := range []struct { + mode string + want string + }{ + {mode: "decompress", want: "7-Zip xz decompression"}, + {mode: "extract", want: "7-Zip tar extraction"}, + } { + t.Run(test.mode+" failure", func(t *testing.T) { + t.Setenv("LLGO_7ZIP_FAIL", test.mode) + if err := extractTarXzWith7ZipCommand("7z", "input.tar.xz", "output", command); err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("error = %v, want substring %q", err, test.want) + } + }) + } + + t.Run("extract start failure", func(t *testing.T) { + calls := 0 + factory := func(_ string, _ ...string) *exec.Cmd { + calls++ + if calls == 2 { + return exec.Command(filepath.Join(t.TempDir(), "missing")) + } + return command("7z", "-so") + } + if err := extractTarXzWith7ZipCommand("7z", "input.tar.xz", "output", factory); err == nil || !strings.Contains(err.Error(), "start 7-Zip tar extraction") { + t.Fatalf("error = %v, want extract start failure", err) + } + }) + + t.Run("decompress start failure", func(t *testing.T) { + calls := 0 + factory := func(_ string, _ ...string) *exec.Cmd { + calls++ + if calls == 1 { + return exec.Command(filepath.Join(t.TempDir(), "missing")) + } + return command("7z", "-si") + } + if err := extractTarXzWith7ZipCommand("7z", "input.tar.xz", "output", factory); err == nil || !strings.Contains(err.Error(), "start 7-Zip xz decompression") { + t.Fatalf("error = %v, want decompressor start failure", err) + } + }) +} + +func TestExtractTarXzForWindowsUses7Zip(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("the test helper is a POSIX shell script") + } + programFiles := t.TempDir() + sevenZip := filepath.Join(programFiles, "7-Zip", "7z.exe") + if err := os.MkdirAll(filepath.Dir(sevenZip), 0o755); err != nil { + t.Fatal(err) + } + script := `#!/bin/sh +if [ "$2" = "-so" ]; then + printf 'streamed tar payload' +else + cat > "$LLGO_7ZIP_OUTPUT" +fi +` + if err := os.WriteFile(sevenZip, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + output := filepath.Join(t.TempDir(), "payload") + t.Setenv("LLGO_7ZIP_OUTPUT", output) + if err := extractTarXzForGOOS("windows", programFiles, "", "input.tar.xz", "output"); err != nil { + t.Fatal(err) + } + payload, err := os.ReadFile(output) + if err != nil { + t.Fatal(err) + } + if got, want := string(payload), "streamed tar payload"; got != want { + t.Fatalf("streamed payload = %q, want %q", got, want) + } +} + // Helper function to create a test HTTP server func createTestServer(t *testing.T, files map[string]string) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -840,8 +969,12 @@ func TestExtractTarXzError(t *testing.T) { if err == nil { t.Fatal("extractTarXz succeeded for a missing archive") } - if !strings.Contains(err.Error(), "tar -xf:") { - t.Fatalf("extractTarXz error = %q, want tar command context", err) + want := "tar -xf:" + if runtime.GOOS == "windows" && windowsSevenZip(os.Getenv("ProgramFiles")) != "" { + want = "7-Zip xz decompression:" + } + if !strings.Contains(err.Error(), want) { + t.Fatalf("extractTarXz error = %q, want %q command context", err, want) } } diff --git a/internal/crosscompile/windows_toolchain.go b/internal/crosscompile/windows_toolchain.go new file mode 100644 index 0000000000..6fa37e8c48 --- /dev/null +++ b/internal/crosscompile/windows_toolchain.go @@ -0,0 +1,289 @@ +package crosscompile + +import ( + "bytes" + "fmt" + "os/exec" + "path/filepath" + "slices" + "strings" +) + +// NativeToolchainInput contains Go-compatible native compiler command +// prefixes and external-linker flags. CC, CXX, and ExternalLinker are parsed +// command prefixes: element zero is the executable and the remainder are +// arguments that must precede LLGo's generated flags. ExternalFlags is the +// plain flag list parsed from -extldflags. +type NativeToolchainInput struct { + CC []string + CXX []string + ExternalLinker []string + ExternalFlags []string + Dir string + Environ []string +} + +type toolIdentity struct { + target string + version string +} + +type toolProbe func(command []string, input NativeToolchainInput) (toolIdentity, error) + +const windowsGNUABIBuildTag = "llgo_windows_gnu" + +func resolveWindowsToolchain(goarch string, input NativeToolchainInput, probe toolProbe) (Export, error) { + if _, err := windowsTargetTriple(goarch, PlatformABIMsvc); err != nil { + return Export{}, err + } + // Probe an implicit Clang before adding target flags. Official Windows LLVM + // defaults to MSVC while MSYS2 CLANG64 defaults to GNU/MinGW, so either + // native installation works without requiring CC/CXX environment settings. + // The compiler target selects the profile; the invoking shell does not. + implicitCC := len(input.CC) == 0 + cc := slices.Clone(input.CC) + if implicitCC { + cc = []string{"clang"} + } + + ccIdentity, err := probe(cc, input) + if err != nil { + return Export{}, fmt.Errorf("probe CC %q: %w", cc[0], err) + } + toolchain, err := windowsToolchainForTriple(goarch, ccIdentity.target) + if err != nil { + return Export{}, fmt.Errorf("CC %q: %w", cc[0], err) + } + if err := requireClangGNUDriver("CC", cc, ccIdentity); err != nil { + return Export{}, err + } + if implicitCC { + cc = append(cc, "--target="+toolchain.TargetTriple) + if toolchain.ABI == PlatformABIMsvc { + // The standalone LLVM and native dependency profile use the dynamic + // MSVC runtime. Explicit compiler commands remain the caller's choice. + cc = append(cc, "-fms-runtime-lib=dll") + } + } + // Visual Studio's developer variables identify only the MSVC dependency + // profile. A GNU/MinGW profile must obtain its CRT, C++ runtime, sysroot, + // and version identity from its own setup instead of inheriting the host's + // MSVC installation and cache identity. + if toolchain.ABI == PlatformABIMsvc { + toolchain.SDKVersion = windowsEnvironmentValue(input.Environ, "WindowsSDKVersion") + toolchain.CRTVersion = windowsEnvironmentValue(input.Environ, "UCRTVersion") + toolchain.ToolsetVersion = windowsEnvironmentValue(input.Environ, "VCToolsVersion") + } + + // An unset CXX follows the profile selected by CC. This is important when + // an explicit CC selects MinGW: the host's default clang++ target must not + // silently switch the C++ half of the build back to MSVC. + cxxDefault := []string{"clang++", "--target=" + toolchain.TargetTriple} + if toolchain.ABI == PlatformABIMsvc { + cxxDefault = append(cxxDefault, "-fms-runtime-lib=dll") + } + cxx := commandOrDefault(input.CXX, cxxDefault...) + cxxIdentity, err := probe(cxx, input) + if err != nil { + return Export{}, fmt.Errorf("probe CXX %q: %w", cxx[0], err) + } + cxxToolchain, err := windowsToolchainForTriple(goarch, cxxIdentity.target) + if err != nil { + return Export{}, fmt.Errorf("CXX %q: %w", cxx[0], err) + } + if err := requireClangGNUDriver("CXX", cxx, cxxIdentity); err != nil { + return Export{}, err + } + if err := compatibleWindowsToolchains(toolchain, cxxToolchain); err != nil { + return Export{}, fmt.Errorf("CC and CXX are incompatible: %w", err) + } + + export := Export{ + CC: cc[0], + CCArgs: slices.Clone(cc[1:]), + CXX: cxx[0], + CXXArgs: slices.Clone(cxx[1:]), + CCIdentity: ccIdentity.version, + CXXIdentity: cxxIdentity.version, + Toolchain: toolchain, + LLVMTarget: toolchain.TargetTriple, + } + if toolchain.ABI == PlatformABIGNU { + // x86-64 libffi distinguishes its GNUW64 and Win64 interfaces even + // though both toolchain profiles use the native Windows C ABI. Keep + // that implementation detail automatic and in the package cache key. + export.BuildTags = []string{windowsGNUABIBuildTag} + } + if len(input.ExternalLinker) != 0 { + linkerIdentity, err := probe(input.ExternalLinker, input) + if err != nil { + return Export{}, fmt.Errorf("probe -extld %q: %w", input.ExternalLinker[0], err) + } + linkerToolchain, err := windowsToolchainForTriple(goarch, linkerIdentity.target) + if err != nil { + return Export{}, fmt.Errorf("-extld %q: %w", input.ExternalLinker[0], err) + } + if err := requireClangGNUDriver("-extld", input.ExternalLinker, linkerIdentity); err != nil { + return Export{}, err + } + if err := compatibleWindowsToolchains(toolchain, linkerToolchain); err != nil { + return Export{}, fmt.Errorf("-extld is incompatible with CC: %w", err) + } + export.Linker = input.ExternalLinker[0] + export.LinkerArgs = slices.Clone(input.ExternalLinker[1:]) + export.LinkerIdentity = linkerIdentity.version + } + export.LDFLAGS = slices.Clone(input.ExternalFlags) + return export, nil +} + +func windowsEnvironmentValue(environ []string, name string) string { + for i := len(environ) - 1; i >= 0; i-- { + key, value, ok := strings.Cut(environ[i], "=") + if ok && strings.EqualFold(key, name) { + return strings.TrimRight(strings.TrimSpace(value), `\/`) + } + } + return "" +} + +func commandOrDefault(command []string, defaults ...string) []string { + if len(command) == 0 { + return slices.Clone(defaults) + } + return slices.Clone(command) +} + +func requireClangGNUDriver(setting string, command []string, identity toolIdentity) error { + for _, arg := range command { + name := strings.TrimSuffix(strings.ToLower(filepath.Base(arg)), ".exe") + if name == "clang-cl" || arg == "--driver-mode=cl" || strings.HasPrefix(arg, "--driver-mode=cl=") { + return fmt.Errorf("%s %q uses clang-cl flag syntax; LLGo currently requires Clang's GNU-compatible driver", setting, command[0]) + } + } + 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) +} + +func compatibleWindowsToolchains(a, b NativeToolchain) error { + if a.ABI != b.ABI || a.TargetTriple != b.TargetTriple || a.CRT != b.CRT || a.CXXRuntime != b.CXXRuntime { + return fmt.Errorf("profiles differ: %s versus %s", describeWindowsToolchain(a), describeWindowsToolchain(b)) + } + return nil +} + +func describeWindowsToolchain(toolchain NativeToolchain) string { + return fmt.Sprintf("target=%s ABI=%s CRT=%s C++=%s", toolchain.TargetTriple, toolchain.ABI, toolchain.CRT, toolchain.CXXRuntime) +} + +func windowsToolchainForTriple(goarch, target string) (NativeToolchain, error) { + target = strings.ToLower(strings.TrimSpace(strings.Split(target, "\n")[0])) + if target == "" { + return NativeToolchain{}, fmt.Errorf("compiler reported an empty target triple") + } + if strings.Contains(target, "cygwin") || strings.Contains(target, "cygnus") || strings.Contains(target, "msys") { + return NativeToolchain{}, fmt.Errorf("target %q uses the unsupported MSYS/Cygwin POSIX-emulation ABI", target) + } + arch, err := goarchForTargetTriple(target) + if err != nil { + return NativeToolchain{}, err + } + if arch != goarch { + return NativeToolchain{}, fmt.Errorf("target %q is %s, want GOARCH=%s", target, arch, goarch) + } + + toolchain := NativeToolchain{ + ObjectFormat: ObjectFormatCOFF, + Driver: DriverFlavorClangGNU, + } + switch { + case strings.Contains(target, "windows-msvc"): + toolchain.ABI = PlatformABIMsvc + toolchain.Linker = LinkerFlavorCOFFLLD + toolchain.CRT = CRTFlavorUCRT + toolchain.CXXRuntime = CXXRuntimeMSVC + case strings.Contains(target, "windows-gnu"), strings.Contains(target, "mingw32"): + toolchain.ABI = PlatformABIGNU + toolchain.Linker = LinkerFlavorMinGWLLD + default: + return NativeToolchain{}, fmt.Errorf("target %q is not an explicit Windows MSVC or GNU/MinGW target", target) + } + toolchain.TargetTriple, err = windowsTargetTriple(goarch, toolchain.ABI) + if err != nil { + return NativeToolchain{}, err + } + return toolchain, nil +} + +func windowsTargetTriple(goarch string, abi PlatformABI) (string, error) { + var arch string + switch goarch { + case "386": + arch = "i686" + case "amd64": + arch = "x86_64" + case "arm64": + arch = "aarch64" + default: + return "", fmt.Errorf("unsupported Windows GOARCH %q", goarch) + } + switch abi { + case PlatformABIMsvc: + return arch + "-pc-windows-msvc", nil + case PlatformABIGNU: + return arch + "-w64-windows-gnu", nil + default: + return "", fmt.Errorf("unsupported Windows ABI %q", abi) + } +} + +func goarchForTargetTriple(target string) (string, error) { + arch, _, _ := strings.Cut(target, "-") + switch arch { + case "i386", "i486", "i586", "i686", "x86": + return "386", nil + case "x86_64", "amd64": + return "amd64", nil + case "aarch64", "arm64": + return "arm64", nil + default: + return "", fmt.Errorf("target %q has unsupported Windows architecture %q", target, arch) + } +} + +func probeNativeTool(command []string, input NativeToolchainInput) (toolIdentity, error) { + run := func(arg string) ([]byte, error) { + args := make([]string, 0, len(command)) + args = append(args, command[1:]...) + args = append(args, arg) + cmd := exec.Command(command[0], args...) + cmd.Dir = input.Dir + if input.Environ != nil { + cmd.Env = input.Environ + } + return cmd.CombinedOutput() + } + + targetOutput, targetErr := run("-dumpmachine") + if targetErr != nil || len(bytes.TrimSpace(targetOutput)) == 0 { + targetOutput, targetErr = run("--print-target-triple") + } + if targetErr != nil { + return toolIdentity{}, fmt.Errorf("read target triple: %w: %s", targetErr, strings.TrimSpace(string(targetOutput))) + } + versionOutput, err := run("--version") + if err != nil { + return toolIdentity{}, fmt.Errorf("read compiler identity: %w: %s", err, strings.TrimSpace(string(versionOutput))) + } + return toolIdentity{ + target: firstOutputLine(targetOutput), + version: firstOutputLine(versionOutput), + }, nil +} + +func firstOutputLine(output []byte) string { + line, _, _ := bytes.Cut(bytes.TrimSpace(output), []byte{'\n'}) + return strings.TrimSpace(strings.TrimSuffix(string(line), "\r")) +} diff --git a/internal/crosscompile/windows_toolchain_test.go b/internal/crosscompile/windows_toolchain_test.go new file mode 100644 index 0000000000..33b42e98a0 --- /dev/null +++ b/internal/crosscompile/windows_toolchain_test.go @@ -0,0 +1,479 @@ +package crosscompile + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "slices" + "strings" + "testing" +) + +func TestResolveWindowsToolchainDefaultsToMSVC(t *testing.T) { + probe := fixedToolProbe(t, map[string]toolIdentity{ + commandKey("clang"): {target: "x86_64-pc-windows-msvc", version: "clang version 19.1.7"}, + commandKey("clang++", "--target=x86_64-pc-windows-msvc", "-fms-runtime-lib=dll"): {target: "x86_64-pc-windows-msvc", version: "clang version 19.1.7"}, + }) + input := NativeToolchainInput{ + ExternalFlags: []string{"/incremental:no"}, + Environ: []string{ + "WindowsSDKVersion=10.0.26100.0\\", + "UCRTVersion=10.0.26100.0", + "VCToolsVersion=14.44.35207\\", + }, + } + export, err := resolveWindowsToolchain("amd64", input, probe) + if err != nil { + t.Fatal(err) + } + wantToolchain := NativeToolchain{ + ABI: PlatformABIMsvc, + ObjectFormat: ObjectFormatCOFF, + Driver: DriverFlavorClangGNU, + Linker: LinkerFlavorCOFFLLD, + TargetTriple: "x86_64-pc-windows-msvc", + CRT: CRTFlavorUCRT, + CXXRuntime: CXXRuntimeMSVC, + SDKVersion: "10.0.26100.0", + CRTVersion: "10.0.26100.0", + ToolsetVersion: "14.44.35207", + } + if export.Toolchain != wantToolchain { + t.Fatalf("Toolchain = %+v, want %+v", export.Toolchain, wantToolchain) + } + if export.CC != "clang" || !slices.Equal(export.CCArgs, []string{"--target=x86_64-pc-windows-msvc", "-fms-runtime-lib=dll"}) { + t.Fatalf("CC command = %q %q", export.CC, export.CCArgs) + } + if export.CXX != "clang++" || !slices.Equal(export.CXXArgs, []string{"--target=x86_64-pc-windows-msvc", "-fms-runtime-lib=dll"}) { + t.Fatalf("CXX command = %q %q", export.CXX, export.CXXArgs) + } + if export.CCIdentity != "clang version 19.1.7" || export.CXXIdentity != "clang version 19.1.7" { + t.Fatalf("compiler identities = %q, %q", export.CCIdentity, export.CXXIdentity) + } + if !slices.Equal(export.LDFLAGS, input.ExternalFlags) { + t.Fatalf("LDFLAGS = %q, want %q", export.LDFLAGS, input.ExternalFlags) + } + if len(export.BuildTags) != 0 { + t.Fatalf("MSVC BuildTags = %q, want none", export.BuildTags) + } +} + +func TestResolveWindowsToolchainDefaultsToGNUProfile(t *testing.T) { + probe := fixedToolProbe(t, map[string]toolIdentity{ + commandKey("clang"): {target: "x86_64-w64-mingw32", version: "clang version 19.1.7"}, + commandKey("clang++", "--target=x86_64-w64-windows-gnu"): {target: "x86_64-w64-windows-gnu", version: "clang version 19.1.7"}, + }) + export, err := resolveWindowsToolchain("amd64", NativeToolchainInput{ + Environ: []string{ + "MSYSTEM=CLANG64", + "WindowsSDKVersion=10.0.26100.0\\", + "UCRTVersion=10.0.26100.0", + "VCToolsVersion=14.44.35207\\", + }, + }, probe) + if err != nil { + t.Fatal(err) + } + if export.Toolchain.ABI != PlatformABIGNU || export.Toolchain.TargetTriple != "x86_64-w64-windows-gnu" { + t.Fatalf("GNU toolchain = %+v", export.Toolchain) + } + if export.Toolchain.SDKVersion != "" || export.Toolchain.CRTVersion != "" || export.Toolchain.ToolsetVersion != "" { + t.Fatalf("GNU profile inherited the host MSVC environment: %+v", export.Toolchain) + } + if export.CC != "clang" || !slices.Equal(export.CCArgs, []string{"--target=x86_64-w64-windows-gnu"}) { + t.Fatalf("CC command = %q %q", export.CC, export.CCArgs) + } + if export.CXX != "clang++" || !slices.Equal(export.CXXArgs, []string{"--target=x86_64-w64-windows-gnu"}) { + t.Fatalf("CXX command = %q %q", export.CXX, export.CXXArgs) + } + if !slices.Equal(export.BuildTags, []string{windowsGNUABIBuildTag}) { + t.Fatalf("GNU BuildTags = %q, want %q", export.BuildTags, windowsGNUABIBuildTag) + } +} + +func TestWindowsEnvironmentValueUsesLastCaseInsensitiveEntry(t *testing.T) { + environ := []string{"UCRTVersion=old", "other=value", "ucrtversion=10.0.26100.0\\"} + if got, want := windowsEnvironmentValue(environ, "UCRTVersion"), "10.0.26100.0"; got != want { + t.Fatalf("windowsEnvironmentValue() = %q, want %q", got, want) + } +} + +func TestResolveWindowsToolchainExplicitGNUProfile(t *testing.T) { + cc := []string{"wrapper", "clang", "--target=x86_64-w64-windows-gnu"} + cxx := []string{"clang++", "--target=x86_64-w64-windows-gnu"} + extld := []string{"clang", "--target=x86_64-unknown-windows-gnu"} + probe := fixedToolProbe(t, map[string]toolIdentity{ + commandKey(cc...): {target: "x86_64-w64-mingw32", version: "clang version 19.1.7"}, + commandKey(cxx...): {target: "x86_64-w64-windows-gnu", version: "clang version 19.1.7"}, + commandKey(extld...): {target: "x86_64-unknown-windows-gnu", version: "clang version 19.1.7"}, + }) + export, err := resolveWindowsToolchain("amd64", NativeToolchainInput{ + CC: cc, + ExternalLinker: extld, + Environ: []string{ + "WindowsSDKVersion=10.0.26100.0\\", + "UCRTVersion=10.0.26100.0", + "VCToolsVersion=14.44.35207\\", + }, + }, probe) + if err != nil { + t.Fatal(err) + } + if export.Toolchain.ABI != PlatformABIGNU || export.Toolchain.TargetTriple != "x86_64-w64-windows-gnu" { + t.Fatalf("GNU toolchain = %+v", export.Toolchain) + } + if export.Toolchain.CRT != CRTFlavorUnknown || export.Toolchain.CXXRuntime != CXXRuntimeUnknown { + t.Fatalf("ambiguous GNU runtime was guessed: %+v", export.Toolchain) + } + if export.Toolchain.SDKVersion != "" || export.Toolchain.CRTVersion != "" || export.Toolchain.ToolsetVersion != "" { + t.Fatalf("GNU profile inherited the host MSVC environment: %+v", export.Toolchain) + } + if export.CXX != cxx[0] || !slices.Equal(export.CXXArgs, cxx[1:]) { + t.Fatalf("derived CXX = %q %q, want %q", export.CXX, export.CXXArgs, cxx) + } + if export.Linker != extld[0] || !slices.Equal(export.LinkerArgs, extld[1:]) { + t.Fatalf("external linker = %q %q, want %q", export.Linker, export.LinkerArgs, extld) + } + if !slices.Equal(export.BuildTags, []string{windowsGNUABIBuildTag}) { + t.Fatalf("GNU BuildTags = %q, want %q", export.BuildTags, windowsGNUABIBuildTag) + } +} + +func TestResolveWindowsToolchainRejectsIncompatibleInputs(t *testing.T) { + clang := toolIdentity{target: "x86_64-pc-windows-msvc", version: "clang version 19.1.7"} + for _, test := range []struct { + name string + goarch string + input NativeToolchainInput + ids map[string]toolIdentity + probe toolProbe + wantErr string + }{ + { + name: "unsupported GOARCH", + goarch: "arm", + wantErr: `unsupported Windows GOARCH "arm"`, + }, + { + name: "wrong architecture", + goarch: "arm64", + input: NativeToolchainInput{CC: []string{"clang"}}, + ids: map[string]toolIdentity{commandKey("clang"): clang}, + wantErr: "want GOARCH=arm64", + }, + { + name: "Cygwin ABI", + goarch: "amd64", + input: NativeToolchainInput{CC: []string{"clang"}}, + ids: map[string]toolIdentity{ + commandKey("clang"): {target: "x86_64-pc-cygwin", version: "clang version 19.1.7"}, + }, + wantErr: "unsupported MSYS/Cygwin", + }, + { + name: "clang-cl", + goarch: "amd64", + input: NativeToolchainInput{CC: []string{"clang-cl"}}, + ids: map[string]toolIdentity{commandKey("clang-cl"): clang}, + wantErr: "clang-cl flag syntax", + }, + { + name: "non-Clang driver", + goarch: "amd64", + input: NativeToolchainInput{CC: []string{"gcc"}}, + ids: map[string]toolIdentity{ + commandKey("gcc"): {target: "x86_64-pc-windows-msvc", version: "gcc version 15"}, + }, + wantErr: "unsupported driver", + }, + { + name: "CXX ABI mismatch", + goarch: "amd64", + input: NativeToolchainInput{ + CC: []string{"clang"}, + CXX: []string{"clang++"}, + }, + ids: map[string]toolIdentity{ + commandKey("clang"): clang, + commandKey("clang++"): {target: "x86_64-w64-windows-gnu", version: "clang version 19.1.7"}, + }, + wantErr: "CC and CXX are incompatible", + }, + { + name: "CXX probe error", + goarch: "amd64", + input: NativeToolchainInput{ + CC: []string{"clang"}, + CXX: []string{"missing-cxx"}, + }, + ids: map[string]toolIdentity{commandKey("clang"): clang}, + wantErr: "probe CXX", + }, + { + name: "CXX non-Windows target", + goarch: "amd64", + input: NativeToolchainInput{ + CC: []string{"clang"}, + CXX: []string{"clang++"}, + }, + ids: map[string]toolIdentity{ + commandKey("clang"): clang, + commandKey("clang++"): {target: "x86_64-unknown-linux-gnu", version: "clang version 19.1.7"}, + }, + wantErr: "not an explicit Windows", + }, + { + name: "CXX unsupported driver", + goarch: "amd64", + input: NativeToolchainInput{ + CC: []string{"clang"}, + CXX: []string{"g++"}, + }, + ids: map[string]toolIdentity{ + commandKey("clang"): clang, + commandKey("g++"): {target: "x86_64-pc-windows-msvc", version: "gcc version 15"}, + }, + wantErr: "CXX \"g++\" uses an unsupported driver", + }, + { + name: "external linker mismatch", + goarch: "amd64", + input: NativeToolchainInput{ + CC: []string{"clang"}, + CXX: []string{"clang++"}, + ExternalLinker: []string{"other-clang"}, + }, + ids: map[string]toolIdentity{ + commandKey("clang"): clang, + commandKey("clang++"): clang, + commandKey("other-clang"): {target: "x86_64-w64-windows-gnu", version: "clang version 19.1.7"}, + }, + wantErr: "-extld is incompatible with CC", + }, + { + name: "external linker probe error", + goarch: "amd64", + input: NativeToolchainInput{ + CC: []string{"clang"}, + CXX: []string{"clang++"}, + ExternalLinker: []string{"missing-linker"}, + }, + ids: map[string]toolIdentity{ + commandKey("clang"): clang, + commandKey("clang++"): clang, + }, + wantErr: "probe -extld", + }, + { + name: "external linker non-Windows target", + goarch: "amd64", + input: NativeToolchainInput{ + CC: []string{"clang"}, + CXX: []string{"clang++"}, + ExternalLinker: []string{"other-clang"}, + }, + ids: map[string]toolIdentity{ + commandKey("clang"): clang, + commandKey("clang++"): clang, + commandKey("other-clang"): {target: "x86_64-unknown-linux-gnu", version: "clang version 19.1.7"}, + }, + wantErr: "not an explicit Windows", + }, + { + name: "external linker unsupported driver", + goarch: "amd64", + input: NativeToolchainInput{ + CC: []string{"clang"}, + CXX: []string{"clang++"}, + ExternalLinker: []string{"gcc"}, + }, + ids: map[string]toolIdentity{ + commandKey("clang"): clang, + commandKey("clang++"): clang, + commandKey("gcc"): {target: "x86_64-pc-windows-msvc", version: "gcc version 15"}, + }, + wantErr: "-extld \"gcc\" uses an unsupported driver", + }, + { + name: "probe error", + goarch: "amd64", + input: NativeToolchainInput{CC: []string{"missing"}}, + probe: func([]string, NativeToolchainInput) (toolIdentity, error) { + return toolIdentity{}, errors.New("not found") + }, + wantErr: "probe CC", + }, + } { + t.Run(test.name, func(t *testing.T) { + probe := test.probe + if probe == nil { + probe = fixedToolProbe(t, test.ids) + } + _, err := resolveWindowsToolchain(test.goarch, test.input, probe) + if err == nil || !strings.Contains(err.Error(), test.wantErr) { + t.Fatalf("error = %v, want substring %q", err, test.wantErr) + } + }) + } +} + +func TestWindowsTargetTriple(t *testing.T) { + for _, test := range []struct { + goarch string + abi PlatformABI + want string + }{ + {"386", PlatformABIMsvc, "i686-pc-windows-msvc"}, + {"amd64", PlatformABIMsvc, "x86_64-pc-windows-msvc"}, + {"arm64", PlatformABIMsvc, "aarch64-pc-windows-msvc"}, + {"386", PlatformABIGNU, "i686-w64-windows-gnu"}, + {"amd64", PlatformABIGNU, "x86_64-w64-windows-gnu"}, + {"arm64", PlatformABIGNU, "aarch64-w64-windows-gnu"}, + } { + got, err := windowsTargetTriple(test.goarch, test.abi) + if err != nil || got != test.want { + t.Errorf("windowsTargetTriple(%q, %q) = %q, %v; want %q", test.goarch, test.abi, got, err, test.want) + } + } + if _, err := windowsTargetTriple("amd64", PlatformABIUnknown); err == nil { + t.Fatal("windowsTargetTriple accepted an unknown ABI") + } +} + +func TestWindowsToolchainForTripleErrorsAndAliases(t *testing.T) { + for _, test := range []struct { + name string + goarch string + target string + wantABI PlatformABI + wantErr string + }{ + {name: "empty", goarch: "amd64", wantErr: "empty target triple"}, + {name: "non-Windows", goarch: "amd64", target: "x86_64-unknown-linux-gnu", wantErr: "not an explicit Windows"}, + {name: "unsupported arch", goarch: "amd64", target: "mips64-pc-windows-msvc", wantErr: "unsupported Windows architecture"}, + {name: "i386 alias", goarch: "386", target: "i386-pc-windows-msvc", wantABI: PlatformABIMsvc}, + {name: "amd64 alias", goarch: "amd64", target: "amd64-pc-windows-msvc", wantABI: PlatformABIMsvc}, + {name: "arm64 alias", goarch: "arm64", target: "arm64-w64-windows-gnu", wantABI: PlatformABIGNU}, + } { + t.Run(test.name, func(t *testing.T) { + got, err := windowsToolchainForTriple(test.goarch, test.target) + if test.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), test.wantErr) { + t.Fatalf("error = %v, want substring %q", err, test.wantErr) + } + return + } + if err != nil { + t.Fatal(err) + } + if got.ABI != test.wantABI { + t.Fatalf("ABI = %q, want %q", got.ABI, test.wantABI) + } + }) + } +} + +func TestProbeNativeTool(t *testing.T) { + if os.Getenv("LLGO_NATIVE_TOOL_PROBE_HELPER") == "1" { + if got, want := canonicalTestPath(mustGetwd()), canonicalTestPath(os.Getenv("LLGO_NATIVE_TOOL_PROBE_DIR")); got != want { + os.Exit(2) + } + arg := os.Args[len(os.Args)-1] + switch arg { + case "-dumpmachine": + if slices.Contains(os.Args, "--fail-dumpmachine") || slices.Contains(os.Args, "--fail-all-targets") { + os.Exit(3) + } + _, _ = os.Stdout.WriteString("x86_64-pc-windows-msvc\r\n") + case "--print-target-triple": + if slices.Contains(os.Args, "--fail-all-targets") { + os.Exit(5) + } + _, _ = os.Stdout.WriteString("x86_64-pc-windows-msvc\n") + case "--version": + if slices.Contains(os.Args, "--fail-version") { + os.Exit(6) + } + _, _ = os.Stdout.WriteString("clang version 19.1.7\nsecond line\n") + default: + os.Exit(4) + } + os.Exit(0) + } + + dir := t.TempDir() + environ := append(slices.Clone(os.Environ()), + "LLGO_NATIVE_TOOL_PROBE_HELPER=1", + "LLGO_NATIVE_TOOL_PROBE_DIR="+dir, + ) + for _, command := range [][]string{ + {os.Args[0], "-test.run=^TestProbeNativeTool$", "--"}, + {os.Args[0], "-test.run=^TestProbeNativeTool$", "--", "--fail-dumpmachine"}, + } { + identity, err := probeNativeTool(command, NativeToolchainInput{Dir: dir, Environ: environ}) + if err != nil { + t.Fatal(err) + } + want := toolIdentity{target: "x86_64-pc-windows-msvc", version: "clang version 19.1.7"} + if identity != want { + t.Fatalf("identity = %+v, want %+v", identity, want) + } + } + for _, test := range []struct { + name string + marker string + wantErr string + }{ + {name: "target", marker: "--fail-all-targets", wantErr: "read target triple"}, + {name: "version", marker: "--fail-version", wantErr: "read compiler identity"}, + } { + t.Run(test.name+" error", func(t *testing.T) { + command := []string{os.Args[0], "-test.run=^TestProbeNativeTool$", "--", test.marker} + _, err := probeNativeTool(command, NativeToolchainInput{Dir: dir, Environ: environ}) + if err == nil || !strings.Contains(err.Error(), test.wantErr) { + t.Fatalf("error = %v, want substring %q", err, test.wantErr) + } + }) + } +} + +func fixedToolProbe(t *testing.T, identities map[string]toolIdentity) toolProbe { + t.Helper() + return func(command []string, _ NativeToolchainInput) (toolIdentity, error) { + identity, ok := identities[commandKey(command...)] + if !ok { + return toolIdentity{}, errors.New("unexpected command: " + strings.Join(command, " ")) + } + return identity, nil + } +} + +func commandKey(command ...string) string { + return strings.Join(command, "\x00") +} + +func mustGetwd() string { + dir, err := os.Getwd() + if err != nil { + panic(err) + } + return dir +} + +func canonicalTestPath(path string) string { + resolved, err := filepath.EvalSymlinks(path) + if err != nil { + panic(err) + } + return filepath.Clean(resolved) +} + +func TestCommandOrDefaultDoesNotAliasInput(t *testing.T) { + input := []string{"clang", "--target=x86_64-pc-windows-msvc"} + got := commandOrDefault(input, "unused") + got[0] = "changed" + if reflect.DeepEqual(got, input) || input[0] != "clang" { + t.Fatalf("commandOrDefault aliased input: got %q input %q", got, input) + } +} diff --git a/internal/debuginfo/builder.go b/internal/debuginfo/builder.go index 840c2ca8ae..353c8c0b8e 100644 --- a/internal/debuginfo/builder.go +++ b/internal/debuginfo/builder.go @@ -71,14 +71,20 @@ func (b *Builder) addDebuggerMarker() { marker.SetInitializer(llvm.ConstInt(i8, 1, false)) marker.SetGlobalConstant(true) marker.SetLinkage(llvm.LinkOnceODRLinkage) - marker.SetVisibility(llvm.HiddenVisibility) if strings.Contains(strings.ToLower(b.module.Target()), "windows") { // COFF does not turn linkonce_odr into a coalescible section on its // own. Every debug-enabled package emits this marker, so associate it - // with an any-selection COMDAT just like other ODR definitions. + // with an any-selection COMDAT just like other ODR definitions. Export + // the selected definition because llvm.used keeps the global during + // code generation but does not make its COFF section a linker GC root. + // The export directive both retains the marker and makes it discoverable + // through LLDB's PE symbol enumeration. comdat := b.module.Comdat(debuggerMarkerSymbol) comdat.SetSelectionKind(llvm.AnyComdatSelectionKind) marker.SetComdat(comdat) + marker.SetDLLStorageClass(llvm.DLLExportStorageClass) + } else { + marker.SetVisibility(llvm.HiddenVisibility) } ptr := llvm.PointerType(i8, 0) diff --git a/internal/debuginfo/builder_test.go b/internal/debuginfo/builder_test.go index f96acbb0ea..7485aaa4d1 100644 --- a/internal/debuginfo/builder_test.go +++ b/internal/debuginfo/builder_test.go @@ -81,9 +81,18 @@ func TestBuilderWindowsDebuggerMarkerUsesComdat(t *testing.T) { if got := marker.Comdat().SelectionKind(); got != llvm.AnyComdatSelectionKind { t.Fatalf("debugger marker COMDAT selection = %v, want any", got) } + if got := marker.DLLStorageClass(); got != llvm.DLLExportStorageClass { + t.Fatalf("debugger marker DLL storage class = %v, want export", got) + } + if got := marker.Visibility(); got != llvm.DefaultVisibility { + t.Fatalf("debugger marker visibility = %v, want default", got) + } if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { t.Fatalf("Windows debug module is invalid: %v\n%s", err, module.String()) } + if ir := module.String(); !strings.Contains(ir, `@__llgo_debugger_marker_v1 = linkonce_odr dllexport constant i8 1`) { + t.Fatalf("Windows debugger marker is not exported:\n%s", ir) + } } func TestBuilderMetadataOperations(t *testing.T) { diff --git a/internal/goflags/ldflags.go b/internal/goflags/ldflags.go index e4931228f7..96a27e4d42 100644 --- a/internal/goflags/ldflags.go +++ b/internal/goflags/ldflags.go @@ -25,6 +25,7 @@ import ( "strings" "github.com/xgo-dev/llgo/internal/build" + "github.com/xgo-dev/llgo/internal/quoted" ) // ParsedLinkFlags contains the Go linker options that LLGo currently handles. @@ -56,7 +57,8 @@ func ParseLinkFlags(buildFlags []string) (ParsedLinkFlags, error) { if err != nil { return ParsedLinkFlags{}, fmt.Errorf("invalid -ldflags value %q: %w", value, err) } - for _, linkFlag := range linkFlags { + for i := 0; i < len(linkFlags); i++ { + linkFlag := linkFlags[i] switch { case linkFlag == "-s" || linkFlag == "--s": opts.Options.OmitSymbolTable = true @@ -78,6 +80,22 @@ func ParseLinkFlags(buildFlags []string) (ParsedLinkFlags, error) { } else { opts.Options.DWARF = build.DWARFPreserve } + case linkFlag == "-extld": + if i+1 >= len(linkFlags) { + return ParsedLinkFlags{}, fmt.Errorf("-extld requires a value") + } + i++ + opts.Options.ExternalLinker = linkFlags[i] + case strings.HasPrefix(linkFlag, "-extld="): + opts.Options.ExternalLinker = strings.TrimPrefix(linkFlag, "-extld=") + case linkFlag == "-extldflags": + if i+1 >= len(linkFlags) { + return ParsedLinkFlags{}, fmt.Errorf("-extldflags requires a value") + } + i++ + opts.Options.ExternalLinkerFlags = linkFlags[i] + case strings.HasPrefix(linkFlag, "-extldflags="): + opts.Options.ExternalLinkerFlags = strings.TrimPrefix(linkFlag, "-extldflags=") default: opts.Ignored = append(opts.Ignored, linkFlag) } @@ -144,39 +162,6 @@ func splitPerPackageArgumentList(value string) (pattern string, flags []string, pattern = strings.TrimSpace(value[:i]) value = value[i+1:] } - flags, err = splitQuotedFields(value) + flags, err = quoted.Split(value) return pattern, flags, err } - -// splitQuotedFields follows the quoting rules used by the Go command for -// -ldflags: single or double quotes may surround one argument, with no escape -// processing inside the quoted text. -func splitQuotedFields(value string) ([]string, error) { - var fields []string - for len(value) > 0 { - value = strings.TrimLeft(value, " \t\n\r") - if value == "" { - break - } - if value[0] == '\'' || value[0] == '"' { - quote := value[0] - value = value[1:] - i := strings.IndexByte(value, quote) - if i < 0 { - return nil, fmt.Errorf("unterminated %c string", quote) - } - fields = append(fields, value[:i]) - value = value[i+1:] - continue - } - - i := strings.IndexAny(value, " \t\n\r") - if i < 0 { - fields = append(fields, value) - break - } - fields = append(fields, value[:i]) - value = value[i:] - } - return fields, nil -} diff --git a/internal/goflags/ldflags_test.go b/internal/goflags/ldflags_test.go index ad353e1246..182e550962 100644 --- a/internal/goflags/ldflags_test.go +++ b/internal/goflags/ldflags_test.go @@ -109,12 +109,40 @@ func TestParseLinkFlagsQuotesPatternAndIgnored(t *testing.T) { if !opts.Options.OmitSymbolTable || !opts.Options.EffectiveOmitDWARF() { t.Fatalf("options = %+v, want -s and effective -w", opts.Options) } - wantIgnored := []string{"-extldflags=-static -pthread", "-unknown"} + if opts.Options.ExternalLinkerFlags != "-static -pthread" { + t.Fatalf("external linker flags = %q", opts.Options.ExternalLinkerFlags) + } + wantIgnored := []string{"-unknown"} if !reflect.DeepEqual(opts.Ignored, wantIgnored) { t.Fatalf("ignored = %q, want %q", opts.Ignored, wantIgnored) } } +func TestParseLinkFlagsExternalLinker(t *testing.T) { + opts, err := ParseLinkFlags([]string{ + `-ldflags=-extld 'clang --target=x86_64-pc-windows-msvc' -extldflags '/debug:dwarf /incremental:no'`, + }) + if err != nil { + t.Fatal(err) + } + if got, want := opts.Options.ExternalLinker, "clang --target=x86_64-pc-windows-msvc"; got != want { + t.Fatalf("ExternalLinker = %q, want %q", got, want) + } + if got, want := opts.Options.ExternalLinkerFlags, "/debug:dwarf /incremental:no"; got != want { + t.Fatalf("ExternalLinkerFlags = %q, want %q", got, want) + } +} + +func TestParseLinkFlagsEmptyExternalLinkerValues(t *testing.T) { + opts, err := ParseLinkFlags([]string{`-ldflags=-extld= -extldflags=`}) + if err != nil { + t.Fatal(err) + } + if opts.Options.ExternalLinker != "" || opts.Options.ExternalLinkerFlags != "" { + t.Fatalf("options = %+v, want empty external linker values", opts.Options) + } +} + func TestParseLinkFlagsBoolSpellings(t *testing.T) { trueValues := []string{"1", "t", "T", "true", "TRUE", "True"} for _, value := range trueValues { @@ -157,6 +185,8 @@ func TestParseLinkFlagsErrors(t *testing.T) { {name: "missing w boolean", flags: []string{"-ldflags=-w="}}, {name: "invalid s boolean", flags: []string{"-ldflags=-s=maybe"}}, {name: "invalid w boolean", flags: []string{"-ldflags=-w=yes"}}, + {name: "missing extld value", flags: []string{"-ldflags=-extld"}}, + {name: "missing extldflags value", flags: []string{"-ldflags=-extldflags"}}, {name: "unterminated quote", flags: []string{`-ldflags=-s '-w=false`}}, } for _, tt := range tests { diff --git a/internal/quoted/quoted.go b/internal/quoted/quoted.go new file mode 100644 index 0000000000..ecd8012758 --- /dev/null +++ b/internal/quoted/quoted.go @@ -0,0 +1,48 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license. + +// Package quoted implements the argument-list quoting rules used by the Go +// command for compiler environment variables and linker flag values. +package quoted + +import "fmt" + +func isSpaceByte(c byte) bool { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' +} + +// Split splits s into fields, allowing single or double quotes around a +// complete field. As in cmd/internal/quoted.Split, quoted text is not +// unescaped and quotes appearing after the beginning of a field are literal. +func Split(s string) ([]string, error) { + var fields []string + for len(s) > 0 { + for len(s) > 0 && isSpaceByte(s[0]) { + s = s[1:] + } + if len(s) == 0 { + break + } + if s[0] == '\'' || s[0] == '"' { + quote := s[0] + s = s[1:] + i := 0 + for i < len(s) && s[i] != quote { + i++ + } + if i >= len(s) { + return nil, fmt.Errorf("unterminated %c string", quote) + } + fields = append(fields, s[:i]) + s = s[i+1:] + continue + } + i := 0 + for i < len(s) && !isSpaceByte(s[i]) { + i++ + } + fields = append(fields, s[:i]) + s = s[i:] + } + return fields, nil +} diff --git a/internal/quoted/quoted_test.go b/internal/quoted/quoted_test.go new file mode 100644 index 0000000000..59057e8624 --- /dev/null +++ b/internal/quoted/quoted_test.go @@ -0,0 +1,41 @@ +package quoted + +import ( + "reflect" + "strings" + "testing" +) + +func TestSplitMatchesGoCommandQuoting(t *testing.T) { + for _, test := range []struct { + name string + value string + want []string + wantErr string + }{ + {name: "empty", value: "", want: nil}, + {name: "space", value: " ", want: nil}, + {name: "two", value: "a b", want: []string{"a", "b"}}, + {name: "single quote", value: `'a b'`, want: []string{"a b"}}, + {name: "double quote", value: `"a b"`, want: []string{"a b"}}, + {name: "adjacent quotes", value: `'a '"b "`, want: []string{"a ", "b "}}, + {name: "no unescaping", value: `\'`, want: []string{`\'`}}, + {name: "unterminated", value: `'a`, wantErr: "unterminated ' string"}, + } { + t.Run(test.name, func(t *testing.T) { + got, err := Split(test.value) + if test.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), test.wantErr) { + t.Fatalf("Split(%q) error = %v, want %q", test.value, err, test.wantErr) + } + return + } + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, test.want) { + t.Fatalf("Split(%q) = %#v, want %#v", test.value, got, test.want) + } + }) + } +} diff --git a/runtime/internal/clite/bdwgc/config_default.go b/runtime/internal/clite/bdwgc/config_default.go index a98fcff81c..192ac110ed 100644 --- a/runtime/internal/clite/bdwgc/config_default.go +++ b/runtime/internal/clite/bdwgc/config_default.go @@ -1,5 +1,3 @@ -//go:build !windows - package bdwgc const LLGoPackage = "link: $(pkg-config --libs bdw-gc); -lgc" diff --git a/runtime/internal/clite/bdwgc/config_windows.go b/runtime/internal/clite/bdwgc/config_windows.go deleted file mode 100644 index 9012222176..0000000000 --- a/runtime/internal/clite/bdwgc/config_windows.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build windows - -package bdwgc - -// MSVC-targeted clang resolves -lgc as gc.lib, while MSYS2 packages the -// ABI-compatible COFF import archive under the GNU spelling. -const LLGoPackage = "link: -Wl,$(pkg-config --variable=libdir bdw-gc)/libgc.dll.a" diff --git a/runtime/internal/clite/ffi/_wrap/libffi.c b/runtime/internal/clite/ffi/_wrap/libffi.c index e5217657a7..627fc0c801 100644 --- a/runtime/internal/clite/ffi/_wrap/libffi.c +++ b/runtime/internal/clite/ffi/_wrap/libffi.c @@ -18,8 +18,17 @@ #endif #if defined(_WIN32) && defined(__x86_64__) +#if defined(__MINGW32__) +_Static_assert(FFI_GNUW64 == 2, + "LLGo's Windows MinGW x86-64 ABI constant is stale"); +_Static_assert(FFI_DEFAULT_ABI == FFI_GNUW64, + "Windows MinGW x86-64 requires libffi's GNUW64 ABI"); +#else +_Static_assert(FFI_WIN64 == 1, + "LLGo's Windows x86-64 ABI constant is stale"); _Static_assert(FFI_DEFAULT_ABI == FFI_WIN64, "Windows x86-64 requires libffi's MSVC ABI"); +#endif #elif defined(_WIN32) && defined(__i386__) _Static_assert(FFI_DEFAULT_ABI == FFI_MS_CDECL, "Windows x86 requires libffi's MSVC cdecl ABI"); diff --git a/runtime/internal/clite/ffi/abi_windows_amd64.go b/runtime/internal/clite/ffi/abi_windows_amd64.go index 5cd8470a7c..7dee06f0de 100644 --- a/runtime/internal/clite/ffi/abi_windows_amd64.go +++ b/runtime/internal/clite/ffi/abi_windows_amd64.go @@ -1,4 +1,4 @@ -//go:build windows && amd64 +//go:build windows && amd64 && !llgo_windows_gnu package ffi diff --git a/runtime/internal/clite/ffi/abi_windows_amd64_gnu.go b/runtime/internal/clite/ffi/abi_windows_amd64_gnu.go new file mode 100644 index 0000000000..cd8a1d98fd --- /dev/null +++ b/runtime/internal/clite/ffi/abi_windows_amd64_gnu.go @@ -0,0 +1,5 @@ +//go:build windows && amd64 && llgo_windows_gnu + +package ffi + +const DefaultAbi = 2 // FFI_GNUW64 diff --git a/runtime/internal/clite/libuv/_demo/async/async.go b/runtime/internal/clite/libuv/_demo/async/async.go deleted file mode 100644 index 978b4acb1c..0000000000 --- a/runtime/internal/clite/libuv/_demo/async/async.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "github.com/goplus/lib/c" - "github.com/goplus/lib/c/libuv" -) - -func ensure(b bool, msg string) { - if !b { - panic(msg) - } -} - -func main() { - loop := libuv.LoopNew() - defer loop.Close() - - a := &libuv.Async{} - r := loop.Async(a, func(a *libuv.Async) { - println("async callback") - a.Close(nil) // or loop.Stop() - }) - ensure(r == 0, "Async failed") - - go func() { - println("begin async task") - c.Usleep(100 * 1000) - println("send async event") - ensure(a.Send() == 0, "Send failed") - }() - - loop.Run(libuv.RUN_DEFAULT) - println("done") -} - -/*Expected Output: -begin async task -send async event -async callback -done -*/ diff --git a/runtime/internal/clite/libuv/_demo/async_fs/async_fs.go b/runtime/internal/clite/libuv/_demo/async_fs/async_fs.go deleted file mode 100644 index 5c41ee4d13..0000000000 --- a/runtime/internal/clite/libuv/_demo/async_fs/async_fs.go +++ /dev/null @@ -1,116 +0,0 @@ -package main - -import ( - "unsafe" - - "github.com/goplus/lib/c" - "github.com/goplus/lib/c/libuv" - "github.com/goplus/lib/c/os" -) - -const BUFFER_SIZE = 1024 - -var ( - loop *libuv.Loop - openReq libuv.Fs - closeReq libuv.Fs - - buffer [BUFFER_SIZE]c.Char - iov libuv.Buf - file libuv.File -) - -func main() { - // Print the libuv version - c.Printf(c.Str("libuv version: %d\n"), libuv.Version()) - - // Initialize the loop - loop = libuv.DefaultLoop() - - // Open the file - libuv.FsOpen(loop, &openReq, c.Str("example.txt"), os.O_RDONLY, 0, onOpen) - - // Run the loop - result := loop.Run(libuv.RUN_DEFAULT) - - if result != 0 { - c.Fprintf(c.Stderr, c.Str("Error in Run: %s\n"), libuv.Strerror(libuv.Errno(result))) - } - - // Cleanup - defer cleanup() -} - -func onOpen(req *libuv.Fs) { - // Check for errors - if req.GetResult() < 0 { - c.Fprintf(c.Stderr, c.Str("Error opening file: %s\n"), libuv.Strerror(libuv.Errno(req.GetResult()))) - loop.Close() - return - } - - // Store the file descriptor - file = libuv.File(req.GetResult()) - - // Init buffer - iov = libuv.InitBuf((*c.Char)(unsafe.Pointer(&buffer[0])), c.Uint(unsafe.Sizeof(buffer))) - - // Read the file - readFile() - -} - -func readFile() { - // Initialize the request every time - var readReq libuv.Fs - - // Read the file - readRes := libuv.FsRead(loop, &readReq, file, &iov, 1, -1, onRead) - if readRes != 0 { - c.Printf(c.Str("Error in FsRead: %s (code: %d)\n"), libuv.Strerror(libuv.Errno(readRes)), readRes) - readReq.ReqCleanup() - loop.Close() - } -} - -func onRead(req *libuv.Fs) { - // Cleanup the request - defer req.ReqCleanup() - // Check for errors - if req.GetResult() < 0 { - c.Fprintf(c.Stderr, c.Str("Read error: %s\n"), libuv.Strerror(libuv.Errno(req.GetResult()))) - } else if req.GetResult() == 0 { - // Close the file - closeRes := libuv.FsClose(loop, &closeReq, libuv.File(openReq.GetResult()), onClose) - if closeRes != 0 { - c.Printf(c.Str("Error in FsClose: %s (code: %d)\n"), libuv.Strerror(libuv.Errno(closeRes)), closeRes) - loop.Close() - return - } - } else { - c.Printf(c.Str("Read %d bytes\n"), req.GetResult()) - c.Printf(c.Str("Read content: %.*s\n"), req.GetResult(), (*c.Char)(unsafe.Pointer(&buffer[0]))) - // Read the file again - readFile() - } -} - -func onClose(req *libuv.Fs) { - // Check for errors - if req.GetResult() < 0 { - c.Fprintf(c.Stderr, c.Str("Error closing file: %s\n"), libuv.Strerror(libuv.Errno(req.GetResult()))) - } else { - c.Printf(c.Str("\nFile closed successfully.\n")) - } -} - -func cleanup() { - // Cleanup the requests - openReq.ReqCleanup() - closeReq.ReqCleanup() - // Close the loop - result := loop.Close() - if result != 0 { - c.Fprintf(c.Stderr, c.Str("Error in LoopClose: %s\n"), libuv.Strerror(libuv.Errno(result))) - } -} diff --git a/runtime/internal/clite/libuv/_demo/async_fs/example.txt b/runtime/internal/clite/libuv/_demo/async_fs/example.txt deleted file mode 100755 index 190a18037c..0000000000 --- a/runtime/internal/clite/libuv/_demo/async_fs/example.txt +++ /dev/null @@ -1 +0,0 @@ -123 diff --git a/runtime/internal/clite/libuv/_demo/echo_server/echo_server.go b/runtime/internal/clite/libuv/_demo/echo_server/echo_server.go deleted file mode 100644 index 1265ebf177..0000000000 --- a/runtime/internal/clite/libuv/_demo/echo_server/echo_server.go +++ /dev/null @@ -1,112 +0,0 @@ -package main - -import ( - "github.com/goplus/lib/c" - "github.com/goplus/lib/c/libuv" - "github.com/goplus/lib/c/net" -) - -var DEFAULT_PORT c.Int = 8080 -var DEFAULT_BACKLOG c.Int = 128 - -type WriteReq struct { - Req libuv.Write - Buf libuv.Buf -} - -func main() { - // Initialize the default event loop - var loop = libuv.DefaultLoop() - - // Initialize a TCP server - server := &libuv.Tcp{} - libuv.InitTcp(loop, server) - - // Set up the address to bind the server to - var addr net.SockaddrIn - libuv.Ip4Addr(c.Str("0.0.0.0"), DEFAULT_PORT, &addr) - c.Printf(c.Str("Listening on %s:%d\n"), c.Str("0.0.0.0"), DEFAULT_PORT) - - // Bind the server to the specified address and port - server.Bind((*net.SockAddr)(c.Pointer(&addr)), 0) - res := (*libuv.Stream)(server).Listen(DEFAULT_BACKLOG, OnNewConnection) - if res != 0 { - c.Fprintf(c.Stderr, c.Str("Listen error: %s\n"), libuv.Strerror(libuv.Errno(res))) - return - } - - // Start listening for incoming connections - loop.Run(libuv.RUN_DEFAULT) -} - -func FreeWriteReq(req *libuv.Write) { - wr := (*WriteReq)(c.Pointer(req)) - // Free the buffer base. - if wr.Buf.Base != nil { - c.Free(c.Pointer(wr.Buf.Base)) - wr.Buf.Base = nil - } -} - -func AllocBuffer(handle *libuv.Handle, suggestedSize uintptr, buf *libuv.Buf) { - // Allocate memory for the buffer based on the suggested size. - buf.Base = (*c.Char)(c.Malloc(suggestedSize)) - buf.Len = suggestedSize -} - -func EchoWrite(req *libuv.Write, status c.Int) { - if status != 0 { - c.Fprintf(c.Stderr, c.Str("Write error: %s\n"), libuv.Strerror(libuv.Errno(status))) - } - FreeWriteReq(req) -} - -func EchoRead(client *libuv.Stream, nread c.Long, buf *libuv.Buf) { - if nread > 0 { - req := new(WriteReq) - // Initialize the buffer with the data read. - req.Buf = libuv.InitBuf(buf.Base, c.Uint(nread)) - // Write the data back to the client. - req.Req.Write(client, &req.Buf, 1, EchoWrite) - return - } - if nread < 0 { - // Handle read errors and EOF. - if (libuv.Errno)(nread) != libuv.EOF { - c.Fprintf(c.Stderr, c.Str("Read error: %s\n"), libuv.Strerror(libuv.Errno(nread))) - } - (*libuv.Handle)(c.Pointer(client)).Close(nil) - } - // Free the buffer if it's no longer needed. - if buf.Base != nil { - c.Free(c.Pointer(buf.Base)) - } -} - -func OnNewConnection(server *libuv.Stream, status c.Int) { - if status < 0 { - c.Fprintf(c.Stderr, c.Str("New connection error: %s\n"), libuv.Strerror(libuv.Errno(status))) - return - } - - // Allocate memory for a new client. - client := &libuv.Tcp{} - - if client == nil { - c.Fprintf(c.Stderr, c.Str("Failed to allocate memory for client\n")) - return - } - - // Initialize the client TCP handle. - if libuv.InitTcp(libuv.DefaultLoop(), client) < 0 { - c.Fprintf(c.Stderr, c.Str("Failed to initialize client\n")) - return - } - - // Accept the new connection and start reading data. - if server.Accept((*libuv.Stream)(client)) == 0 { - (*libuv.Stream)(client).StartRead(AllocBuffer, EchoRead) - } else { - (*libuv.Handle)(c.Pointer(client)).Close(nil) - } -} diff --git a/runtime/internal/clite/libuv/_demo/go.mod b/runtime/internal/clite/libuv/_demo/go.mod deleted file mode 100644 index a9819efa57..0000000000 --- a/runtime/internal/clite/libuv/_demo/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/xgo-dev/llgo/runtime/internal/clite/libuv/_demo - -go 1.24.2 - -require github.com/goplus/lib v0.2.0 diff --git a/runtime/internal/clite/libuv/_demo/go.sum b/runtime/internal/clite/libuv/_demo/go.sum deleted file mode 100644 index 512980a575..0000000000 --- a/runtime/internal/clite/libuv/_demo/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/goplus/lib v0.2.0 h1:AjqkN1XK5H23wZMMlpaUYAMCDAdSBQ2NMFrLtSh7W4g= -github.com/goplus/lib v0.2.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/runtime/internal/clite/libuv/config_default.go b/runtime/internal/clite/libuv/config_default.go index 710cbd0630..0a4f5ce25a 100644 --- a/runtime/internal/clite/libuv/config_default.go +++ b/runtime/internal/clite/libuv/config_default.go @@ -1,5 +1,3 @@ -//go:build !windows - package libuv const ( diff --git a/runtime/internal/clite/libuv/config_windows.go b/runtime/internal/clite/libuv/config_windows.go deleted file mode 100644 index 44ddbd27a5..0000000000 --- a/runtime/internal/clite/libuv/config_windows.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build windows - -package libuv - -const ( - // An MSVC-targeted clang resolves -luv as uv.lib, while MSYS2 packages - // the ABI-compatible COFF import archive as libuv.dll.a. Name it - // explicitly; the DLL owns its MinGW implementation dependencies and - // exposes the same C ABI to MSVC callers. - LLGoPackage = "link: -Wl,$(pkg-config --variable=libdir libuv)/libuv.dll.a" - LLGoFiles = "_wrap/libuv.c" -) diff --git a/runtime/internal/runtime/_wrap/setjmp_windows_amd64.c b/runtime/internal/runtime/_wrap/setjmp_windows_amd64.c new file mode 100644 index 0000000000..3f3d17a007 --- /dev/null +++ b/runtime/internal/runtime/_wrap/setjmp_windows_amd64.c @@ -0,0 +1,60 @@ +/* + * 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. + */ + +#if defined(__x86_64__) || defined(_M_X64) + +/* + * _setjmpex stores the standard 256-byte Windows jump-buffer layout. CRT + * longjmp behavior varies by linkage and may invoke the Windows virtual + * unwinder, which cannot leave a vectored handler that interrupted generated + * Go code. LLGo owns Go defer/panic unwinding, so restore the non-volatile + * Win64 ABI state directly. + */ +__attribute__((naked, noinline, noreturn)) void llgo_longjmp(void *env, + int value) { + __asm__ volatile( + "movq %rcx, %r8\n\t" + "movq 0x50(%r8), %r9\n\t" + "movq 0x8(%r8), %rbx\n\t" + "movq 0x10(%r8), %rsp\n\t" + "movq 0x18(%r8), %rbp\n\t" + "movq 0x20(%r8), %rsi\n\t" + "movq 0x28(%r8), %rdi\n\t" + "movq 0x30(%r8), %r12\n\t" + "movq 0x38(%r8), %r13\n\t" + "movq 0x40(%r8), %r14\n\t" + "movq 0x48(%r8), %r15\n\t" + "ldmxcsr 0x58(%r8)\n\t" + "fldcw 0x5c(%r8)\n\t" + "movdqa 0x60(%r8), %xmm6\n\t" + "movdqa 0x70(%r8), %xmm7\n\t" + "movdqa 0x80(%r8), %xmm8\n\t" + "movdqa 0x90(%r8), %xmm9\n\t" + "movdqa 0xa0(%r8), %xmm10\n\t" + "movdqa 0xb0(%r8), %xmm11\n\t" + "movdqa 0xc0(%r8), %xmm12\n\t" + "movdqa 0xd0(%r8), %xmm13\n\t" + "movdqa 0xe0(%r8), %xmm14\n\t" + "movdqa 0xf0(%r8), %xmm15\n\t" + "movl %edx, %eax\n\t" + "testl %eax, %eax\n\t" + "jne 1f\n\t" + "incl %eax\n\t" + "1:\n\t" + "jmp *%r9"); +} + +#endif diff --git a/runtime/internal/runtime/setjmp_windows.go b/runtime/internal/runtime/setjmp_windows.go index 3dc4a4838e..44ffb2f1cf 100644 --- a/runtime/internal/runtime/setjmp_windows.go +++ b/runtime/internal/runtime/setjmp_windows.go @@ -11,9 +11,11 @@ import ( // Windows has no signal-mask variant of setjmp. These are logical runtime // declarations: the SSA builder lowers 386 to _setjmp3 with a zero unwind- // record count, amd64 to _setjmpex with a nil frame, and arm64 to LLGo's -// ABI-only context entry. Go defers are unwound by LLGo itself, so the Windows -// virtual unwinder must not run here. A Go or C wrapper would save the wrapper's -// frame, which no longer exists at longjmp. +// ABI-only context entry. amd64 also uses an ABI-only restore because CRT +// longjmp behavior varies by CRT linkage and may invoke the Windows virtual +// unwinder. Go defers are unwound by LLGo itself, so the platform unwinder +// must not run here. A Go or C setjmp wrapper would save the wrapper's frame, +// which no longer exists at longjmp. // //go:linkname Sigsetjmp C.setjmp func Sigsetjmp(env *SigjmpBuf, savemask c.Int) c.Int diff --git a/runtime/internal/runtime/z_setjmp_files_default.go b/runtime/internal/runtime/z_setjmp_files_default.go index c0c61daa6c..bb96a32014 100644 --- a/runtime/internal/runtime/z_setjmp_files_default.go +++ b/runtime/internal/runtime/z_setjmp_files_default.go @@ -1,4 +1,4 @@ -//go:build !windows || !arm64 +//go:build !windows || (!amd64 && !arm64) package runtime diff --git a/runtime/internal/runtime/z_setjmp_files_windows_amd64.go b/runtime/internal/runtime/z_setjmp_files_windows_amd64.go new file mode 100644 index 0000000000..1626c8abd2 --- /dev/null +++ b/runtime/internal/runtime/z_setjmp_files_windows_amd64.go @@ -0,0 +1,5 @@ +//go:build windows && amd64 + +package runtime + +const platformSetjmpLLGoFiles = "; _wrap/setjmp_windows_amd64.c" diff --git a/ssa/eh.go b/ssa/eh.go index c632836902..94730c33e4 100644 --- a/ssa/eh.go +++ b/ssa/eh.go @@ -185,7 +185,8 @@ func (b Builder) windowsSetjmp(jb Expr) Expr { } func (b Builder) windowsLongjmp(jb, retval Expr) { - if b.Prog.target.effectiveGOARCH() != "arm64" { + goarch := b.Prog.target.effectiveGOARCH() + if goarch != "amd64" && goarch != "arm64" { b.cLongjmp(jb, retval) return } diff --git a/ssa/eh_patch_test.go b/ssa/eh_patch_test.go index ab84fed62f..76e4707bcb 100644 --- a/ssa/eh_patch_test.go +++ b/ssa/eh_patch_test.go @@ -83,17 +83,20 @@ func TestWindowsSigjmpBufferAlignment(t *testing.T) { func TestWindowsSetjmpABI(t *testing.T) { tests := []struct { - arch string - setjmp string - longjmp string + name string + arch string + llvmTarget string + setjmp string + longjmp string }{ - {arch: "386", setjmp: "@_setjmp3", longjmp: "@longjmp"}, - {arch: "amd64", setjmp: "@_setjmpex", longjmp: "@longjmp"}, - {arch: "arm64", setjmp: "@llgo_setjmp", longjmp: "@llgo_longjmp"}, + {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: "arm64", arch: "arm64", setjmp: "@llgo_setjmp", longjmp: "@llgo_longjmp"}, } for _, test := range tests { - t.Run(test.arch, func(t *testing.T) { - prog := ssatest.NewProgram(t, &ssa.Target{GOOS: "windows", GOARCH: test.arch}) + t.Run(test.name, func(t *testing.T) { + prog := ssatest.NewProgram(t, &ssa.Target{GOOS: "windows", GOARCH: test.arch, LLVMTarget: test.llvmTarget}) pkg := prog.NewPackage("foo", "foo") fn := pkg.NewFunc("f", ssa.NoArgsNoRet, ssa.InGo) @@ -131,16 +134,19 @@ func TestWindowsSetjmpABI(t *testing.T) { func TestWindowsDirectSetjmpABI(t *testing.T) { for _, test := range []struct { - arch string - setjmp string - longjmp string + name string + arch string + llvmTarget string + setjmp string + longjmp string }{ - {arch: "386", setjmp: "@_setjmp3", longjmp: "@longjmp"}, - {arch: "amd64", setjmp: "@_setjmpex", longjmp: "@longjmp"}, - {arch: "arm64", setjmp: "@llgo_setjmp", longjmp: "@llgo_longjmp"}, + {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: "arm64", arch: "arm64", setjmp: "@llgo_setjmp", longjmp: "@llgo_longjmp"}, } { - t.Run(test.arch, func(t *testing.T) { - prog := ssatest.NewProgram(t, &ssa.Target{GOOS: "windows", GOARCH: test.arch}) + t.Run(test.name, func(t *testing.T) { + prog := ssatest.NewProgram(t, &ssa.Target{GOOS: "windows", GOARCH: test.arch, LLVMTarget: test.llvmTarget}) pkg := prog.NewPackage("foo", "foo") fn := pkg.NewFunc("f", ssa.NoArgsNoRet, ssa.InGo) diff --git a/ssa/target.go b/ssa/target.go index 94f67c2c4c..5230dcf931 100644 --- a/ssa/target.go +++ b/ssa/target.go @@ -146,6 +146,14 @@ func (p *Target) Spec() (spec TargetSpec) { goos := p.effectiveGOOS() goarm := p.goArchitectureSetting(p.GOARM) spec.Triple = intllvm.GetTargetTripleWithGOARM(goos, goarch, goarm) + // A native toolchain profile can select a physical ABI triple that differs + // from the GOOS/GOARCH default (for example Windows GNU instead of MSVC). + // Named embedded targets keep their existing backend triple here: their + // external compiler may support a target such as Xtensa that the host LLVM + // library used to build LLGo does not register. + if p.Target == "" && p.LLVMTarget != "" { + spec.Triple = p.LLVMTarget + } // Build validates these settings before constructing Target. Spec also // accepts hand-built Targets, so it intentionally uses each resolver's // documented Go-default fallback when its error cannot be returned here. diff --git a/ssa/target_features_test.go b/ssa/target_features_test.go index 60b3c279e0..66084605fe 100644 --- a/ssa/target_features_test.go +++ b/ssa/target_features_test.go @@ -57,6 +57,24 @@ func TestTargetArchitectureFeatures(t *testing.T) { } } +func TestTargetSpecUsesResolvedLLVMTarget(t *testing.T) { + target := &Target{ + GOOS: "windows", + GOARCH: "amd64", + LLVMTarget: "x86_64-w64-windows-gnu", + } + if got := target.Spec().Triple; got != target.LLVMTarget { + t.Fatalf("target triple = %q, want resolved LLVM target %q", got, target.LLVMTarget) + } + + prog := NewProgram(target) + defer prog.Dispose() + pkg := prog.NewPackage("p", "example.com/p") + if got := pkg.Module().Target(); got != target.LLVMTarget { + t.Fatalf("module target = %q, want %q", got, target.LLVMTarget) + } +} + func TestArchitectureFeatureTargetMachines(t *testing.T) { for _, target := range []*Target{ {GOOS: "windows", GOARCH: "386", GO386: "softfloat"}, diff --git a/test/go/recover_fault_test.go b/test/go/recover_fault_test.go index 25e38e65e7..da87de0524 100644 --- a/test/go/recover_fault_test.go +++ b/test/go/recover_fault_test.go @@ -23,6 +23,13 @@ func faultCopy(dst, src []byte) (n int, err error) { } func TestRecoverAfterFaultPreservesNamedResult(t *testing.T) { + // 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) + old := debug.SetPanicOnFault(true) defer debug.SetPanicOnFault(old) diff --git a/xtool/env/env.go b/xtool/env/env.go index 097f008ab8..5c155d26c1 100644 --- a/xtool/env/env.go +++ b/xtool/env/env.go @@ -20,11 +20,13 @@ import ( "fmt" "os" "os/exec" + "path" "path/filepath" "regexp" "runtime" "strings" + "github.com/xgo-dev/llgo/internal/quoted" "github.com/xgo-dev/llgo/xtool/safesplit" ) @@ -76,7 +78,9 @@ func expandEnvWithCmd(s, dir string, environ []string) (string, bool) { var out []byte var err error executable := cmd - if environ != nil { + if cmd == "pkg-config" { + executable = PkgConfigCommand(dir, environ) + } else if environ != nil { executable = lookPathInEnvironment(cmd, dir, environ) } command := exec.Command(executable, args[1:]...) @@ -91,7 +95,14 @@ func expandEnvWithCmd(s, dir string, environ []string) (string, bool) { return "" } - return strings.Replace(strings.TrimSpace(string(out)), "\n", " ", -1) + output := strings.Replace(strings.TrimSpace(string(out)), "\n", " ", -1) + if cmd == "llvm-config" && runtime.GOOS == "windows" { + output, err = normalizeWindowsLLVMConfigOutput(output) + if err != nil { + return "" + } + } + return output }) lookup := os.Getenv if environ != nil { @@ -108,6 +119,80 @@ func expandEnvWithCmd(s, dir string, environ []string) (string, bool) { return strings.TrimSpace(os.Expand(expanded, lookup)), config } +// normalizeWindowsLLVMConfigOutput translates llvm-config's MSVC-style +// library output into flags accepted by Clang's GNU-compatible driver. The +// upstream Windows archive prints absolute .lib paths and -LIBPATH:, whereas +// LLGo's external-link specification consumes conventional -L/-l flags. +func normalizeWindowsLLVMConfigOutput(output string) (string, error) { + fields, err := quoted.Split(strings.ReplaceAll(output, `\`, "/")) + if err != nil { + return "", fmt.Errorf("parse llvm-config output %q: %w", output, err) + } + flags := make([]string, 0, len(fields)) + libDirs := make(map[string]struct{}) + addLibDir := func(dir string) { + if dir == "" || dir == "." { + return + } + if _, ok := libDirs[dir]; ok { + return + } + libDirs[dir] = struct{}{} + flags = append(flags, "-L"+dir) + } + for _, field := range fields { + upper := strings.ToUpper(field) + if strings.HasPrefix(upper, "-LIBPATH:") { + addLibDir(field[len("-LIBPATH:"):]) + continue + } + if strings.HasSuffix(strings.ToLower(field), ".lib") { + dir, file := path.Split(field) + addLibDir(strings.TrimSuffix(dir, "/")) + name := strings.TrimSuffix(file, path.Ext(file)) + name = strings.TrimPrefix(name, "lib") + flags = append(flags, "-l"+name) + continue + } + flags = append(flags, field) + } + return strings.Join(flags, " "), nil +} + +// PkgConfigCommand returns the pkg-config executable selected by PKG_CONFIG. +// Like the Go command, it parses the setting with cmd/internal/quoted rules and +// uses its first field as the executable name. +func PkgConfigCommand(dir string, environ []string) string { + value := environmentValue("PKG_CONFIG", environ) + if value == "" { + value = "pkg-config" + } + args, err := quoted.Split(value) + if err != nil { + panic(fmt.Sprintf("could not parse environment variable PKG_CONFIG with value %q: %v", value, err)) + } + if len(args) == 0 { + return "pkg-config" + } + if environ != nil { + return lookPathInEnvironment(args[0], dir, environ) + } + return args[0] +} + +func environmentValue(name string, environ []string) string { + if environ == nil { + return os.Getenv(name) + } + for i := len(environ) - 1; i >= 0; i-- { + key, value, ok := strings.Cut(environ[i], "=") + if ok && (key == name || runtime.GOOS == "windows" && strings.EqualFold(key, name)) { + return value + } + } + return "" +} + func lookPathInEnvironment(name, dir string, environ []string) string { if strings.ContainsRune(name, filepath.Separator) { return name diff --git a/xtool/env/env_test.go b/xtool/env/env_test.go index 29f8ad83ae..9761c4924c 100644 --- a/xtool/env/env_test.go +++ b/xtool/env/env_test.go @@ -68,6 +68,99 @@ func TestExpandEnvToArgsWithConfiguresSubprocess(t *testing.T) { } } +func TestExpandEnvToArgsWithUsesPkgConfigSetting(t *testing.T) { + dir := filepath.Join(t.TempDir(), "pkg config") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + tool := filepath.Join(dir, "native-pkgconf") + if runtime.GOOS == "windows" { + tool += ".exe" + } + copyExecutable(t, tool) + got := ExpandEnvToArgsWith( + "$(pkg-config --libs fixture)", + dir, + []string{ + "PATH=" + t.TempDir(), + "PKG_CONFIG='" + tool + "' --ignored-like-go", + "LLGO_ENV_TEST=request", + helperEnvironment + "=1", + }, + ) + if len(got) != 2 || got[0] != "-Lrequest" || !strings.HasPrefix(got[1], "-I") { + t.Fatalf("ExpandEnvToArgsWith using PKG_CONFIG = %q, want -Lrequest and one include directory", got) + } +} + +func TestPkgConfigCommandMatchesGoSelection(t *testing.T) { + t.Setenv("PKG_CONFIG", `'configured pkg-config' --ignored-like-go`) + if got, want := PkgConfigCommand("", nil), "configured pkg-config"; got != want { + t.Fatalf("PkgConfigCommand using process environment = %q, want %q", got, want) + } + if got, want := PkgConfigCommand("", []string{"PKG_CONFIG= "}), "pkg-config"; got != want { + t.Fatalf("PkgConfigCommand using an empty field list = %q, want %q", got, want) + } + t.Run("invalid quoting", func(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("PkgConfigCommand accepted an unterminated quote") + } + }() + PkgConfigCommand("", []string{"PKG_CONFIG='unterminated"}) + }) +} + +func TestExpandEnvToArgsWithFindsLLVMConfigInExplicitPath(t *testing.T) { + dir := t.TempDir() + tool := filepath.Join(dir, "llvm-config") + if runtime.GOOS == "windows" { + tool += ".exe" + } + copyExecutable(t, tool) + got := ExpandEnvToArgsWith( + "$(llvm-config --libs)", + dir, + []string{"PATH=" + dir, "LLGO_ENV_TEST=request", helperEnvironment + "=1"}, + ) + if len(got) != 2 || got[0] != "-Lrequest" || !strings.HasPrefix(got[1], "-I") { + t.Fatalf("ExpandEnvToArgsWith using llvm-config = %q, want -Lrequest and one include directory", got) + } +} + +func TestNormalizeWindowsLLVMConfigOutput(t *testing.T) { + tests := []struct { + name string + output string + want string + }{ + { + name: "absolute libraries and duplicate search path", + output: `C:\LLVM\lib\LLVMCore.lib C:\LLVM\lib\libclang.lib -LIBPATH:C:\LLVM\lib`, + want: `-LC:/LLVM/lib -lLLVMCore -lclang`, + }, + { + name: "quoted search path", + output: `"-LIBPATH:C:\Program Files\LLVM\lib" LLVMFileCheck.lib /DEBUG`, + want: `-LC:/Program Files/LLVM/lib -lLLVMFileCheck /DEBUG`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := normalizeWindowsLLVMConfigOutput(test.output) + if err != nil { + t.Fatal(err) + } + if got != test.want { + t.Fatalf("normalizeWindowsLLVMConfigOutput() = %q, want %q", got, test.want) + } + }) + } + if _, err := normalizeWindowsLLVMConfigOutput(`"unterminated`); err == nil { + t.Fatal("normalizeWindowsLLVMConfigOutput accepted invalid quoting") + } +} + func TestLookPathInEnvironmentBoundaries(t *testing.T) { dir := t.TempDir() toolName := "fixture-tool" diff --git a/xtool/nm/nm.go b/xtool/nm/nm.go index e849ac81d6..f37d3592ce 100644 --- a/xtool/nm/nm.go +++ b/xtool/nm/nm.go @@ -135,6 +135,7 @@ func listOutput(data []byte) (items []*ObjectFile, err error) { item := &ObjectFile{} lines := bytes.Split(data, sep) for _, line := range lines { + line = bytes.TrimSuffix(line, []byte{'\r'}) if len(line) == 0 { if item.File == "" && len(item.Symbols) > 0 { items = append(items, item) diff --git a/xtool/nm/nm_test.go b/xtool/nm/nm_test.go new file mode 100644 index 0000000000..33eb04185c --- /dev/null +++ b/xtool/nm/nm_test.go @@ -0,0 +1,32 @@ +/* + * 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 nm + +import "testing" + +func TestListOutputCRLF(t *testing.T) { + items, err := listOutput([]byte("\r\nfoo.o:\r\n00000000 T Foo\r\n")) + if err != nil { + t.Fatal(err) + } + if len(items) != 1 || items[0].File != "foo.o" { + t.Fatalf("items = %#v, want one foo.o object", items) + } + if symbols := items[0].Symbols; len(symbols) != 1 || symbols[0].Name != "Foo" || symbols[0].Type != Text { + t.Fatalf("symbols = %#v, want text symbol Foo", symbols) + } +}