diff --git a/.github/actions/setup-demo-deps/action.yml b/.github/actions/setup-demo-deps/action.yml index 3985364f60..714e44ae0a 100644 --- a/.github/actions/setup-demo-deps/action.yml +++ b/.github/actions/setup-demo-deps/action.yml @@ -4,7 +4,14 @@ description: "Install dependencies required by the LLGo demo suite" runs: using: "composite" steps: - - name: Install cargs demo library + - name: Set up Python 3.12 on Windows + if: runner.os == 'Windows' + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install cargs demo library on Unix + if: runner.os != 'Windows' shell: bash run: | set -euo pipefail @@ -27,10 +34,6 @@ runs: ;; esac ;; - *) - echo "Unsupported runner OS: ${RUNNER_OS}" >&2 - exit 1 - ;; esac libs_dir="${GITHUB_WORKSPACE}/_demo/c/cargs/libs" @@ -45,7 +48,67 @@ runs: echo "PKG_CONFIG_PATH=${libs_dir}/lib/pkgconfig:${PKG_CONFIG_PATH:-}" >> "${GITHUB_ENV}" - - name: Install Python demo dependencies + - name: Build cargs demo library on Windows + if: runner.os == 'Windows' + shell: pwsh + env: + # Pin the target of the upstream v1.2.0 annotated tag so source + # downloads cannot change if that tag is moved. + CARGS_COMMIT: 0fbac1a0c6ebb7ecd72f0d7ae89c2b79eb3a12eb + 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" } + } + + $libsDir = Join-Path $env:GITHUB_WORKSPACE "_demo\c\cargs\libs" + $includeDir = Join-Path $libsDir "include" + $libDir = Join-Path $libsDir "lib" + $pcDir = Join-Path $env:RUNNER_TEMP "llgo-pkgconfig" + New-Item -ItemType Directory -Force $includeDir, $libDir, $pcDir | Out-Null + + $source = Join-Path $env:RUNNER_TEMP "cargs.c" + $baseUrl = "https://raw.githubusercontent.com/likle/cargs/$env:CARGS_COMMIT" + Invoke-WebRequest "$baseUrl/include/cargs.h" -OutFile (Join-Path $includeDir "cargs.h") + Invoke-WebRequest "$baseUrl/src/cargs.c" -OutFile $source + + $object = Join-Path $env:RUNNER_TEMP "cargs.obj" + & $env:CC "--target=$targetTriple" -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 ($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. + # 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. + $pcIncludeDir = $includeDir.Replace('\', '/') + $pcLibDir = $libDir.Replace('\', '/') + @" + libdir=$pcLibDir + includedir=$pcIncludeDir + + Name: cargs + Description: A simple argument parser library + Version: 1.2.0 + Libs: -L"`${libdir}" -lcargs + 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 run: | set -euo pipefail @@ -59,3 +122,38 @@ runs: echo "PKG_CONFIG_PATH=${pcdir}:${PKG_CONFIG_PATH:-}" >> "${GITHUB_ENV}" echo "LLGO_FULL_RPATH=true" >> "${GITHUB_ENV}" + + - name: Install Python demo dependencies on Windows + if: runner.os == 'Windows' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + + 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() + $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" + } + + $pcDir = Join-Path $env:RUNNER_TEMP "llgo-pkgconfig" + New-Item -ItemType Directory -Force $pcDir | Out-Null + $pcIncludeDir = $includeDir.Replace('\', '/') + $pcLibDir = $libDir.Replace('\', '/') + @" + libdir=$pcLibDir + includedir=$pcIncludeDir + + Name: Python + Description: Python library for LLGo embedding demos + Version: 3.12 + Libs: -L"`${libdir}" -lpython$version + Cflags: -I"`${includedir}" + "@ | Set-Content -Encoding ascii (Join-Path $pcDir "python3-embed.pc") diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml index f6c791d623..379d5c6939 100644 --- a/.github/actions/setup-deps/action.yml +++ b/.github/actions/setup-deps/action.yml @@ -81,6 +81,7 @@ runs: "$prefix-libuv" \ "$prefix-gc" \ "$prefix-libatomic_ops" \ + "$prefix-cjson" \ make for package in clang clang-libs compiler-rt llvm llvm-libs lld libc++ libunwind; do @@ -90,13 +91,12 @@ runs: exit 1 fi done - installed_libxml2="$(pacman -Q "$prefix-libxml2" | awk '{print $2}')" if [[ "$installed_libxml2" != "2.12.9-2" ]]; then echo "expected libxml2 2.12.9-2, got $installed_libxml2" >&2 exit 1 fi - for package in libuv gc libatomic_ops; do + for package in libuv gc libatomic_ops cjson; do pacman -Q "$prefix-$package" done @@ -188,11 +188,18 @@ runs: Add-Content -Encoding utf8 $env:GITHUB_ENV "$name=$value" } } - @("cl.exe", "lib.exe", "link.exe", "rc.exe", "mt.exe") | + # 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. diff --git a/.github/actions/setup-embed-deps/action.yml b/.github/actions/setup-embed-deps/action.yml index 86a3de225c..b88e5995ca 100644 --- a/.github/actions/setup-embed-deps/action.yml +++ b/.github/actions/setup-embed-deps/action.yml @@ -26,12 +26,29 @@ runs: chmod +x .github/workflows/install-esp-qemu.sh QEMU_DIR=".cache/qemu" .github/workflows/install-esp-qemu.sh "$QEMU_DIR" - echo "${PWD}/${QEMU_DIR}/bin" >> $GITHUB_PATH + qemu_bin="${PWD}/${QEMU_DIR}/bin" + if [[ "$RUNNER_OS" == Windows ]]; then + # Git Bash understands /d/... paths, but LLGo launches QEMU through + # Windows CreateProcess and therefore needs a native path. + qemu_bin="$(cygpath -w "$qemu_bin")" + fi + echo "$qemu_bin" >> "$GITHUB_PATH" - - name: Verify ESP QEMU installation + - name: Verify ESP QEMU installation on Unix + if: runner.os != 'Windows' shell: bash run: | - which qemu-system-riscv32 - which qemu-system-xtensa + command -v qemu-system-riscv32 + command -v qemu-system-xtensa + qemu-system-riscv32 --version + qemu-system-xtensa --version + + - name: Verify ESP QEMU installation on Windows + if: runner.os == 'Windows' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + Get-Command qemu-system-riscv32 | Select-Object -ExpandProperty Source + Get-Command qemu-system-xtensa | Select-Object -ExpandProperty Source qemu-system-riscv32 --version qemu-system-xtensa --version diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml index 1ce97f4bd5..0eef2d0e42 100644 --- a/.github/actions/setup-go/action.yml +++ b/.github/actions/setup-go/action.yml @@ -6,7 +6,7 @@ inputs: required: false # Keep this pin synchronized with the primary CI and release smoke-test # matrices. Upgrade it in a dedicated toolchain-update PR. - default: "1.26.5" + default: "1.26.7" runs: using: "composite" steps: diff --git a/.github/actions/test-helloworld/action.yml b/.github/actions/test-helloworld/action.yml index 9020e7887b..b2597261e7 100644 --- a/.github/actions/test-helloworld/action.yml +++ b/.github/actions/test-helloworld/action.yml @@ -38,7 +38,14 @@ runs: Hello, LLGo! Hello, LLGo! Hello LLGo by cpp/std.Str" - OUTPUT=$(llgo run . 2>&1 | tee /dev/stderr) + if [[ "$RUNNER_OS" == "Windows" ]]; then + # ExitProcess does not flush UCRT streams, so keep C output + # observable without leaking this setting into the ESP build. + OUTPUT=$(LLGO_STDIO_NOBUF=1 llgo run . 2>&1) + else + OUTPUT=$(llgo run . 2>&1) + fi + echo "$OUTPUT" if echo "$OUTPUT" | grep -qF "$EXPECTED"; then echo "Basic test passed" else diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 21b8397594..2f7910226b 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -21,11 +21,17 @@ jobs: - os: ubuntu-24.04 id: linux display: Linux + timeout: 20 - os: macos-latest id: macos display: macOS + timeout: 20 + - os: windows-2022 + id: windows + display: Windows + timeout: 30 runs-on: ${{ matrix.os }} - timeout-minutes: 20 + timeout-minutes: ${{ matrix.timeout }} env: GOMAXPROCS: "2" LLGO_ROOT: ${{ github.workspace }} @@ -37,6 +43,7 @@ jobs: - name: Determine pull request merge-base if: github.event_name == 'pull_request' id: merge-base + shell: bash run: | git fetch https://github.com/${{ github.event.pull_request.base.repo.full_name }}.git ${{ github.event.pull_request.base.ref }} base_sha=$(git merge-base FETCH_HEAD ${{ github.event.pull_request.head.sha }}) @@ -62,11 +69,27 @@ jobs: - name: Measure pull request base if: github.event_name == 'pull_request' + id: measure-base + # 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' }} + shell: bash run: | benchmark/baseline/run.sh \ - "$GITHUB_WORKSPACE/.benchmark/source" \ - "$GITHUB_WORKSPACE/.benchmark/base-llgo" \ - "$GITHUB_WORKSPACE/.benchmark/base-results" + .benchmark/source \ + .benchmark/base-llgo \ + .benchmark/base-results + + - name: Note unavailable Windows baseline + if: >- + github.event_name == 'pull_request' && + matrix.id == 'windows' && + steps.measure-base.outcome == 'failure' + shell: bash + run: | + echo '### Windows benchmark baseline' >> "$GITHUB_STEP_SUMMARY" + echo 'The pull request base does not yet build on Windows. The current revision is measured as new; same-runner comparisons begin when the base supports Windows.' >> "$GITHUB_STEP_SUMMARY" - name: Check out pull request head benchmark source if: github.event_name == 'pull_request' @@ -78,15 +101,16 @@ jobs: persist-credentials: false - name: Measure current revision + shell: bash run: | - source_root="$GITHUB_WORKSPACE" + source_root=. if [[ "$GITHUB_EVENT_NAME" == pull_request ]]; then - source_root="$GITHUB_WORKSPACE/.benchmark/source" + source_root=.benchmark/source fi benchmark/baseline/run.sh \ "$source_root" \ - "$GITHUB_WORKSPACE/.benchmark/llgo" \ - "$GITHUB_WORKSPACE/.benchmark/results" + .benchmark/llgo \ + .benchmark/results - name: Record benchmark result uses: xgo-dev/setup-benchmark-go-action@v1.0.6 @@ -95,6 +119,7 @@ jobs: benchmark-file: .benchmark/results/benchmark.txt baseline-benchmark-file: >- ${{ github.event_name == 'pull_request' && + steps.measure-base.outcome == 'success' && '.benchmark/base-results/benchmark.txt' || '' }} platform-id: ${{ matrix.id }} platform-label: ${{ matrix.display }} diff --git a/.github/workflows/build-cache.yml b/.github/workflows/build-cache.yml index 62429be59d..3850b47063 100644 --- a/.github/workflows/build-cache.yml +++ b/.github/workflows/build-cache.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 30 strategy: matrix: - os: [macos-latest, ubuntu-latest] + os: [macos-latest, ubuntu-latest, windows-2022] llvm: [19] runs-on: ${{matrix.os}} steps: @@ -34,26 +34,11 @@ jobs: uses: ./.github/actions/setup-go - name: Install wamr (for wasm tests) - if: startsWith(matrix.os, 'macos') - 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/darwin/build - cd wasm-micro-runtime/product-mini/platforms/darwin/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 (for wasm tests on Linux) - if: startsWith(matrix.os, 'ubuntu') - 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 + shell: bash + run: bash dev/build_iwasm.sh - name: Install llgo (dev mode) + shell: bash run: | go install -tags=dev ./cmd/llgo echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV @@ -63,4 +48,5 @@ jobs: run: pip3 install --break-system-packages esptool==5.1.0 - name: Run build cache tests + shell: bash run: bash test/buildcache/test.sh diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 6021b8234c..2d491f0931 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -53,6 +53,7 @@ jobs: os: - macos-latest - ubuntu-latest + - windows-2022 runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -74,10 +75,28 @@ jobs: set -x source doc/_readme/scripts/install_ubuntu.sh + - name: Set up Python 3.12 on Windows + if: startsWith(matrix.os, 'windows') + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install dependencies on Windows + if: startsWith(matrix.os, 'windows') + uses: ./.github/actions/setup-deps + - name: Install llgo with tools + shell: bash run: | set -e set -x + if [[ "$RUNNER_OS" == "Windows" ]]; then + python_lib=$(python -c 'import os, sys; print(os.path.join(sys.base_prefix, "libs", f"python{sys.version_info.major}{sys.version_info.minor}"))') + export LLGO_LIB_PYTHON="$python_lib" + export LLGO_STDIO_NOBUF=1 + echo "LLGO_LIB_PYTHON=$LLGO_LIB_PYTHON" >> "$GITHUB_ENV" + echo "LLGO_STDIO_NOBUF=$LLGO_STDIO_NOBUF" >> "$GITHUB_ENV" + fi git() { if [ "$1" = "clone" ]; then # do nothing because we already have the branch @@ -90,6 +109,7 @@ jobs: echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV - name: Test doc code blocks + shell: bash run: | set -e set -x @@ -102,6 +122,7 @@ jobs: os: - macos-latest - ubuntu-latest + - windows-2022 runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -124,10 +145,28 @@ jobs: source doc/_readme/scripts/install_ubuntu.sh echo "PATH=/usr/lib/llvm-19/bin:$PATH" >> $GITHUB_ENV + - name: Set up Python 3.12 on Windows + if: startsWith(matrix.os, 'windows') + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install dependencies on Windows + if: startsWith(matrix.os, 'windows') + uses: ./.github/actions/setup-deps + - name: Install llgo with tools + shell: bash run: | set -e set -x + if [[ "$RUNNER_OS" == "Windows" ]]; then + python_lib=$(python -c 'import os, sys; print(os.path.join(sys.base_prefix, "libs", f"python{sys.version_info.major}{sys.version_info.minor}"))') + export LLGO_LIB_PYTHON="$python_lib" + export LLGO_STDIO_NOBUF=1 + echo "LLGO_LIB_PYTHON=$LLGO_LIB_PYTHON" >> "$GITHUB_ENV" + echo "LLGO_STDIO_NOBUF=$LLGO_STDIO_NOBUF" >> "$GITHUB_ENV" + fi git() { if [ "$1" = "clone" ]; then # do nothing because we already have the branch @@ -140,6 +179,7 @@ jobs: echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV - name: Test doc code blocks + shell: bash run: | set -e set -x diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c2bc55dec2..21dfa96684 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,13 +16,23 @@ concurrency: jobs: test: - timeout-minutes: 60 + # 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. + timeout-minutes: ${{ startsWith(matrix.os, 'windows') && 90 || 60 }} strategy: + fail-fast: false matrix: - os: - - macos-latest - - ubuntu-latest - llvm: [19] + include: + - os: macos-latest + llvm: 19 + go: "1.26.7" + - os: ubuntu-latest + llvm: 19 + go: "1.26.7" + - os: windows-2022 + llvm: 19 + go: "1.26.7" runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -35,6 +45,7 @@ jobs: uses: ./.github/actions/setup-embed-deps - name: Clang information + shell: bash run: | echo $PATH which clang @@ -42,45 +53,79 @@ jobs: - name: Set up Go uses: ./.github/actions/setup-go + with: + go-version: ${{ matrix.go }} - name: Install further optional dependencies for demos - run: | - py_deps=( - numpy # for github.com/goplus/lib/py/numpy - torch # for github.com/goplus/lib/py/torch - ) - pip3.12 install --break-system-packages "${py_deps[@]}" - # Align python3-embed with python-3.12-embed to avoid ABI mismatches. - pcdir=$HOME/pc - mkdir -p "$pcdir" - libdir=$(pkg-config --variable=libdir python-3.12-embed) - ln -s "$libdir/pkgconfig/python-3.12-embed.pc" "$pcdir/python3-embed.pc" - echo "PKG_CONFIG_PATH=$pcdir:${PKG_CONFIG_PATH}" >> $GITHUB_ENV - echo "LLGO_FULL_RPATH=true" >> $GITHUB_ENV + uses: ./.github/actions/setup-demo-deps - name: Set LLGO_ROOT + shell: bash run: echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV - name: Build - run: go build -v ./... + shell: bash + run: | + set -euo pipefail + go build -v ./... - # Both platforms upload coverage: OS-specific paths (ELF vs Mach-O - # emission, per-OS runtime shims) are otherwise invisible to - # codecov/patch and fail it on lines only the other OS executes. + - name: Validate Windows host-only integration + if: runner.os == 'Windows' + shell: bash + run: | + set -euo pipefail + + # Keep the concurrency stress from the former dedicated workflow; + # one ordinary package run does not prove mutual exclusion. + go test -count=20 -timeout=10m -run 'Lock' ./internal/crosscompile + + # The runtime is a nested module and is therefore outside ./.... + # These host-side FFI tests still need native Windows coverage. + ( + cd runtime + go test -count=1 -timeout=10m \ + ./internal/ffi ./internal/clite/ffi ./internal/lib/reflect + for arch in 386 arm64; do + CGO_ENABLED=0 GOOS=windows GOARCH="$arch" go test -c \ + -o "$RUNNER_TEMP/llgo-ffi-$arch.test.exe" ./internal/ffi + done + ) + + ffi_include="$(pkg-config --variable=includedir libffi)" + clang -target x86_64-pc-windows-msvc -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" + + # Every host uploads coverage: OS-specific ELF, Mach-O, and COFF paths + # plus per-OS runtime shims are otherwise invisible to codecov/patch. - name: Test with coverage # 45m: the caller-info acceptance suite (test/go) legitimately grew # the covered run past the old 30m budget on macOS runners. + shell: bash run: | set -euo pipefail + go_test=(go test) + if [[ "$RUNNER_OS" == Windows ]]; then + # Go's internal linker cannot consume every GNU COFF import + # archive used by the race runtime and LLVM. Use the configured + # 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 + export LLGO_STDIO_NOBUF=1 + fi + # test/go intentionally contains compiler edge cases that make the - # Go 1.26.5 printf analyzer panic. Keep the normal go test vet gate + # Go 1.26 printf analyzer panic. Keep the normal go test vet gate # for every other package, and disable vet only for that package. go list ./... \ | grep -v '^github.com/xgo-dev/llgo/test/go$' \ - | xargs go test -timeout 45m -coverprofile="coverage-main.txt" -covermode=atomic \ + | xargs "${go_test[@]}" -timeout 45m -coverprofile="coverage-main.txt" -covermode=atomic \ -bench '^BenchmarkGo126' -benchtime=1x - go test -timeout 45m -vet=off -coverprofile="coverage-test-go.txt" -covermode=atomic ./test/go + "${go_test[@]}" -timeout 45m -vet=off \ + -coverprofile="coverage-test-go.txt" -covermode=atomic ./test/go head -n 1 coverage-main.txt > coverage.txt tail -n +2 coverage-main.txt >> coverage.txt @@ -92,6 +137,7 @@ jobs: run: go test -v -timeout 60m ./cl -run '^TestRunEmbedEmulator$' - name: Check std symbol coverage + shell: bash run: bash doc/_readme/scripts/check_std_cover.sh - name: Upload coverage reports to Codecov diff --git a/.github/workflows/goroot.yml b/.github/workflows/goroot.yml index ec9af260da..71056c5250 100644 --- a/.github/workflows/goroot.yml +++ b/.github/workflows/goroot.yml @@ -24,18 +24,15 @@ jobs: strategy: fail-fast: false matrix: - # These are reproducibility pins, not floating series selectors. - # Upgrade them together in a dedicated toolchain-update PR. - os: [macos-latest, ubuntu-latest] - go-version: ["1.25.0", "1.26.5"] + # GOROOT compatibility follows the project's current Go 1.26 release + # line. Old xfail/not-applicable classifications are retired with the + # corpus. + os: [macos-latest, ubuntu-latest, windows-2022] + go-version: ["1.26.7"] shard-index: ["0", "1", "2", "3"] include: - - go-version: "1.25.0" - lane: compatibility - - go-version: "1.26.5" + - go-version: "1.26.7" lane: primary - # Keep both supported runtime generations on Linux and macOS. Go 1.25 - # is intentionally omitted because it is not a compatibility target. runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v7 @@ -61,6 +58,7 @@ jobs: run: go mod download - name: Run GOROOT runner + shell: bash run: | min_swap_free_mib=512 if [[ "$RUNNER_OS" == "macOS" ]]; then @@ -68,6 +66,11 @@ jobs: # does not mean the runner cannot make forward progress. min_swap_free_mib=0 fi + if [[ "$RUNNER_OS" == "Windows" ]]; then + # Generated programs write into pipes and terminate through + # ExitProcess, which does not run CRT stream teardown. + export LLGO_STDIO_NOBUF=1 + fi set +e bash dev/test_goroot.sh -- \ -directive-mode ci \ @@ -81,6 +84,7 @@ jobs: - name: Summarize GOROOT shard if: always() + shell: bash env: MATRIX_OS: ${{ matrix.os }} GO_VERSION: ${{ matrix.go-version }} @@ -94,6 +98,8 @@ jobs: platform=linux/amd64 if [[ "$MATRIX_OS" == "macos-latest" ]]; then platform=darwin/arm64 + elif [[ "$MATRIX_OS" == "windows-2022" ]]; then + platform=windows/amd64 fi selected=$(sed -nE 's/.* shard=[^ ]+ cases=([0-9]+) directive_mode=.*/\1/p' "$log" | tail -1) @@ -200,16 +206,15 @@ jobs: { echo '## GOROOT run summary' echo - echo "Received $report_count/16 shard reports." + echo "Received $report_count/12 shard reports." echo echo '| Platform / toolchain | Shards | Selected | Observed | Passed | Failed | Skipped |' echo '|---|---:|---:|---:|---:|---:|---:|' - write_row 'Darwin · Go 1.25.0' darwin/arm64 1.25.0 4 - write_row 'Darwin · Go 1.26.5' darwin/arm64 1.26.5 4 - write_row '**Darwin total**' darwin/arm64 '' 8 - write_row 'Linux · Go 1.25.0' linux/amd64 1.25.0 4 - write_row 'Linux · Go 1.26.5' linux/amd64 1.26.5 4 - write_row '**Linux total**' linux/amd64 '' 8 + write_row 'Darwin · Go 1.26.7' darwin/arm64 1.26.7 4 + 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 echo echo '_Passed means the runner classification succeeded; expected xfail/not-applicable failures and classified flakes are counted as Passed._' echo @@ -224,8 +229,8 @@ jobs: fi } >>"$GITHUB_STEP_SUMMARY" - if [[ "$report_count" -ne 16 ]]; then - echo "error: expected 16 shard reports, got $report_count" >&2 + if [[ "$report_count" -ne 12 ]]; then + echo "error: expected 12 shard reports, got $report_count" >&2 exit 1 fi if [[ "$GOROOT_RESULT" != success ]]; then diff --git a/.github/workflows/install-esp-qemu.sh b/.github/workflows/install-esp-qemu.sh index 9ab7a4379e..13f2b59061 100755 --- a/.github/workflows/install-esp-qemu.sh +++ b/.github/workflows/install-esp-qemu.sh @@ -7,6 +7,7 @@ INSTALL_DIR="${1:-.cache/qemu}" # Detect platform OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) +EXE_SUFFIX="" # Map architecture names case "$ARCH" in @@ -30,6 +31,12 @@ case "$OS" in linux) PLATFORM="${ARCH}-linux-gnu" ;; + mingw*|msys*|cygwin*) + # Espressif publishes the Windows emulator as an x86-64 MinGW host + # binary. This is independent of the ESP firmware target architecture. + PLATFORM="x86_64-w64-mingw32" + EXE_SUFFIX=".exe" + ;; *) echo "Unsupported OS: $OS" exit 1 @@ -58,7 +65,7 @@ done # Verify installation for exe in qemu-system-riscv32 qemu-system-xtensa; do - if [ ! -x "${INSTALL_DIR}/bin/${exe}" ]; then + if [ ! -x "${INSTALL_DIR}/bin/${exe}${EXE_SUFFIX}" ]; then echo "Error: ${exe} not found after extraction" exit 1 fi diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index d37781368e..d6af0d1ce7 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -33,7 +33,7 @@ jobs: lane: compatibility - os: ubuntu-latest llvm: 19 - go: "1.26.5" + go: "1.26.7" lane: primary - os: macos-latest llvm: 19 @@ -41,7 +41,11 @@ jobs: lane: compatibility - os: macos-latest llvm: 19 - go: "1.26.5" + go: "1.26.7" + lane: primary + - os: windows-2022 + llvm: 19 + go: "1.26.7" lane: primary runs-on: ${{matrix.os}} steps: @@ -59,6 +63,7 @@ jobs: uses: ./.github/actions/setup-go - name: Install + shell: bash run: | go install ./... echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV @@ -69,6 +74,7 @@ jobs: go-version: ${{matrix.go}} - name: Test demo without RPATH (expect failure) + if: runner.os != 'Windows' run: | echo "Testing demo without RPATH (should fail)..." export LLGO_FULL_RPATH=false @@ -80,6 +86,15 @@ jobs: echo "✓ cargs demo correctly failed without RPATH" fi + - name: Test static cargs without a runtime search path + if: runner.os == 'Windows' + shell: bash + run: | + set -euo pipefail + export LLGO_FULL_RPATH=false + pkg-config --libs cargs + (cd ./_demo/c/cargs && llgo run .) + - name: Test demos run: bash .github/workflows/test_demo.sh @@ -87,6 +102,7 @@ jobs: run: bash .github/workflows/test_demo.sh --embedded - name: Test C header generation + shell: bash run: | echo "Testing C header generation in different build modes..." cd _demo/go/export @@ -94,6 +110,7 @@ jobs: ./test.sh - name: Test export with different symbol names on embedded targets + shell: bash run: | echo "Testing //export with different symbol names on embedded targets..." cd _demo/embed/export @@ -101,6 +118,7 @@ jobs: ./verify_export.sh - name: Test ESP serial smoke (build + emulator) + shell: bash run: | echo "Testing ESP32/ESP32-C3 build + emulator smoke..." cd _demo/embed @@ -108,9 +126,10 @@ jobs: ./test-esp-serial-startup.sh - name: Test ESP32-C3 startup regression + shell: bash run: | echo "Testing ESP32-C3 startup regressions..." - pip3 install --break-system-packages esptool==5.1.0 + python -m pip install esptool==5.1.0 cd _demo/embed chmod +x test_esp32c3_startup.sh ./test_esp32c3_startup.sh @@ -127,6 +146,28 @@ 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' }} + shell: msys2 {0} + env: + MSYS2_LLDB_PACKAGE_VERSION: "19.1.7-1" + MSYS2_PYTHON_PACKAGE_VERSION: "3.12.8-2" + run: | + set -euo pipefail + repo=https://repo.msys2.org/mingw/clang64 + prefix=mingw-w64-clang-x86_64 + pacman_args=(--noconfirm -U) + for dependency in cc-libs expat bzip2 mpdecimal ncurses openssl sqlite3 tcl tk tzdata; do + pacman_args+=(--assume-installed "$prefix-$dependency") + done + pacman "${pacman_args[@]}" \ + "$repo/$prefix-python-$MSYS2_PYTHON_PACKAGE_VERSION-any.pkg.tar.zst" \ + "$repo/$prefix-lldb-$MSYS2_LLDB_PACKAGE_VERSION-any.pkg.tar.zst" + + lldb_path=/clang64/bin/lldb.exe + "$lldb_path" --version + echo "LLGO_LLDB=$(cygpath -m "$lldb_path")" >> "$GITHUB_ENV" + - name: LLDB integration tests if: ${{ matrix.lane == 'primary' }} run: | @@ -140,22 +181,24 @@ jobs: test: name: test (${{ matrix.lane }}, ${{ matrix.os }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}, shard ${{ matrix.shard }}) continue-on-error: ${{ matrix.lane == 'compatibility' }} - timeout-minutes: ${{ startsWith(matrix.os, 'macos') && 45 || 30 }} + 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.5"] + go: ["1.25.0", "1.26.7"] # 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" lane: compatibility - - go: "1.26.5" + - go: "1.26.7" lane: primary exclude: # The full demo lane above exercises Go 1.25 user-project/runtime @@ -166,6 +209,13 @@ jobs: go: "1.25.0" - 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. + - os: windows-2022 + go: "1.25.0" + - os: windows-2022 + shard: "1" runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -174,17 +224,13 @@ jobs: with: llvm-version: ${{matrix.llvm}} - name: Install further optional dependencies for demos - run: | - py_deps=( - numpy # for github.com/goplus/lib/py/numpy - torch # for github.com/goplus/lib/py/torch - ) - pip3.12 install --break-system-packages "${py_deps[@]}" + uses: ./.github/actions/setup-demo-deps - name: Set up Go for build uses: ./.github/actions/setup-go - name: Install + shell: bash run: | go install ./... echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV @@ -193,14 +239,30 @@ jobs: uses: ./.github/actions/setup-go with: go-version: ${{matrix.go}} + + - name: Check Windows native runtime and FFI + if: runner.os == 'Windows' + shell: pwsh + run: | + $llgo = (Get-Command llgo.exe).Source + .\.github\workflows\test_windows_runtime.ps1 -LLGo $llgo + - name: run llgo test env: SHARD_INDEX: ${{ matrix.shard }} - SHARD_TOTAL: ${{ startsWith(matrix.os, 'macos') && '1' || '2' }} - TEST_JOBS: ${{ startsWith(matrix.os, 'macos') && '3' || '4' }} + SHARD_TOTAL: ${{ startsWith(matrix.os, 'ubuntu') && '2' || '1' }} + TEST_JOBS: ${{ startsWith(matrix.os, 'windows') && '2' || startsWith(matrix.os, 'macos') && '3' || '4' }} + shell: bash run: | set -euo pipefail + if [[ "$RUNNER_OS" == Windows ]]; then + # test/go launches the compiler from native subprocesses. Keep an + # absolute Windows spelling without splitting the package list. + export LLGO_TEST_COMPILER="$(cygpath -w "$(command -v llgo)")" + export LLGO_STDIO_NOBUF=1 + fi + pkgs=() while IFS= read -r pkg; do pkgs+=("${pkg}") @@ -222,7 +284,7 @@ jobs: std_pkgs=() for pkg in "${selected[@]}"; do - if [[ "${{ matrix.os }}" == ubuntu-latest && "${{ matrix.go }}" == 1.26.5 && "${pkg}" == */test/std/* ]]; then + if [[ "${{ matrix.os }}" == ubuntu-latest && "${{ matrix.go }}" == 1.26.7 && "${pkg}" == */test/std/* ]]; then std_pkgs+=("${pkg}") fi done @@ -249,13 +311,17 @@ jobs: lane: compatibility - os: ubuntu-latest llvm: 19 - go: "1.26.5" + go: "1.26.7" lane: primary - # Keep the Go 1.26 user-module compatibility matrix on both host - # platforms; release artifact smoke tests alone only cover go 1.26. + # Keep the Go 1.26 user-module compatibility matrix on every native + # host platform; release artifact smoke tests alone only cover go 1.26. - os: macos-latest llvm: 19 - go: "1.26.5" + go: "1.26.7" + lane: primary + - os: windows-2022 + llvm: 19 + go: "1.26.7" lane: primary runs-on: ${{matrix.os}} steps: @@ -269,6 +335,7 @@ jobs: uses: ./.github/actions/setup-go - name: Install llgo + shell: bash run: | go install ./... echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV @@ -360,7 +427,7 @@ jobs: strategy: fail-fast: false matrix: - go: ["1.25.0", "1.26.5"] + go: ["1.25.0", "1.26.7"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index e7fe6ef01e..bea20fe3cb 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -135,22 +135,22 @@ jobs: - os: macos-15-intel goos: darwin goarch: amd64 - go-version: "1.26.5" + go-version: "1.26.7" go-mod-version: "1.26" - os: macos-latest goos: darwin goarch: arm64 - go-version: "1.26.5" + go-version: "1.26.7" go-mod-version: "1.26" - os: ubuntu-latest goos: linux goarch: amd64 - go-version: "1.26.5" + go-version: "1.26.7" go-mod-version: "1.26" - os: ubuntu-24.04-arm goos: linux goarch: arm64 - go-version: "1.26.5" + go-version: "1.26.7" go-mod-version: "1.26" runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/stdlib-coverage.yml b/.github/workflows/stdlib-coverage.yml index 8aa81e6151..95fa8b4686 100644 --- a/.github/workflows/stdlib-coverage.yml +++ b/.github/workflows/stdlib-coverage.yml @@ -19,6 +19,7 @@ jobs: os: - macos-latest - ubuntu-latest + - windows-2022 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v7 @@ -27,4 +28,5 @@ jobs: uses: ./.github/actions/setup-go - name: Check stdlib function coverage - run: doc/_readme/scripts/check_std_cover.sh + shell: bash + run: bash doc/_readme/scripts/check_std_cover.sh diff --git a/.github/workflows/test_windows_runtime.ps1 b/.github/workflows/test_windows_runtime.ps1 new file mode 100644 index 0000000000..26c4182e99 --- /dev/null +++ b/.github/workflows/test_windows_runtime.ps1 @@ -0,0 +1,137 @@ +param( + [Parameter(Mandatory = $true)] + [string]$LLGo +) + +$ErrorActionPreference = "Stop" + +if (-not (Test-Path $LLGo)) { + throw "LLGo compiler was not found at $LLGo" +} +$LLGo = (Resolve-Path $LLGo).Path +$root = (Get-Location).Path +$clangExe = (Get-Command clang.exe).Source +$llvmNmExe = (Get-Command llvm-nm.exe).Source +$readObjExe = (Get-Command llvm-readobj.exe).Source +$out = Join-Path $env:RUNNER_TEMP ("llgo-windows-runtime-" + [Guid]::NewGuid()) +New-Item -ItemType Directory $out | Out-Null + +$env:LLGO_ROOT = $root +$env:LLGO_BUILD_CACHE = "off" + +# The common Windows runner executes amd64 binaries. Compile the raw SyscallN +# 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" +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" } +)) { + $syscallObj = Join-Path $out ("syscall-{0}.obj" -f $syscallTarget.Triple) + & $clangExe "--target=$($syscallTarget.Triple)" -c $syscallAsm -o $syscallObj + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $symbols = & $llvmNmExe --defined-only $syscallObj | Out-String + if (-not $symbols.Contains($syscallTarget.Symbol)) { + throw "$($syscallTarget.Triple) bridge is missing $($syscallTarget.Symbol)" + } +} + +$runtime = Join-Path $out "windows-runtime-smoke.exe" +$stdlib = Join-Path $out "windows-stdlib-smoke.exe" +$ffi = Join-Path $out "windows-ffi-smoke.exe" +$empty = Join-Path $out "windows-empty-smoke.exe" +$coreFault = Join-Path $out "windows-core-fault-smoke.exe" +$network = Join-Path $out "windows-network-smoke.exe" + +# These fixtures cover minimal-runtime links and process behavior that a +# testing binary can accidentally satisfy through optional stdlib imports. +Push-Location runtime +try { + & $LLGo build -o $runtime .\_test\windowsruntime + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & $LLGo build -tags=nogc -o $stdlib .\_test\windowsstdlib + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & $LLGo build -o $ffi .\_test\windowsffi + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & $LLGo build -o $empty .\_test\windowsempty + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & $LLGo build -o $coreFault .\_test\windowscorefault + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + & $LLGo build -o $network .\_test\windowsnetwork + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +} finally { + Pop-Location +} + +& .\.github\workflows\check_windows_imports.ps1 ` + -ReadObj $readObjExe ` + -Artifacts @($runtime, $stdlib, $ffi, $empty, $coreFault, $network) + +Write-Host "==> windows-runtime-smoke.exe" +& $runtime +if ($LASTEXITCODE -ne 0) { + throw "windows-runtime-smoke.exe exited with code $LASTEXITCODE" +} + +Write-Host "==> windows-runtime-smoke.exe (unrecovered fault)" +$env:LLGO_TEST_UNRECOVERED_FAULT = "1" +$savedErrorActionPreference = $ErrorActionPreference +try { + # Windows PowerShell 5 turns redirected native stderr into a terminating + # NativeCommandError. This invocation is expected to fail and its stderr is + # the value asserted below. + $ErrorActionPreference = "Continue" + $faultOutput = & $runtime 2>&1 | Out-String + $faultExitCode = $LASTEXITCODE +} finally { + $ErrorActionPreference = $savedErrorActionPreference + Remove-Item Env:LLGO_TEST_UNRECOVERED_FAULT +} +Write-Host $faultOutput +$normalizedFaultOutput = $faultOutput.Replace('\', '/') +if ($faultExitCode -eq 0) { + throw "unrecovered Windows fault exited successfully" +} +foreach ($expected in @( + "runtime error: invalid memory address or nil pointer dereference", + "main.windowsNilFault", + "windowsruntime/main.go" +)) { + if (-not $normalizedFaultOutput.Contains($expected)) { + throw "unrecovered Windows fault output is missing '$expected'" + } +} +if ($normalizedFaultOutput.Contains("github.com/xgo-dev/llgo/runtime/internal/clite/tls.init")) { + throw "unrecovered Windows fault traceback continued past runtime.goexit" +} + +Write-Host "==> windows-stdlib-smoke.exe" +& $stdlib +if ($LASTEXITCODE -ne 0) { + throw "windows-stdlib-smoke.exe exited with code $LASTEXITCODE" +} + +Write-Host "==> windows-stdlib-smoke.exe (os.Exit)" +$env:LLGO_TEST_OS_EXIT = "1" +& $stdlib +$exitCode = $LASTEXITCODE +Remove-Item Env:LLGO_TEST_OS_EXIT +if ($exitCode -ne 23) { + throw "os.Exit(23) returned exit code $exitCode" +} + +foreach ($artifact in @( + @{ Name = "windows-ffi-smoke.exe"; Path = $ffi }, + @{ Name = "windows-empty-smoke.exe"; Path = $empty }, + @{ Name = "windows-core-fault-smoke.exe"; Path = $coreFault }, + @{ Name = "windows-network-smoke.exe"; Path = $network } +)) { + Write-Host "==> $($artifact.Name)" + & $artifact.Path + if ($LASTEXITCODE -ne 0) { + throw "$($artifact.Name) exited with code $LASTEXITCODE" + } +} diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml deleted file mode 100644 index de1a5e2116..0000000000 --- a/.github/workflows/windows.yml +++ /dev/null @@ -1,346 +0,0 @@ -name: Windows native compiler - -on: - push: - branches: - - main - pull_request: - branches: ["**"] - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - std-cover: - name: standard-library coverage (windows-amd64, Go 1.26.7) - runs-on: windows-2022 - timeout-minutes: 30 - steps: - - uses: actions/checkout@v7 - - - name: Set up Go - uses: ./.github/actions/setup-go - with: - go-version: "1.26.7" - - - name: Check Windows standard-library coverage - shell: bash - run: bash doc/_readme/scripts/check_std_cover.sh - - native-compiler: - name: native PE/COFF (windows-amd64, LLVM 19, Go 1.26.7) - runs-on: windows-2022 - timeout-minutes: 30 - steps: - - uses: actions/checkout@v7 - - - name: Set up Go - uses: ./.github/actions/setup-go - with: - go-version: "1.26.7" - - - name: Set up dependencies - uses: ./.github/actions/setup-deps - with: - llvm-version: 19 - - - name: Test Windows compiler host - shell: msys2 {0} - run: | - set -euo pipefail - - go version - clang --version - pkg-config --modversion llvm-19 - where.exe cl.exe - where.exe lib.exe - where.exe link.exe - - # Compile the released binding through its normal Windows build - # path, without byollvm or global CGO flag overrides. - go test -run '^$' -timeout=10m github.com/xgo-dev/llvm - go test -count=1 -timeout=10m -covermode=atomic \ - -coverprofile=coverage-windows-host.txt \ - ./internal/goarch ./internal/xtool/llvm ./internal/meta - - # Repetition verifies that the native file lock actually excludes - # concurrent cache publishers rather than merely compiling on - # Windows. The rest of crosscompile enters the matrix with R2. - go test -count=20 -timeout=10m -run 'Lock' -covermode=atomic \ - -coverprofile=coverage-windows-lock.txt \ - ./internal/crosscompile - - # These packages already compile and their test processes start on - # Windows. Their platform/runtime-dependent cases are enabled by - # the later proposal stages instead of being hidden by build tags. - go test -run '^$' -timeout=10m \ - ./ssa ./internal/build - - # R2 exercises actual PE/COFF outputs without depending on the - # Win32 runtime work staged for R4: an executable, a flat C archive, - # a DLL/import library, and both directions of MSVC interoperability. - LLGO_REQUIRE_MSVC=1 go test -count=1 -timeout=10m \ - -run '^(TestWindows(NativeArtifacts|ConsumesMSVCLibrary|LinkObjFilesExactOutput)|TestResolveBuildConfigDefaultsAndValidation|TestApplyBuildModeCompileFlags|TestCSharedLinkArgs|TestFullRpathArgs|TestCSharedExportArgs|TestIsArchiveInput|TestBuildOutFmtsBuildModes)$' \ - -covermode=atomic -coverprofile=coverage-windows-artifacts.txt \ - ./internal/build - - # Exercise the R4 compiler paths on their native host. These tests - # verify Win32 dynamic imports and the process-exit edge in the - # generated main module without requiring a completed stdlib port. - go test -count=1 -timeout=10m \ - -run '^(TestLowerWindowsCgoImportPointer|TestCompilePackageModuleLowersWindowsCgoImportPointer|TestLowerWindowsCgoImportPointerErrors|TestSplitWindowsCgoImportAlias|TestGenMainModuleWindowsExitsAfterMain)$' \ - -covermode=atomic -coverprofile=coverage-windows-build-runtime.txt \ - ./internal/build - - go test -count=1 -timeout=10m \ - -run '^(TestNativeToolchain|TestNativeWindows|TestCOFFLTOLevel)$' \ - -covermode=atomic -coverprofile=coverage-windows-coff-flags.txt \ - ./internal/crosscompile - - go test -count=1 -timeout=10m \ - -run '^(TestWriteResponseFile|TestWriteWindowsResponseArg|TestWriteGNUResponseArg|TestResponseFileStyle|TestUseResponseFile|TestLongWindowsCommandUsesClangResponseFile)$' \ - -covermode=atomic -coverprofile=coverage-windows-response.txt \ - ./internal/clang - - go test -count=1 -timeout=10m \ - -run '^(TestWindowsODRDefinitionsUseCOMDAT|TestUnixODRDefinitionsDoNotGainCOMDAT)$' \ - -covermode=atomic -coverprofile=coverage-windows-comdat.txt \ - ./ssa - - # Run every setjmp/sigjmp unit path on Windows. In particular this - # covers the 386 zero unwind-record count and the amd64/arm64 frame - # operands that differ from the POSIX lowering. - go test -count=1 -timeout=10m \ - -run '^(TestSetjmpLongjmpIRPaths|TestSigjmpUsesSetjmpOnExplicitTarget|TestWindowsSigjmpBufferAlignment|TestWindowsSetjmpABI)$' \ - -covermode=atomic -coverprofile=coverage-windows-ssa-eh.txt \ - ./ssa - - go test -count=1 -timeout=10m \ - -run '^(TestTargetArchAndNewTransformerArchSelection|TestMSVC.*)$' \ - -covermode=atomic -coverprofile=coverage-windows-cabi.txt \ - ./internal/cabi - - # R5 compiler and metadata paths are ordinary Go code even though - # their output is exercised by the native LLGo smokes below. Keep - # them in the Windows coverage upload as well as executing the - # generated Windows IR fixtures. - go test -count=1 -timeout=10m \ - -run '^(TestFuncInfoMetadataDoesNotPreserveFunctions|TestSetjmpLongjmpIRPaths|TestSigjmpUsesSetjmpOnExplicitTarget|TestWindowsSigjmpBufferAlignment|TestWindowsSetjmpABI|TestWindowsSetjmpRejectsUnsupportedArchitecture|TestNeedsFramePointer|TestSetjmpReturnsTwice|TestDeferInitBuilderInheritsDebugLocation)$' \ - -covermode=atomic -coverprofile=coverage-windows-execution-ssa.txt \ - ./ssa - go test -count=1 -timeout=10m \ - -run '^(TestCompileRuntimeCallerPanicPCLineMetadata|TestCompileRuntimeCallerStorePanicPCLineMetadataIsWindowsOnly|TestCompileRuntimeCallerPCLineMetadataOnWindows|TestFuncInfoDisplayName|TestFuncInfoMetadataEmission|TestFuncInfoWrapperMetadataIsWindowsOnly|TestDirectiveFilename|TestDirectiveFilenameWindowsRootedSlash|TestPrecedingLineDirectiveFilename|TestParseLineDirectiveFilename|TestRuntimeSourceFilename|TestCompileRuntimeCallerFrameUsesGoNameForLinkname|TestErrBuiltin)$' \ - -covermode=atomic -coverprofile=coverage-windows-execution-cl.txt \ - ./cl - go test -count=1 -timeout=10m \ - -run '^(TestFuncInfo.*|TestEncodePacksWrapperFlagWithoutGrowingRecord|TestCOFFFuncInfoEntrySiteIsAssociative|TestExternalFuncInfoTableKeepsPayloadOutOfIR|TestLowerWindowsCgoImportPointerErrors)$' \ - -covermode=atomic -coverprofile=coverage-windows-execution-build.txt \ - ./internal/build ./internal/build/funcinfo - go test -count=1 -timeout=10m \ - -run '^TestBuilderWindowsDebuggerMarkerUsesComdat$' \ - -covermode=atomic -coverprofile=coverage-windows-execution-debug.txt \ - ./internal/debuginfo - go test -count=1 -timeout=10m -run '^TestFromTestlibc$/^setjmp$' ./ssa - go test -count=1 -timeout=10m -run '^TestFromTestgo$/^(cgopython|defer5)$' ./ssa - go test -count=1 -timeout=10m -run '^TestFromTestlibgo$/^mapzero$' ./ssa - - ( - cd runtime - go test -count=1 -timeout=10m \ - -covermode=atomic -coverprofile=../coverage-windows-ffi.txt \ - ./internal/ffi ./internal/clite/ffi ./internal/lib/reflect - for arch in 386 arm64; do - CGO_ENABLED=0 GOOS=windows GOARCH="$arch" go test -c \ - -o "$RUNNER_TEMP/llgo-ffi-$arch.test.exe" ./internal/ffi - done - ) - - ffi_include="$(pkg-config --variable=includedir libffi)" - clang -target x86_64-pc-windows-msvc -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" - - - name: Upload Windows coverage - uses: codecov/codecov-action@v7 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage-windows-host.txt,coverage-windows-lock.txt,coverage-windows-artifacts.txt,coverage-windows-build-runtime.txt,coverage-windows-coff-flags.txt,coverage-windows-response.txt,coverage-windows-comdat.txt,coverage-windows-ssa-eh.txt,coverage-windows-cabi.txt,coverage-windows-execution-ssa.txt,coverage-windows-execution-cl.txt,coverage-windows-execution-build.txt,coverage-windows-execution-debug.txt,coverage-windows-ffi.txt - flags: windows-native - - - name: Build and run the native Windows runtime and FFI smoke tests - shell: pwsh - run: | - $ErrorActionPreference = "Stop" - $env:LLGO_ROOT = (Get-Location).Path - $env:LLGO_BUILD_CACHE = "off" - - go build -o llgo-windows-smoke.exe ./cmd/llgo - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - # Keep every Go-supported Windows architecture's raw SyscallN bridge - # assembly-valid even before the full 386 native execution lane lands. - $clangExe = (Get-Command clang.exe).Source - $llvmNmExe = (Get-Command llvm-nm.exe).Source - $syscallAsm = Join-Path $env:LLGO_ROOT ` - "runtime\internal\lib\runtime\_wrap\syscall_windows.S" - 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" } - )) { - $syscallObj = Join-Path $env:RUNNER_TEMP ` - ("syscall-{0}.obj" -f $syscallTarget.Triple) - & $clangExe "--target=$($syscallTarget.Triple)" ` - -c $syscallAsm -o $syscallObj - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - $symbols = & $llvmNmExe --defined-only $syscallObj | Out-String - if (-not $symbols.Contains($syscallTarget.Symbol)) { - throw "$($syscallTarget.Triple) bridge is missing $($syscallTarget.Symbol)" - } - } - - Push-Location runtime - # Run the core runtime smoke with BDWGC enabled. The fixture checks - # allocation, an explicit collection, MemStats, and a finalizer. - ..\llgo-windows-smoke.exe build ` - -o ..\windows-runtime-smoke.exe ` - .\_test\windowsruntime - $buildExitCode = $LASTEXITCODE - if ($buildExitCode -eq 0) { - ..\llgo-windows-smoke.exe build -tags=nogc ` - -o ..\windows-stdlib-smoke.exe ` - .\_test\windowsstdlib - $buildExitCode = $LASTEXITCODE - } - if ($buildExitCode -eq 0) { - ..\llgo-windows-smoke.exe build ` - -o ..\windows-ffi-smoke.exe ` - .\_test\windowsffi - $buildExitCode = $LASTEXITCODE - } - if ($buildExitCode -eq 0) { - # A program with no imports only links the core runtime. Keep this - # separate from the richer smoke programs so runtime.exit cannot - # accidentally be supplied by an optional standard-library shim. - ..\llgo-windows-smoke.exe build ` - -o ..\windows-empty-smoke.exe ` - .\_test\windowsempty - $buildExitCode = $LASTEXITCODE - } - if ($buildExitCode -eq 0) { - # Recover a hardware-backed nil call without relying on the - # optional public-runtime traceback layer. - ..\llgo-windows-smoke.exe build ` - -o ..\windows-core-fault-smoke.exe ` - .\_test\windowscorefault - $buildExitCode = $LASTEXITCODE - } - if ($buildExitCode -eq 0) { - ..\llgo-windows-smoke.exe build ` - -o ..\windows-network-smoke.exe ` - .\_test\windowsnetwork - $buildExitCode = $LASTEXITCODE - } - Pop-Location - if ($buildExitCode -ne 0) { - exit $buildExitCode - } - $readObjExe = (Get-Command llvm-readobj.exe).Source - & .\.github\workflows\check_windows_imports.ps1 ` - -ReadObj $readObjExe ` - -Artifacts @( - ".\windows-runtime-smoke.exe", - ".\windows-stdlib-smoke.exe", - ".\windows-ffi-smoke.exe", - ".\windows-empty-smoke.exe", - ".\windows-core-fault-smoke.exe", - ".\windows-network-smoke.exe" - ) - .\windows-runtime-smoke.exe - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - $env:LLGO_TEST_UNRECOVERED_FAULT = "1" - # This process is expected to write a panic to stderr and fail. - # Temporarily keep native stderr non-terminating so it can be - # asserted below even in Windows PowerShell 5. - $savedErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = "Continue" - try { - $faultOutput = .\windows-runtime-smoke.exe 2>&1 | Out-String - $faultExitCode = $LASTEXITCODE - } finally { - $ErrorActionPreference = $savedErrorActionPreference - } - Remove-Item Env:LLGO_TEST_UNRECOVERED_FAULT - Write-Host $faultOutput - $normalizedFaultOutput = $faultOutput.Replace('\', '/') - if ($faultExitCode -eq 0) { - throw "unrecovered Windows fault exited successfully" - } - foreach ($expected in @( - "runtime error: invalid memory address or nil pointer dereference", - "main.windowsNilFault", - "windowsruntime/main.go" - )) { - if (-not $normalizedFaultOutput.Contains($expected)) { - throw "unrecovered Windows fault output is missing '$expected'" - } - } - if ($faultOutput.Contains("github.com/xgo-dev/llgo/runtime/internal/clite/tls.init")) { - throw "unrecovered Windows fault traceback continued past runtime.goexit" - } - .\windows-stdlib-smoke.exe - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - $env:LLGO_TEST_OS_EXIT = "1" - .\windows-stdlib-smoke.exe - $exitCode = $LASTEXITCODE - Remove-Item Env:LLGO_TEST_OS_EXIT - if ($exitCode -ne 23) { - throw "os.Exit(23) returned exit code $exitCode" - } - - .\windows-ffi-smoke.exe - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - .\windows-empty-smoke.exe - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - .\windows-core-fault-smoke.exe - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - .\windows-network-smoke.exe - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - # Exercise cmd/llgo's test path with testing-package behavior, cgo - # errno propagation, synchronization stress, CPU profiling, and - # runtime trace clock hooks. - # The runtime/FFI binaries above only cover build/run. - .\llgo-windows-smoke.exe test ` - -p=1 ` - -count=1 ` - -timeout=10m ` - ./test/windows ` - ./test/cgo ` - ./test/std/sync ` - ./test/std/runtime/pprof ` - ./test/std/runtime/trace - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } diff --git a/_demo/c/asmfullcall/asmfullcall_windows_amd64.go b/_demo/c/asmfullcall/asmfullcall_windows_amd64.go new file mode 100644 index 0000000000..4855b0b6c5 --- /dev/null +++ b/_demo/c/asmfullcall/asmfullcall_windows_amd64.go @@ -0,0 +1,27 @@ +//go:build windows && amd64 + +package main + +import "unsafe" + +func verify() { + asmFull("nop", nil) + + addr := uintptr(unsafe.Pointer(&testVar)) + asmFull("movq {value}, ({addr})", map[string]any{ + "addr": addr, + "value": 43, + }) + check(43, testVar) + + res1 := asmFull("movq {value}, {}", map[string]any{ + "value": 41, + }) + check(41, int(res1)) + + res2 := asmFull("leaq ({a},{b}), {}", map[string]any{ + "a": 25, + "b": 17, + }) + check(42, int(res2)) +} diff --git a/_demo/c/asmfullcall/asmfullcall_windows_arm64.go b/_demo/c/asmfullcall/asmfullcall_windows_arm64.go new file mode 100644 index 0000000000..ca9c7220ac --- /dev/null +++ b/_demo/c/asmfullcall/asmfullcall_windows_arm64.go @@ -0,0 +1,27 @@ +//go:build windows && arm64 + +package main + +import "unsafe" + +func verify() { + asmFull("nop", nil) + + addr := uintptr(unsafe.Pointer(&testVar)) + asmFull("str {value}, [{addr}]", map[string]any{ + "addr": addr, + "value": 43, + }) + check(43, testVar) + + res1 := asmFull("mov {}, {value}", map[string]any{ + "value": 41, + }) + check(41, int(res1)) + + res2 := asmFull("add {}, {a}, {b}", map[string]any{ + "a": 25, + "b": 17, + }) + check(42, int(res2)) +} diff --git a/_demo/c/cabi/wrap/wrap.c b/_demo/c/cabi/wrap/wrap.c index e551911851..8b00aeab92 100644 --- a/_demo/c/cabi/wrap/wrap.c +++ b/_demo/c/cabi/wrap/wrap.c @@ -1,4 +1,4 @@ -extern int printf(const char *format, ...); +#include int demo32(int v) { return v+100; diff --git a/_demo/c/cexec/exec.go b/_demo/c/cexec/exec.go index 7525250518..bfaab37275 100644 --- a/_demo/c/cexec/exec.go +++ b/_demo/c/cexec/exec.go @@ -1,3 +1,5 @@ +//go:build !windows + package main import ( diff --git a/_demo/c/cexec/exec_windows.go b/_demo/c/cexec/exec_windows.go new file mode 100644 index 0000000000..5f7f1af0ce --- /dev/null +++ b/_demo/c/cexec/exec_windows.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/goplus/lib/c" + "github.com/goplus/lib/c/os" +) + +func main() { + cmd := c.Str("cmd.exe") + os.Execlp(cmd, cmd, c.Str("/c"), c.Str("echo Hello from execlp"), nil) + panic("_execlp returned") +} diff --git a/_demo/c/cppintf/foo/bar/bar.cpp b/_demo/c/cppintf/foo/bar/bar.cpp index 5e226c2664..9f037385f7 100644 --- a/_demo/c/cppintf/foo/bar/bar.cpp +++ b/_demo/c/cppintf/foo/bar/bar.cpp @@ -8,8 +8,15 @@ interface ICallback { extern "C" void f(ICallback* cb) { printf("val: %d\ncalc(2): %lf\n", cb->val(), cb->calc(2)); + fflush(stdout); } void g(ICallback* cb) { f(cb); } + +#if defined(_WIN32) +extern "C" void llgo_cppintf_g(ICallback* cb) { + g(cb); +} +#endif diff --git a/_demo/c/cppintf/foo/foo.go b/_demo/c/cppintf/foo/foo.go index ebd047ef2f..eeeb2349f9 100644 --- a/_demo/c/cppintf/foo/foo.go +++ b/_demo/c/cppintf/foo/foo.go @@ -20,6 +20,3 @@ type CallbackVtbl struct { //go:linkname F C.f func F(cb *Callback) - -//go:linkname G C._Z1gP9ICallback -func G(cb *Callback) diff --git a/_demo/c/cppintf/foo/g_default.go b/_demo/c/cppintf/foo/g_default.go new file mode 100644 index 0000000000..0656c1550d --- /dev/null +++ b/_demo/c/cppintf/foo/g_default.go @@ -0,0 +1,8 @@ +//go:build !windows + +package foo + +import _ "unsafe" + +//go:linkname G C._Z1gP9ICallback +func G(cb *Callback) diff --git a/_demo/c/cppintf/foo/g_windows.go b/_demo/c/cppintf/foo/g_windows.go new file mode 100644 index 0000000000..9aaaff0bca --- /dev/null +++ b/_demo/c/cppintf/foo/g_windows.go @@ -0,0 +1,12 @@ +//go:build windows + +package foo + +import _ "unsafe" + +// MSVC and Itanium targets use different C++ symbol mangling. Cross the +// language boundary through the C ABI wrapper while retaining g as a C++ +// function in bar.cpp. +// +//go:linkname G C.llgo_cppintf_g +func G(cb *Callback) diff --git a/_demo/c/cppmintf/foo/bar/bar.cpp b/_demo/c/cppmintf/foo/bar/bar.cpp index 99c6e97d7b..05ac8853a0 100644 --- a/_demo/c/cppmintf/foo/bar/bar.cpp +++ b/_demo/c/cppmintf/foo/bar/bar.cpp @@ -14,4 +14,5 @@ class Callback : public ICalc, public IVal { extern "C" void f(Callback* cb) { printf("val: %d\ncalc(2): %lf\n", cb->val(), cb->calc(2)); + fflush(stdout); } diff --git a/_demo/c/fcntl/fcntl.go b/_demo/c/fcntl/fcntl.go index e129046793..1727d08cb3 100644 --- a/_demo/c/fcntl/fcntl.go +++ b/_demo/c/fcntl/fcntl.go @@ -1,3 +1,5 @@ +//go:build !windows + package main import ( diff --git a/_demo/c/fcntl/fcntl_windows.go b/_demo/c/fcntl/fcntl_windows.go new file mode 100644 index 0000000000..264411f374 --- /dev/null +++ b/_demo/c/fcntl/fcntl_windows.go @@ -0,0 +1,47 @@ +package main + +import ( + "unsafe" + + "github.com/goplus/lib/c" + "github.com/goplus/lib/c/os" +) + +// Windows has no fcntl API. Exercise the equivalent Universal CRT descriptor +// operations while the Unix source continues to cover F_GETFL/F_SETFL. +func main() { + filename := c.Str("testfile.txt") + data := c.Str("Hello, os!") + defer os.Remove(filename) + + fd := os.Open(filename, os.O_CREAT|os.O_WRONLY|os.O_TRUNC|os.O_BINARY, 0o644) + if fd == -1 { + panic("open for write failed") + } + if n := os.Write(fd, c.Pointer(data), c.Strlen(data)); n != int(c.Strlen(data)) { + os.Close(fd) + panic("write failed") + } + if os.Close(fd) != 0 { + panic("close after write failed") + } + + fd = os.Open(filename, os.O_RDONLY|os.O_BINARY) + if fd == -1 { + panic("open for read failed") + } + var buffer [20]c.Char + n := os.Read(fd, c.Pointer(unsafe.SliceData(buffer[:])), uintptr(len(buffer)-1)) + if n < 0 { + os.Close(fd) + panic("read failed") + } + buffer[n] = 0 + if os.Close(fd) != 0 { + panic("close after read failed") + } + if got := c.GoString(&buffer[0]); got != "Hello, os!" { + panic("unexpected file contents: " + got) + } + c.Printf(c.Str("Read %d bytes: %s\n"), n, &buffer[0]) +} diff --git a/_demo/c/go.mod b/_demo/c/go.mod index b6450a729d..286169c3b9 100644 --- a/_demo/c/go.mod +++ b/_demo/c/go.mod @@ -3,3 +3,5 @@ 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-20260827141156-c40a4eb3593b diff --git a/_demo/c/go.sum b/_demo/c/go.sum index 54e0f00c86..28ecde6430 100644 --- a/_demo/c/go.sum +++ b/_demo/c/go.sum @@ -1,2 +1,4 @@ -github.com/goplus/lib v0.3.0 h1:y0ZGb5Q/RikW1oMMB4Di7XIZIpuzh/7mlrR8HNbxXCA= -github.com/goplus/lib v0.3.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225 h1:OtpnoHPW/7IolIhBEkwe4KDTo6Mf0st+CScYJX/h8XY= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b h1:H0ZWQZkfp79M09RhzaDGRPBlWmLFeS8upv36sy5VpI8= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/_demo/c/netdbdemo/netdb.go b/_demo/c/netdbdemo/netdb.go index ced8335bd8..9b883b1905 100644 --- a/_demo/c/netdbdemo/netdb.go +++ b/_demo/c/netdbdemo/netdb.go @@ -14,7 +14,13 @@ func main() { port := "80" var result *net.AddrInfo - c.Printf(c.Str("%d\n"), net.Getaddrinfo(c.Str(host), c.Str(port), &hints, &result)) - - c.Printf(c.Str("%d\n"), net.Freeaddrinfo(result)) + if resultCode := net.Getaddrinfo(c.Str(host), c.Str(port), &hints, &result); resultCode != 0 { + panic("getaddrinfo failed") + } + if result == nil { + panic("getaddrinfo returned no addresses") + } + net.Freeaddrinfo(result) + c.Printf(c.Str("resolved %s:%s\n"), c.Str(host), c.Str(port)) + c.Fflush(c.Stdout) } diff --git a/_demo/c/socket/client/client.go b/_demo/c/socket/client/client.go index b462a2c063..4cd317fd60 100644 --- a/_demo/c/socket/client/client.go +++ b/_demo/c/socket/client/client.go @@ -5,18 +5,19 @@ import ( "github.com/goplus/lib/c" "github.com/goplus/lib/c/net" - "github.com/goplus/lib/c/os" ) func main() { sockfd := net.Socket(net.AF_INET, net.SOCK_STREAM, 0) + if sockfd == net.InvalidSocket { + panic("socket failed") + } msg := c.Str("Hello, World!") - defer os.Close(sockfd) + defer net.Close(sockfd) server := net.GetHostByName(c.Str("localhost")) - if server == nil { - c.Perror(c.Str("hostname get error")) - return + if server == nil || server.AddrList == nil || *server.AddrList == nil { + panic("hostname lookup failed") } servAddr := &net.SockaddrIn{} @@ -24,9 +25,12 @@ func main() { servAddr.Port = net.Htons(uint16(1234)) c.Memcpy(unsafe.Pointer(&servAddr.Addr.Addr), unsafe.Pointer(*server.AddrList), uintptr(server.Length)) - if res := net.Connect(sockfd, (*net.SockAddr)(unsafe.Pointer(servAddr)), c.Uint(16)); res < 0 { - c.Perror(c.Str("connect error")) - return + if res := net.Connect(sockfd, (*net.SockAddr)(unsafe.Pointer(servAddr)), net.SocklenT(unsafe.Sizeof(*servAddr))); res < 0 { + println("connect error:", socketError()) + panic("connect failed") + } + length := c.Strlen(msg) + if sent := net.Send(sockfd, unsafe.Pointer(msg), length, 0); sent != c.Long(length) { + panic("send failed") } - os.Write(sockfd, unsafe.Pointer(msg), c.Strlen(msg)) } diff --git a/_demo/c/socket/client/error_default.go b/_demo/c/socket/client/error_default.go new file mode 100644 index 0000000000..f213c6e053 --- /dev/null +++ b/_demo/c/socket/client/error_default.go @@ -0,0 +1,5 @@ +//go:build !windows + +package main + +func socketError() int { return 0 } diff --git a/_demo/c/socket/client/error_windows.go b/_demo/c/socket/client/error_windows.go new file mode 100644 index 0000000000..b96f3145ad --- /dev/null +++ b/_demo/c/socket/client/error_windows.go @@ -0,0 +1,7 @@ +//go:build windows + +package main + +import "github.com/goplus/lib/c/net" + +func socketError() int { return int(net.LastError()) } diff --git a/_demo/c/socket/server/server.go b/_demo/c/socket/server/server.go index db1ec22ae4..971105706a 100644 --- a/_demo/c/socket/server/server.go +++ b/_demo/c/socket/server/server.go @@ -5,14 +5,16 @@ import ( "github.com/goplus/lib/c" "github.com/goplus/lib/c/net" - "github.com/goplus/lib/c/os" ) func main() { var buffer [256]c.Char sockfd := net.Socket(net.AF_INET, net.SOCK_STREAM, 0) - defer os.Close(sockfd) + if sockfd == net.InvalidSocket { + panic("socket failed") + } + defer net.Close(sockfd) servAddr := &net.SockaddrIn{ Family: net.AF_INET, @@ -20,24 +22,29 @@ func main() { Addr: net.InAddr{Addr: 0x00000000}, Zero: [8]c.Char{0, 0, 0, 0, 0, 0, 0, 0}, } - if res := net.Bind(sockfd, servAddr, c.Uint(unsafe.Sizeof(*servAddr))); res < 0 { - c.Perror(c.Str("bind error")) - return + if res := net.Bind(sockfd, servAddr, net.SocklenT(unsafe.Sizeof(*servAddr))); res < 0 { + panic("bind failed") } if net.Listen(sockfd, 5) < 0 { - c.Printf(c.Str("listen error")) - return + panic("listen failed") } c.Printf(c.Str("Listening on port 1234...\n")) + c.Fflush(c.Stdout) - cliAddr, clilen := &net.SockaddrIn{}, c.Uint(unsafe.Sizeof(servAddr)) + cliAddr := &net.SockaddrIn{} + clilen := net.SocklenT(unsafe.Sizeof(*cliAddr)) newsockfd := net.Accept(sockfd, cliAddr, &clilen) - defer os.Close(newsockfd) - c.Printf(c.Str("Connection accepted.")) - - os.Read(newsockfd, unsafe.Pointer(unsafe.SliceData(buffer[:])), 256) - c.Printf(c.Str("Received: %s"), &buffer[0]) - + if newsockfd == net.InvalidSocket { + panic("accept failed") + } + defer net.Close(newsockfd) + count := net.Recv(newsockfd, unsafe.Pointer(unsafe.SliceData(buffer[:])), uintptr(len(buffer)-1), 0) + if count <= 0 { + panic("receive failed") + } + buffer[int(count)] = 0 + c.Printf(c.Str("Connection accepted.\nReceived: %s\n"), &buffer[0]) + c.Fflush(c.Stdout) } diff --git a/_demo/c/syncdebug/once.go b/_demo/c/syncdebug/once.go new file mode 100644 index 0000000000..cf21518721 --- /dev/null +++ b/_demo/c/syncdebug/once.go @@ -0,0 +1,64 @@ +package main + +import llsync "github.com/goplus/lib/c/pthread/sync" + +var ( + onceCount int + onceDelta int +) + +func addOnceDelta() { onceCount += onceDelta } +func incrementOnce() { onceCount++ } + +func testOnce() { + var once llsync.Once + onceCount, onceDelta = 0, 2 + if once.Do(addOnceDelta) != 0 || once.Do(incrementOnce) != 0 { + panic("once failed") + } + if onceCount != 2 { + panic("once ran more than once") + } + + var closureOnce llsync.Once + closureCount, closureDelta := 0, 3 + if closureOnce.DoFunc(func() { closureCount += closureDelta }) != 0 || + closureOnce.DoFunc(func() { closureCount++ }) != 0 { + panic("closure once failed") + } + if closureCount != 3 { + panic("closure once ran more than once") + } + + var concurrentOnce llsync.Once + concurrentValue := 0 + done := make(chan struct{}, 4) + for value := 1; value <= 4; value++ { + value := value + go func() { + if concurrentOnce.DoFunc(func() { concurrentValue = value }) != 0 { + panic("concurrent closure once failed") + } + done <- struct{}{} + }() + } + for i := 0; i < 4; i++ { + <-done + } + if concurrentValue < 1 || concurrentValue > 4 { + panic("concurrent closure once did not run") + } + + var outerOnce, innerOnce llsync.Once + nestedValue := 0 + if outerOnce.DoFunc(func() { + if innerOnce.DoFunc(func() { nestedValue = 5 }) != 0 { + panic("nested inner once failed") + } + }) != 0 { + panic("nested outer once failed") + } + if nestedValue != 5 { + panic("nested closure once did not run") + } +} diff --git a/_demo/c/syncdebug/semaphore_darwin.go b/_demo/c/syncdebug/semaphore_darwin.go new file mode 100644 index 0000000000..f3325d800c --- /dev/null +++ b/_demo/c/syncdebug/semaphore_darwin.go @@ -0,0 +1,22 @@ +//go:build darwin + +package main + +import ( + "github.com/goplus/lib/c" + cos "github.com/goplus/lib/c/os" + csyscall "github.com/goplus/lib/c/syscall" +) + +func initWorkerSemaphore() bool { + // Darwin declares unnamed POSIX semaphores for source compatibility, but + // sem_init always fails with ENOSYS. Verify that platform contract while + // mutex, condition-variable, and pthread synchronization remain exercised. + return workerSem.Init(0, 0) == -1 && cos.Errno() == c.Int(csyscall.ENOSYS) +} + +// No semaphore was created after the expected ENOSYS result above. The +// condition variable still performs the worker handoff on Darwin. +func postWorkerSemaphore() bool { return true } +func waitWorkerSemaphore() bool { return true } +func destroyWorkerSemaphore() bool { return true } diff --git a/_demo/c/syncdebug/semaphore_default.go b/_demo/c/syncdebug/semaphore_default.go new file mode 100644 index 0000000000..85b5a155f1 --- /dev/null +++ b/_demo/c/syncdebug/semaphore_default.go @@ -0,0 +1,19 @@ +//go:build !darwin + +package main + +func initWorkerSemaphore() bool { + return workerSem.Init(0, 0) == 0 +} + +func postWorkerSemaphore() bool { + return workerSem.Post() == 0 +} + +func waitWorkerSemaphore() bool { + return workerSem.Wait() == 0 +} + +func destroyWorkerSemaphore() bool { + return workerSem.Destroy() == 0 +} diff --git a/_demo/c/syncdebug/syncdebug.go b/_demo/c/syncdebug/syncdebug.go index bd40df241a..7bf4024360 100644 --- a/_demo/c/syncdebug/syncdebug.go +++ b/_demo/c/syncdebug/syncdebug.go @@ -6,6 +6,8 @@ import ( "sync" "unsafe" + "github.com/goplus/lib/c" + "github.com/goplus/lib/c/pthread" llsync "github.com/goplus/lib/c/pthread/sync" ) @@ -16,6 +18,27 @@ type L struct { w io.Writer } +var ( + workerCond llsync.Cond + workerMutex llsync.Mutex + workerReady bool + workerSem llsync.Sem +) + +func syncWorker(c.Pointer) c.Pointer { + workerMutex.Lock() + workerReady = true + if workerCond.Signal() != 0 { + workerMutex.Unlock() + return c.Pointer(c.Str("condition signal failed")) + } + workerMutex.Unlock() + if !postWorkerSemaphore() { + return c.Pointer(c.Str("semaphore post failed")) + } + return nil +} + func main() { l := &L{s: "hello", i: 123, w: os.Stdout} println("sizeof(L):", unsafe.Sizeof(L{})) @@ -28,4 +51,57 @@ func main() { l.w.Write([]byte(l.s)) l.w.Write([]byte("\n")) l.mu.Unlock() + + testOnce() + + var rw llsync.RWLock + if rw.Init(nil) != 0 { + panic("rwlock init failed") + } + rw.Lock() + value := 42 + rw.Unlock() + rw.RLock() + if value != 42 { + panic("rwlock value changed") + } + rw.RUnlock() + rw.Destroy() + + if workerMutex.Init(nil) != 0 { + panic("worker mutex init failed") + } + if workerCond.Init(nil) != 0 { + panic("worker condition init failed") + } + if !initWorkerSemaphore() { + panic("worker semaphore init failed") + } + workerMutex.Lock() + var thread pthread.Thread + if pthread.Create(&thread, nil, syncWorker, nil) != 0 { + panic("thread create failed") + } + for !workerReady { + if workerCond.Wait(&workerMutex) != 0 { + panic("condition wait failed") + } + } + workerMutex.Unlock() + if !waitWorkerSemaphore() { + panic("semaphore wait failed") + } + var result c.Pointer + if pthread.Join(thread, &result) != 0 { + panic("thread join failed") + } + if result != nil { + panic(c.GoString((*c.Char)(result))) + } + if !destroyWorkerSemaphore() { + panic("worker semaphore destroy failed") + } + workerCond.Destroy() + workerMutex.Destroy() + println("C synchronization passed") } diff --git a/_demo/c/thread/thd.go b/_demo/c/thread/thd.go index f5dbd07c46..0539ca2a7e 100644 --- a/_demo/c/thread/thd.go +++ b/_demo/c/thread/thd.go @@ -8,18 +8,39 @@ import ( var key pthread.Key func main() { - key.Create(nil) - key.Set(c.Pointer(c.Str("main value\n"))) + if key.Create(nil) != 0 { + panic("key create failed") + } + defer key.Delete() + if key.Set(c.Pointer(c.Str("main value\n"))) != 0 { + panic("main key set failed") + } var thd pthread.Thread - pthread.Create(&thd, nil, func(arg c.Pointer) c.Pointer { - key.Set(c.Pointer(c.Str("thread value\n"))) + if err := pthread.Create(&thd, nil, func(arg c.Pointer) c.Pointer { + if key.Set(c.Pointer(c.Str("thread value\n"))) != 0 { + return c.Pointer(c.Str("thread key set failed")) + } + if c.GoString((*c.Char)(key.Get())) != "thread value\n" { + return c.Pointer(c.Str("thread key read failed")) + } c.Printf(c.Str("Hello, thread\nTLS: %s"), key.Get()) return c.Pointer(c.Str("Back to main\n")) - }, nil) + }, nil); err != 0 { + panic("thread create failed") + } var retval c.Pointer - pthread.Join(thd, &retval) + if pthread.Join(thd, &retval) != 0 { + panic("thread join failed") + } + if c.GoString((*c.Char)(retval)) != "Back to main\n" { + panic(c.GoString((*c.Char)(retval))) + } + if c.GoString((*c.Char)(key.Get())) != "main value\n" { + panic("main TLS value changed") + } c.Printf(c.Str("%sTLS: %s"), retval, key.Get()) + c.Fflush(c.Stdout) } diff --git a/_demo/embed/go.mod b/_demo/embed/go.mod index 9d8a374c62..7f727e6b14 100644 --- a/_demo/embed/go.mod +++ b/_demo/embed/go.mod @@ -3,3 +3,5 @@ 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-20260827141156-c40a4eb3593b diff --git a/_demo/embed/go.sum b/_demo/embed/go.sum index 54e0f00c86..28ecde6430 100644 --- a/_demo/embed/go.sum +++ b/_demo/embed/go.sum @@ -1,2 +1,4 @@ -github.com/goplus/lib v0.3.0 h1:y0ZGb5Q/RikW1oMMB4Di7XIZIpuzh/7mlrR8HNbxXCA= -github.com/goplus/lib v0.3.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225 h1:OtpnoHPW/7IolIhBEkwe4KDTo6Mf0st+CScYJX/h8XY= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b h1:H0ZWQZkfp79M09RhzaDGRPBlWmLFeS8upv36sy5VpI8= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/_demo/embed/test_esp32c3_startup.sh b/_demo/embed/test_esp32c3_startup.sh index 647cb234d2..24bc08d345 100755 --- a/_demo/embed/test_esp32c3_startup.sh +++ b/_demo/embed/test_esp32c3_startup.sh @@ -76,12 +76,27 @@ run_case_and_compare() { return 1 } -# Check if esptool.py is installed -# esptool.py is required to parse ESP32-C3 BIN file format and verify -# that constructor-related data is included in the firmware -if ! command -v esptool.py &> /dev/null; then - echo "✗ FAIL: esptool.py not found" - echo "Please install: pip3 install esptool==5.1.0" +# Check if esptool is installed. Invoke its Python module so the test does not +# depend on the platform-specific console-script name. Prefer python3 because +# pip3 may install into a different interpreter than an unversioned python. +# Fall back to the Windows Python launcher when neither executable owns the +# installed module. +ESPTOOL_PYTHON=() +for python_cmd in python3 python; do + if command -v "$python_cmd" > /dev/null 2>&1 && "$python_cmd" -c 'import esptool' &> /dev/null; then + ESPTOOL_PYTHON=("$python_cmd") + break + fi +done +if [ ${#ESPTOOL_PYTHON[@]} -eq 0 ] && command -v py > /dev/null 2>&1 && py -3 -c 'import esptool' &> /dev/null; then + ESPTOOL_PYTHON=(py -3) +fi + +# esptool is required to parse ESP32-C3 BIN file format and verify +# that constructor-related data is included in the firmware. +if [ ${#ESPTOOL_PYTHON[@]} -eq 0 ]; then + echo "✗ FAIL: esptool not found" + echo "Please install: python3 -m pip install esptool==5.1.0" exit 1 fi @@ -167,11 +182,11 @@ echo ".init_array section size: $INIT_ARRAY_SIZE" echo "" echo "=== Test 3: Verify __init_array_start included in BIN file ===" -# Get BIN file segment information using esptool.py +# Get BIN file segment information using esptool # ESP32-C3 BIN files contain multiple segments with load addresses. # We need to verify the __init_array_start address is covered by one segment. # -# Real output from: esptool.py --chip esp32c3 image_info test.bin +# Real output from: python3 -m esptool --chip esp32c3 image_info test.bin # # Segments Information # ==================== @@ -193,8 +208,8 @@ echo "=== Test 3: Verify __init_array_start included in BIN file ===" # $5+ = IRAM (memory type) # # We extract $2 (length) and $3 (load addr) to verify __init_array_start is within bounds -if ! BIN_INFO=$(esptool.py --chip esp32c3 image_info "$TEST_BIN" 2>&1); then - echo "✗ FAIL: esptool.py failed to parse BIN file" +if ! BIN_INFO=$("${ESPTOOL_PYTHON[@]}" -m esptool --chip esp32c3 image_info "$TEST_BIN" 2>&1); then + echo "✗ FAIL: esptool failed to parse BIN file" echo "$BIN_INFO" exit 1 fi diff --git a/_demo/go/export/test.sh b/_demo/go/export/test.sh index 094c528b3d..63af24d573 100755 --- a/_demo/go/export/test.sh +++ b/_demo/go/export/test.sh @@ -141,6 +141,16 @@ fi print_status "Starting C header generation tests..." print_status "Working directory: $SCRIPT_DIR" +SHARED_LIB="libexport.so" +case "$OSTYPE" in + darwin*) + SHARED_LIB="libexport.dylib" + ;; + msys*|cygwin*|win32*) + SHARED_LIB="libexport.dll" + ;; +esac + echo "" build_failures=0 run_build_mode_tests=true @@ -163,11 +173,6 @@ if [[ "$run_build_mode_tests" == true ]]; then # Test 1: c-shared mode print_status "=== Test 1: Building with -buildmode c-shared ===" -if [[ "$OSTYPE" == "darwin"* ]]; then - SHARED_LIB="libexport.dylib" -else - SHARED_LIB="libexport.so" -fi if $LLGO_SCRIPT build -buildmode c-shared -o "$SHARED_LIB" .; then print_status "Build succeeded" @@ -185,9 +190,9 @@ if $LLGO_SCRIPT build -buildmode c-shared -o "$SHARED_LIB" .; then # Test C demo with shared library print_status "=== Testing C demo with shared library ===" if cd use; then - if LINK_TYPE=shared make clean && LINK_TYPE=shared LLGOFLAGS=-ldflags=-w=false make; then + if make LINK_TYPE=shared clean && make LINK_TYPE=shared LLGOFLAGS=-ldflags=-w=false; then print_status "C demo build succeeded with shared library" - if LINK_TYPE=shared make run; then + if make LINK_TYPE=shared run; then print_status "C demo execution succeeded with shared library" else print_error "C demo execution failed with shared library" diff --git a/_demo/go/export/use/Makefile b/_demo/go/export/use/Makefile index 5691dcb62a..44dda629a4 100644 --- a/_demo/go/export/use/Makefile +++ b/_demo/go/export/use/Makefile @@ -17,21 +17,40 @@ LLGOFLAGS ?= # Platform detection UNAME_S := $(shell uname -s) +WINDOWS := $(filter MINGW% MSYS% CYGWIN%,$(UNAME_S)) +SHARED_LINK_FLAGS = -L.. -lexport ifeq ($(UNAME_S),Darwin) SHARED_EXT = dylib PLATFORM_LIBS = +else ifneq ($(WINDOWS),) + SHARED_EXT = dll + GOARCH = $(shell go env GOARCH) + 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 + 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" else SHARED_EXT = so PLATFORM_LIBS = $(shell pkg-config --libs libunwind 2>/dev/null || echo -lunwind) endif -STATIC_STDLIB_LIBS = $(shell pkg-config --libs libffi 2>/dev/null || echo -lffi) -lresolv +ifeq ($(WINDOWS),) + STATIC_STDLIB_LIBS = $(shell pkg-config --libs libffi 2>/dev/null || echo -lffi) -lresolv +endif # Library and flags based on link type ifeq ($(LINK_TYPE),shared) BUILDMODE = c-shared OUTPUT = libexport.$(SHARED_EXT) LIBRARY = ../$(OUTPUT) - LDFLAGS = -L.. -lexport $(RUNTIME_LIBS) $(PLATFORM_LIBS) + LDFLAGS = $(SHARED_LINK_FLAGS) $(RUNTIME_LIBS) $(PLATFORM_LIBS) BUILD_MSG = "Building Go shared library..." LINK_MSG = "Linking with shared library..." else @@ -62,7 +81,11 @@ run: $(TARGET) @echo "Running C demo..." ifeq ($(LINK_TYPE),shared) @echo "Setting library path for shared library..." +ifneq ($(WINDOWS),) + PATH=..:$$PATH ./$(TARGET) +else LD_LIBRARY_PATH=.. DYLD_LIBRARY_PATH=.. ./$(TARGET) +endif else ./$(TARGET) endif @@ -70,7 +93,7 @@ endif # Clean build artifacts clean: rm -f $(TARGET) - rm -f ../libexport.a ../libexport.h ../libexport.so ../libexport.dylib + rm -f ../libexport.a ../libexport.h ../libexport.so ../libexport.dylib ../libexport.dll ../libexport.lib # Help target help: diff --git a/_demo/go/export/use/main.c b/_demo/go/export/use/main.c index ae84e2d00a..c4b107c003 100644 --- a/_demo/go/export/use/main.c +++ b/_demo/go/export/use/main.c @@ -91,10 +91,6 @@ int main() { printf("=== C Export Demo ===\n"); fflush(stdout); // Force output - // Initialize packages - call init functions first - github_com_xgo_dev_llgo__demo_go_export_c_init(); - main_init(); - // Verify that funcinfo is not merely linkable: runtime.Callers must yield // a symbolized frame and runtime.FuncForPC must resolve that PC's details. main_FuncInfoResult func_info = GetFuncInfo(); diff --git a/_demo/go/go.mod b/_demo/go/go.mod index 29365f8665..2a10a65310 100644 --- a/_demo/go/go.mod +++ b/_demo/go/go.mod @@ -3,3 +3,5 @@ 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-20260827141156-c40a4eb3593b diff --git a/_demo/go/go.sum b/_demo/go/go.sum index ef2a0923de..28ecde6430 100644 --- a/_demo/go/go.sum +++ b/_demo/go/go.sum @@ -1,2 +1,4 @@ -github.com/goplus/lib v0.3.1 h1:Xws4DBVvgOMu58awqB972wtvTacDbk3nqcbHjdx9KSg= -github.com/goplus/lib v0.3.1/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225 h1:OtpnoHPW/7IolIhBEkwe4KDTo6Mf0st+CScYJX/h8XY= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b h1:H0ZWQZkfp79M09RhzaDGRPBlWmLFeS8upv36sy5VpI8= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/_demo/go/syscallraw/main.go b/_demo/go/syscallraw/main.go index 79bcd2bae6..b17dbcdf4d 100644 --- a/_demo/go/syscallraw/main.go +++ b/_demo/go/syscallraw/main.go @@ -1,3 +1,5 @@ +//go:build !windows + package main import ( diff --git a/_demo/go/syscallraw/main_windows.go b/_demo/go/syscallraw/main_windows.go new file mode 100644 index 0000000000..aa8f7902db --- /dev/null +++ b/_demo/go/syscallraw/main_windows.go @@ -0,0 +1,28 @@ +package main + +import ( + "runtime" + "syscall" + "unsafe" +) + +func main() { + msg := []byte("Hello from Syscall!\n") + writeFile := syscall.NewLazyDLL("kernel32.dll").NewProc("WriteFile") + var written uint32 + r1, _, callErr := syscall.SyscallN( + writeFile.Addr(), + uintptr(syscall.Stdout), + uintptr(unsafe.Pointer(&msg[0])), + uintptr(len(msg)), + uintptr(unsafe.Pointer(&written)), + 0, + ) + runtime.KeepAlive(msg) + if r1 == 0 { + panic(callErr) + } + if written != uint32(len(msg)) { + panic("short WriteFile") + } +} diff --git a/_demo/go/sysopen-1654/main.go b/_demo/go/sysopen-1654/main.go index 1cd255361f..7380417ae8 100644 --- a/_demo/go/sysopen-1654/main.go +++ b/_demo/go/sysopen-1654/main.go @@ -1,3 +1,5 @@ +//go:build !windows + package main import ( diff --git a/_demo/go/sysopen-1654/main_windows.go b/_demo/go/sysopen-1654/main_windows.go new file mode 100644 index 0000000000..0d2e034ff1 --- /dev/null +++ b/_demo/go/sysopen-1654/main_windows.go @@ -0,0 +1,33 @@ +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "syscall" +) + +// Regression test for syscall.Open failure on Windows. CreateFile with +// CREATE_NEW must return InvalidHandle and ERROR_FILE_EXISTS for an existing +// path. +func main() { + path := filepath.Join(os.TempDir(), fmt.Sprintf("sysopen-1654-%d.tmp", os.Getpid())) + if err := os.WriteFile(path, []byte("x"), 0o600); err != nil { + panic(fmt.Sprintf("prepare temp file failed: %v", err)) + } + defer os.Remove(path) + + fd, err := syscall.Open(path, syscall.O_CREAT|syscall.O_EXCL|syscall.O_RDWR, 0o600) + if err == nil { + panic(fmt.Sprintf("unexpected nil error: fd=%#x", uintptr(fd))) + } + if fd != syscall.InvalidHandle { + panic(fmt.Sprintf("unexpected handle on failure: got=%#x want=%#x err=%v", uintptr(fd), uintptr(syscall.InvalidHandle), err)) + } + if !errors.Is(err, syscall.ERROR_FILE_EXISTS) { + panic(fmt.Sprintf("unexpected error: got=%v want=%v", err, syscall.ERROR_FILE_EXISTS)) + } + + fmt.Println("ok") +} diff --git a/_demo/py/go.mod b/_demo/py/go.mod index 177eabce91..4151f2f1ad 100644 --- a/_demo/py/go.mod +++ b/_demo/py/go.mod @@ -3,3 +3,5 @@ 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-20260827141156-c40a4eb3593b diff --git a/_demo/py/go.sum b/_demo/py/go.sum index 512980a575..28ecde6430 100644 --- a/_demo/py/go.sum +++ b/_demo/py/go.sum @@ -1,2 +1,4 @@ -github.com/goplus/lib v0.2.0 h1:AjqkN1XK5H23wZMMlpaUYAMCDAdSBQ2NMFrLtSh7W4g= -github.com/goplus/lib v0.2.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225 h1:OtpnoHPW/7IolIhBEkwe4KDTo6Mf0st+CScYJX/h8XY= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b h1:H0ZWQZkfp79M09RhzaDGRPBlWmLFeS8upv36sy5VpI8= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/_xtool/go.mod b/_xtool/go.mod index b37d2e761d..1720131501 100644 --- a/_xtool/go.mod +++ b/_xtool/go.mod @@ -3,3 +3,5 @@ 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-20260827141156-c40a4eb3593b diff --git a/_xtool/go.sum b/_xtool/go.sum index 512980a575..28ecde6430 100644 --- a/_xtool/go.sum +++ b/_xtool/go.sum @@ -1,2 +1,4 @@ -github.com/goplus/lib v0.2.0 h1:AjqkN1XK5H23wZMMlpaUYAMCDAdSBQ2NMFrLtSh7W4g= -github.com/goplus/lib v0.2.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225 h1:OtpnoHPW/7IolIhBEkwe4KDTo6Mf0st+CScYJX/h8XY= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b h1:H0ZWQZkfp79M09RhzaDGRPBlWmLFeS8upv36sy5VpI8= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/benchmark/baseline/main.go b/benchmark/baseline/main.go index 94cbf4f01a..672300f6ba 100644 --- a/benchmark/baseline/main.go +++ b/benchmark/baseline/main.go @@ -22,6 +22,7 @@ import ( "context" "debug/elf" "debug/macho" + "debug/pe" "encoding/json" "errors" "flag" @@ -219,25 +220,21 @@ func collect(ctx context.Context, root, llgo, out string, buildRuns, runRuns int return err } - env := append(os.Environ(), - "GOMAXPROCS=2", - "LLGO_ROOT="+root, - "LLGO_FULL_RPATH=true", - ) + env := benchmarkEnv(root) var sizes, timings []metric for _, item := range workloads { - binary := filepath.Join(binDir, item.name) + binary := nativeExecutable(filepath.Join(binDir, item.name)) buildArgs := append([]string{"build"}, item.flags...) 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 := run(ctx, env, io.Discard, llgo, buildArgs...); err != nil { + if err := runQuiet(ctx, 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 := run(ctx, env, io.Discard, llgo, buildArgs...); err != nil { + if err := runMeasured(ctx, env, llgo, buildArgs...); err != nil { return fmt.Errorf("build %s: %w", item.name, err) } buildDurations = append(buildDurations, time.Since(start)) @@ -279,6 +276,45 @@ func collect(ctx context.Context, root, llgo, out string, buildRuns, runRuns int return writeMetrics(filepath.Join(out, "time.json"), timings) } +func benchmarkEnv(root string) []string { + env := append(os.Environ(), + "GOMAXPROCS=2", + "LLGO_ROOT="+root, + "LLGO_FULL_RPATH=true", + ) + if runtime.GOOS == "windows" { + // Windows programs terminate through ExitProcess, which does not run + // UCRT stream teardown when benchmark output is captured by a pipe. + env = append(env, "LLGO_STDIO_NOBUF=1") + } + return env +} + +func runQuiet(ctx context.Context, env []string, name string, args ...string) error { + var output bytes.Buffer + if err := run(ctx, env, &output, name, args...); err != nil { + if detail := strings.TrimSpace(output.String()); detail != "" { + return fmt.Errorf("%w\n%s", err, detail) + } + return err + } + return nil +} + +// runMeasured keeps the successful benchmark path free of diagnostic buffer +// 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...) + if err == nil { + return nil + } + if detailErr := runQuiet(ctx, env, name, args...); detailErr != nil { + return detailErr + } + return err +} + func run(ctx context.Context, env []string, output io.Writer, name string, args ...string) error { cmd := exec.CommandContext(ctx, name, args...) cmd.Env = env @@ -340,9 +376,22 @@ func executableFootprint(path string) (footprint, error) { return out, nil } + if f, err := pe.Open(path); err == nil { + defer f.Close() + addPESections(&out, f.Sections) + return out, nil + } + return footprint{}, fmt.Errorf("unsupported executable format: %s", path) } +func nativeExecutable(path string) string { + if runtime.GOOS == "windows" && filepath.Ext(path) == "" { + return path + ".exe" + } + return path +} + func addELFSections(out *footprint, sections []*elf.Section) { for _, section := range sections { if section.Flags&elf.SHF_ALLOC == 0 { @@ -373,6 +422,23 @@ func addMachOSections(out *footprint, sections []*macho.Section) { } } +func addPESections(out *footprint, sections []*pe.Section) { + for _, section := range sections { + size := uint64(section.VirtualSize) + if size == 0 { + size = uint64(section.Size) + } + switch { + case section.Characteristics&pe.IMAGE_SCN_CNT_UNINITIALIZED_DATA != 0: + out.bss += size + case section.Characteristics&(pe.IMAGE_SCN_CNT_CODE|pe.IMAGE_SCN_MEM_EXECUTE) != 0: + out.text += size + case section.Characteristics&pe.IMAGE_SCN_CNT_INITIALIZED_DATA != 0: + out.data += size + } + } +} + func validateArtifact(dir string) error { sizeNames := make(map[string]string, len(workloads)*4) timeNames := make(map[string]string, len(workloads)*2) diff --git a/benchmark/baseline/main_test.go b/benchmark/baseline/main_test.go index 7f060a0881..459e04ae86 100644 --- a/benchmark/baseline/main_test.go +++ b/benchmark/baseline/main_test.go @@ -21,6 +21,7 @@ import ( "context" "debug/elf" "debug/macho" + "debug/pe" "encoding/json" "errors" "fmt" @@ -28,6 +29,7 @@ import ( "math" "os" "path/filepath" + "runtime" "slices" "strings" "testing" @@ -274,6 +276,48 @@ func TestAddMachOSections(t *testing.T) { } } +func TestAddPESections(t *testing.T) { + var got footprint + addPESections(&got, []*pe.Section{ + {SectionHeader: pe.SectionHeader{VirtualSize: 10, Characteristics: pe.IMAGE_SCN_CNT_CODE}}, + {SectionHeader: pe.SectionHeader{VirtualSize: 20, Characteristics: pe.IMAGE_SCN_MEM_EXECUTE}}, + {SectionHeader: pe.SectionHeader{VirtualSize: 30, Characteristics: pe.IMAGE_SCN_CNT_INITIALIZED_DATA}}, + {SectionHeader: pe.SectionHeader{VirtualSize: 40, Characteristics: pe.IMAGE_SCN_CNT_UNINITIALIZED_DATA}}, + {SectionHeader: pe.SectionHeader{Size: 50, Characteristics: pe.IMAGE_SCN_CNT_INITIALIZED_DATA}}, + {SectionHeader: pe.SectionHeader{VirtualSize: 99}}, + }) + if got.text != 30 || got.data != 80 || got.bss != 40 { + t.Fatalf("PE footprint = %+v", got) + } +} + +func TestWindowsBenchmarkConfiguration(t *testing.T) { + t.Setenv("LLGO_STDIO_NOBUF", "") + + wantExecutable := filepath.Join("tmp", "program") + wantNoBuf := false + if runtime.GOOS == "windows" { + wantExecutable += ".exe" + wantNoBuf = true + } + if got := nativeExecutable(filepath.Join("tmp", "program")); got != wantExecutable { + t.Fatalf("nativeExecutable() = %q, want %q", got, wantExecutable) + } + if got := nativeExecutable(filepath.Join("tmp", "program.exe")); got != filepath.Join("tmp", "program.exe") { + t.Fatalf("nativeExecutable() with extension = %q", got) + } + + hasNoBuf := false + for _, value := range benchmarkEnv("test-root") { + if value == "LLGO_STDIO_NOBUF=1" { + hasNoBuf = true + } + } + if hasNoBuf != wantNoBuf { + t.Fatalf("benchmarkEnv() has LLGO_STDIO_NOBUF = %v, want %v", hasNoBuf, wantNoBuf) + } +} + func TestCollect(t *testing.T) { if os.PathSeparator != '/' { t.Skip("fake compiler uses a POSIX shell") @@ -418,6 +462,35 @@ func TestRunReportsCommand(t *testing.T) { } } +func TestRunQuietIncludesCommandOutput(t *testing.T) { + err := runQuiet( + context.Background(), + os.Environ(), + "go", + "definitely-not-a-go-command", + ) + if err == nil || !strings.Contains(err.Error(), "unknown command") { + t.Fatalf("runQuiet error = %v", err) + } + + err = runQuiet(context.Background(), os.Environ(), "definitely-not-an-llgo-command") + if err == nil || !strings.Contains(err.Error(), "definitely-not-an-llgo-command") { + t.Fatalf("runQuiet error without command output = %v", err) + } +} + +func TestRunMeasuredIncludesCommandOutputAfterFailure(t *testing.T) { + err := runMeasured( + context.Background(), + os.Environ(), + "go", + "definitely-not-a-go-command", + ) + if err == nil || !strings.Contains(err.Error(), "unknown command") { + t.Fatalf("runMeasured error = %v", err) + } +} + func TestValidateArtifactReportsMissingFiles(t *testing.T) { dir := t.TempDir() if err := validateArtifact(dir); err == nil || !strings.Contains(err.Error(), "size.json") { diff --git a/benchmark/baseline/run.sh b/benchmark/baseline/run.sh index c0ffdf02ec..0738a2d9a6 100755 --- a/benchmark/baseline/run.sh +++ b/benchmark/baseline/run.sh @@ -14,6 +14,12 @@ mkdir -p "$(dirname "$2")" "$3" llgo_output="$(cd "$(dirname "$2")" && pwd)/$(basename "$2")" result_directory="$(cd "$3" && pwd)" +# An explicit output path is not given the platform suffix by go build. Keep +# the compiler discoverable by both Bash and native Windows subprocesses. +if [[ "$(go env GOOS)" == windows && "$llgo_output" != *.exe ]]; then + llgo_output+=.exe +fi + ( cd "$source_root" LLGO_ROOT="$source_root" go build -p=1 -o "$llgo_output" ./cmd/llgo diff --git a/cl/_testgo/cgopython/cgopython.go b/cl/_testgo/cgopython/cgopython.go index da81e0d596..7aec60bdbc 100644 --- a/cl/_testgo/cgopython/cgopython.go +++ b/cl/_testgo/cgopython/cgopython.go @@ -2,11 +2,11 @@ package main /* -#cgo !windows pkg-config: python3-embed +#cgo pkg-config: python3-embed #ifdef _WIN32 -// This compile-only fixture checks LLGo's C-call and defer lowering; it does -// not link or execute Python. Keep its Windows declaration surface local so -// the compiler test does not depend on a separately installed Python SDK. +// Keep the Windows declaration surface local so cross-target IR tests do not +// depend on Windows Python SDK headers. Native runs still obtain the Python +// import library from python3-embed above. void Py_Initialize(void); void Py_Finalize(void); int PyRun_SimpleString(const char *command); diff --git a/cl/_testlibc/once/in.go b/cl/_testlibc/once/in.go index f601e714f0..f8af8fd1ad 100644 --- a/cl/_testlibc/once/in.go +++ b/cl/_testlibc/once/in.go @@ -6,13 +6,13 @@ import ( "github.com/goplus/lib/c/pthread/sync" ) -// The C-backed Once implementation must retain a concrete callback rather -// than duplicating the closure body at each call site. POSIX lowers directly -// to pthread_once; Windows calls the shared INIT_ONCE adapter. +// The C-backed Once implementation accepts the same bare C callback on every +// host. POSIX lowers directly to pthread_once; Windows calls the shared +// INIT_ONCE adapter without exposing the Go closure representation. // CHECK-LABEL: define void @main.f(){{.*}} { // DARWIN: call i32 @pthread_once(ptr @main.once, ptr @"main.f$1") // LINUX: call i32 @pthread_once(ptr @main.once, ptr @"main.f$1") -// WINDOWS: call i32 @"github.com/goplus/lib/c/pthread/sync.(*Once).Do"(ptr @main.once, { ptr, ptr } { ptr @"main.f$1", ptr null }) +// WINDOWS: call i32 @"github.com/goplus/lib/c/pthread/sync.(*Once).Do"(ptr @main.once, ptr @"main.f$1") // CHECK-NEXT: ret void // CHECK-LABEL: define void @"main.f$1"(){{.*}} { // CHECK: call i32 (ptr, ...) @printf(ptr @{{[0-9]+}}) diff --git a/cl/_testlibc/setjmp/in.go b/cl/_testlibc/setjmp/in.go index a4ad9bb109..8793536eaf 100644 --- a/cl/_testlibc/setjmp/in.go +++ b/cl/_testlibc/setjmp/in.go @@ -22,7 +22,7 @@ import ( // CHECK: {{^_llgo_[0-9]+:}} // DARWIN-ARM64: [[STDERR:%[0-9]+]] = load ptr, ptr @__stderrp // LINUX-AMD64: [[STDERR:%[0-9]+]] = load ptr, ptr @stderr -// WINDOWS: [[STDERR:%[0-9]+]] = load ptr, ptr @stderr +// WINDOWS: [[STDERR:%[0-9]+]] = load ptr, ptr @"github.com/goplus/lib/c.Stderr" // CHECK-NEXT: call i32 (ptr, ptr, ...) @fprintf(ptr [[STDERR]], ptr @{{[0-9]+}}, ptr getelementptr (i8, ptr getelementptr (i8, ptr @{{[0-9]+}}, i{{32|64}} 1), i{{32|64}} 1)) // DARWIN-NEXT: call void @siglongjmp(ptr [[JMPBUF]], i32 1) // LINUX-NEXT: call void @siglongjmp(ptr [[JMPBUF]], i32 1) diff --git a/cmd/internal/lldb/lldb.go b/cmd/internal/lldb/lldb.go index 32d8abdf6d..86ddfc282f 100644 --- a/cmd/internal/lldb/lldb.go +++ b/cmd/internal/lldb/lldb.go @@ -33,7 +33,10 @@ import ( "github.com/xgo-dev/llgo/internal/mockable" ) -const minimumUpstreamLLDBVersion = 18 +const ( + minimumUpstreamLLDBVersion = 18 + configureTargetCommand = `script llgo_plugin.configure_target(lldb.debugger)` +) var ( //go:embed llgo_plugin.py @@ -92,8 +95,9 @@ func run(configuredPath string, args []string, stdin io.Reader, stdout, stderr i return fmt.Errorf("llgo lldb: write plugin: %w", err) } - lldbArgs := make([]string, 0, len(args)+2) + lldbArgs := make([]string, 0, len(args)+4) lldbArgs = append(lldbArgs, "-O", lldbImportCommand(pluginPath)) + lldbArgs = append(lldbArgs, "-o", configureTargetCommand) lldbArgs = append(lldbArgs, args...) command := exec.Command(path, lldbArgs...) diff --git a/cmd/internal/lldb/lldb_test.go b/cmd/internal/lldb/lldb_test.go index 7dbc428c93..e573e8e738 100644 --- a/cmd/internal/lldb/lldb_test.go +++ b/cmd/internal/lldb/lldb_test.go @@ -129,7 +129,7 @@ grep -q __llgo_debugger_marker_v1 "$plugin" t.Fatal(err) } got := string(data) - for _, want := range []string{"-O\n", "command script import \"", "--batch\n", "./program\n", "-o\n", "run\n"} { + for _, want := range []string{"-O\n", "command script import \"", "-o\n", configureTargetCommand + "\n", "--batch\n", "./program\n", "run\n"} { if !strings.Contains(got, want) { t.Fatalf("LLDB arguments %q do not contain %q", got, want) } @@ -173,6 +173,7 @@ func TestEmbeddedPluginIdentity(t *testing.T) { "__llgo_debugger_marker_v1", "is_llgo_compiler", "inspect_target", + "configure_target", "LLGO_DEBUGGER_SCHEMAS", "LLGO_RUNTIME_LAYOUTS", "string_summary", diff --git a/cmd/internal/lldb/llgo_plugin.py b/cmd/internal/lldb/llgo_plugin.py index 843c77cf57..ad34479c63 100644 --- a/cmd/internal/lldb/llgo_plugin.py +++ b/cmd/internal/lldb/llgo_plugin.py @@ -111,7 +111,13 @@ def register_type_formatters(debugger: lldb.SBDebugger) -> None: lldb.SBTypeSynthetic.CreateWithClassName( "llgo_plugin.SliceSyntheticProvider", _type_options()), ) - category.SetEnabled(True) + category.SetEnabled(False) + + +def configure_target(debugger: lldb.SBDebugger) -> None: + category = debugger.GetCategory(LLGO_TYPE_CATEGORY) + category.SetEnabled( + inspect_target(debugger.GetSelectedTarget()).supported) def _marker_versions(target: lldb.SBTarget) -> Tuple[int, ...]: @@ -592,7 +598,7 @@ def print_all_variables(debugger: lldb.SBDebugger, _command: str, result: lldb.S output: List[str] = [] try: for var in variables: - type_name = map_type_name(var.GetType().GetName()) + type_name = go_type_name(var.GetType()) formatted = format_value( var, debugger, include_type=False, indent=0) output.append(f"var {var.GetName()} {type_name} = {formatted}") @@ -614,13 +620,13 @@ def format_value(var: lldb.SBValue, debugger: lldb.SBDebugger, include_type: boo var_type = var.GetType() type_class = var_type.GetTypeClass() - type_name = map_type_name(var_type.GetName()) + type_name = go_type_name(var_type) # Handle typedef types original_type_name = type_name while var_type.IsTypedefType(): var_type = var_type.GetTypedefedType() - type_name = map_type_name(var_type.GetName()) + type_name = go_type_name(var_type) type_class = var_type.GetTypeClass() if var_type.IsPointerType(): @@ -696,7 +702,7 @@ def format_array(var: lldb.SBValue, debugger: lldb.SBDebugger, indent: int) -> s elements.append(value) array_size = var.GetNumChildren() - element_type = map_type_name(var.GetType().GetArrayElementType().GetName()) + element_type = go_type_name(var.GetType().GetArrayElementType()) type_name = f"[{array_size}]{element_type}" if len(elements) > 5: # wrap line if too many elements @@ -741,6 +747,24 @@ def format_pointer(var: lldb.SBValue, _debugger: lldb.SBDebugger, _indent: int, return var.GetValue() # Return the address as a string +TYPE_NAME_MAPPING: Dict[str, str] = { + 'long': 'int', + 'void': 'unsafe.Pointer', + 'char': 'byte', + 'short': 'int16', + 'int': 'int32', + 'long long': 'int64', + 'unsigned char': 'uint8', + 'unsigned short': 'uint16', + 'unsigned int': 'uint32', + 'unsigned long': 'uint', + 'unsigned long long': 'uint64', + 'float': 'float32', + 'double': 'float64', +} +TYPE_NAMES_BY_LENGTH = sorted(TYPE_NAME_MAPPING, key=len, reverse=True) + + def map_type_name(type_name: str) -> str: # Handle pointer types if type_name.endswith('*'): @@ -748,25 +772,19 @@ def map_type_name(type_name: str) -> str: mapped_base_type = map_type_name(base_type) return f"*{mapped_base_type}" - # Map other types - type_mapping: Dict[str, str] = { - 'long': 'int', - 'void': 'unsafe.Pointer', - 'char': 'byte', - 'short': 'int16', - 'int': 'int32', - 'long long': 'int64', - 'unsigned char': 'uint8', - 'unsigned short': 'uint16', - 'unsigned int': 'uint32', - 'unsigned long': 'uint', - 'unsigned long long': 'uint64', - 'float': 'float32', - 'double': 'float64', - } - - for c_type, go_type in type_mapping.items(): + # Prefer longer C spellings so "unsigned long long" is considered before + # its prefixes. The mapping is shared because this runs for every value in + # recursive container formatting. + for c_type in TYPE_NAMES_BY_LENGTH: if type_name.startswith(c_type): - return type_name.replace(c_type, go_type, 1) + return type_name.replace(c_type, TYPE_NAME_MAPPING[c_type], 1) return type_name + + +def go_type_name(value_type: lldb.SBType) -> str: + if value_type.IsPointerType(): + return f"*{go_type_name(value_type.GetPointeeType())}" + if value_type.IsTypedefType(): + return value_type.GetName() + return map_type_name(value_type.GetName()) diff --git a/cmd/llgo/lldbtest/runtest.sh b/cmd/llgo/lldbtest/runtest.sh index 1bb0188e08..04c518af9b 100755 --- a/cmd/llgo/lldbtest/runtest.sh +++ b/cmd/llgo/lldbtest/runtest.sh @@ -43,11 +43,15 @@ build_project "$package_path" || exit 1 test_tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/llgo-lldbtest.XXXXXX") trap 'rm -rf "$test_tmp_dir"' EXIT result_file="$test_tmp_dir/exit-code" +result_file_for_lldb="$result_file" +if command -v cygpath >/dev/null 2>&1; then + result_file_for_lldb=$(cygpath -m "$result_file") +fi # Prepare LLDB commands lldb_commands=( "command script import ./test.py" - "script test.run_tests_with_result('./debug.out', ['main.go'], $verbose, $interactive, $plugin_path, '$result_file')" + "script test.run_tests_with_result('./debug.out', ['main.go'], $verbose, $interactive, $plugin_path, '$result_file_for_lldb')" "quit" ) @@ -75,36 +79,37 @@ if [ "$exit_code" -ne 0 ]; then fi llgo lldb -lldb "$LLDB_PATH" -- --batch "./debug.out" \ - -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.schema_version == 1 and info.runtime_layout_version == 1 and info.pointer_size == lldb.target.GetAddressByteSize() and info.byte_order != "unknown"' \ - -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "LLGo debugger schema v1 (runtime layout v1)" in result.GetOutput()' \ - -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo vars", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()' \ - -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo print s", result); assert not result.Succeeded() and "requires a stopped process" in result.GetError()' + -o 'script import os; info = llgo_plugin.inspect_target(lldb.target); (info.schema_version == 1 and info.runtime_layout_version == 1 and info.pointer_size == lldb.target.GetAddressByteSize() and info.byte_order != "unknown") or os._exit(1)' \ + -o 'script import os; result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); (result.Succeeded() and "LLGo debugger schema v1 (runtime layout v1)" in result.GetOutput()) or os._exit(1)' \ + -o 'script import os; result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo vars", result); (not result.Succeeded() and "requires a stopped process" in result.GetError()) or os._exit(1)' \ + -o 'script import os; result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo print s", result); (not result.Succeeded() and "requires a stopped process" in result.GetError()) or os._exit(1)' # The LLGo formatter must not attach itself to an ordinary C target. non_llgo_dir="$test_tmp_dir/non-llgo" +host_exe_ext=$(go env GOEXE) mkdir -p "$non_llgo_dir" 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" - -llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/non-llgo" \ - -o 'script info = llgo_plugin.inspect_target(lldb.target); assert not info.marker_versions and not info.supported' \ - -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2' \ - -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "Not an LLGo target" in result.GetOutput()' \ - -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); assert result.Succeeded() and "2" in result.GetOutput()' + "${CC:-cc}" -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)' \ + -o 'script import os; result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); (result.Succeeded() and "Not an LLGo target" in result.GetOutput()) or os._exit(1)' \ + -o 'script import os; result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); (result.Succeeded() and "2" in result.GetOutput()) or os._exit(1)' # 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" - -llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/unsupported-llgo" \ - -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.marker_versions == (2,) and not info.supported' \ - -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2' \ - -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetOutput()' \ - -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo vars", result); assert not result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetError()' \ - -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); assert result.Succeeded() and "2" in result.GetOutput()' + "${CC:-cc}" -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)' \ + -o 'script import os; result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); (result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetOutput()) or os._exit(1)' \ + -o 'script import os; result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo vars", result); (not result.Succeeded() and "Unsupported LLGo debugger marker version(s): v2" in result.GetError()) or os._exit(1)' \ + -o 'script import os; result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("p 1+1", result); (result.Succeeded() and "2" in result.GetOutput()) or os._exit(1)' # 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" - -llgo lldb -lldb "$LLDB_PATH" -- --batch "$non_llgo_dir/ambiguous-llgo" \ - -o 'script info = llgo_plugin.inspect_target(lldb.target); assert info.marker_versions == (1, 2) and not info.supported' \ - -o 'script value = lldb.target.FindFirstGlobalVariable("cstring"); assert value.IsValid() and value.GetSummary() is None and value.GetNumChildren() == 2' \ - -o 'script result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); assert result.Succeeded() and "Unsupported LLGo debugger marker version(s): v1, v2" in result.GetOutput()' + "${CC:-cc}" -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)' \ + -o 'script import os; result = lldb.SBCommandReturnObject(); lldb.debugger.GetCommandInterpreter().HandleCommand("llgo status", result); (result.Succeeded() and "Unsupported LLGo debugger marker version(s): v1, v2" in result.GetOutput()) or os._exit(1)' diff --git a/cmd/llgo/lldbtest/test.py b/cmd/llgo/lldbtest/test.py index fa40a129cb..274c79f25d 100644 --- a/cmd/llgo/lldbtest/test.py +++ b/cmd/llgo/lldbtest/test.py @@ -310,6 +310,7 @@ def setup(self) -> None: if not self.target: raise LLDBTestException( f"Failed to create target for {self.executable_path}") + llgo_plugin.configure_target(self.debugger) target_info = llgo_plugin.inspect_target(self.target) if not target_info.supported: diff --git a/dev/_llgo_setup.sh b/dev/_llgo_setup.sh index ffd6aae3c0..91f570ee98 100755 --- a/dev/_llgo_setup.sh +++ b/dev/_llgo_setup.sh @@ -25,12 +25,22 @@ _llgo_require_repo_context() { } _llgo_compute_bin_path() { - local gobin + local gobin gohostos + gohostos="$(cd "${LLGO_ROOT}" && go env GOHOSTOS)" gobin="$(cd "${LLGO_ROOT}" && go env GOBIN)" if [ -z "$gobin" ]; then local gopath_raw gopath_raw="$(cd "${LLGO_ROOT}" && go env GOPATH)" - gobin="${gopath_raw%%:*}/bin" + if [ "$gohostos" = "windows" ]; then + # A drive colon belongs to the path; Windows separates multiple + # GOPATH entries with semicolons. + gobin="${gopath_raw%%;*}/bin" + else + gobin="${gopath_raw%%:*}/bin" + fi + fi + if [ "$gohostos" = "windows" ] && command -v cygpath >/dev/null 2>&1; then + gobin="$(cygpath -u "$gobin")" fi LLGO_BIN="${gobin}/llgo" } diff --git a/dev/build_iwasm.sh b/dev/build_iwasm.sh index e8d8210182..86219eda54 100755 --- a/dev/build_iwasm.sh +++ b/dev/build_iwasm.sh @@ -27,25 +27,44 @@ 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 -# Determine platform -if [[ "$(uname -s)" == "Darwin" ]]; then - PLATFORM="darwin" -elif [[ "$(uname -s)" == "Linux" ]]; then - PLATFORM="linux" -else - echo "Unsupported platform: $(uname -s)" - exit 1 -fi +# 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 + Darwin) + PLATFORM="darwin" + ;; + Linux) + PLATFORM="linux" + ;; + 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 + ) + ;; + *) + echo "Unsupported platform: $(uname -s)" + exit 1 + ;; +esac echo "Building for platform: ${PLATFORM}" mkdir -p wasm-micro-runtime/product-mini/platforms/${PLATFORM}/build cd wasm-micro-runtime/product-mini/platforms/${PLATFORM}/build -# Configure with same options as CI -cmake \ +# The test helper executes Wasm bytecode only, so AOT is unnecessary; LLGo's +# generated modules require reference-types support. +cmake "${CMAKE_GENERATOR_ARGS[@]}" \ -D WAMR_BUILD_EXCE_HANDLING=1 \ + -D WAMR_BUILD_AOT=0 \ -D WAMR_BUILD_FAST_INTERP=0 \ + -D WAMR_BUILD_REF_TYPES=1 \ -D WAMR_BUILD_SHARED_MEMORY=1 \ -D WAMR_BUILD_LIB_WASI_THREADS=1 \ -D WAMR_BUILD_LIB_PTHREAD=1 \ @@ -54,21 +73,21 @@ cmake \ .. echo "Compiling iwasm..." -make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) +cmake --build . --parallel "$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)" # Copy iwasm to cache directory echo "Installing iwasm to ${IWASM_BIN_DIR}..." -cp iwasm "${IWASM_BIN_DIR}/" +cp "${IWASM_NAME}" "${IWASM_BIN_DIR}/" # Cleanup cd / rm -rf "${TEMP_DIR}" echo "" -echo "✓ iwasm successfully built and installed to ${IWASM_BIN_DIR}/iwasm" +echo "✓ iwasm successfully built and installed to ${IWASM_BIN_DIR}/${IWASM_NAME}" echo "" echo "To use this iwasm, add to your PATH:" echo " export PATH=\"${IWASM_BIN_DIR}:\$PATH\"" echo "" echo "Or run directly:" -echo " ${IWASM_BIN_DIR}/iwasm --version" +echo " ${IWASM_BIN_DIR}/${IWASM_NAME} --version" diff --git a/dev/test_goroot.sh b/dev/test_goroot.sh index f611751f86..ec6e2eeb5e 100755 --- a/dev/test_goroot.sh +++ b/dev/test_goroot.sh @@ -60,6 +60,9 @@ run_with_heartbeat() { for goroot in "${goroots[@]}"; do go_bin="$goroot/bin/go" + if [[ "${OS:-}" == "Windows_NT" ]]; then + go_bin+=".exe" + fi if [[ ! -x "$go_bin" ]]; then echo "error: missing go binary: $go_bin" >&2 exit 2 diff --git a/doc/_readme/go.mod b/doc/_readme/go.mod index f761063f29..a6cb011f00 100644 --- a/doc/_readme/go.mod +++ b/doc/_readme/go.mod @@ -3,3 +3,5 @@ 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-20260827141156-c40a4eb3593b diff --git a/doc/_readme/go.sum b/doc/_readme/go.sum index 512980a575..28ecde6430 100644 --- a/doc/_readme/go.sum +++ b/doc/_readme/go.sum @@ -1,2 +1,4 @@ -github.com/goplus/lib v0.2.0 h1:AjqkN1XK5H23wZMMlpaUYAMCDAdSBQ2NMFrLtSh7W4g= -github.com/goplus/lib v0.2.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225 h1:OtpnoHPW/7IolIhBEkwe4KDTo6Mf0st+CScYJX/h8XY= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b h1:H0ZWQZkfp79M09RhzaDGRPBlWmLFeS8upv36sy5VpI8= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/doc/_readme/scripts/run.sh b/doc/_readme/scripts/run.sh index 3c8b834f52..0dbb6c3b87 100644 --- a/doc/_readme/scripts/run.sh +++ b/doc/_readme/scripts/run.sh @@ -2,13 +2,21 @@ DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) cd "$DIR" || exit 1 -python3 -m venv .venv +python_cmd=python3 +if [[ "${OS:-}" == "Windows_NT" ]]; then + python_cmd=python +fi +"$python_cmd" -m venv .venv # shellcheck source=/dev/null -source .venv/bin/activate +if [[ -f .venv/Scripts/activate ]]; then + source .venv/Scripts/activate +else + source .venv/bin/activate +fi pip3 install numpy PYTHONPATH="" -PYTHONPATH=$(python -c "import sys; print(':'.join(sys.path))") +PYTHONPATH=$(python -c "import os, sys; print(os.pathsep.join(sys.path))") export PYTHONPATH for sub in ./*/; do diff --git a/go.mod b/go.mod index 0a5e153a2f..0e9b91f34a 100644 --- a/go.mod +++ b/go.mod @@ -26,3 +26,5 @@ require ( ) replace github.com/xgo-dev/llgo/runtime => ./runtime + +replace github.com/goplus/lib => github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b diff --git a/go.sum b/go.sum index 5307d03482..b5bb8f9499 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225 h1:OtpnoHPW/7IolIhBEkwe4KDTo6Mf0st+CScYJX/h8XY= +github.com/cpunion/lib v0.0.0-20260822071805-ebe22363a225/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b h1:H0ZWQZkfp79M09RhzaDGRPBlWmLFeS8upv36sy5VpI8= +github.com/cpunion/lib v0.0.0-20260827141156-c40a4eb3593b/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -8,8 +12,6 @@ github.com/goplus/cobra v1.9.12 h1:0F9EdEbeGyITGz+mqoHoJ5KpUw97p1CkxV74IexHw5s= github.com/goplus/cobra v1.9.12/go.mod h1:p4LhfNJDKEpiGjGiNn0crUXL5dUPA5DX2ztYpEJR34E= github.com/goplus/gogen v1.23.5 h1:76w3zmAHI+ECI7bPr0enUd0du9+t1IYyXmp43CbIpSs= github.com/goplus/gogen v1.23.5/go.mod h1:Y7ulYW3wonQ3d9er00b0uGFEV/IUZa6okWJZh892ACQ= -github.com/goplus/lib v0.3.1 h1:Xws4DBVvgOMu58awqB972wtvTacDbk3nqcbHjdx9KSg= -github.com/goplus/lib v0.3.1/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= github.com/goplus/mod v0.22.0 h1:knZCdR5m2Nr1/cU1XqG1lND4USG6mXxx/Ca272RYjQk= github.com/goplus/mod v0.22.0/go.mod h1:APrczG2FtFcQelU4vTq9xw+GrVs4sPPKnRfGidWBlXY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 207f30b479..b3afefe473 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -1171,6 +1171,9 @@ func TestRunPrintfWithStdioNobuf(t *testing.T) { func TestTestOutputFileLogic(t *testing.T) { // Test output file path determination logic for test mode + outputDir := filepath.Join(t.TempDir(), "output") + outputFile := filepath.Join(outputDir, "mytest.test") + directoryOutput := outputDir + string(filepath.Separator) tests := []struct { name string pkgName string @@ -1192,10 +1195,10 @@ func TestTestOutputFileLogic(t *testing.T) { { name: "with -o absolute file path", pkgName: "mypackage", - conf: &Config{Mode: ModeTest, OutFile: "/tmp/mytest.test", AppExt: ".test"}, + conf: &Config{Mode: ModeTest, OutFile: outputFile, AppExt: ".test"}, multiPkg: false, wantBase: "mytest", - wantDir: "/tmp", + wantDir: outputDir, description: "-o with absolute file path: use specified file", }, { @@ -1210,10 +1213,10 @@ func TestTestOutputFileLogic(t *testing.T) { { name: "with -o directory", pkgName: "mypackage.test", - conf: &Config{Mode: ModeTest, OutFile: "/tmp/build/", AppExt: ".test"}, + conf: &Config{Mode: ModeTest, OutFile: directoryOutput, AppExt: ".test"}, multiPkg: false, wantBase: "mypackage.test", - wantDir: "/tmp/build/", + wantDir: directoryOutput, description: "-o with directory: write pkg.test in that directory", }, { diff --git a/internal/build/macho_size.go b/internal/build/macho_size.go index cfa82b62b8..695284c83e 100644 --- a/internal/build/macho_size.go +++ b/internal/build/macho_size.go @@ -39,6 +39,7 @@ type darwinSizeCommand func(name string, args ...string) ([]byte, error) type darwinSizeFileOps struct { stat func(string) (os.FileInfo, error) open func(string) (*os.File, error) + closeFile func(*os.File) error createTemp func(string, string) (*os.File, error) openFile func(string, int, os.FileMode) (*os.File, error) rename func(string, string) error @@ -48,6 +49,7 @@ func darwinSizeOSFileOps() darwinSizeFileOps { return darwinSizeFileOps{ stat: os.Stat, open: os.Open, + closeFile: (*os.File).Close, createTemp: os.CreateTemp, openFile: os.OpenFile, rename: os.Rename, @@ -154,7 +156,11 @@ func stripAndSignDarwinLocalsUsing(path string, verbose bool, run darwinSizeComm if err != nil { return err } - defer source.Close() + defer func() { + if source != nil { + _ = files.closeFile(source) + } + }() tmp, err := files.createTemp(filepath.Dir(path), "."+filepath.Base(path)+".strip-*") if err != nil { @@ -171,6 +177,12 @@ func stripAndSignDarwinLocalsUsing(path string, verbose bool, run darwinSizeComm if _, err := io.Copy(tmp, source); err != nil { return err } + // Windows does not replace an existing path while that file still has an + // open handle. The staged copy no longer needs the original after io.Copy. + if err := files.closeFile(source); err != nil { + return err + } + source = nil if err := tmp.Sync(); err != nil { return err } diff --git a/internal/build/macho_size_test.go b/internal/build/macho_size_test.go index 55eece7a76..20f63fcad9 100644 --- a/internal/build/macho_size_test.go +++ b/internal/build/macho_size_test.go @@ -184,6 +184,10 @@ func TestStripAndSignDarwinLocalsStaging(t *testing.T) { t.Run("success", func(t *testing.T) { path := newExecutable(t) + before, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } var commands []string run := func(name string, args ...string) ([]byte, error) { commands = append(commands, name) @@ -206,8 +210,8 @@ func TestStripAndSignDarwinLocalsStaging(t *testing.T) { if err != nil { t.Fatal(err) } - if st.Mode().Perm() != 0o751 { - t.Fatalf("final executable mode = %v, want 0751", st.Mode().Perm()) + if st.Mode().Perm() != before.Mode().Perm() { + t.Fatalf("final executable mode = %v, want original %v", st.Mode().Perm(), before.Mode().Perm()) } }) @@ -298,6 +302,18 @@ func TestStripAndSignDarwinLocalsFileFailures(t *testing.T) { } }, }, + { + name: "close-source", + mutate: func(files *darwinSizeFileOps) { + closeFile := files.closeFile + files.closeFile = func(file *os.File) error { + if err := closeFile(file); err != nil { + return err + } + return fileFailure + } + }, + }, { name: "open-signed-stage", mutate: func(files *darwinSizeFileOps) { diff --git a/internal/build/outputs.go b/internal/build/outputs.go index f82c214fae..26ba83ee69 100644 --- a/internal/build/outputs.go +++ b/internal/build/outputs.go @@ -73,7 +73,8 @@ func determineBaseNameAndDir(pkgName string, conf *Config, multiPkg bool) (baseN case ModeTest: if conf.OutFile != "" { // Handle -o flag for test mode - if strings.HasSuffix(conf.OutFile, "/") || isDir(conf.OutFile) { + if strings.HasSuffix(conf.OutFile, "/") || + strings.HasSuffix(conf.OutFile, `\`) || isDir(conf.OutFile) { // If OutFile ends in / or is a directory, write pkg.test in that directory // pkgName for test packages already includes .test suffix return pkgName, conf.OutFile diff --git a/internal/clang/clang.go b/internal/clang/clang.go index 81b537ef57..b57253d4f0 100644 --- a/internal/clang/clang.go +++ b/internal/clang/clang.go @@ -23,6 +23,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "strings" "github.com/xgo-dev/llgo/xtool/safesplit" @@ -123,9 +124,65 @@ func (c *Cmd) Link(args ...string) error { allArgs := make([]string, 0, len(flags)+len(args)) allArgs = append(allArgs, flags...) allArgs = append(allArgs, args...) + allArgs = resolveMSVCImportLibraries(c.Dir, allArgs) 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. +func resolveMSVCImportLibraries(baseDir string, args []string) []string { + if !slices.ContainsFunc(args, func(arg string) bool { + return strings.Contains(arg, "-windows-msvc") + }) { + return args + } + var dirs []string + for i, arg := range args { + switch { + case arg == "-L" && i+1 < len(args): + dirs = append(dirs, args[i+1]) + case strings.HasPrefix(arg, "-L") && len(arg) > 2: + dirs = append(dirs, arg[2:]) + } + } + resolved := args + changed := false + for i, arg := range args { + if !strings.HasPrefix(arg, "-l") || len(arg) <= 2 || arg[2] == ':' { + continue + } + name := arg[2:] + if findLibrary(baseDir, dirs, name+".lib") != "" { + continue + } + if archive := findLibrary(baseDir, dirs, "lib"+name+".dll.a"); archive != "" { + if !changed { + resolved = slices.Clone(args) + changed = true + } + resolved[i] = archive + } + } + return resolved +} + +func findLibrary(baseDir string, dirs []string, name string) string { + for _, dir := range dirs { + path := filepath.Join(dir, name) + if !filepath.IsAbs(path) { + path = filepath.Join(baseDir, path) + } + if info, err := os.Stat(path); err == nil && !info.IsDir() { + return path + } + } + return "" +} + // mergeCompilerFlags merges environment CCFLAGS/CFLAGS with config flags. func (c *Cmd) mergeCompilerFlags() []string { var flags []string diff --git a/internal/clang/clang_test.go b/internal/clang/clang_test.go index e82f530f7f..08105bd484 100644 --- a/internal/clang/clang_test.go +++ b/internal/clang/clang_test.go @@ -28,6 +28,7 @@ import ( "path/filepath" "reflect" "runtime" + "slices" "strings" "testing" ) @@ -97,6 +98,37 @@ func TestWriteGNUResponseArg(t *testing.T) { } } +func TestResolveMSVCImportLibraries(t *testing.T) { + root := t.TempDir() + for _, dir := range []string{"first", "second"} { + if err := os.Mkdir(filepath.Join(root, dir), 0o755); err != nil { + t.Fatal(err) + } + } + clangImport := filepath.Join(root, "first", "libclang.dll.a") + for _, file := range []string{ + clangImport, + filepath.Join(root, "first", "libnative.dll.a"), + filepath.Join(root, "second", "native.lib"), + } { + if err := os.WriteFile(file, nil, 0o644); err != nil { + t.Fatal(err) + } + } + args := []string{"-target", "x86_64-pc-windows-msvc", "-Lfirst", "-L", "second", "-lclang", "-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"} + if got := resolveMSVCImportLibraries(root, args); !slices.Equal(got, want) { + t.Fatalf("resolved libraries = %q, want %q", got, want) + } + args[1] = "x86_64-w64-windows-gnu" + if got := resolveMSVCImportLibraries(root, args); !slices.Equal(got, args) { + t.Fatalf("GNU target libraries changed to %q", got) + } +} + func TestWriteResponseFile(t *testing.T) { args := []string{"plain", `C:\path with spaces\object.o`, `quote"and\slash`, `trailing\`, ""} for _, tt := range []struct { diff --git a/internal/crosscompile/compile/compile.go b/internal/crosscompile/compile/compile.go index ee499a1f70..5af6efa3ca 100644 --- a/internal/crosscompile/compile/compile.go +++ b/internal/crosscompile/compile/compile.go @@ -66,35 +66,74 @@ func (g CompileGroup) Compile( for _, file := range g.Files { var tempObjFile *os.File - tempObjFile, err = os.CreateTemp(tmpCompileDir, fmt.Sprintf("%s*.o", strings.ReplaceAll(file, string(os.PathSeparator), "-"))) + tempObjFile, err = os.CreateTemp(tmpCompileDir, objectFilePattern(file)) if err != nil { return } + tempObjName := tempObjFile.Name() + if err = tempObjFile.Close(); err != nil { + return + } lang := "c" if filepath.Ext(file) == ".S" { lang = "assembler-with-cpp" } - err = compiler.Compile("-o", tempObjFile.Name(), "-x", lang, "-c", file) + err = compiler.Compile("-o", tempObjName, "-x", lang, "-c", file) if err != nil { return } - objFiles = append(objFiles, tempObjFile.Name()) + objFiles = append(objFiles, tempObjName) } - args := []string{"rcs", archive} - args = append(args, objFiles...) - ccDir := filepath.Dir(options.CC) llvmAr := filepath.Join(ccDir, "llvm-ar") - cmd := exec.Command(llvmAr, args...) - // TODO(MeteorsLiu): support verbose - // cmd.Stdout = os.Stdout - // cmd.Stderr = os.Stderr - err = cmd.Run() - return + responseFile, err := writeArchiveResponseFile(tmpCompileDir, objFiles) + if err != nil { + return err + } + // newlib contains hundreds of object files, whose expanded paths exceed + // Windows' CreateProcess command-line limit. LLVM tools support response + // files on every host, so keep the object list out of the process command + // line rather than splitting one archive update into platform-only batches. + cmd := exec.Command(llvmAr, "rcs", archive, "@"+responseFile) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err = cmd.Run(); err != nil { + return fmt.Errorf("llvm-ar rcs %s: %w", archive, err) + } + return nil +} + +func writeArchiveResponseFile(dir string, objFiles []string) (string, error) { + var contents strings.Builder + for _, objFile := range objFiles { + // LLVM's response-file parser accepts forward slashes on Windows. Quote + // each argument so temporary roots containing spaces remain one path. + contents.WriteByte('"') + contents.WriteString(filepath.ToSlash(objFile)) + contents.WriteString("\"\n") + } + responseFile := filepath.Join(dir, "objects.rsp") + if err := os.WriteFile(responseFile, []byte(contents.String()), 0o600); err != nil { + return "", err + } + return responseFile, nil +} + +func objectFilePattern(source string) string { + name := filepath.Base(source) + name = strings.Map(func(r rune) rune { + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '.', r == '-', r == '_': + return r + default: + return '-' + } + }, name) + return fmt.Sprintf("%s-*.o", name) } // CompileConfig represents compilation configuration diff --git a/internal/crosscompile/compile/compile_test.go b/internal/crosscompile/compile/compile_test.go index 3c2b50c5aa..511338bc1e 100644 --- a/internal/crosscompile/compile/compile_test.go +++ b/internal/crosscompile/compile/compile_test.go @@ -3,6 +3,7 @@ package compile import ( + "fmt" "os" "path/filepath" "strings" @@ -209,6 +210,56 @@ func TestCompile(t *testing.T) { }) } +func TestObjectFilePattern(t *testing.T) { + if got, want := objectFilePattern(filepath.Join("source tree", "foo:bar.c")), "foo-bar.c-*.o"; got != want { + t.Fatalf("objectFilePattern = %q, want %q", got, want) + } +} + +func TestWriteArchiveResponseFile(t *testing.T) { + t.Run("quoted object paths", func(t *testing.T) { + dir := t.TempDir() + objFiles := []string{ + filepath.Join(dir, "first.o"), + filepath.Join(dir, "directory with spaces", "second.o"), + } + responseFile, err := writeArchiveResponseFile(dir, objFiles) + if err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(responseFile) + if err != nil { + t.Fatal(err) + } + want := fmt.Sprintf("\"%s\"\n\"%s\"\n", filepath.ToSlash(objFiles[0]), filepath.ToSlash(objFiles[1])) + if string(got) != want { + t.Fatalf("response file = %q, want %q", got, want) + } + }) + + t.Run("write error", func(t *testing.T) { + missingDir := filepath.Join(t.TempDir(), "missing") + if _, err := writeArchiveResponseFile(missingDir, []string{"object.o"}); err == nil { + t.Fatal("writeArchiveResponseFile succeeded in a missing directory") + } + }) +} + +func TestCompileArchiveErrorIncludesContext(t *testing.T) { + outputDir := t.TempDir() + group := CompileGroup{OutputFileName: "broken.a"} + err := group.Compile(outputDir, CompileOptions{ + CC: filepath.Join(t.TempDir(), "missing-clang"), + }) + if err == nil { + t.Fatal("Compile succeeded without llvm-ar") + } + want := "llvm-ar rcs " + filepath.Join(outputDir, group.OutputFileName) + if !strings.Contains(err.Error(), want) { + t.Fatalf("Compile error = %q, want context %q", err, want) + } +} + func TestLibConfig_String(t *testing.T) { tests := []struct { name string diff --git a/internal/crosscompile/compile/libc/libc_test.go b/internal/crosscompile/compile/libc/libc_test.go index 12a11a41a9..74fc640e17 100644 --- a/internal/crosscompile/compile/libc/libc_test.go +++ b/internal/crosscompile/compile/libc/libc_test.go @@ -72,7 +72,7 @@ func TestGetPicolibcConfig_LibConfig(t *testing.T) { } func TestGetPicolibcCompileConfig(t *testing.T) { - baseDir := "/test/base" + baseDir := filepath.FromSlash("/test/base") target := "test-target" config := GetPicolibcCompileConfig(baseDir, target) @@ -187,7 +187,7 @@ func TestGetPicolibcConfig_EdgeCases(t *testing.T) { }) t.Run("EmptyTarget", func(t *testing.T) { - config := GetPicolibcCompileConfig("/test/base", "") + config := GetPicolibcCompileConfig(filepath.FromSlash("/test/base"), "") // Check output file name formatting expectedOutput := "libc-.a" @@ -198,7 +198,7 @@ func TestGetPicolibcConfig_EdgeCases(t *testing.T) { } func TestPicolibcFileStructure(t *testing.T) { - baseDir := "/test/base" + baseDir := filepath.FromSlash("/test/base") target := "test-target" config := GetPicolibcCompileConfig(baseDir, target) @@ -217,11 +217,11 @@ func TestPicolibcFileStructure(t *testing.T) { tinystdioFiles := 0 for _, file := range group.Files { - if strings.Contains(file, "/string/") { + if strings.Contains(file, string(filepath.Separator)+"string"+string(filepath.Separator)) { stringFiles++ - } else if strings.Contains(file, "/stdlib/") { + } else if strings.Contains(file, string(filepath.Separator)+"stdlib"+string(filepath.Separator)) { stdlibFiles++ - } else if strings.Contains(file, "/tinystdio/") { + } else if strings.Contains(file, string(filepath.Separator)+"tinystdio"+string(filepath.Separator)) { tinystdioFiles++ } } @@ -245,7 +245,7 @@ func TestPicolibcFileStructure(t *testing.T) { } func TestPicolibcCompilerFlags(t *testing.T) { - baseDir := "/test/base" + baseDir := filepath.FromSlash("/test/base") target := "test-target" config := GetPicolibcCompileConfig(baseDir, target) @@ -311,7 +311,7 @@ func TestPicolibcCompilerFlags(t *testing.T) { } func TestGetNewlibESP32ConfigRISCV(t *testing.T) { - baseDir := "/test/base" + baseDir := filepath.FromSlash("/test/base") target := "riscv32-unknown-elf" config := getNewlibESP32ConfigRISCV(baseDir, target) @@ -535,7 +535,7 @@ func TestGetNewlibESP32ConfigRISCV(t *testing.T) { } func TestGetNewlibESP32ConfigXtensa(t *testing.T) { - baseDir := "/test/base" + baseDir := filepath.FromSlash("/test/base") target := "xtensa-esp32-elf" config := getNewlibESP32ConfigXtensa(baseDir, target) @@ -654,7 +654,7 @@ func TestEdgeCases(t *testing.T) { }) t.Run("EmptyTarget_RISCV", func(t *testing.T) { - config := getNewlibESP32ConfigRISCV("/test/base", "") + config := getNewlibESP32ConfigRISCV(filepath.FromSlash("/test/base"), "") // Check output file name formatting (first group is libsemihost) expectedOutput := "libsemihost-.a" @@ -675,7 +675,7 @@ func TestEdgeCases(t *testing.T) { }) t.Run("EmptyTarget_Xtensa", func(t *testing.T) { - config := getNewlibESP32ConfigXtensa("/test/base", "") + config := getNewlibESP32ConfigXtensa(filepath.FromSlash("/test/base"), "") // Check output file name formatting expectedOutput := "libcrt0-.a" @@ -686,7 +686,7 @@ func TestEdgeCases(t *testing.T) { } func TestGroupConfiguration(t *testing.T) { - baseDir := "/test/base" + baseDir := filepath.FromSlash("/test/base") target := "test-target" t.Run("RISCV_GroupCount", func(t *testing.T) { @@ -740,7 +740,7 @@ func TestGroupConfiguration(t *testing.T) { } func TestCompilerFlags(t *testing.T) { - baseDir := "/test/base" + baseDir := filepath.FromSlash("/test/base") target := "test-target" t.Run("RISCV_CFlags", func(t *testing.T) { diff --git a/internal/crosscompile/compile/rtlib/rt_test.go b/internal/crosscompile/compile/rtlib/rt_test.go index 5f5f254861..8f4ea33d30 100644 --- a/internal/crosscompile/compile/rtlib/rt_test.go +++ b/internal/crosscompile/compile/rtlib/rt_test.go @@ -1,6 +1,7 @@ package rtlib import ( + "path/filepath" "strings" "testing" ) @@ -49,7 +50,7 @@ func TestPlatformSpecifiedFiles(t *testing.T) { {"x86_64-pc-windows", 0}, } - builtinsDir := "/test/builtins" + builtinsDir := filepath.FromSlash("/test/builtins") for _, tt := range tests { t.Run(tt.target, func(t *testing.T) { result := platformSpecifiedFiles(builtinsDir, tt.target) @@ -61,7 +62,7 @@ func TestPlatformSpecifiedFiles(t *testing.T) { } func TestWithPlatformSpecifiedFiles(t *testing.T) { - baseDir := "/test/base" + baseDir := filepath.FromSlash("/test/base") target := "riscv32-unknown-elf" inputFiles := []string{"file1.c", "file2.c"} @@ -88,7 +89,7 @@ func TestWithPlatformSpecifiedFiles(t *testing.T) { } func TestGetCompilerRTConfig(t *testing.T) { - baseDir := "/test/base" + baseDir := filepath.FromSlash("/test/base") target := "riscv32-unknown-elf" config := GetCompilerRTCompileConfig(baseDir, target) @@ -129,7 +130,7 @@ func TestGetCompilerRTConfig_DifferentTargets(t *testing.T) { "xtensa", } - baseDir := "/test/base" + baseDir := filepath.FromSlash("/test/base") for _, target := range targets { t.Run(target, func(t *testing.T) { config := GetCompilerRTCompileConfig(baseDir, target) diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index 848336331f..e0a2490752 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -153,10 +153,14 @@ var ( ) var ( - espClangBaseUrl = "https://github.com/goplus/espressif-llvm-project-prebuilt/releases/download/19.1.2_20250905-3" - espClangVersion = "19.1.2_20250905-3" + espClangBaseUrl = "https://github.com/goplus/espressif-llvm-project-prebuilt/releases/download/19.1.2_20250905-3" + espClangVersion = "19.1.2_20250905-3" + espClangWindowsBaseUrl = "https://github.com/espressif/llvm-project/releases/download/esp-19.1.2_20250312" + espClangWindowsVersion = "19.1.2_20250312" ) +const espClangWindowsPlatform = "x86_64-w64-mingw32" + // cacheRoot can be overridden for testing var cacheRoot = env.LLGoCacheDir @@ -226,13 +230,14 @@ func getESPClangRoot(forceEspClang bool) (clangRoot string, err error) { // Try to download ESP Clang if platform is supported platformSuffix := getESPClangPlatform(runtime.GOOS, runtime.GOARCH) if platformSuffix != "" { - cacheClangDir := filepath.Join(cacheRoot(), "crosscompile", "esp-clang-"+espClangVersion) + baseURL, version := espClangDownload(platformSuffix) + cacheClangDir := filepath.Join(cacheRoot(), "crosscompile", "esp-clang-"+version) if _, err = os.Stat(cacheClangDir); err != nil { if !errors.Is(err, fs.ErrNotExist) { return } fmt.Fprintln(os.Stderr, "ESP Clang not found in LLGO_ROOT or cache, will download.") - if err = checkDownloadAndExtractESPClang(platformSuffix, cacheClangDir); err != nil { + if err = checkDownloadAndExtractESPClang(baseURL, version, platformSuffix, cacheClangDir); err != nil { return } } @@ -265,13 +270,25 @@ func getESPClangPlatform(goos, goarch string) string { } case "windows": switch goarch { - case "amd64": - return "x86_64-w64-mingw32" + case "amd64", "arm64": + // Espressif publishes an x86-64 Windows host toolchain. Windows on + // ARM64 runs it through the system's x64 emulation layer. + return espClangWindowsPlatform } } return "" } +func espClangDownload(platformSuffix string) (baseURL, version string) { + if platformSuffix == espClangWindowsPlatform { + // The LLGo-hosted 20250905 build does not publish a Windows archive. + // Use Espressif's official LLVM 19 Windows build instead of constructing + // a URL that can only return 404. + return espClangWindowsBaseUrl, espClangWindowsVersion + } + return espClangBaseUrl, espClangVersion +} + // ldFlagsFromFileName extracts the library name from a filename for use in linker flags // For example, "libmath.a" becomes "math" for use with "-lmath" func ldFlagsFromFileName(fileName string) string { @@ -628,16 +645,24 @@ func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (expor return export, fmt.Errorf("target '%s' does not have a valid CPU configuration", targetName) } - // Check for ESP Clang support for target-based builds - clangRoot, err := getESPClangRoot(true) - if err != nil { - return + // Espressif's Windows toolchain only ships the ESP backends. Use the + // full MSYS2 LLVM distribution for other embedded targets (for example + // ARM and AVR), while retaining the established ESP toolchain selection + // on Unix hosts and for ESP targets. + var clangRoot string + if useSystemClangForTarget(runtime.GOOS, target, config.BuildTags) { + export.CC = "clang++" + } else { + var clangErr error + clangRoot, clangErr = getESPClangRoot(true) + if clangErr != nil { + err = clangErr + return + } + export.ClangRoot = clangRoot + export.CC = filepath.Join(clangRoot, "bin", "clang++") } - // Set ClangRoot and CC if clang is available - export.ClangRoot = clangRoot - export.CC = filepath.Join(clangRoot, "bin", "clang++") - // Convert target config to Export - only export necessary fields export.BuildTags = config.BuildTags export.GOOS = config.GOOS @@ -678,9 +703,10 @@ func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (expor ldflags := []string{"-S", "--icf=none"} ccflags := []string{level.Flag()} cflags := []string{"-Wno-override-module", "-Qunused-arguments", "-Wno-unused-command-line-argument"} - if config.LLVMTarget != "" { - cflags = append(cflags, "--target="+config.LLVMTarget) - ccflags = append(ccflags, "--target="+config.LLVMTarget) + clangTarget := clangDriverTargetForHost(runtime.GOOS, config.LLVMTarget, config.BuildTags) + if clangTarget != "" { + cflags = append(cflags, "--target="+clangTarget) + ccflags = append(ccflags, "--target="+clangTarget) } // Expand template variables in cflags expandedCFlags := env.ExpandEnvSlice(config.CFlags, envs) @@ -794,7 +820,10 @@ func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (expor // Handle Linker - keep it for external usage if config.Linker != "" { - export.Linker = filepath.Join(clangRoot, "bin", config.Linker) + export.Linker = config.Linker + if clangRoot != "" { + export.Linker = filepath.Join(clangRoot, "bin", config.Linker) + } } if config.LinkerScript != "" { ldflags = append(ldflags, "-T", config.LinkerScript) @@ -861,6 +890,33 @@ func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (expor return export, nil } +func useSystemClangForTarget(hostGOOS, targetTriple string, buildTags []string) bool { + if hostGOOS != "windows" || strings.HasPrefix(targetTriple, "xtensa") { + return false + } + for _, tag := range buildTags { + if tag == "esp" { + return false + } + } + return true +} + +// clangDriverTargetForHost returns the target spelling accepted by the host +// Clang driver. LLGo's Unix ESP toolchains use the historical "xtensa" +// spelling, but Espressif's official Windows distribution selects its Xtensa +// multilibs using the canonical GCC-compatible triple. +func clangDriverTargetForHost(hostGOOS, llvmTarget string, buildTags []string) string { + if hostGOOS == "windows" && llvmTarget == "xtensa" { + for _, tag := range buildTags { + if tag == "esp" { + return "xtensa-esp-unknown-elf" + } + } + } + return llvmTarget +} + // Use extends the original Use function to support target-based configuration // If targetName is provided, it takes precedence over goos/goarch func Use(goos, goarch, targetName string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool) (export Export, err error) { diff --git a/internal/crosscompile/crosscompile_test.go b/internal/crosscompile/crosscompile_test.go index a76d7ae2ce..78f368c882 100644 --- a/internal/crosscompile/crosscompile_test.go +++ b/internal/crosscompile/crosscompile_test.go @@ -4,7 +4,10 @@ package crosscompile import ( + "net/http" + "net/http/httptest" "os" + "path/filepath" "runtime" "slices" "strings" @@ -22,6 +25,30 @@ const ( libPrefix = "-L" ) +func TestESPClangHostDownload(t *testing.T) { + tests := []struct { + goos, goarch string + wantPlatform string + wantVersion string + }{ + {"darwin", "arm64", "aarch64-apple-darwin", espClangVersion}, + {"linux", "amd64", "x86_64-linux-gnu", espClangVersion}, + {"windows", "amd64", espClangWindowsPlatform, espClangWindowsVersion}, + {"windows", "arm64", espClangWindowsPlatform, espClangWindowsVersion}, + } + for _, test := range tests { + platform := getESPClangPlatform(test.goos, test.goarch) + if platform != test.wantPlatform { + t.Errorf("getESPClangPlatform(%q, %q) = %q, want %q", test.goos, test.goarch, platform, test.wantPlatform) + continue + } + _, version := espClangDownload(platform) + if version != test.wantVersion { + t.Errorf("espClangDownload(%q) version = %q, want %q", platform, version, test.wantVersion) + } + } +} + func TestUseCrossCompileSDK(t *testing.T) { // Skip long-running tests unless explicitly enabled if testing.Short() { @@ -219,6 +246,13 @@ func TestUseTarget(t *testing.T) { expectCPU: "generic-rv32", expectMarch: "-march=rv32imac", // Generic RISC-V32 uses rv32imac (with A extension) }, + { + name: "ESP32 Target (Xtensa)", + targetName: "esp32", + expectError: false, + expectLLVM: "xtensa", + expectCPU: "esp32", + }, { name: "ESP32-C3 Target (ESP RISC-V)", targetName: "esp32c3", @@ -258,7 +292,8 @@ func TestUseTarget(t *testing.T) { // Check if LLVM target is in CCFLAGS if tc.expectLLVM != "" { found := false - expectedFlag := "--target=" + tc.expectLLVM + expectedLLVM := clangDriverTargetForHost(runtime.GOOS, tc.expectLLVM, export.BuildTags) + expectedFlag := "--target=" + expectedLLVM for _, flag := range export.CCFLAGS { if flag == expectedFlag { found = true @@ -313,13 +348,117 @@ func TestUseTarget(t *testing.T) { t.Errorf("Expected %s in CCFLAGS, got %v", tc.expectMarch, export.CCFLAGS) } } - t.Logf("Target %s: BuildTags=%v, CFlags=%v, CCFlags=%v, LDFlags=%v", tc.targetName, export.BuildTags, export.CFLAGS, export.CCFLAGS, export.LDFLAGS) }) } } +func TestUseTargetWindowsSystemClang(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Windows host toolchain selection") + } + + export, err := UseTarget("rp2040", optlevel.Oz, lto.Thin) + if err != nil { + t.Fatal(err) + } + if export.CC != "clang++" { + t.Fatalf("RP2040 compiler on Windows = %q, want clang++", export.CC) + } + if export.ClangRoot != "" { + t.Fatalf("RP2040 Clang root on Windows = %q, want system toolchain", export.ClangRoot) + } + if export.Linker != "ld.lld" { + t.Fatalf("RP2040 linker on Windows = %q, want ld.lld", export.Linker) + } +} + +func TestUseTargetESPClangDownloadError(t *testing.T) { + server := httptest.NewServer(http.NotFoundHandler()) + t.Cleanup(server.Close) + + llgoRoot := t.TempDir() + runtimeDir := filepath.Join(llgoRoot, "runtime") + if err := os.MkdirAll(runtimeDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile( + filepath.Join(runtimeDir, "go.mod"), + []byte("module github.com/xgo-dev/llgo/runtime\n"), 0o644, + ); err != nil { + t.Fatal(err) + } + targetsDir := filepath.Join(llgoRoot, "targets") + if err := os.MkdirAll(targetsDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile( + filepath.Join(targetsDir, "esp-test.json"), + []byte(`{"llvm-target":"xtensa","cpu":"esp32","build-tags":["esp"]}`), 0o644, + ); err != nil { + t.Fatal(err) + } + t.Setenv("LLGO_ROOT", llgoRoot) + + originalCacheRoot := cacheRoot + originalBaseURL := espClangBaseUrl + originalWindowsBaseURL := espClangWindowsBaseUrl + cacheDir := t.TempDir() + cacheRoot = func() string { return cacheDir } + espClangBaseUrl = server.URL + espClangWindowsBaseUrl = server.URL + t.Cleanup(func() { + cacheRoot = originalCacheRoot + espClangBaseUrl = originalBaseURL + espClangWindowsBaseUrl = originalWindowsBaseURL + }) + + _, err := UseTarget("esp-test", optlevel.Oz, lto.Thin) + if err == nil || !strings.Contains(err.Error(), "404 Not Found") { + t.Fatalf("UseTarget(esp-test) error = %v, want download 404", err) + } +} + +func TestUseSystemClangForTarget(t *testing.T) { + for _, test := range []struct { + goos string + target string + buildTags []string + want bool + }{ + {goos: "windows", target: "thumbv6m-unknown-unknown-eabi", want: true}, + {goos: "windows", target: "avr", want: true}, + {goos: "windows", target: "riscv32-esp-elf", buildTags: []string{"esp"}, want: false}, + {goos: "windows", target: "xtensa", buildTags: []string{"esp32", "esp"}, want: false}, + {goos: "windows", target: "xtensa", want: false}, + {goos: "linux", target: "thumbv6m-unknown-unknown-eabi", want: false}, + } { + if got := useSystemClangForTarget(test.goos, test.target, test.buildTags); got != test.want { + t.Errorf("useSystemClangForTarget(%q, %q, %v) = %v, want %v", test.goos, test.target, test.buildTags, got, test.want) + } + } +} + +func TestClangDriverTargetForHost(t *testing.T) { + for _, test := range []struct { + goos string + target string + buildTags []string + want string + }{ + {goos: "windows", target: "xtensa", buildTags: []string{"esp32", "esp"}, want: "xtensa-esp-unknown-elf"}, + {goos: "windows", target: "xtensa", want: "xtensa"}, + {goos: "windows", target: "riscv32-esp-elf", buildTags: []string{"esp"}, want: "riscv32-esp-elf"}, + {goos: "linux", target: "xtensa", buildTags: []string{"esp"}, want: "xtensa"}, + {goos: "darwin", target: "xtensa", buildTags: []string{"esp"}, want: "xtensa"}, + } { + if got := clangDriverTargetForHost(test.goos, test.target, test.buildTags); got != test.want { + t.Errorf("clangDriverTargetForHost(%q, %q, %v) = %q, want %q", test.goos, test.target, test.buildTags, got, test.want) + } + } +} + func TestUseWithTarget(t *testing.T) { // Test target-based configuration takes precedence export, err := Use("linux", "amd64", "esp32", false, true, optlevel.Oz, lto.Thin, false) diff --git a/internal/crosscompile/fetch.go b/internal/crosscompile/fetch.go index aa030be85c..566380b705 100644 --- a/internal/crosscompile/fetch.go +++ b/internal/crosscompile/fetch.go @@ -11,6 +11,7 @@ import ( "os/exec" "path" "path/filepath" + "runtime" "strings" "github.com/xgo-dev/llgo/internal/env" @@ -45,7 +46,7 @@ func checkDownloadAndExtractWasiSDK(dir string) (wasiSdkRoot string, err error) } // checkDownloadAndExtractESPClang downloads and extracts ESP Clang binaries and libraries -func checkDownloadAndExtractESPClang(platformSuffix, dir string) error { +func checkDownloadAndExtractESPClang(baseURL, version, platformSuffix, dir string) error { // Check if already exists if _, err := os.Stat(dir); err == nil { return nil @@ -64,8 +65,8 @@ func checkDownloadAndExtractESPClang(platformSuffix, dir string) error { return nil } - clangUrl := fmt.Sprintf("%s/clang-esp-%s-%s.tar.xz", espClangBaseUrl, espClangVersion, platformSuffix) - description := fmt.Sprintf("ESP Clang %s-%s", espClangVersion, platformSuffix) + clangUrl := fmt.Sprintf("%s/clang-esp-%s-%s.tar.xz", baseURL, version, platformSuffix) + description := fmt.Sprintf("ESP Clang %s-%s", version, platformSuffix) // Use temporary extraction directory for ESP Clang special handling tempExtractDir := dir + ".extract" @@ -302,9 +303,53 @@ func extractTarGz(tarGzFile, dest string) error { } func extractTarXz(tarXzFile, dest string) error { - // Use external tar command to extract .tar.xz files - cmd := exec.Command("tar", "-xf", tarXzFile, "-C", dest) - return cmd.Run() + 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), + } + } + } + cmd := exec.Command(tarCommand, tarArgs...) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("tar -xf: %w: %s", err, strings.TrimSpace(string(output))) + } + 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 + } + } + if systemRoot != "" { + nativeTar := filepath.Join(systemRoot, "System32", "tar.exe") + if fileExists(nativeTar) { + return nativeTar, "" + } + } + return "tar", "" +} + +func fileExists(name string) bool { + _, err := os.Stat(name) + return err == nil } func extractZip(zipFile, dest string) error { diff --git a/internal/crosscompile/fetch_test.go b/internal/crosscompile/fetch_test.go index cb2eea96ff..5fd547d732 100644 --- a/internal/crosscompile/fetch_test.go +++ b/internal/crosscompile/fetch_test.go @@ -14,6 +14,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "sync" "sync/atomic" @@ -52,6 +53,109 @@ func createTestTarGz(t *testing.T, files map[string]string) string { return tempFile.Name() } +func createTestTarXz(t *testing.T, files map[string]string) string { + t.Helper() + _, xzErr := exec.LookPath("xz") + if runtime.GOOS == "windows" && xzErr != nil { + // Windows CI provides xz through MSYS2. Windows 11's bundled bsdtar is + // a fallback for local development VMs that do not install xz separately. + sourceDir := t.TempDir() + for name, content := range files { + file := filepath.Join(sourceDir, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(file, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + xzFile := filepath.Join(t.TempDir(), "test.tar.xz") + tarCommand := filepath.Join(os.Getenv("SystemRoot"), "System32", "tar.exe") + if output, err := exec.Command(tarCommand, "-cJf", xzFile, "-C", sourceDir, ".").CombinedOutput(); err != nil { + t.Fatalf("compress test tar.xz: %v: %s", err, strings.TrimSpace(string(output))) + } + return xzFile + } + + tarFile, err := os.CreateTemp("", "test*.tar") + if err != nil { + t.Fatal(err) + } + tw := tar.NewWriter(tarFile) + for name, content := range files { + hdr := &tar.Header{Name: name, Mode: 0o644, Size: int64(len(content))} + if err := tw.WriteHeader(hdr); err != nil { + t.Fatal(err) + } + if _, err := tw.Write([]byte(content)); err != nil { + t.Fatal(err) + } + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + if err := tarFile.Close(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { os.Remove(tarFile.Name()) }) + + compressed, err := exec.Command("xz", "-c", tarFile.Name()).Output() + if err != nil { + t.Fatalf("compress test tar.xz: %v", err) + } + xzFile := tarFile.Name() + ".xz" + if err := os.WriteFile(xzFile, compressed, 0o644); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { os.Remove(xzFile) }) + return xzFile +} + +func TestWindowsTarXzTools(t *testing.T) { + touch := func(t *testing.T, name string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(name, nil, 0o755); err != nil { + t.Fatal(err) + } + } + + t.Run("MSYS2", 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) + } + }) + + 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) + } + }) + + t.Run("PathFallback", func(t *testing.T) { + gotTar, gotXz := windowsTarXzTools("", "") + if gotTar != "tar" || gotXz != "" { + t.Fatalf("windowsTarXzTools() = (%q, %q), want (tar, empty)", gotTar, gotXz) + } + }) +} + // 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) { @@ -256,7 +360,6 @@ func TestExtractTarGz(t *testing.T) { } archivePath := createTestTarGz(t, files) - defer os.Remove(archivePath) // Extract to temp directory tempDir := t.TempDir() @@ -596,7 +699,7 @@ func TestESPClangExtractionLogic(t *testing.T) { } // Test that function skips download for existing directory - err = checkDownloadAndExtractESPClang("linux", espClangDir) + err = checkDownloadAndExtractESPClang(espClangBaseUrl, espClangVersion, "linux", espClangDir) if err != nil { t.Fatalf("checkDownloadAndExtractESPClang failed: %v", err) } @@ -678,8 +781,7 @@ func TestESPClangDownloadWhenNotExists(t *testing.T) { "esp-clang/include/esp32.h": "#define ESP32 1", } - archivePath := createTestTarGz(t, files) - defer os.Remove(archivePath) + archivePath := createTestTarXz(t, files) // Read the archive content archiveContent, err := os.ReadFile(archivePath) @@ -707,7 +809,7 @@ func TestESPClangDownloadWhenNotExists(t *testing.T) { espClangDir := filepath.Join(tempCacheRoot, "esp-clang-test") // Test download and extract when directory doesn't exist - err = checkDownloadAndExtractESPClang("linux", espClangDir) + err = checkDownloadAndExtractESPClang(espClangBaseUrl, espClangVersion, "linux", espClangDir) if err != nil { t.Fatalf("checkDownloadAndExtractESPClang failed: %v", err) } @@ -733,11 +835,20 @@ func TestESPClangDownloadWhenNotExists(t *testing.T) { } } +func TestExtractTarXzError(t *testing.T) { + err := extractTarXz(filepath.Join(t.TempDir(), "missing.tar.xz"), t.TempDir()) + 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) + } +} + func TestESPClangDownloadLicenseFailure(t *testing.T) { - archivePath := createTestTarGz(t, map[string]string{ + archivePath := createTestTarXz(t, map[string]string{ "esp-clang/bin/clang": "fake esp clang binary", }) - defer os.Remove(archivePath) archiveContent, err := os.ReadFile(archivePath) if err != nil { @@ -748,10 +859,6 @@ func TestESPClangDownloadLicenseFailure(t *testing.T) { }) defer server.Close() - originalESPClangBaseURL := espClangBaseUrl - espClangBaseUrl = server.URL - defer func() { espClangBaseUrl = originalESPClangBaseURL }() - llgoRoot := t.TempDir() if err := os.MkdirAll(filepath.Join(llgoRoot, "runtime"), 0o755); err != nil { t.Fatal(err) @@ -765,7 +872,7 @@ func TestESPClangDownloadLicenseFailure(t *testing.T) { t.Setenv("LLGO_ROOT", llgoRoot) destDir := filepath.Join(t.TempDir(), "esp-clang") - err = checkDownloadAndExtractESPClang("linux", destDir) + err = checkDownloadAndExtractESPClang(server.URL, espClangVersion, "linux", destDir) if err == nil || !strings.Contains(err.Error(), "read ESP Clang license") { t.Fatalf("checkDownloadAndExtractESPClang() error = %v, want license read error", err) } @@ -899,22 +1006,22 @@ func TestExtractZip(t *testing.T) { } }) - // 3. Test non-writable destination - t.Run("UnwritableDestination", func(t *testing.T) { + // 3. Test a destination that cannot contain extracted files. Unlike Unix + // permission bits, this remains deterministic on Windows and as root. + t.Run("NonDirectoryDestination", func(t *testing.T) { // Create test ZIP file if err := createTestZip(zipPath); err != nil { t.Fatal(err) } - // Create read-only destination directory - readOnlyDir := filepath.Join(tempDir, "readonly") - if err := os.MkdirAll(readOnlyDir, 0400); err != nil { + notDirectory := filepath.Join(tempDir, "not-a-directory") + if err := os.WriteFile(notDirectory, nil, 0o644); err != nil { t.Fatal(err) } // Execute extraction and expect error - if err := extractZip(zipPath, readOnlyDir); err == nil { - t.Error("Expected error for unwritable destination, got nil") + if err := extractZip(zipPath, notDirectory); err == nil { + t.Error("Expected error for non-directory destination, got nil") } }) } diff --git a/internal/crosscompile/libc_test.go b/internal/crosscompile/libc_test.go index dcd177ce23..9dbb94cb3e 100644 --- a/internal/crosscompile/libc_test.go +++ b/internal/crosscompile/libc_test.go @@ -14,7 +14,7 @@ import ( ) func TestGetLibcCompileConfigByName(t *testing.T) { - baseDir := "/test/base" + baseDir := filepath.FromSlash("/test/base") target := "armv7" mcpu := "cortex-m4" @@ -79,7 +79,7 @@ func TestGetLibcCompileConfigByName(t *testing.T) { } func TestGetRTCompileConfigByName(t *testing.T) { - baseDir := "/test/base" + baseDir := filepath.FromSlash("/test/base") target := "wasm32" needSkipDownload = true @@ -127,33 +127,33 @@ func TestCompilerRTCompileConfigPaths(t *testing.T) { }{ { name: "RISC-V 32", - baseDir: "/test/base/dir", + baseDir: filepath.FromSlash("/test/base/dir"), target: "riscv32-unknown-elf", - expected: "riscv/mulsi3.S", // Expected platform file for RISC-V 32 + expected: filepath.FromSlash("riscv/mulsi3.S"), // Expected platform file for RISC-V 32 }, { name: "RISC-V 64", - baseDir: "/another/dir", + baseDir: filepath.FromSlash("/another/dir"), target: "riscv64-unknown-elf", expected: "addtf3.c", // Expected platform file for RISC-V 64 }, { name: "ARM", - baseDir: "/arm/dir", + baseDir: filepath.FromSlash("/arm/dir"), target: "armv7-unknown-linux-gnueabihf", - expected: "arm/aeabi_cdcmp.S", // Expected platform file for ARM + expected: filepath.FromSlash("arm/aeabi_cdcmp.S"), // Expected platform file for ARM }, { name: "AVR", - baseDir: "/avr/dir", + baseDir: filepath.FromSlash("/avr/dir"), target: "avr", - expected: "avr/divmodhi4.S", // Expected platform file for AVR + expected: filepath.FromSlash("avr/divmodhi4.S"), // Expected platform file for AVR }, { name: "XTENSA", - baseDir: "/xtensa/dir", + baseDir: filepath.FromSlash("/xtensa/dir"), target: "xtensa", - expected: "xtensa/ieee754_sqrtf.S", // Expected platform file for XTENSA + expected: filepath.FromSlash("xtensa/ieee754_sqrtf.S"), // Expected platform file for XTENSA }, } needSkipDownload = true @@ -209,7 +209,7 @@ func TestCompilerRTCompileConfigPaths(t *testing.T) { // TestCompilerRTCompileConfigPathRelations tests the general path relationships // in the CompileConfig for a specific target. func TestCompilerRTCompileConfigPathRelations(t *testing.T) { - baseDir := "/test/base/dir" + baseDir := filepath.FromSlash("/test/base/dir") target := "riscv64-unknown-elf" // Get the compile configuration @@ -254,18 +254,18 @@ func TestGetPicolibcCompileConfigPaths(t *testing.T) { target string }{ { - name: "Unix-like path", - baseDir: "/test/base/dir", + name: "Absolute path", + baseDir: filepath.FromSlash("/test/base/dir"), target: "riscv64-unknown-elf", }, { - name: "Windows-like path", - baseDir: "C:\\test\\base\\dir", + name: "Cleaned path", + baseDir: filepath.Join(filepath.FromSlash("/test/base"), "parent", "..", "dir"), target: "x86_64-pc-windows-msvc", }, { name: "Relative path", - baseDir: "test/base/dir", + baseDir: filepath.FromSlash("test/base/dir"), target: "armv7-unknown-linux-gnueabihf", }, } @@ -332,7 +332,7 @@ func TestGetPicolibcCompileConfigPaths(t *testing.T) { // TestGetPicolibcCompileConfigSpecificPaths tests specific path constructions // in the CompileConfig for a given baseDir and target. func TestGetPicolibcCompileConfigSpecificPaths(t *testing.T) { - baseDir := "/test/base/dir" + baseDir := filepath.FromSlash("/test/base/dir") target := "riscv64-unknown-elf" needSkipDownload = true diff --git a/runtime/_test/windowsruntime/main.go b/runtime/_test/windowsruntime/main.go index c530e6873b..226dbae139 100644 --- a/runtime/_test/windowsruntime/main.go +++ b/runtime/_test/windowsruntime/main.go @@ -302,6 +302,12 @@ func checkWindowsRandomSource() { } } +var nativeOnceValue int + +func initializeNativeOnceValue() { + nativeOnceValue = 7 +} + func main() { values := make(chan int) go func() { @@ -313,10 +319,10 @@ func main() { var once nativesync.Once done := make(chan struct{}, 4) - onceValue := 0 + nativeOnceValue = 0 for i := 0; i < 4; i++ { go func() { - if result := once.Do(func() { onceValue = 7 }); result != 0 { + if result := once.Do(initializeNativeOnceValue); result != 0 { panic("native once failed") } done <- struct{}{} @@ -325,7 +331,7 @@ func main() { for i := 0; i < 4; i++ { <-done } - if onceValue != 7 { + if nativeOnceValue != 7 { panic("native once ran incorrectly") } diff --git a/runtime/internal/clite/ffi/config_default.go b/runtime/internal/clite/ffi/config_default.go deleted file mode 100644 index e6438e0da2..0000000000 --- a/runtime/internal/clite/ffi/config_default.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !windows - -package ffi - -const ( - LLGoPackage = "link: $(pkg-config --libs libffi); -lffi" - LLGoFiles = "$(pkg-config --cflags libffi): _wrap/libffi.c" -) diff --git a/runtime/internal/clite/ffi/config_windows.go b/runtime/internal/clite/ffi/config_windows.go deleted file mode 100644 index adb9363bd7..0000000000 --- a/runtime/internal/clite/ffi/config_windows.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build windows - -package ffi - -const ( - // Avoid clang's MSVC-style -lffi lookup and pass the pinned MSYS2 - // package's ABI-compatible COFF import archive explicitly. - LLGoPackage = "link: -Wl,$(pkg-config --variable=libdir libffi)/libffi.dll.a" - LLGoFiles = "$(pkg-config --cflags libffi): _wrap/libffi.c" -) diff --git a/runtime/internal/clite/ffi/ffi_link.go b/runtime/internal/clite/ffi/ffi_link.go index 0ca32c891b..f232733b65 100644 --- a/runtime/internal/clite/ffi/ffi_link.go +++ b/runtime/internal/clite/ffi/ffi_link.go @@ -6,6 +6,11 @@ import ( c "github.com/xgo-dev/llgo/runtime/internal/clite" ) +const ( + LLGoPackage = "link: $(pkg-config --libs libffi); -lffi" + LLGoFiles = "$(pkg-config --cflags libffi): _wrap/libffi.c" +) + /* ffi_status ffi_prep_cif(ffi_cif *cif, diff --git a/runtime/internal/lib/runtime/_wrap/profile_windows.c b/runtime/internal/lib/runtime/_wrap/profile_windows.c index 2bd2a5af27..de9f0010c6 100644 --- a/runtime/internal/lib/runtime/_wrap/profile_windows.c +++ b/runtime/internal/lib/runtime/_wrap/profile_windows.c @@ -109,6 +109,11 @@ static volatile int llgo_prof_ring_lock; static volatile int llgo_prof_active; static volatile int llgo_prof_sampler_running; static volatile uint64_t llgo_prof_lost; +/* Test-only synchronization state. With no target registered, normal + * profiling adds one atomic load per captured thread; the fetch-add only runs + * when a test thread matches. */ +static volatile llgo_dword llgo_prof_test_thread_id; +static volatile uint64_t llgo_prof_test_thread_samples; static llgo_handle llgo_prof_thread; static llgo_handle llgo_prof_stop_event; @@ -285,8 +290,14 @@ static void llgo_prof_sample_process(void) ResumeThread(thread); } CloseHandle(thread); - if (captured) + if (captured) { llgo_prof_record(&sample); + if (entry.thread_id == + __atomic_load_n(&llgo_prof_test_thread_id, + __ATOMIC_ACQUIRE)) + __atomic_fetch_add(&llgo_prof_test_thread_samples, 1, + __ATOMIC_RELEASE); + } } while (Thread32Next(snapshot, &entry)); } CloseHandle(snapshot); @@ -450,3 +461,23 @@ int llgo_cpu_profile_test_fault_recovery(void) return -1; #endif } + +uint64_t llgo_cpu_profile_test_current_thread_samples(void) +{ + llgo_dword thread_id = GetCurrentThreadId(); + + if (__atomic_load_n(&llgo_prof_test_thread_id, __ATOMIC_ACQUIRE) != + thread_id) { + __atomic_store_n(&llgo_prof_test_thread_id, 0, __ATOMIC_RELEASE); + /* The following thread-id release publishes the zeroed counter. */ + __atomic_store_n(&llgo_prof_test_thread_samples, 0, __ATOMIC_RELAXED); + __atomic_store_n(&llgo_prof_test_thread_id, thread_id, + __ATOMIC_RELEASE); + } + return __atomic_load_n(&llgo_prof_test_thread_samples, __ATOMIC_ACQUIRE); +} + +void llgo_cpu_profile_test_clear_thread(void) +{ + __atomic_store_n(&llgo_prof_test_thread_id, 0, __ATOMIC_RELEASE); +} diff --git a/runtime/internal/sync/_wrap/sync_windows.c b/runtime/internal/sync/_wrap/sync_windows.c index fe7af3b744..d60802f395 100644 --- a/runtime/internal/sync/_wrap/sync_windows.c +++ b/runtime/internal/sync/_wrap/sync_windows.c @@ -26,7 +26,15 @@ typedef unsigned int llgo_size_t; __declspec(dllimport) void LLGO_WINAPI AcquireSRWLockExclusive(llgo_srwlock *lock); __declspec(dllimport) void LLGO_WINAPI +AcquireSRWLockShared(llgo_srwlock *lock); +__declspec(dllimport) void LLGO_WINAPI ReleaseSRWLockExclusive(llgo_srwlock *lock); +__declspec(dllimport) void LLGO_WINAPI +ReleaseSRWLockShared(llgo_srwlock *lock); +__declspec(dllimport) unsigned char LLGO_WINAPI +TryAcquireSRWLockExclusive(llgo_srwlock *lock); +__declspec(dllimport) unsigned char LLGO_WINAPI +TryAcquireSRWLockShared(llgo_srwlock *lock); __declspec(dllimport) void LLGO_WINAPI WakeConditionVariable(llgo_condition_variable *condition); @@ -58,36 +66,67 @@ __declspec(dllimport) llgo_dword LLGO_WINAPI GetLastError(void); #define LLGO_INFINITE ((llgo_dword)0xffffffffUL) enum { + llgo_error_busy = 16, llgo_error_invalid_parameter = 22, llgo_error_timeout = 1460, llgo_timedout = 110, }; +typedef void (*llgo_once_fn)(void); +typedef void (*llgo_once_context_fn)(void *); + typedef struct { - void *code; - void *context; -} llgo_go_func; + llgo_once_context_fn callback; + void *data; +} llgo_once_call; -extern void llgo_win_once_invoke(llgo_go_func *fn); +typedef struct { + llgo_once_fn fn; +} llgo_once_raw_call; static llgo_bool LLGO_WINAPI llgo_once_callback( llgo_init_once *once, void *parameter, void **context) { + llgo_once_call *call = (llgo_once_call *)parameter; (void)once; (void)context; - llgo_win_once_invoke((llgo_go_func *)parameter); + call->callback(call->data); return 1; } -int llgo_win_once(llgo_init_once *once, llgo_go_func *fn) +static int llgo_win_once_execute(llgo_init_once *once, + llgo_once_context_fn callback, void *data) { - if (fn == 0 || fn->code == 0) + llgo_once_call call; + if (callback == 0) return 87; /* ERROR_INVALID_PARAMETER */ - if (InitOnceExecuteOnce(once, llgo_once_callback, fn, 0)) + call.callback = callback; + call.data = data; + if (InitOnceExecuteOnce(once, llgo_once_callback, &call, 0)) return 0; return (int)GetLastError(); } +static void llgo_win_once_invoke_raw(void *data) +{ + ((llgo_once_raw_call *)data)->fn(); +} + +int llgo_win_once(llgo_init_once *once, llgo_once_fn fn) +{ + llgo_once_raw_call call; + if (fn == 0) + return 87; /* ERROR_INVALID_PARAMETER */ + call.fn = fn; + return llgo_win_once_execute(once, llgo_win_once_invoke_raw, &call); +} + +int llgo_win_once_context(llgo_init_once *once, + llgo_once_context_fn callback, void *data) +{ + return llgo_win_once_execute(once, callback, data); +} + void llgo_win_mutex_lock(llgo_srwlock *lock) { AcquireSRWLockExclusive(lock); @@ -98,6 +137,46 @@ void llgo_win_mutex_unlock(llgo_srwlock *lock) ReleaseSRWLockExclusive(lock); } +/* + * github.com/goplus/lib/c/pthread/sync uses the same SRWLOCK layout as the + * runtime. Keep its extended mutex and rwlock entry points in this object so + * an LLGo process has one native synchronization backend instead of two. + */ +int llgo_win_mutex_trylock(llgo_srwlock *lock) +{ + return TryAcquireSRWLockExclusive(lock) ? 0 : llgo_error_busy; +} + +void llgo_win_rwlock_rlock(llgo_srwlock *lock) +{ + AcquireSRWLockShared(lock); +} + +int llgo_win_rwlock_tryrlock(llgo_srwlock *lock) +{ + return TryAcquireSRWLockShared(lock) ? 0 : llgo_error_busy; +} + +void llgo_win_rwlock_runlock(llgo_srwlock *lock) +{ + ReleaseSRWLockShared(lock); +} + +void llgo_win_rwlock_lock(llgo_srwlock *lock) +{ + AcquireSRWLockExclusive(lock); +} + +int llgo_win_rwlock_trylock(llgo_srwlock *lock) +{ + return TryAcquireSRWLockExclusive(lock) ? 0 : llgo_error_busy; +} + +void llgo_win_rwlock_unlock(llgo_srwlock *lock) +{ + ReleaseSRWLockExclusive(lock); +} + int llgo_win_cond_signal(llgo_condition_variable *condition) { WakeConditionVariable(condition); diff --git a/runtime/internal/sync/once.go b/runtime/internal/sync/once.go new file mode 100644 index 0000000000..7d87605e52 --- /dev/null +++ b/runtime/internal/sync/once.go @@ -0,0 +1,23 @@ +/* + * 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 sync + +// OnceFunc is the native callback accepted by Once.Do. The hosted runtime uses +// only non-capturing initialization functions at this low-level C ABI boundary. +// +//llgo:type C +type OnceFunc func() diff --git a/runtime/internal/sync/sync_unix.go b/runtime/internal/sync/sync_unix.go index 6b29102438..8f028f5037 100644 --- a/runtime/internal/sync/sync_unix.go +++ b/runtime/internal/sync/sync_unix.go @@ -47,7 +47,7 @@ type Once struct { } // llgo:link (*Once).Do C.pthread_once -func (o *Once) Do(f func()) c.Int { return 0 } +func (o *Once) Do(f OnceFunc) c.Int { return 0 } // MutexAttr has the native layout of pthread_mutexattr_t. type MutexAttr struct { diff --git a/runtime/internal/sync/sync_windows.go b/runtime/internal/sync/sync_windows.go index 4b89894698..f0aaaa19ab 100644 --- a/runtime/internal/sync/sync_windows.go +++ b/runtime/internal/sync/sync_windows.go @@ -40,15 +40,10 @@ type Once struct { } //go:linkname winOnce C.llgo_win_once -func winOnce(once *Once, f *func()) c.Int +func winOnce(once *Once, f OnceFunc) c.Int -//export llgo_win_once_invoke -func llgo_win_once_invoke(f *func()) { - (*f)() -} - -func (o *Once) Do(f func()) c.Int { - return winOnce(o, &f) +func (o *Once) Do(f OnceFunc) c.Int { + return winOnce(o, f) } type MutexAttr struct{} diff --git a/ssa/di.go b/ssa/di.go index ad7c93d220..bf8a228a86 100644 --- a/ssa/di.go +++ b/ssa/di.go @@ -215,11 +215,29 @@ func (b diBuilder) createType(name string, ty Type, pos token.Position) DIType { panic(fmt.Errorf("can't create debug info of basic type: %v, %T", ty.RawType(), ty.RawType())) } + basicName := name + if b.prog.Target().effectiveGOOS() == "windows" { + switch t.Kind() { + case types.Int: + basicName = fmt.Sprintf("int%d", b.prog.SizeOf(ty)*8) + case types.Uint, types.Uintptr: + basicName = fmt.Sprintf("uint%d", b.prog.SizeOf(ty)*8) + } + } typ = b.di.CreateBasicType(llvm.DIBasicType{ - Name: name, + Name: basicName, SizeInBits: b.prog.SizeOf(b.prog.rawType(t)) * 8, Encoding: encoding, }) + if basicName != name { + typ = b.di.CreateTypedef(llvm.DITypedef{ + Name: name, + Type: typ, + File: b.file(pos.Filename).ll, + Line: pos.Line, + AlignInBits: uint32(b.prog.sizes.Alignof(t) * 8), + }) + } case *types.Pointer: return b.createPointerType(name, b.prog.rawType(t.Elem()), pos) case *types.Named: @@ -679,6 +697,14 @@ func (b Builder) di() diBuilder { } func (b Builder) DIParam(variable *types.Var, v Expr, dv DIVar, scope DIScope, pos token.Position, blk BasicBlock) { + if b.Prog.Target().effectiveGOOS() == "windows" { + if _, ok := v.Type.RawType().Underlying().(*types.Pointer); ok { + addr := b.AllocaT(v.Type) + b.Store(addr, v) + b.DIDeclare(variable, addr, dv, scope, pos, blk) + return + } + } b.DIValue(variable, v, dv, scope, pos, blk) } diff --git a/ssa/di_debug_test.go b/ssa/di_debug_test.go index 4250c63eec..04fe53cae6 100644 --- a/ssa/di_debug_test.go +++ b/ssa/di_debug_test.go @@ -166,6 +166,67 @@ type Shape struct { } } +func TestWindowsDebugPointerParameter(t *testing.T) { + const goarch = "amd64" + for _, test := range []struct { + goos string + wantDeclare bool + }{ + {goos: "linux"}, + {goos: "windows", wantDeclare: true}, + } { + t.Run(test.goos, func(t *testing.T) { + fset := token.NewFileSet() + file := fset.AddFile("param.go", -1, 100) + pkgTypes := types.NewPackage("example.com/p", "p") + param := types.NewParam(file.Pos(20), pkgTypes, "p", types.NewPointer(types.Typ[types.Int])) + sig := types.NewSignatureType(nil, nil, nil, types.NewTuple(param), nil, false) + object := types.NewFunc(file.Pos(10), pkgTypes, "f", sig) + + prog := NewProgram(&Target{GOOS: test.goos, GOARCH: goarch, OptLevel: optlevel.O0}) + defer prog.Dispose() + prog.TypeSizes(types.SizesFor("gc", goarch)) + pkg := prog.NewPackage("p", "example.com/p") + pkg.InitDebug("p", "example.com/p", fset) + fn := pkg.NewFunc("example.com/p.f", sig, InGo) + builder := fn.MakeBody(1) + defer builder.Dispose() + pos := fset.Position(param.Pos()) + builder.DebugFunction(fn, object.Scope(), fset.Position(object.Pos()), pos) + debugParam := builder.DIVarParam(fn, pos, param.Name(), prog.Type(param.Type(), InGo), 1) + builder.DIParam(param, fn.Param(0), debugParam, fn, pos, fn.Block(0)) + for _, kind := range []types.BasicKind{types.Uint, types.Uintptr} { + typ := types.Typ[kind] + global := pkg.NewVar("example.com/p."+typ.Name(), types.NewPointer(typ), InGo) + builder.DIGlobal(global.Expr, typ.Name(), pos) + } + builder.Return() + pkg.FinalizeDebug() + + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatalf("debug metadata is invalid: %v\n%s", err, pkg.Module().String()) + } + ir := pkg.Module().String() + if got := strings.Contains(ir, "#dbg_declare"); got != test.wantDeclare { + t.Fatalf("dbg_declare presence = %v, want %v:\n%s", got, test.wantDeclare, ir) + } + if test.goos == "windows" { + for _, want := range []string{ + `!DIBasicType(name: "int64"`, + `!DIBasicType(name: "uint64"`, + `DW_TAG_typedef, name: "int"`, + `DW_TAG_typedef, name: "uint"`, + `DW_TAG_typedef, name: "uintptr"`, + } { + if !strings.Contains(ir, want) { + t.Errorf("Windows debug metadata is missing %q:\n%s", want, ir) + } + } + } + }) + } +} + func TestDIGlobalIgnoresStorageLessFrontendVariable(t *testing.T) { var builder Builder builder.DIGlobal(pyVarExpr(Nil, "attribute"), "module.attribute", token.Position{}) diff --git a/test/buildcache/test.sh b/test/buildcache/test.sh index 47cbb3ffd7..eece11c670 100755 --- a/test/buildcache/test.sh +++ b/test/buildcache/test.sh @@ -73,7 +73,11 @@ compare_snapshot() { local snapshot_file="$2" local actual_file="$3" - if diff -q "$snapshot_file" "$actual_file" > /dev/null 2>&1; then + # Git and native Windows programs may independently choose CRLF or LF. + # Compare logical lines so checkout policy does not change cache results. + if diff -q \ + <(sed 's/\r$//' "$snapshot_file") \ + <(sed 's/\r$//' "$actual_file") > /dev/null 2>&1; then echo -e "${GREEN}✓ PASS${NC}: $test_name" return 0 else @@ -83,7 +87,9 @@ compare_snapshot() { echo "Actual:" cat "$actual_file" echo "Diff:" - diff "$snapshot_file" "$actual_file" || true + diff \ + <(sed 's/\r$//' "$snapshot_file") \ + <(sed 's/\r$//' "$actual_file") || true return 1 fi } @@ -296,6 +302,9 @@ else fi LLGO_IWASM="$LLGO_IWASM_DIR/iwasm" +case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) LLGO_IWASM+=".exe" ;; +esac # Build iwasm if it doesn't exist in llgo cache if [ ! -f "$LLGO_IWASM" ]; then diff --git a/test/std/runtime/pprof/pprof_test.go b/test/std/runtime/pprof/pprof_test.go index 7757536f18..3783500a66 100644 --- a/test/std/runtime/pprof/pprof_test.go +++ b/test/std/runtime/pprof/pprof_test.go @@ -14,6 +14,7 @@ import ( func cpuProfileHotLoop(d time.Duration) uint64 { deadline := time.Now().Add(d) x := uint64(1) + waitForCPUProfileSample() for time.Now().Before(deadline) { for i := 0; i < 10000; i++ { x = x*1664525 + 1013904223 @@ -22,6 +23,11 @@ func cpuProfileHotLoop(d time.Duration) uint64 { return x } +// waitForCPUProfileSample is overridden on LLGo/Windows amd64 and arm64 so the +// statistical profile checks wait for the real sampler instead of guessing a +// longer run time. Other targets keep the existing duration-based behavior. +var waitForCPUProfileSample = func() {} + func requireCPUProfileContains(t *testing.T, data []byte, function string) { t.Helper() zr, err := gzip.NewReader(bytes.NewReader(data)) diff --git a/test/std/runtime/pprof/pprof_windows_llgo_test.go b/test/std/runtime/pprof/pprof_windows_llgo_test.go index 467cca8a9f..0f397fd8d7 100644 --- a/test/std/runtime/pprof/pprof_windows_llgo_test.go +++ b/test/std/runtime/pprof/pprof_windows_llgo_test.go @@ -4,12 +4,35 @@ package pprof_test import ( "testing" + "time" _ "unsafe" ) //go:linkname testCPUProfileWindowsFaultRecovery C.llgo_cpu_profile_test_fault_recovery func testCPUProfileWindowsFaultRecovery() int32 +//go:linkname currentThreadCPUProfileSamples C.llgo_cpu_profile_test_current_thread_samples +func currentThreadCPUProfileSamples() uint64 + +//go:linkname clearCPUProfileTestThread C.llgo_cpu_profile_test_clear_thread +func clearCPUProfileTestThread() + +func init() { + waitForCPUProfileSample = func() { + before := currentThreadCPUProfileSamples() + deadline := time.Now().Add(time.Second) + // Keep this thread on-CPU until the sampler captures it. Sleeping or + // yielding here could deschedule the thread and defeat the barrier. + for currentThreadCPUProfileSamples() == before { + if time.Now().After(deadline) { + clearCPUProfileTestThread() + panic("Windows CPU profiler did not sample the current thread within 1s") + } + } + clearCPUProfileTestThread() + } +} + func TestCPUProfileWindowsFaultRecovery(t *testing.T) { if got := testCPUProfileWindowsFaultRecovery(); got != 1 { t.Fatalf("guarded frame walk returned %d frames, want interrupted PC only", got) diff --git a/xtool/env/env.go b/xtool/env/env.go index cef7858a3a..097f008ab8 100644 --- a/xtool/env/env.go +++ b/xtool/env/env.go @@ -22,6 +22,7 @@ import ( "os/exec" "path/filepath" "regexp" + "runtime" "strings" "github.com/xgo-dev/llgo/xtool/safesplit" @@ -111,6 +112,7 @@ func lookPathInEnvironment(name, dir string, environ []string) string { if strings.ContainsRune(name, filepath.Separator) { return name } + extensions := windowsExecutableExtensions(name, environ) path := "" prefix := "PATH=" for i := len(environ) - 1; i >= 0; i-- { @@ -127,13 +129,48 @@ func lookPathInEnvironment(name, dir string, environ []string) string { entry = filepath.Join(dir, entry) } candidate := filepath.Join(entry, name) - if info, err := os.Stat(candidate); err == nil && !info.IsDir() && info.Mode()&0o111 != 0 { - return candidate + if extensions == nil { + if isExecutableFile(candidate) { + return candidate + } + continue + } + for _, extension := range extensions { + candidateWithExtension := candidate + extension + if isExecutableFile(candidateWithExtension) { + return candidateWithExtension + } } } return name } +func windowsExecutableExtensions(name string, environ []string) []string { + if runtime.GOOS != "windows" || filepath.Ext(name) != "" { + return nil + } + extensions := ".COM;.EXE;.BAT;.CMD" + for i := len(environ) - 1; i >= 0; i-- { + key, value, ok := strings.Cut(environ[i], "=") + if ok && strings.EqualFold(key, "PATHEXT") { + extensions = value + break + } + } + exts := make([]string, 0, 4) + for _, ext := range filepath.SplitList(extensions) { + if ext != "" { + exts = append(exts, ext) + } + } + return exts +} + +func isExecutableFile(path string) bool { + info, err := os.Stat(path) + return err == nil && !info.IsDir() && (runtime.GOOS == "windows" || info.Mode()&0o111 != 0) +} + func parseSubcmd(s string) []string { return reFlag.FindAllString(s, -1) } diff --git a/xtool/env/env_test.go b/xtool/env/env_test.go index 2fc0f4d884..29f8ad83ae 100644 --- a/xtool/env/env_test.go +++ b/xtool/env/env_test.go @@ -90,6 +90,23 @@ func TestLookPathInEnvironmentBoundaries(t *testing.T) { if got := lookPathInEnvironment("missing-tool", dir, []string{"PATH=" + t.TempDir()}); got != "missing-tool" { t.Fatalf("lookPathInEnvironment missing tool = %q", got) } + if runtime.GOOS == "windows" { + customName := "custom-tool" + customTool := filepath.Join(dir, customName+".LLGO") + if err := os.WriteFile(customTool, nil, 0o644); err != nil { + t.Fatal(err) + } + got := lookPathInEnvironment(customName, dir, []string{ + "PATH=" + dir, + "PATHEXT=.EXE", + "pathext=.LLGO", + }) + gotInfo, gotErr := os.Stat(got) + wantInfo, wantErr := os.Stat(customTool) + if gotErr != nil || wantErr != nil || !os.SameFile(gotInfo, wantInfo) { + t.Fatalf("lookPathInEnvironment with PATHEXT = %q, want same file as %q", got, customTool) + } + } if got := ExpandEnvToArgsWith("$LLGO_ENV_MISSING", dir, []string{"PATH=" + dir}); got != nil { t.Fatalf("missing explicit environment variable = %q, want nil", got) } diff --git a/xtool/env/llvm/llvm_config_windows_llvm20.go b/xtool/env/llvm/llvm_config_windows.go similarity index 73% rename from xtool/env/llvm/llvm_config_windows_llvm20.go rename to xtool/env/llvm/llvm_config_windows.go index 36bac6ca39..180cf0ad28 100644 --- a/xtool/env/llvm/llvm_config_windows_llvm20.go +++ b/xtool/env/llvm/llvm_config_windows.go @@ -1,4 +1,4 @@ -//go:build !byollvm && windows && !llvm14 && !llvm15 && !llvm16 && !llvm17 && !llvm18 && !llvm19 +//go:build !byollvm && windows /* * Copyright (c) 2024 The XGo Authors (xgo.dev). All rights reserved. @@ -18,4 +18,7 @@ package llvm +// MSYS2 installs the selected LLVM version as an unversioned executable in +// the active environment's bin directory. Unlike Unix package layouts, the +// executable name is therefore independent of LLGo's llvmNN build tag. const ldLLVMConfigBin = `llvm-config.exe`