From 59cc8c3d67b40e2e8d1d320410e71d0a55bdd176 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Wed, 5 Aug 2026 23:17:45 +0800 Subject: [PATCH 1/7] cmd/dist: standardize GoALLC toolchain builds --- .github/workflows/goallc.yml | 3 + GOALLC.md | 81 +++-- doc/goallc-build.md | 282 ++++++++++++++++++ doc/goallc-llvm-goobj.md | 19 +- src/cmd/dist/build.go | 42 ++- src/cmd/dist/build_test.go | 75 ++++- src/cmd/dist/goallc_plugin.go | 165 ++++++++-- src/cmd/go.mod | 2 +- src/cmd/go.sum | 4 +- src/cmd/llvmplugin/README.md | 16 +- .../github.com/goallc/go-llvm/README.markdown | 3 + .../goallc/go-llvm/llvm_link_static.go | 1 + src/cmd/vendor/modules.txt | 2 +- 13 files changed, 602 insertions(+), 93 deletions(-) create mode 100644 doc/goallc-build.md diff --git a/.github/workflows/goallc.yml b/.github/workflows/goallc.yml index d759b986f6ea99..00bc4d90ba0cac 100644 --- a/.github/workflows/goallc.yml +++ b/.github/workflows/goallc.yml @@ -1,6 +1,9 @@ name: GoALLC on: + push: + branches: + - go1.27.master pull_request: workflow_dispatch: diff --git a/GOALLC.md b/GOALLC.md index e04bde654f75f8..51a861b757803c 100644 --- a/GOALLC.md +++ b/GOALLC.md @@ -442,35 +442,43 @@ rebase 后曾通过: ## 9. 当前 LLVM 23 集成构建方法 -LLVM 从仓库根目录配置: +规范构建、安装、cache identity 和故障排查约定见 +[doc/goallc-build.md](doc/goallc-build.md)。llvm-project 仓库中的辅助脚本只负责 +生成标准 LLVM payload: ```sh -cmake -S llvm -B llvm/cmake-build-debug -G Ninja \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_INSTALL_PREFIX=/private/tmp/goallc-llvm23-install \ - -DLLVM_ENABLE_PROJECTS=lld \ - -DLLVM_TARGETS_TO_BUILD='X86;AArch64' \ - -DLLVM_ENABLE_ASSERTIONS=ON \ - -DLLVM_BUILD_TESTS=OFF \ - -DLLVM_INCLUDE_TESTS=ON \ - -DLLVM_BUILD_TOOLS=ON \ - -DBUILD_SHARED_LIBS=OFF \ - -DLLVM_BUILD_LLVM_DYLIB=ON \ - -DLLVM_ENABLE_RTTI=OFF \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -ninja -C llvm/cmake-build-debug -j6 +cd /path/to/llvm-project +./llvm/utils/goallc/build-payload.bash ``` -准备统一的 LLVM payload 布局: +脚本把 LLVM build tree 和标准 `cmake --install` payload 分开,staging 验证后 +整体刷新 payload。Go toolchain 仍通过原生入口构建,`cmd/dist` 会构建并原子安装 +匹配的 plugin: ```sh -$GOROOT/llvm/bin/llvm-config -$GOROOT/llvm/bin/llvm-ar -$GOROOT/llvm/include/llvm-c/Core.h -$GOROOT/llvm/lib/libLLVM.dylib # dynamic -$GOROOT/llvm/lib/libLLVM*.a # static component archives -$GOROOT/llvm/lib/GoALLCStatepoints.dylib # Darwin pass plugin -# Linux 对应 lib/GoALLCStatepoints.so +cd src +GOALLC_CCACHE=/path/to/ccache \ +./make.bash \ + -llvm-dir=/path/to/llvm-payload \ + -llvm-version=23 \ + -llvm-link=dynamic +``` + +不要再用源码头文件与 build tree 的 `bin`/`lib` 拼接 payload。开发测试所需的 +`FileCheck` 通过 `LLVM_INSTALL_UTILS=ON` 一并安装。 + +Linux amd64 CI 不从源码重复构建 LLVM;它固定下载 llvm-project Release +`goallc-llvm23.1.0-v1` 的 payload,并校验 archive SHA-256、LLVM revision、 +relocatable prefix 和完整安装布局后再运行 `make.bash`。发布与升级约定见上述 +构建文档。 + +默认目录和模式可用环境变量覆盖: + +```sh +GOALLC_LLVM_SOURCE=/path/to/llvm-project \ +GOALLC_LLVM_BUILD=/path/to/llvm-build \ +GOALLC_LLVM_INSTALL=/path/to/llvm-payload \ +/path/to/llvm-project/llvm/utils/goallc/build-payload.bash ``` 不要把 LLVM build 目录全局写入 `DYLD_LIBRARY_PATH`;这会让 Homebrew 的 @@ -479,19 +487,9 @@ payload rpath。 GoALLC 的功能性 pass 源码位于 Go 仓库 `src/cmd/llvmplugin`,不放入 LLVM 源码树。LLVM 只保留通用的 `llc -load-pass-plugin` 和 pre-codegen callback -机制。准备 LLVM payload 后、运行 `make.bash` 前,必须用该 payload 自己的 -CMake config 构建并安装插件: - -```sh -LLVM_PAYLOAD=/path/to/llvm -PLUGIN_BUILD=/path/to/empty/plugin-build -cmake -S "$GOROOT/src/cmd/llvmplugin" -B "$PLUGIN_BUILD" -G Ninja \ - -DLLVM_DIR="$LLVM_PAYLOAD/lib/cmake/llvm" \ - -DCMAKE_INSTALL_PREFIX="$LLVM_PAYLOAD" -cmake --build "$PLUGIN_BUILD" -ctest --test-dir "$PLUGIN_BUILD" --output-on-failure -cmake --install "$PLUGIN_BUILD" -``` +机制。`make.bash` 会使用选定 payload 的 CMake config 自动构建插件;输入内容 +变化时清空旧 CMake/Ninja 状态再通过 ccache 重建,不能把其他 LLVM 构建出的 +plugin 复制过来。 插件 core 在 Go 仓库中实现 Go ABI 函数的 pointer liveness、稳定 safepoint ID、`gc.statepoint` / `gc.relocate` 和 GC leaf 识别。当前 pointer 分类保守 @@ -532,13 +530,14 @@ go test -tags='llvm23 dynamicllvm' ./... go test -tags='llvm23 staticllvm' ./... ``` -构建 Go 集成工具链: +复用已有标准 payload 时直接运行 Go 的构建入口: ```sh -cd /Volumes/Disk1/00.Work/00.Code/goallc/go/src -./make.bash -./make.bash -llvm-dir=/path/to/llvm -llvm-version=23 -llvm-link=dynamic -./make.bash -llvm-dir=/path/to/llvm -llvm-version=23 -llvm-link=static +cd "$GOROOT/src" +./make.bash \ + -llvm-dir=/path/to/llvm-payload \ + -llvm-version=23 \ + -llvm-link=dynamic ``` `cmd/dist` 使用显式 `-llvm-dir`,未指定时使用 `$GOROOT/llvm`。LLVM diff --git a/doc/goallc-build.md b/doc/goallc-build.md new file mode 100644 index 00000000000000..89b9cb5988da9d --- /dev/null +++ b/doc/goallc-build.md @@ -0,0 +1,282 @@ +# GoALLC 构建、安装与缓存约定 + +本文固化 GoALLC LLVM payload、pass plugin 和 Go toolchain 的构建边界。Go +toolchain 的构建入口仍是标准的 `src/make.bash`;LLVM 仓库中的辅助脚本只负责 +准备完整的 LLVM payload,不建立第二套 Go 构建入口。 + +## 产物边界 + +构建包含三个有顺序的阶段: + +1. `llvm-project` 生成 LLVM build tree; +2. `cmake --install` 生成独立、完整的 LLVM payload; +3. Go `make.bash` 使用该 payload 构建匹配的 pass plugin 和 Go toolchain。 + +LLVM build tree 是 Ninja 的增量工作区,不是可供 Go 使用的 payload。有效 payload +必须是一个标准安装树,至少包含: + +```text +bin/llvm-config +bin/llvm-ar +bin/llc +bin/opt +bin/FileCheck +include/llvm-c/Core.h +include/llvm/Config/llvm-config.h +lib/cmake/llvm/LLVMConfig.cmake +lib/cmake/llvm/AddLLVM.cmake +lib/libLLVM.{dylib,so} +lib/libLLVMCore.a +``` + +源码头文件、build tree 中的生成头文件、工具、CMake package、共享库和静态组件库 +必须来自同一次安装。以下混合目录不是合法 payload: + +```text +llvm/include # 从源码/build tree 手工复制 +llvm/bin # 指向 cmake-build/bin 的软链接 +llvm/lib # 指向 cmake-build/lib 的软链接 +``` + +`cmd/dist` 会核对 `llvm-config --prefix`、生成头文件和 CMake 开发文件,遇到上述 +混合布局时直接失败。 + +## 规范构建流程 + +首先使用 llvm-project 仓库拥有的构建器准备 LLVM payload: + +```sh +cd /path/to/llvm-project +./llvm/utils/goallc/build-payload.bash +``` + +脚本默认使用自身所在的 llvm-project checkout,配置 Release + assertions,构建 +X86 和 AArch64,并安装到 llvm-project 根下独立的 +`build-goallc-payload-release`。它不构建 plugin 或 Go。 + +然后通过 Go 原有入口构建 toolchain;`make.bash` 内部的 `cmd/dist` 会校验 +payload、构建并原子安装匹配的 plugin,再完成正常的三阶段构建: + +```sh +cd src +GOALLC_CCACHE=/path/to/ccache \ +./make.bash \ + -llvm-dir=/path/to/llvm-payload \ + -llvm-version=23 \ + -llvm-link=dynamic +``` + +LLVM payload 脚本的常用配置通过环境变量指定: + +```sh +GOALLC_LLVM_SOURCE=/path/to/llvm-project \ +GOALLC_LLVM_BUILD=/path/to/llvm-build \ +GOALLC_LLVM_INSTALL=/path/to/llvm-payload \ +GOALLC_BUILD_TYPE=Release \ +GOALLC_LLVM_TARGETS='X86;AArch64' \ +GOALLC_MACOS_DEPLOYMENT_TARGET=13.0 \ +GOALLC_BUILD_JOBS=12 \ +/path/to/llvm-project/llvm/utils/goallc/build-payload.bash +``` + +将同一个 `GOALLC_LLVM_INSTALL` 路径显式传给 `make.bash -llvm-dir`。不要再把 +`$GOROOT/llvm` 手工维护成混合目录;如需保留兼容入口,可让整个 `$GOROOT/llvm` +软链接到这个安装树。 + +payload 还必须位于 LLVM build tree 之外。LLVM 的 `llvm-config` 会把 build tree +内部的路径识别为 development layout,即使那里是 `cmake --install` 的结果, +其 prefix 和库路径仍会回到 build tree。 + +## ccache + +LLVM payload 脚本优先使用显式的 `GOALLC_CCACHE`,然后检测 Apple Silicon +Homebrew 的 `/opt/homebrew/bin/ccache`,最后查找 `PATH` 中的 `ccache`。 +构建 Go 时把同一个 `GOALLC_CCACHE` 传给 `make.bash`,`cmd/dist` 会将其配置为 +plugin 的 C/C++ compiler launcher。 + +本机推荐配置为: + +```sh +export GOALLC_CCACHE=/opt/homebrew/bin/ccache +export CCACHE_DIR=/Volumes/Disk1/00.Work/.cache/ccache +ccache -M 100G +``` + +可用以下命令确认已有 CMake tree 没有绕过缓存: + +```sh +grep 'CMAKE_.*_COMPILER_LAUNCHER' "$GOALLC_LLVM_BUILD/CMakeCache.txt" +ninja -C "$GOALLC_LLVM_BUILD" -t commands | grep ccache | head +ccache -s +``` + +ccache 只负责加速,不参与正确性判断。 + +Darwin 默认额外设置 `CMAKE_OSX_DEPLOYMENT_TARGET=13.0`,与当前 Go linker 在 +`cmd/link/internal/ld/macho.go` 中写入的最低支持版本一致。否则 CMake 会继承 +当前 SDK(例如 26.0),动态链接产生版本警告,static aggregate 中的每个 LLVM +object 都会携带过高的最低系统版本。Go 更新最低支持版本时应同步这里;特殊构建 +可用 `GOALLC_MACOS_DEPLOYMENT_TARGET` 覆盖。`cmd/dist` 会从 payload manifest +读取该值并用于 plugin CMake;非规范脚本生成的 payload 没有 manifest 时,可在 +运行 `make.bash` 时显式设置标准的 `MACOSX_DEPLOYMENT_TARGET`。 + +## 安装的原子性 + +脚本先用 `DESTDIR` 把 LLVM 安装到 payload 同一文件系统中的临时目录,并在临时 +目录检查工具、生成头文件、CMake package、动态库和静态组件库。随后整体切换 +目录,再在正式路径执行 `llvm-config` 检查 prefix、版本和 target;旧 payload +保留到这些检查全部通过,失败时自动恢复。LLVM 构建或安装中断不会把半套新文件 +留在正式 payload 中。 + +安装后生成: + +```text +$LLVM_PAYLOAD/share/goallc/build-manifest +``` + +它记录 LLVM revision、dirty 状态、LLVM 版本、build type、targets、build 目录、 +安装前缀、ccache 路径和 static system-library closure,供诊断使用;时间戳和 +manifest 本身不作为编译缓存身份。 + +## 发布的 LLVM payload 与 CI + +GoALLC LLVM payload 发布在 +[`goallc/llvm-project` Releases](https://github.com/goallc/llvm-project/releases)。 +当前 Linux amd64 CI 固定使用: + +```text +tag: goallc-llvm23.1.0-v1 +revision: 407485ab2cd2545f66e84b5bbd1907cbcc0e6e4f +asset: goallc-llvm23.1.0-v1-linux-amd64.tar.zst +``` + +发布 tag 由 llvm-project 的 `.github/workflows/goallc-release.yml` 构建;归档和 +对应 `.sha256` 文件同时上传。Go CI 在 `.github/workflows/goallc.yml` 中固定 +release tag、asset 名和 LLVM revision,并下载同版本的 checksum 文件校验 digest, +relocatable prefix、manifest、工具、头文件、CMake package 和动态依赖,再运行 +`make.bash`。禁止依赖 `latest` release、系统 LLVM 或可变 URL。 + +升级 LLVM 时按以下顺序操作:先从新的 llvm-project commit 发布新 tag,确认 +Release asset 可重定位且 checksum 稳定,再在一个 Go PR 中同时更新上述三个固定 +值。旧 release 和 checksum 不覆盖,以便历史 Go commit 的 CI 可以复现。 + +## plugin 的构建与缓存 + +不需要在 `make.bash` 前手工构建或复制 plugin。`cmd/dist` 使用 payload 自己的 +`LLVMConfig.cmake` 配置 `src/cmd/llvmplugin`,生成后以新 inode 原子安装到: + +```text +$LLVM_PAYLOAD/lib/GoALLCStatepoints.dylib # Darwin +$LLVM_PAYLOAD/lib/GoALLCStatepoints.so # Linux +``` + +plugin 输入身份包含: + +- Go 仓库中的 plugin C/C++/CMake 源码; +- payload 中安装的 LLVM/LLVM-C headers 和 LLVM CMake package; +- `llc`、`llvm-config` 和动态 `libLLVM` 的内容。 + +身份变化时,`cmd/dist` 删除旧的 plugin CMake/Ninja 工作区后重新配置;编译仍走 +ccache。这样即使 payload 来自保留旧 mtime 的压缩包,也不会因为 Ninja 的时间戳 +判断复用旧 object。安装 plugin 的内容哈希记录在: + +```text +$GOROOT/pkg/goallc-llvmplugin/goallc-plugin.stamp +``` + +构建完成后可独立运行 plugin 和 Go 基础设施验证: + +```sh +LLVM_PAYLOAD=/path/to/llvm-payload +PLUGIN_BUILD="$GOROOT/pkg/goallc-llvmplugin" +PLUGIN="$LLVM_PAYLOAD/lib/GoALLCStatepoints.dylib" # Linux 使用 .so + +"$GOROOT/bin/go" build -o "$PLUGIN_BUILD/goallc-objview" cmd/objview +cmake -S "$GOROOT/src/cmd/llvmplugin" -B "$PLUGIN_BUILD" -G Ninja \ + -DLLVM_DIR="$LLVM_PAYLOAD/lib/cmake/llvm" \ + -DCMAKE_INSTALL_PREFIX="$LLVM_PAYLOAD" \ + -DBUILD_TESTING=ON \ + -DGOALLC_OBJVIEW_EXECUTABLE="$PLUGIN_BUILD/goallc-objview" +cmake --build "$PLUGIN_BUILD" --target GoALLCStatepoints +ctest --test-dir "$PLUGIN_BUILD" --output-on-failure +"$GOROOT/bin/go" test cmd/dist cmd/llvmtoolexec +GOALLC_LLC="$LLVM_PAYLOAD/bin/llc" \ +GOALLC_PASS_PLUGIN="$PLUGIN" \ + "$GOROOT/bin/go" test -count=1 -run '^TestLLVMInitTaskOrder$' cmd/llvmtoolexec +``` + +完整语言特性矩阵仍由 LLVM 白名单/黑名单测试负责;基础设施验证不把矩阵中已知 +未实现特性的失败误判成 payload 构建失败。 + +## Go action cache + +LLVM 模式下,原生 `compile -V=full` 不足以标识最终 object。`llvmtoolexec` 只对 +带 `-enablellvm` 的 compile action,把 wrapper、`llc`、可选 `opt`、pass +pipeline、plugin 和动态 `libLLVM` 的内容加入 tool identity。因此在相同路径 +替换 LLVM 或 plugin 会使 LLVM package 失效重编;没有启用 LLVM 的 package +保持原生 Go cache key。 + +不要用 `go clean -cache` 掩盖身份错误。只有在排查 action-cache 实现本身时才做 +clean-cache A/B 对比。 + +## static LLVM + +静态 binding 仍通过标准 Go 构建入口选择: + +```sh +cd "$GOROOT/src" +GOALLC_CCACHE=/path/to/ccache \ +./make.bash \ + -llvm-dir=/path/to/llvm-payload \ + -llvm-version=23 \ + -llvm-link=static +``` + +标准 LLVM install 会包含 LLVM 组件 archive;`cmd/dist` 根据 +`llvm-config --link-static --libfiles` 聚合它们。`-lm`、`-lz`、`-lzstd`、 +`-lxml2` 等平台 system libraries 不复制进 aggregate,而由 go-llvm 的平台 +LDFLAGS 在最终链接时解析,因此 Linux 仍需安装相应 development package。 +`llvm-config` 返回未知 system library,或最终链接找不到依赖时必须失败,不能退回 +系统 LLVM。 + +Darwin 的 zstd 通过 `pkg-config libzstd` 取得实际 Homebrew/MacPorts library +目录;只追加裸 `-lzstd` 在库不位于系统默认搜索路径时仍会链接失败。 +`CMAKE_OSX_DEPLOYMENT_TARGET` 只能约束本次构建的 LLVM/plugin object;如果 +包管理器提供的 zstd 本身要求更高版本的 macOS,链接器会明确告警,最终工具链的 +实际运行下限也会被该外部 dylib 抬高。需要发布到较旧系统时,必须改用以相同 +deployment target 构建的 zstd,不能把告警当作 payload 缓存问题处理。 + +## 手工构建时的最低要求 + +需要调试脚本本身时,可以手工执行 CMake,但仍必须遵守完整安装边界: + +```sh +cmake -S /path/to/llvm-project/llvm -B /path/to/llvm-build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/path/to/llvm-payload \ + -DCMAKE_INSTALL_MESSAGE=NEVER \ + -DCMAKE_C_COMPILER_LAUNCHER=/path/to/ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=/path/to/ccache \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 \ + -DLLVM_TARGETS_TO_BUILD='X86;AArch64' \ + -DLLVM_ENABLE_ASSERTIONS=ON \ + -DLLVM_BUILD_TOOLS=ON \ + -DLLVM_BUILD_UTILS=ON \ + -DLLVM_INSTALL_UTILS=ON \ + -DLLVM_INCLUDE_TESTS=ON \ + -DLLVM_BUILD_LLVM_DYLIB=ON \ + -DLLVM_LINK_LLVM_DYLIB=ON +cmake --build /path/to/llvm-build --parallel 12 +cmake --install /path/to/llvm-build + +cd "$GOROOT/src" +MACOSX_DEPLOYMENT_TARGET=13.0 \ + ./make.bash -llvm-dir=/path/to/llvm-payload -llvm-version=23 -llvm-link=dynamic +``` + +不要设置全局 `DYLD_LIBRARY_PATH`、`LD_LIBRARY_PATH`、`CGO_CPPFLAGS` 或 +`CGO_LDFLAGS` 来补齐路径。Go binding 使用选定 payload 的固定 include/lib 和 +rpath;需要这些全局变量才能成功通常说明 payload 不完整或选错了 LLVM。 + +LLVM build/install、plugin build、Go `make.bash` 和验证测试必须按顺序执行, +不要在同一个 `$GOROOT/pkg` 或 payload 上并发运行。 diff --git a/doc/goallc-llvm-goobj.md b/doc/goallc-llvm-goobj.md index 378e4d820b4d32..d1e03504cf4b35 100644 --- a/doc/goallc-llvm-goobj.md +++ b/doc/goallc-llvm-goobj.md @@ -347,19 +347,16 @@ home,而不是另建 locals spill。Args 和 Locals 表按完整 pair 一起 初始化的结果槽保持为空。StackObjects 仍未生成;追踪 alloca 地址也不能描述 alloca 对象内部保存的指针字段。 -在构建 Go toolchain 前,使用同一个 LLVM payload 的 CMake config 构建、 -测试并安装插件: +`make.bash` 会使用同一个 LLVM payload 的 CMake config 构建插件,并以新 inode +原子安装。规范的 LLVM payload、plugin、Go 构建和验证流程见 +[goallc-build.md](goallc-build.md): ```sh -LLVM_PAYLOAD=/path/to/goallc-llvm -PLUGIN_BUILD=/path/to/empty/plugin-build - -cmake -S "$GOROOT/src/cmd/llvmplugin" -B "$PLUGIN_BUILD" -G Ninja \ - -DLLVM_DIR="$LLVM_PAYLOAD/lib/cmake/llvm" \ - -DCMAKE_INSTALL_PREFIX="$LLVM_PAYLOAD" -cmake --build "$PLUGIN_BUILD" -ctest --test-dir "$PLUGIN_BUILD" --output-on-failure -cmake --install "$PLUGIN_BUILD" +cd "$GOROOT/src" +./make.bash \ + -llvm-dir=/path/to/goallc-llvm \ + -llvm-version=23 \ + -llvm-link=dynamic ``` 安装结果为 Darwin 上的 diff --git a/src/cmd/dist/build.go b/src/cmd/dist/build.go index d033311b661a93..55dea21758b8d5 100644 --- a/src/cmd/dist/build.go +++ b/src/cmd/dist/build.go @@ -1754,17 +1754,35 @@ func configureGoallcLLVM() { goallcLLVMDir = filepath.Clean(absDir) for _, required := range []string{ - pathf("%s/include/llvm-c", goallcLLVMDir), - pathf("%s/lib", goallcLLVMDir), + pathf("%s/include/llvm-c/Core.h", goallcLLVMDir), + pathf("%s/include/llvm/Config/llvm-config.h", goallcLLVMDir), pathf("%s/lib/cmake/llvm/LLVMConfig.cmake", goallcLLVMDir), - pathf("%s/bin/llc", goallcLLVMDir), - pathf("%s/bin/llvm-config", goallcLLVMDir), + pathf("%s/lib/cmake/llvm/AddLLVM.cmake", goallcLLVMDir), + pathf("%s/lib/cmake/llvm/LLVMExports.cmake", goallcLLVMDir), + pathf("%s/lib/libLLVMCore.a", goallcLLVMDir), } { if _, err := os.Stat(required); err != nil { fatalf("invalid LLVM payload %q: %v", goallcLLVMDir, err) } } + for _, required := range []string{ + pathf("%s/bin/llc", goallcLLVMDir), + pathf("%s/bin/llvm-ar", goallcLLVMDir), + pathf("%s/bin/llvm-config", goallcLLVMDir), + } { + if err := requireExecutableFile(required); err != nil { + fatalf("invalid LLVM payload %q: %s: %v", goallcLLVMDir, required, err) + } + } llvmConfig := pathf("%s/bin/llvm-config", goallcLLVMDir) + prefixOutput, err := exec.Command(llvmConfig, "--prefix").Output() + if err != nil { + fatalf("running %s --prefix: %v", llvmConfig, err) + } + prefix := strings.TrimSpace(string(prefixOutput)) + if !sameGoallcLLVMDirectory(prefix, goallcLLVMDir) { + fatalf("invalid LLVM payload %q: llvm-config reports prefix %q; use a complete cmake --install tree instead of combining build-tree bin/lib directories with copied headers", goallcLLVMDir, prefix) + } versionOutput, err := exec.Command(llvmConfig, "--version").Output() if err != nil { fatalf("running %s --version: %v", llvmConfig, err) @@ -1798,6 +1816,22 @@ func configureGoallcLLVM() { writeGoallcLLVMPayloadConfig() } +func sameGoallcLLVMDirectory(a, b string) bool { + resolve := func(path string) string { + path, err := filepath.Abs(path) + if err != nil { + return "" + } + if resolved, err := filepath.EvalSymlinks(path); err == nil { + path = resolved + } + return filepath.Clean(path) + } + a = resolve(a) + b = resolve(b) + return a != "" && a == b +} + func configureGoallcLLVMPayloadLink(bindingDir string) { link := pathf("%s/llvm", bindingDir) if info, err := os.Lstat(link); err == nil { diff --git a/src/cmd/dist/build_test.go b/src/cmd/dist/build_test.go index 0ace07bf276e8a..c741e6060b91e2 100644 --- a/src/cmd/dist/build_test.go +++ b/src/cmd/dist/build_test.go @@ -65,10 +65,15 @@ func TestGoallcPluginInputID(t *testing.T) { filepath.Join(source, "plugin.cpp"): "int plugin;", filepath.Join(payload, "bin", "llc"): "llc-v1", filepath.Join(payload, "bin", "llvm-config"): "llvm-config-v1", + filepath.Join(payload, "include", "llvm", "Config", "config.h"): "llvm-config-header-v1", + filepath.Join(payload, "include", "llvm-c", "Core.h"): "llvm-c-header-v1", filepath.Join(payload, "lib", "cmake", "llvm", "LLVMConfig.cmake"): "llvm-config-cmake-v1", filepath.Join(payload, "lib", "libLLVM.dylib"): "libllvm-v1", } for path, content := range files { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } if err := os.WriteFile(path, []byte(content), 0o600); err != nil { t.Fatal(err) } @@ -76,14 +81,14 @@ func TestGoallcPluginInputID(t *testing.T) { llc := filepath.Join(payload, "bin", "llc") llvmConfig := filepath.Join(payload, "bin", "llvm-config") library := filepath.Join(payload, "lib", "libLLVM.dylib") - want, err := goallcPluginInputID(source, llc, llvmConfig, library) + want, err := goallcPluginInputID(source, llc, llvmConfig, nil, library) if err != nil { t.Fatal(err) } if err := os.Chtimes(llc, time.Now().Add(-time.Hour), time.Now().Add(time.Hour)); err != nil { t.Fatal(err) } - got, err := goallcPluginInputID(source, llc, llvmConfig, library) + got, err := goallcPluginInputID(source, llc, llvmConfig, nil, library) if err != nil { t.Fatal(err) } @@ -93,7 +98,7 @@ func TestGoallcPluginInputID(t *testing.T) { if err := os.WriteFile(llc, []byte("llc-v2"), 0o600); err != nil { t.Fatal(err) } - changed, err := goallcPluginInputID(source, llc, llvmConfig, library) + changed, err := goallcPluginInputID(source, llc, llvmConfig, nil, library) if err != nil { t.Fatal(err) } @@ -106,11 +111,73 @@ func TestGoallcPluginInputID(t *testing.T) { if err := os.WriteFile(library, []byte("libllvm-v2"), 0o600); err != nil { t.Fatal(err) } - changed, err = goallcPluginInputID(source, llc, llvmConfig, library) + changed, err = goallcPluginInputID(source, llc, llvmConfig, nil, library) if err != nil { t.Fatal(err) } if changed == want { t.Fatal("libLLVM content change did not alter plugin input ID") } + if err := os.WriteFile(library, []byte("libllvm-v1"), 0o600); err != nil { + t.Fatal(err) + } + header := filepath.Join(payload, "include", "llvm", "Config", "config.h") + if err := os.WriteFile(header, []byte("llvm-config-header-v2"), 0o600); err != nil { + t.Fatal(err) + } + changed, err = goallcPluginInputID(source, llc, llvmConfig, nil, library) + if err != nil { + t.Fatal(err) + } + if changed == want { + t.Fatal("LLVM header content change did not alter plugin input ID") + } + changed, err = goallcPluginInputID(source, llc, llvmConfig, []string{"-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0"}, library) + if err != nil { + t.Fatal(err) + } + if changed == want { + t.Fatal("CMake configuration change did not alter plugin input ID") + } +} + +func TestGoallcDarwinDeploymentTarget(t *testing.T) { + payload := t.TempDir() + manifest := filepath.Join(payload, "share", "goallc", "build-manifest") + if err := os.MkdirAll(filepath.Dir(manifest), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(manifest, []byte("format=1\nmacos_deployment_target=13.0\n"), 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("MACOSX_DEPLOYMENT_TARGET", "") + if got, err := goallcDarwinDeploymentTarget(payload); err != nil || got != "13.0" { + t.Fatalf("manifest target: got %q, %v", got, err) + } + t.Setenv("MACOSX_DEPLOYMENT_TARGET", "14.1") + if got, err := goallcDarwinDeploymentTarget(payload); err != nil || got != "14.1" { + t.Fatalf("environment target: got %q, %v", got, err) + } + t.Setenv("MACOSX_DEPLOYMENT_TARGET", "latest") + if _, err := goallcDarwinDeploymentTarget(payload); err == nil { + t.Fatal("invalid target was accepted") + } +} + +func TestSameGoallcLLVMDirectory(t *testing.T) { + root := t.TempDir() + payload := filepath.Join(root, "payload") + if err := os.Mkdir(payload, 0o755); err != nil { + t.Fatal(err) + } + link := filepath.Join(root, "payload-link") + if err := os.Symlink(payload, link); err != nil { + t.Fatal(err) + } + if !sameGoallcLLVMDirectory(payload, link) { + t.Fatal("symlink to the same payload was not accepted") + } + if sameGoallcLLVMDirectory(payload, filepath.Join(root, "missing")) { + t.Fatal("different payload directories were accepted") + } } diff --git a/src/cmd/dist/goallc_plugin.go b/src/cmd/dist/goallc_plugin.go index c8319cf9c1e3dd..1d43bc98f324bd 100644 --- a/src/cmd/dist/goallc_plugin.go +++ b/src/cmd/dist/goallc_plugin.go @@ -61,11 +61,34 @@ func ensureGoallcPassPlugin() { destination := pathf("%s/lib/%s", goallcLLVMDir, goallcPassPluginFilename(gohostos)) stampPath := pathf("%s/goallc-plugin.stamp", buildDir) + buildModeOutput, err := exec.Command(llvmConfig, "--build-mode").Output() + if err != nil { + fatalf("reading LLVM build mode: %v", err) + } + buildMode := strings.TrimSpace(string(buildModeOutput)) + if buildMode == "" { + buildMode = "Release" + } + configuration := []string{ + "-DLLVM_DIR=" + pathf("%s/lib/cmake/llvm", goallcLLVMDir), + "-DCMAKE_INSTALL_PREFIX=" + goallcLLVMDir, + "-DCMAKE_BUILD_TYPE=" + buildMode, + "-DBUILD_TESTING=OFF", + } + if gohostos == "darwin" { + deploymentTarget, err := goallcDarwinDeploymentTarget(goallcLLVMDir) + if err != nil { + fatalf("resolving GoALLC macOS deployment target: %v", err) + } + if deploymentTarget != "" { + configuration = append(configuration, "-DCMAKE_OSX_DEPLOYMENT_TARGET="+deploymentTarget) + } + } runtimeLibraries, err := goallcLLVMRuntimeLibraries(llvmConfig) if err != nil { fatalf("resolving LLVM runtime libraries: %v", err) } - input, err := goallcPluginInputID(sourceDir, llc, llvmConfig, runtimeLibraries...) + input, err := goallcPluginInputID(sourceDir, llc, llvmConfig, configuration, runtimeLibraries...) if err != nil { fatalf("computing GoALLC pass plugin identity: %v", err) } @@ -76,22 +99,22 @@ func ensureGoallcPassPlugin() { } xprintf("Building GoALLC pass plugin for LLVM payload.\n") + // The input identity includes the installed LLVM headers and CMake package, + // but Ninja normally decides whether to rebuild from timestamps. An LLVM + // payload restored from an archive can therefore have different contents + // with older timestamps and leave stale plugin objects behind. Keep ccache, + // but discard the CMake/Ninja state whenever the content identity changes. + xremoveall(buildDir) xmkdirall(buildDir) - buildModeOutput, err := exec.Command(llvmConfig, "--build-mode").Output() - if err != nil { - fatalf("reading LLVM build mode: %v", err) - } - buildMode := strings.TrimSpace(string(buildModeOutput)) - if buildMode == "" { - buildMode = "Release" - } - run("", ShowOutput|CheckExit, "cmake", + cmakeArgs := []string{ + "cmake", "-S", sourceDir, "-B", buildDir, "-G", "Ninja", - "-DLLVM_DIR="+pathf("%s/lib/cmake/llvm", goallcLLVMDir), - "-DCMAKE_BUILD_TYPE="+buildMode, - ) + } + cmakeArgs = append(cmakeArgs, configuration...) + cmakeArgs = append(cmakeArgs, goallcCMakeLauncherArgs()...) + run("", ShowOutput|CheckExit, cmakeArgs...) run("", ShowOutput|CheckExit, "cmake", "--build", buildDir, "--target", "GoALLCStatepoints") built := pathf("%s/%s", buildDir, goallcPassPluginFilename(gohostos)) @@ -107,30 +130,52 @@ func ensureGoallcPassPlugin() { } } -func goallcPluginInputID(sourceDir, llc, llvmConfig string, runtimeLibraries ...string) (string, error) { +func goallcPluginInputID(sourceDir, llc, llvmConfig string, configuration []string, runtimeLibraries ...string) (string, error) { h := sha256.New() - io.WriteString(h, "goallc pass plugin input v1\x00") + io.WriteString(h, "goallc pass plugin input v3\x00") + for _, setting := range configuration { + io.WriteString(h, "configuration\x00"+setting+"\x00") + } var paths []string - err := filepath.WalkDir(sourceDir, func(path string, entry fs.DirEntry, err error) error { - if err != nil { - return err - } - if entry.IsDir() { - if path != sourceDir && (strings.HasPrefix(entry.Name(), "cmake-build") || entry.Name() == ".git") { - return filepath.SkipDir + collect := func(root string, include func(string, fs.DirEntry) bool) error { + return filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() { + if path != root && (strings.HasPrefix(entry.Name(), "cmake-build") || entry.Name() == ".git") { + return filepath.SkipDir + } + return nil + } + if include(path, entry) { + paths = append(paths, path) } return nil - } + }) + } + err := collect(sourceDir, func(path string, entry fs.DirEntry) bool { ext := strings.ToLower(filepath.Ext(path)) - if entry.Name() == "CMakeLists.txt" || ext == ".cmake" || ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".h" || ext == ".hpp" { - paths = append(paths, path) - } - return nil + return entry.Name() == "CMakeLists.txt" || ext == ".cmake" || ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".h" || ext == ".hpp" }) if err != nil { return "", err } - paths = append(paths, llc, llvmConfig, filepath.Join(filepath.Dir(llvmConfig), "..", "lib", "cmake", "llvm", "LLVMConfig.cmake")) + payloadRoot := filepath.Dir(filepath.Dir(llvmConfig)) + for _, root := range []string{ + filepath.Join(payloadRoot, "include", "llvm"), + filepath.Join(payloadRoot, "include", "llvm-c"), + filepath.Join(payloadRoot, "lib", "cmake", "llvm"), + } { + err := collect(root, func(path string, entry fs.DirEntry) bool { + ext := strings.ToLower(filepath.Ext(path)) + return ext == ".cmake" || ext == ".def" || ext == ".h" || ext == ".inc" || ext == ".td" + }) + if err != nil { + return "", err + } + } + paths = append(paths, llc, llvmConfig) paths = append(paths, runtimeLibraries...) sort.Strings(paths) for _, path := range paths { @@ -156,6 +201,70 @@ func goallcPluginInputID(sourceDir, llc, llvmConfig string, runtimeLibraries ... return hex.EncodeToString(h.Sum(nil)), nil } +func goallcDarwinDeploymentTarget(payloadRoot string) (string, error) { + target := os.Getenv("MACOSX_DEPLOYMENT_TARGET") + if target == "" { + manifest := filepath.Join(payloadRoot, "share", "goallc", "build-manifest") + data, err := os.ReadFile(manifest) + if err != nil && !os.IsNotExist(err) { + return "", err + } + if err == nil { + for _, line := range strings.Split(string(data), "\n") { + if value, ok := strings.CutPrefix(line, "macos_deployment_target="); ok { + target = value + break + } + } + } + } + if target == "" { + return "", nil + } + for _, component := range strings.Split(target, ".") { + if component == "" || strings.Trim(component, "0123456789") != "" { + return "", fmt.Errorf("invalid value %q", target) + } + } + return target, nil +} + +func goallcCMakeLauncherArgs() []string { + ccache := os.Getenv("GOALLC_CCACHE") + if ccache == "" { + ccache, _ = exec.LookPath("ccache") + } + if ccache == "" { + return nil + } + abs, err := filepath.Abs(ccache) + if err != nil { + fatalf("resolving GoALLC ccache %q: %v", ccache, err) + } + if err := requireExecutableFile(abs); err != nil { + fatalf("invalid GoALLC ccache %q: %v", abs, err) + } + xprintf("Using ccache for the GoALLC pass plugin: %s\n", abs) + return []string{ + "-DCMAKE_C_COMPILER_LAUNCHER=" + abs, + "-DCMAKE_CXX_COMPILER_LAUNCHER=" + abs, + } +} + +func requireExecutableFile(path string) error { + info, err := os.Stat(path) + if err != nil { + return err + } + if !info.Mode().IsRegular() { + return fmt.Errorf("not a regular file") + } + if info.Mode()&0o111 == 0 { + return fmt.Errorf("not executable") + } + return nil +} + func goallcLLVMRuntimeLibraries(llvmConfig string) ([]string, error) { root := filepath.Dir(filepath.Dir(llvmConfig)) var candidates []string diff --git a/src/cmd/go.mod b/src/cmd/go.mod index 76414c2f822299..0dff9d66869b39 100644 --- a/src/cmd/go.mod +++ b/src/cmd/go.mod @@ -3,7 +3,7 @@ module cmd go 1.27 require ( - github.com/goallc/go-llvm v0.0.0-20260802062312-1f9c9bc944a6 + github.com/goallc/go-llvm v0.0.0-20260805160647-b21d97bec4d0 github.com/google/pprof v0.0.0-20260507013755-92041b743c96 golang.org/x/arch v0.27.1-0.20260521044007-9c1a596a2c97 golang.org/x/build v0.0.0-20260522210304-d55d0041b921 diff --git a/src/cmd/go.sum b/src/cmd/go.sum index 6a983c36d539ee..9dabbd568fd069 100644 --- a/src/cmd/go.sum +++ b/src/cmd/go.sum @@ -1,5 +1,5 @@ -github.com/goallc/go-llvm v0.0.0-20260802062312-1f9c9bc944a6 h1:5Ipo1Z/H/Aet8t3N1+a0o+vh7NVRfXpWeCYngaKHOOM= -github.com/goallc/go-llvm v0.0.0-20260802062312-1f9c9bc944a6/go.mod h1:s70tU8u5zDo1LwIPqBxh6Qgol/tNt1kmn9caPfsTz8A= +github.com/goallc/go-llvm v0.0.0-20260805160647-b21d97bec4d0 h1:5DWFX2i2RT3a0c5sRZMUZslP6X+yuo7sQn+43YRmbFU= +github.com/goallc/go-llvm v0.0.0-20260805160647-b21d97bec4d0/go.mod h1:s70tU8u5zDo1LwIPqBxh6Qgol/tNt1kmn9caPfsTz8A= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20260507013755-92041b743c96 h1:YDDnaZ9afWajDboPMt9Vikqca/yWAX7KAxVzb4lJU1M= diff --git a/src/cmd/llvmplugin/README.md b/src/cmd/llvmplugin/README.md index fc8edf47d0d990..a15868dfa960f8 100644 --- a/src/cmd/llvmplugin/README.md +++ b/src/cmd/llvmplugin/README.md @@ -320,7 +320,21 @@ The type-derived entry map conservatively marks those words for now. A later change should give non-GC address words a distinct IR representation or a signature-level GC-shape attribute. -Build, test, and install it into the LLVM payload that contains `llc`: +The normal GoALLC build does not require a separate plugin install step. +`make.bash -llvm-dir=...` builds this directory against that payload's CMake +package and atomically installs the module. Build Go through its normal entry +point; `doc/goallc-build.md` documents how to prepare the LLVM payload first: + +```sh +cd "$GOROOT/src" +./make.bash \ + -llvm-dir=/path/to/goallc-llvm \ + -llvm-version=23 \ + -llvm-link=dynamic +``` + +For standalone plugin development, build, test, and install it into the LLVM +payload that contains `llc`: ```sh LLVM_PAYLOAD=/path/to/goallc-llvm diff --git a/src/cmd/vendor/github.com/goallc/go-llvm/README.markdown b/src/cmd/vendor/github.com/goallc/go-llvm/README.markdown index fd90b5edc7ad16..9af1e647c8edbc 100644 --- a/src/cmd/vendor/github.com/goallc/go-llvm/README.markdown +++ b/src/cmd/vendor/github.com/goallc/go-llvm/README.markdown @@ -34,6 +34,9 @@ For example: The static command requires `libLLVMGoALLC.a` to have been assembled first. The GoALLC toolchain does this automatically for `-llvm-link=static`. +The final link also consumes the system libraries reported by the selected +LLVM build. On Darwin, zstd is resolved with `pkg-config libzstd`; the +development package must be installed and discoverable through pkg-config. Do not select multiple version tags or multiple link-mode tags in one build. diff --git a/src/cmd/vendor/github.com/goallc/go-llvm/llvm_link_static.go b/src/cmd/vendor/github.com/goallc/go-llvm/llvm_link_static.go index b8dacfb5a49fa3..c171ce503120e6 100644 --- a/src/cmd/vendor/github.com/goallc/go-llvm/llvm_link_static.go +++ b/src/cmd/vendor/github.com/goallc/go-llvm/llvm_link_static.go @@ -4,6 +4,7 @@ package llvm /* #cgo darwin LDFLAGS: -L${SRCDIR} -lLLVMGoALLC -lm -lz -lxml2 +#cgo darwin pkg-config: libzstd #cgo linux LDFLAGS: -L${SRCDIR} -lLLVMGoALLC -lm -lz -lzstd -lxml2 -ldl -lpthread -lrt */ import "C" diff --git a/src/cmd/vendor/modules.txt b/src/cmd/vendor/modules.txt index 4b1b0d2daa3a18..b8e443f423f3de 100644 --- a/src/cmd/vendor/modules.txt +++ b/src/cmd/vendor/modules.txt @@ -1,4 +1,4 @@ -# github.com/goallc/go-llvm v0.0.0-20260802062312-1f9c9bc944a6 +# github.com/goallc/go-llvm v0.0.0-20260805160647-b21d97bec4d0 ## explicit; go 1.24 github.com/goallc/go-llvm # github.com/google/pprof v0.0.0-20260507013755-92041b743c96 From 1673b4c6f8aac2f82f83921392b8ad35549b9c2c Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Thu, 6 Aug 2026 00:44:28 +0800 Subject: [PATCH 2/7] cmd: make LLVM whitelist tooling deterministic --- doc/goallc-build.md | 14 ++ src/cmd/internal/testdir/llvm_test.go | 228 +++++++++++++++++++++++--- src/cmd/llvmtoolexec/main.go | 49 ++++-- src/cmd/llvmtoolexec/main_test.go | 9 + 4 files changed, 271 insertions(+), 29 deletions(-) diff --git a/doc/goallc-build.md b/doc/goallc-build.md index 89b9cb5988da9d..5805a235374ee8 100644 --- a/doc/goallc-build.md +++ b/doc/goallc-build.md @@ -160,6 +160,20 @@ relocatable prefix、manifest、工具、头文件、CMake package 和动态依 Release asset 可重定位且 checksum 稳定,再在一个 Go PR 中同时更新上述三个固定 值。旧 release 和 checksum 不覆盖,以便历史 Go commit 的 CI 可以复现。 +## LLVM 测试工具选择 + +`cmd/internal/testdir` 的 LLVM 测试在开始运行白名单前只选择一次 payload,顺序为 +显式的 `GOALLC_LLVM_DIR`、由 `GOALLC_LLC` 推导的根目录、`make.bash` 写入的 +`$GOROOT/pkg/goallc-llvm-payload`,最后才是兼容入口 `$GOROOT/llvm`。选定后, +`llc`、`opt`、`FileCheck`、`llvm-config` 和 pass plugin 必须全部来自这个 payload; +单项环境变量如果指向另一棵 LLVM 会作为基础设施错误立即失败,不再静默回退。 + +测试启动日志会打印 Go、payload、LLVM 工具、plugin 和优化 pipeline 的绝对路径。 +`llvmtoolexec` 从当前 Go checkout 临时构建,避免复用 `$GOROOT/pkg/tool` 中的旧 +wrapper。运行白名单使用 `default`,并由 wrapper 按 +`command-line-arguments` 包选择 LLVM lowering;用例 recipe 自己的 `-gcflags` +保持原样,不会覆盖或关闭 LLVM 编译。 + ## plugin 的构建与缓存 不需要在 `make.bash` 前手工构建或复制 plugin。`cmd/dist` 使用 payload 自己的 diff --git a/src/cmd/internal/testdir/llvm_test.go b/src/cmd/internal/testdir/llvm_test.go index 6a990a02360cc4..0aad04a1cfac46 100644 --- a/src/cmd/internal/testdir/llvm_test.go +++ b/src/cmd/internal/testdir/llvm_test.go @@ -21,6 +21,8 @@ import ( "testing" ) +const llvmTestToolexecEnv = "GOALLC_TEST_TOOLEXEC" + type llvmTestSet struct { Whitelist map[string]string `json:"whitelist"` Blacklist map[string]string `json:"blacklist"` @@ -43,6 +45,7 @@ func runLLVMTests(t *testing.T, common testCommon) { default: t.Skipf("LLVM GoObj is not configured for %s/%s", runtime.GOOS, runtime.GOARCH) } + configureLLVMTestToolchain(t) policy := readLLVMTestPolicy(t, common.gorootTestDir) platform := runtime.GOOS + "/" + runtime.GOARCH @@ -543,7 +546,7 @@ func (t test) runLLVMProgram(tempDir, executable, source string, flags, args []s // GoObj DWARF emission is not implemented by the LLVM backend yet. Disable // debug data explicitly so runtime qualification measures compile, link, and // execution support instead of failing in the linker's DWARF writer. - cmd := []string{goTool, "build", t.goGcflags(), "-gcflags=-enablellvm", "-ldflags=-w", "-toolexec=" + toolexec, "-o", exe} + cmd := []string{goTool, "build", t.goGcflags(), "-ldflags=-w", "-toolexec=" + toolexec, "-o", exe} cmd = append(cmd, flags...) cmd = append(cmd, source) if _, err := runcmd(cmd...); err != nil { @@ -558,14 +561,16 @@ func (t test) runLLVMProgram(tempDir, executable, source string, flags, args []s func llvmToolexec(t *testing.T, optPasses string) string { t.Helper() - out, err := exec.Command(goTool, "tool", "-n", "llvmtoolexec").CombinedOutput() - if err != nil { - t.Skipf("llvmtoolexec is unavailable: %v\n%s", err, out) - } - wrapper := strings.TrimSpace(string(out)) + wrapper := llvmToolexecPath(t) llc := llvmToolPath(t, "llc", "GOALLC_LLC") - args := []string{wrapper, "-llc=" + llc} + plugin := llvmPassPluginPath(t) + args := []string{ + wrapper, + "-llc=" + llc, + "-pass-plugin=" + plugin, + "-llvm-package=command-line-arguments", + } if optPasses != "" { opt := llvmToolPath(t, "opt", "GOALLC_OPT") args = append(args, "-opt="+opt, "-opt-passes="+optPasses) @@ -577,22 +582,207 @@ func llvmToolexec(t *testing.T, optPasses string) string { return value } -func llvmToolPath(t *testing.T, name, envName string) string { +type llvmTestToolchain struct { + root string + llc string + opt string + fileCheck string + plugin string + wrapper string +} + +func configureLLVMTestToolchain(t *testing.T) llvmTestToolchain { + t.Helper() + root, configured := llvmTestPayloadRoot(t) + if root == "" { + t.Skip("LLVM payload is unavailable; set GOALLC_LLVM_DIR or build Go with -llvm-dir") + } + + llvmConfig := llvmPayloadExecutable(t, root, "llvm-config") + version := llvmCommandOutput(t, llvmConfig, "--version") + if version != "23" && !strings.HasPrefix(version, "23.") { + t.Fatalf("selected LLVM payload %q has version %q, want LLVM 23", root, version) + } + prefix := llvmCommandOutput(t, llvmConfig, "--prefix") + if !sameLLVMTestPath(prefix, root) { + t.Fatalf("selected LLVM payload prefix mismatch: root %q, llvm-config --prefix %q", root, prefix) + } + + tools := llvmTestToolchain{ + root: root, + llc: llvmPayloadTool(t, root, "llc", "GOALLC_LLC"), + opt: llvmPayloadTool(t, root, "opt", "GOALLC_OPT"), + fileCheck: llvmPayloadTool(t, root, "FileCheck", "GOALLC_FILECHECK"), + plugin: llvmPayloadPlugin(t, root), + wrapper: buildLLVMTestToolexec(t), + } + + // Freeze every consumer to the validated payload. In particular, do not let + // an individual subtest silently fall back to a stale GOROOT/llvm tree. + t.Setenv("GOALLC_LLVM_DIR", tools.root) + t.Setenv("GOALLC_LLC", tools.llc) + t.Setenv("GOALLC_OPT", tools.opt) + t.Setenv("GOALLC_FILECHECK", tools.fileCheck) + t.Setenv("GOALLC_PASS_PLUGIN", tools.plugin) + t.Setenv(llvmTestToolexecEnv, tools.wrapper) + t.Logf("LLVM test toolchain: go=%s wrapper=%s payload=%s llc=%s opt=%s FileCheck=%s plugin=%s runtime-pipeline=default", + goTool, tools.wrapper, tools.root, tools.llc, tools.opt, tools.fileCheck, tools.plugin) + if configured != "" { + t.Logf("LLVM payload selected by %s", configured) + } + return tools +} + +func llvmTestPayloadRoot(t *testing.T) (root, configured string) { t.Helper() - var candidates []string - if candidate := os.Getenv(envName); candidate != "" { - candidates = append(candidates, candidate) + if value := strings.TrimSpace(os.Getenv("GOALLC_LLVM_DIR")); value != "" { + return llvmAbsolutePath(t, value, "GOALLC_LLVM_DIR"), "GOALLC_LLVM_DIR" } - if root := os.Getenv("GOALLC_LLVM_DIR"); root != "" { - candidates = append(candidates, filepath.Join(root, "bin", name)) + if value := strings.TrimSpace(os.Getenv("GOALLC_LLC")); value != "" { + llc := llvmRegularFile(t, value, "GOALLC_LLC", true) + return filepath.Dir(filepath.Dir(llc)), "GOALLC_LLC" } - candidates = append(candidates, filepath.Join(testenv.GOROOT(t), "llvm", "bin", name)) - for _, candidate := range candidates { - if info, err := os.Stat(candidate); err == nil && !info.IsDir() { - return candidate + goroot := testenv.GOROOT(t) + payloadConfig := filepath.Join(goroot, "pkg", "goallc-llvm-payload") + if data, err := os.ReadFile(payloadConfig); err == nil { + value := strings.TrimSpace(string(data)) + if value == "" { + t.Fatalf("LLVM payload configuration %s is empty", payloadConfig) } + return llvmAbsolutePath(t, value, payloadConfig), payloadConfig + } else if !os.IsNotExist(err) { + t.Fatalf("reading LLVM payload configuration %s: %v", payloadConfig, err) } - t.Skipf("%s is unavailable; set %s or install the LLVM payload under GOROOT/llvm", name, envName) - return "" + + legacy := filepath.Join(goroot, "llvm") + if _, err := os.Stat(filepath.Join(legacy, "bin", "llvm-config")); err == nil { + return legacy, "GOROOT/llvm" + } else if !os.IsNotExist(err) { + t.Fatalf("checking legacy LLVM payload %s: %v", legacy, err) + } + return "", "" +} + +func llvmPayloadTool(t *testing.T, root, name, envName string) string { + t.Helper() + expected := llvmPayloadExecutable(t, root, name) + if value := strings.TrimSpace(os.Getenv(envName)); value != "" { + configured := llvmRegularFile(t, value, envName, true) + if !sameLLVMTestFile(configured, expected) { + t.Fatalf("%s=%q does not belong to selected LLVM payload %q (expected %q)", envName, configured, root, expected) + } + } + return expected +} + +func llvmPayloadExecutable(t *testing.T, root, name string) string { + t.Helper() + return llvmRegularFile(t, filepath.Join(root, "bin", name), "LLVM payload", true) +} + +func llvmPayloadPlugin(t *testing.T, root string) string { + t.Helper() + name := "GoALLCStatepoints.so" + if runtime.GOOS == "darwin" { + name = "GoALLCStatepoints.dylib" + } + expected := llvmRegularFile(t, filepath.Join(root, "lib", name), "LLVM payload", false) + if value := strings.TrimSpace(os.Getenv("GOALLC_PASS_PLUGIN")); value != "" { + configured := llvmRegularFile(t, value, "GOALLC_PASS_PLUGIN", false) + if !sameLLVMTestFile(configured, expected) { + t.Fatalf("GOALLC_PASS_PLUGIN=%q does not belong to selected LLVM payload %q (expected %q)", configured, root, expected) + } + } + return expected +} + +func llvmToolexecPath(t *testing.T) string { + t.Helper() + if value := strings.TrimSpace(os.Getenv(llvmTestToolexecEnv)); value != "" { + return llvmRegularFile(t, value, llvmTestToolexecEnv, true) + } + out, err := exec.Command(goTool, "tool", "-n", "llvmtoolexec").CombinedOutput() + if err != nil { + t.Fatalf("resolving llvmtoolexec from selected Go tool %q: %v\n%s", goTool, err, out) + } + return llvmRegularFile(t, strings.TrimSpace(string(out)), "go tool llvmtoolexec", true) +} + +func buildLLVMTestToolexec(t *testing.T) string { + t.Helper() + wrapper := filepath.Join(t.TempDir(), "llvmtoolexec") + cmd := exec.Command(goTool, "build", "-o", wrapper, "cmd/llvmtoolexec") + cmd.Dir = testenv.GOROOT(t) + cmd.Env = append(os.Environ(), "GOENV=off", "GOFLAGS=") + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("building llvmtoolexec from the selected Go checkout: %v\n%s", err, out) + } + return llvmRegularFile(t, wrapper, "fresh llvmtoolexec", true) +} + +func llvmCommandOutput(t *testing.T, name string, args ...string) string { + t.Helper() + cmd := exec.Command(name, args...) + cmd.Env = append(os.Environ(), "GOENV=off", "GOFLAGS=") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("running %s %s: %v\n%s", name, strings.Join(args, " "), err, out) + } + return strings.TrimSpace(string(out)) +} + +func llvmAbsolutePath(t *testing.T, value, source string) string { + t.Helper() + path, err := filepath.Abs(value) + if err != nil { + t.Fatalf("resolving %s path %q: %v", source, value, err) + } + return filepath.Clean(path) +} + +func llvmRegularFile(t *testing.T, value, source string, executable bool) string { + t.Helper() + path := llvmAbsolutePath(t, value, source) + info, err := os.Stat(path) + if err != nil { + t.Fatalf("checking %s file %q: %v", source, path, err) + } + if !info.Mode().IsRegular() { + t.Fatalf("%s file %q is not a regular file", source, path) + } + if executable && info.Mode()&0o111 == 0 { + t.Fatalf("%s file %q is not executable", source, path) + } + return path +} + +func sameLLVMTestPath(a, b string) bool { + a, errA := filepath.EvalSymlinks(filepath.Clean(a)) + b, errB := filepath.EvalSymlinks(filepath.Clean(b)) + return errA == nil && errB == nil && a == b +} + +func sameLLVMTestFile(a, b string) bool { + aInfo, aErr := os.Stat(a) + bInfo, bErr := os.Stat(b) + return aErr == nil && bErr == nil && os.SameFile(aInfo, bInfo) +} + +func llvmToolPath(t *testing.T, name, envName string) string { + t.Helper() + root, _ := llvmTestPayloadRoot(t) + if root == "" { + t.Skipf("%s is unavailable; set GOALLC_LLVM_DIR or build Go with -llvm-dir", name) + } + return llvmPayloadTool(t, root, name, envName) +} + +func llvmPassPluginPath(t *testing.T) string { + t.Helper() + root, _ := llvmTestPayloadRoot(t) + if root == "" { + t.Skip("GoALLC pass plugin is unavailable; set GOALLC_LLVM_DIR or build Go with -llvm-dir") + } + return llvmPayloadPlugin(t, root) } diff --git a/src/cmd/llvmtoolexec/main.go b/src/cmd/llvmtoolexec/main.go index 6026afc92ff744..9ef9e527af1d1d 100644 --- a/src/cmd/llvmtoolexec/main.go +++ b/src/cmd/llvmtoolexec/main.go @@ -2,10 +2,12 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// llvm-toolexec is a go build -toolexec wrapper that replaces a Go compiler -// action carrying -enablellvm with an object produced by llc from compiler -// LLVM IR. The compiler still writes __.PKGDEF, so dependents retain normal Go -// export-data handling; it intentionally writes no native machine code. +// llvm-toolexec is a go build -toolexec wrapper that replaces selected Go +// compiler actions with objects produced by llc from compiler LLVM IR. An +// action is selected either by -enablellvm in its compiler flags or by matching +// -llvm-package. The compiler still writes __.PKGDEF, so dependents retain +// normal Go export-data handling; it intentionally writes no native machine +// code. package main import ( @@ -28,6 +30,7 @@ var ( optPath = flag.String("opt", os.Getenv("GOALLC_OPT"), "path to opt") optPasses = flag.String("opt-passes", "", "optional LLVM optimization pipeline to run before llc") passPluginPath = flag.String("pass-plugin", os.Getenv("GOALLC_PASS_PLUGIN"), "path to the GoALLC LLVM pass plugin (default next to llc)") + llvmPackage = flag.String("llvm-package", "", "compile import path to lower through LLVM regardless of package gcflags") keepIR = flag.Bool("keep-ir", false, "keep the compiler-generated .ll sidecar") ) @@ -38,12 +41,25 @@ func main() { } tool, args := flag.Arg(0), flag.Args()[1:] - if filepath.Base(tool) != "compile" || !boolToolFlag(args, "-enablellvm") { + if filepath.Base(tool) != "compile" { run(tool, args...) return } + useLLVM := boolToolFlag(args, "-enablellvm") || compilePackageMatches(args, *llvmPackage) if isFullVersion(args) { - printToolIdentity(tool, args, *llcPath, *optPath, *optPasses, *passPluginPath) + // A version probe has no package import path. If this wrapper can select + // a package itself, conservatively include the LLVM backend in the tool + // identity for every compiler action. Native actions may rebuild when the + // backend changes, but an LLVM action can never reuse a native cache entry. + if *llvmPackage != "" || useLLVM { + printToolIdentity(tool, args, *llcPath, *optPath, *optPasses, *passPluginPath, *llvmPackage) + } else { + run(tool, args...) + } + return + } + if !useLLVM { + run(tool, args...) return } if !isCompileAction(args) { @@ -63,9 +79,12 @@ func main() { if !ok || output == "" { fatalf("compile invocation has no -o output") } - compileArgs := make([]string, 0, len(args)+1) + compileArgs := make([]string, 0, len(args)+2) compileArgs = append(compileArgs, "-llvmironly") compileArgs = append(compileArgs, args...) + if !boolToolFlag(args, "-enablellvm") { + compileArgs = append(compileArgs, "-enablellvm") + } run(tool, compileArgs...) irPath := output + ".ll" @@ -231,11 +250,19 @@ func isCompileAction(args []string) bool { return ok && output != "" } -// printToolIdentity implements the toolexec -V=full protocol for an -// -enablellvm compile action. The native compiler identity alone is +func compilePackageMatches(args []string, packagePath string) bool { + if packagePath == "" { + return false + } + got, ok := toolFlag(args, "-p") + return ok && got == packagePath +} + +// printToolIdentity implements the toolexec -V=full protocol when this wrapper +// may select an LLVM compile action. The native compiler identity alone is // insufficient because llc and the pass plugin also determine the archive // written by this wrapper. -func printToolIdentity(tool string, args []string, llc, configuredOpt, optPasses, configuredPlugin string) { +func printToolIdentity(tool string, args []string, llc, configuredOpt, optPasses, configuredPlugin, llvmPackage string) { llc, err := resolveLLC(llc) if err != nil { fatalf("%v", err) @@ -280,6 +307,8 @@ func printToolIdentity(tool string, args []string, llc, configuredOpt, optPasses identityInput := append([]byte(nil), out...) identityInput = append(identityInput, "\x00opt-passes="...) identityInput = append(identityInput, optPasses...) + identityInput = append(identityInput, "\x00llvm-package="...) + identityInput = append(identityInput, llvmPackage...) identity, err := backendIdentity(identityInput, append([]string{wrapper}, backendFiles...)...) if err != nil { fatalf("computing backend identity: %v", err) diff --git a/src/cmd/llvmtoolexec/main_test.go b/src/cmd/llvmtoolexec/main_test.go index 86358a071c8b37..c35d9803fa7b4c 100644 --- a/src/cmd/llvmtoolexec/main_test.go +++ b/src/cmd/llvmtoolexec/main_test.go @@ -333,6 +333,15 @@ func TestCompileInvocationClassification(t *testing.T) { if !isCompileAction([]string{"-p=main", "-o", "out.a", "main.go"}) { t.Fatal("compile with output was not recognized") } + if !compilePackageMatches([]string{"-p=command-line-arguments", "-o", "out.a"}, "command-line-arguments") { + t.Fatal("selected compile package was not recognized") + } + if compilePackageMatches([]string{"-p=runtime", "-o", "out.a"}, "command-line-arguments") { + t.Fatal("unselected compile package was recognized") + } + if compilePackageMatches([]string{"-p=command-line-arguments", "-o", "out.a"}, "") { + t.Fatal("empty package selector matched a compile action") + } } func TestBoolToolFlag(t *testing.T) { From 3a9edf892640f1f145a9f475dcdaa875a6f17b3e Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Thu, 6 Aug 2026 08:12:08 +0800 Subject: [PATCH 3/7] ci: test GoALLC on Linux arm64 --- .github/workflows/goallc.yml | 23 ++++++++++++++++------- GOALLC.md | 8 ++++---- doc/goallc-build.md | 9 +++++---- src/cmd/internal/testdir/llvm_test.go | 2 +- src/cmd/llvmtoolexec/main_test.go | 1 + 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/.github/workflows/goallc.yml b/.github/workflows/goallc.yml index 00bc4d90ba0cac..9d243b494d1b1a 100644 --- a/.github/workflows/goallc.yml +++ b/.github/workflows/goallc.yml @@ -15,15 +15,24 @@ concurrency: cancel-in-progress: true env: - LLVM_RELEASE: goallc-llvm23.1.0-v1 - LLVM_ARCHIVE: goallc-llvm23.1.0-v1-linux-amd64.tar.zst - LLVM_REVISION: 407485ab2cd2545f66e84b5bbd1907cbcc0e6e4f + LLVM_RELEASE: goallc-llvm23.1.0-v2 + LLVM_REVISION: 632f85a1ea4bc0d2ba302aaa7133fe5fbf592545 jobs: - linux-amd64: - runs-on: ubuntu-22.04 + linux: + name: linux-${{ matrix.arch }} + runs-on: ${{ matrix.runner }} timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-22.04 + - arch: arm64 + runner: ubuntu-22.04-arm env: + LLVM_ARCHIVE: goallc-llvm23.1.0-v2-linux-${{ matrix.arch }}.tar.zst LLVM_ROOT: ${{ github.workspace }}/.goallc/llvm CCACHE_DIR: ${{ github.workspace }}/.ccache CCACHE_MAXSIZE: 5G @@ -55,9 +64,9 @@ jobs: uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: ${{ env.CCACHE_DIR }} - key: goallc-go-linux-amd64-${{ github.sha }} + key: goallc-go-linux-${{ matrix.arch }}-${{ github.sha }} restore-keys: | - goallc-go-linux-amd64- + goallc-go-linux-${{ matrix.arch }}- - name: Download pinned LLVM payload shell: bash diff --git a/GOALLC.md b/GOALLC.md index 51a861b757803c..c919077387a4e1 100644 --- a/GOALLC.md +++ b/GOALLC.md @@ -467,10 +467,10 @@ GOALLC_CCACHE=/path/to/ccache \ 不要再用源码头文件与 build tree 的 `bin`/`lib` 拼接 payload。开发测试所需的 `FileCheck` 通过 `LLVM_INSTALL_UTILS=ON` 一并安装。 -Linux amd64 CI 不从源码重复构建 LLVM;它固定下载 llvm-project Release -`goallc-llvm23.1.0-v1` 的 payload,并校验 archive SHA-256、LLVM revision、 -relocatable prefix 和完整安装布局后再运行 `make.bash`。发布与升级约定见上述 -构建文档。 +Linux amd64 和 arm64 CI 不从源码重复构建 LLVM;它们固定下载 llvm-project +Release `goallc-llvm23.1.0-v2` 中与 runner 架构匹配的 payload,并校验 archive +SHA-256、LLVM revision、relocatable prefix 和完整安装布局后再运行 +`make.bash`。发布与升级约定见上述构建文档。 默认目录和模式可用环境变量覆盖: diff --git a/doc/goallc-build.md b/doc/goallc-build.md index 5805a235374ee8..bf967dc6cb7d63 100644 --- a/doc/goallc-build.md +++ b/doc/goallc-build.md @@ -142,12 +142,13 @@ manifest 本身不作为编译缓存身份。 GoALLC LLVM payload 发布在 [`goallc/llvm-project` Releases](https://github.com/goallc/llvm-project/releases)。 -当前 Linux amd64 CI 固定使用: +当前 Linux amd64 和 arm64 CI 固定使用: ```text -tag: goallc-llvm23.1.0-v1 -revision: 407485ab2cd2545f66e84b5bbd1907cbcc0e6e4f -asset: goallc-llvm23.1.0-v1-linux-amd64.tar.zst +tag: goallc-llvm23.1.0-v2 +revision: 632f85a1ea4bc0d2ba302aaa7133fe5fbf592545 +amd64 asset: goallc-llvm23.1.0-v2-linux-amd64.tar.zst +arm64 asset: goallc-llvm23.1.0-v2-linux-arm64.tar.zst ``` 发布 tag 由 llvm-project 的 `.github/workflows/goallc-release.yml` 构建;归档和 diff --git a/src/cmd/internal/testdir/llvm_test.go b/src/cmd/internal/testdir/llvm_test.go index 0aad04a1cfac46..80100007e1da0a 100644 --- a/src/cmd/internal/testdir/llvm_test.go +++ b/src/cmd/internal/testdir/llvm_test.go @@ -41,7 +41,7 @@ type llvmTestPolicy struct { func runLLVMTests(t *testing.T, common testCommon) { t.Run("LLVM", func(t *testing.T) { switch runtime.GOOS + "/" + runtime.GOARCH { - case "darwin/arm64", "linux/amd64": + case "darwin/arm64", "linux/amd64", "linux/arm64": default: t.Skipf("LLVM GoObj is not configured for %s/%s", runtime.GOOS, runtime.GOARCH) } diff --git a/src/cmd/llvmtoolexec/main_test.go b/src/cmd/llvmtoolexec/main_test.go index c35d9803fa7b4c..35e132e9235cb4 100644 --- a/src/cmd/llvmtoolexec/main_test.go +++ b/src/cmd/llvmtoolexec/main_test.go @@ -699,6 +699,7 @@ func TestLLVMInitTaskOrder(t *testing.T) { t, goTool, "build", "-toolexec="+toolexec, "-gcflags=cmd/llvmtoolexec/testdata/inittask=-enablellvm", + "-ldflags=-w", "-o", executable, "./src/cmd/llvmtoolexec/testdata/inittask", ) From 9784b705b75734777c9df631a809ac292aea134e Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Thu, 6 Aug 2026 09:04:58 +0800 Subject: [PATCH 4/7] cmd/internal/testdir: add LLVM graylist policy --- .github/workflows/goallc.yml | 2 +- GOALLC.md | 2 +- doc/goallc-build.md | 9 +- doc/goallc-llvm-goobj.md | 32 ++- src/cmd/internal/testdir/llvm_test.go | 348 ++++++++++++++++++++------ test/llvm_tests.json | 15 +- 6 files changed, 309 insertions(+), 99 deletions(-) diff --git a/.github/workflows/goallc.yml b/.github/workflows/goallc.yml index 9d243b494d1b1a..0879fd8430cfe6 100644 --- a/.github/workflows/goallc.yml +++ b/.github/workflows/goallc.yml @@ -151,7 +151,7 @@ jobs: ./bin/go test -count=1 \ -run '^TestLLVMInitTaskOrder$' cmd/llvmtoolexec - - name: Test LLVM whitelist + - name: Test LLVM policy (white required, gray advisory) env: GOALLC_LLVM_DIR: ${{ env.LLVM_ROOT }} GOALLC_FILECHECK: ${{ env.LLVM_ROOT }}/bin/FileCheck diff --git a/GOALLC.md b/GOALLC.md index c919077387a4e1..38c16250117816 100644 --- a/GOALLC.md +++ b/GOALLC.md @@ -280,7 +280,7 @@ LLVM 主题分支已经实现: 调用 `llc`、追加唯一的 `_go_.o` 并进入正常 Go linker; - 当前 wrapper 通常只选择简单的 `main` package,不能把完整标准库切换到 LLVM; -- Go branch 已增加复用 `test/codegen` 和现有 `// run` 用例的黑白名单 +- Go branch 已增加复用 `test/codegen` 和现有 `// run` 用例的白/灰/黑名单 回归机制,具体维护方式见 [doc/goallc-llvm-goobj.md](doc/goallc-llvm-goobj.md#回归测试机制)。 diff --git a/doc/goallc-build.md b/doc/goallc-build.md index bf967dc6cb7d63..29bc032c91e99a 100644 --- a/doc/goallc-build.md +++ b/doc/goallc-build.md @@ -163,7 +163,7 @@ Release asset 可重定位且 checksum 稳定,再在一个 Go PR 中同时更 ## LLVM 测试工具选择 -`cmd/internal/testdir` 的 LLVM 测试在开始运行白名单前只选择一次 payload,顺序为 +`cmd/internal/testdir` 的 LLVM 测试在开始运行测试策略前只选择一次 payload,顺序为 显式的 `GOALLC_LLVM_DIR`、由 `GOALLC_LLC` 推导的根目录、`make.bash` 写入的 `$GOROOT/pkg/goallc-llvm-payload`,最后才是兼容入口 `$GOROOT/llvm`。选定后, `llc`、`opt`、`FileCheck`、`llvm-config` 和 pass plugin 必须全部来自这个 payload; @@ -171,7 +171,7 @@ Release asset 可重定位且 checksum 稳定,再在一个 Go PR 中同时更 测试启动日志会打印 Go、payload、LLVM 工具、plugin 和优化 pipeline 的绝对路径。 `llvmtoolexec` 从当前 Go checkout 临时构建,避免复用 `$GOROOT/pkg/tool` 中的旧 -wrapper。运行白名单使用 `default`,并由 wrapper 按 +wrapper。白名单和灰名单的 runtime 用例都使用 `default`,并由 wrapper 按 `command-line-arguments` 包选择 LLVM lowering;用例 recipe 自己的 `-gcflags` 保持原样,不会覆盖或关闭 LLVM 编译。 @@ -220,8 +220,9 @@ GOALLC_PASS_PLUGIN="$PLUGIN" \ "$GOROOT/bin/go" test -count=1 -run '^TestLLVMInitTaskOrder$' cmd/llvmtoolexec ``` -完整语言特性矩阵仍由 LLVM 白名单/黑名单测试负责;基础设施验证不把矩阵中已知 -未实现特性的失败误判成 payload 构建失败。 +完整语言特性矩阵仍由 LLVM 白/灰/黑名单测试负责。白名单失败会让 CI 失败; +灰名单总是运行并报告结果,但失败不影响 CI;黑名单完全不运行,并且只允许记录 +已知会超时或耗尽内存的用例。 ## Go action cache diff --git a/doc/goallc-llvm-goobj.md b/doc/goallc-llvm-goobj.md index d1e03504cf4b35..cffe0d3411d80c 100644 --- a/doc/goallc-llvm-goobj.md +++ b/doc/goallc-llvm-goobj.md @@ -426,13 +426,20 @@ GoALLC 不维护一套与 Go 仓库重复的测试源码。LLVM 测试由 - codegen 候选是 `test/codegen` 下 recipe 为 `// asmcheck` 的文件; - runtime 候选是 testdir 原本扫描目录中 recipe 为 `// run` 的文件; -- `test/llvm_tests.json` 分别维护 codegen 和 runtime 的白名单与黑名单; +- `test/llvm_tests.json` 分别维护 codegen 和 runtime 的白名单、灰名单与黑名单; - codegen source 出现 `// LLVM-OPT` directive 时,runner 还会执行 `opt -passes=default`,并用 `LLVM-OPT` prefix 检查优化后的 IR; -- 白名单是当前必须通过的用例;黑名单支持 glob,用于记录尚未覆盖的范围; - 精确白名单优先于宽泛黑名单; -- runner 会拒绝拼错的白名单、无匹配项的黑名单,以及未被任一名单分类的 - 候选,并报告当前白名单文件数和候选文件总数。 +- 白名单是当前必须通过的用例,任何失败都会使 CI 失败; +- 灰名单是可以执行但尚未要求通过的用例,runner 会记录每个失败和汇总, + 但不会因此使 CI 失败;验证稳定后通过把精确条目加入白名单来提升覆盖; +- 黑名单完全不执行,只能用于已知会超时或耗尽内存的用例;runner 会校验 + blacklist reason 明确包含 timeout 或 OOM; +- 三类的匹配优先级为黑名单、精确白名单、灰名单。灰名单可以用 glob 覆盖 + 尚未支持的范围,黑名单中的精确超时/OOM 条目仍能阻止执行; +- `platform_graylist` 把公共白名单项在指定平台降为灰名单,普通编译或运行失败 + 不再使用 platform blacklist;`platform_blacklist` 同样只保留超时/OOM; +- runner 会拒绝拼错的白名单、无匹配项的灰/黑名单,以及未被三类之一分类的 + 候选,并报告白、灰、黑三类文件数。 ### LLVM IR codegen 检查 @@ -461,7 +468,7 @@ FileCheck --check-prefix=LLVM test/codegen/example.go < package.a.ll ### LLVM runtime 检查 -runtime 白名单不增加新的 recipe,也不复制测试源码。runner 仍解析原文件的 +runtime 白名单和灰名单都不增加新的 recipe,也不复制测试源码。runner 仍解析原文件的 `// run` 参数、build constraint、超时和期望输出,但构建步骤改为用 `-gcflags=-enablellvm` 选择命令行 `main` package,再通过 `cmd/llvmtoolexec` 替换其对象: @@ -484,19 +491,20 @@ runtime 白名单不增加新的 recipe,也不复制测试源码。runner 仍 1. 先确认整个现有测试文件都能由当前 LLVM lowering 处理; 2. codegen 文件在源码中增加针对 LLVM IR 的 FileCheck 指令; -3. 在 `test/llvm_tests.json` 中增加精确白名单项和简短能力说明; -4. runtime 文件只需加入白名单,不修改其原有 `// run` recipe; +3. 新候选默认由灰名单 glob 执行;修复后在 `test/llvm_tests.json` 中增加精确 + 白名单项和简短能力说明; +4. runtime 文件从灰名单提升到白名单时,不修改其原有 `// run` recipe; 5. 运行 LLVM 定向测试,并同时运行对应的原生 asmcheck/run 测试。 -定向运行全部 LLVM 白名单: +定向运行完整 LLVM 策略(白名单和灰名单执行,黑名单跳过): ```sh go test cmd/internal/testdir -run='^Test$/^LLVM$' -v ``` -只运行一个 LLVM codegen 或 runtime 子测试时,可继续在 `-run` 中追加 -对应的 slash-separated subtest 名称。黑名单用于明确尚未覆盖的范围,不应 -把实际失败的用例加入白名单后再按 expected failure 处理。 +只运行一个 LLVM codegen、codegen-graylist、runtime 或 runtime-graylist 子测试时, +可继续在 `-run` 中追加对应的 slash-separated subtest 名称。普通失败留在灰名单; +只有确认会导致超时或 OOM 时才进入黑名单。 ## 当前范围与后续工作 diff --git a/src/cmd/internal/testdir/llvm_test.go b/src/cmd/internal/testdir/llvm_test.go index 80100007e1da0a..9c492220c9f6dc 100644 --- a/src/cmd/internal/testdir/llvm_test.go +++ b/src/cmd/internal/testdir/llvm_test.go @@ -25,7 +25,9 @@ const llvmTestToolexecEnv = "GOALLC_TEST_TOOLEXEC" type llvmTestSet struct { Whitelist map[string]string `json:"whitelist"` + Graylist map[string]string `json:"graylist"` Blacklist map[string]string `json:"blacklist"` + PlatformGraylist map[string]map[string]string `json:"platform_graylist,omitempty"` PlatformBlacklist map[string]map[string]string `json:"platform_blacklist,omitempty"` } @@ -34,10 +36,19 @@ type llvmTestPolicy struct { Runtime llvmTestSet `json:"runtime"` } -// runLLVMTests reuses the existing GOROOT/test inputs. The whitelist contains -// tests that must pass today; the blacklist classifies known unsupported tests. -// Whitelist entries take precedence over broad blacklist patterns, so enabling -// another test is a one-line policy change plus its LLVM IR checks. +type llvmTestClass uint8 + +const ( + llvmTestUnclassified llvmTestClass = iota + llvmTestWhite + llvmTestGray + llvmTestBlack +) + +// runLLVMTests reuses the existing GOROOT/test inputs. Whitelisted tests must +// pass, graylisted tests run for coverage without failing the suite, and +// blacklisted tests do not run. The blacklist is reserved for tests known to +// time out or exhaust memory. func runLLVMTests(t *testing.T, common testCommon) { t.Run("LLVM", func(t *testing.T) { switch runtime.GOOS + "/" + runtime.GOARCH { @@ -49,10 +60,10 @@ func runLLVMTests(t *testing.T, common testCommon) { policy := readLLVMTestPolicy(t, common.gorootTestDir) platform := runtime.GOOS + "/" + runtime.GOARCH - if err := applyLLVMPlatformBlacklist("codegen", platform, &policy.Codegen); err != nil { + if err := applyLLVMPlatformPolicy("codegen", platform, &policy.Codegen); err != nil { t.Fatal(err) } - if err := applyLLVMPlatformBlacklist("runtime", platform, &policy.Runtime); err != nil { + if err := applyLLVMPlatformPolicy("runtime", platform, &policy.Runtime); err != nil { t.Fatal(err) } codegenCandidates := llvmTestCandidates(t, common.gorootTestDir, []string{"codegen"}, "asmcheck") @@ -63,18 +74,34 @@ func runLLVMTests(t *testing.T, common testCommon) { validateLLVMTestSet(t, common.gorootTestDir, "codegen", codegenCandidates, policy.Codegen, true) validateLLVMTestSet(t, common.gorootTestDir, "runtime", runtimeCandidates, policy.Runtime, false) - t.Logf("LLVM codegen whitelist: %d/%d files", len(policy.Codegen.Whitelist), len(codegenCandidates)) - t.Logf("LLVM runtime whitelist: %d/%d files", len(policy.Runtime.Whitelist), len(runtimeCandidates)) + logLLVMTestPolicy(t, "codegen", codegenCandidates, policy.Codegen) + logLLVMTestPolicy(t, "runtime", runtimeCandidates, policy.Runtime) t.Run("codegen", func(t *testing.T) { - names := sortedLLVMWhitelist(policy.Codegen.Whitelist) + names := sortedLLVMTests(t, codegenCandidates, policy.Codegen, llvmTestWhite) for _, name := range names { t.Run(name, func(t *testing.T) { - runLLVMCodegenTest(t, common.gorootTestDir, name) + if err := runLLVMCodegenTest(t, common.gorootTestDir, name); err != nil { + t.Fatal(err) + } }) } }) + t.Run("codegen-graylist", func(t *testing.T) { + names := sortedLLVMTests(t, codegenCandidates, policy.Codegen, llvmTestGray) + failed := 0 + for _, name := range names { + t.Run(name, func(t *testing.T) { + if err := runLLVMCodegenTest(t, common.gorootTestDir, name); err != nil { + failed++ + t.Logf("LLVM graylist failure (allowed): %v", err) + } + }) + } + t.Logf("LLVM codegen graylist: ran %d tests, %d failed", len(names), failed) + }) + t.Run("abi-differential", func(t *testing.T) { runLLVMABIDifferentialTest(t, common.gorootTestDir) }) @@ -101,28 +128,46 @@ func runLLVMTests(t *testing.T, common testCommon) { }) t.Run("runtime", func(t *testing.T) { - names := sortedLLVMWhitelist(policy.Runtime.Whitelist) + names := sortedLLVMTests(t, runtimeCandidates, policy.Runtime, llvmTestWhite) for _, name := range names { t.Run(name, func(t *testing.T) { - dir, file := path.Split(name) - tc := test{ - testCommon: common, - T: t, - dir: strings.TrimSuffix(dir, "/"), - goFile: file, - llvm: true, - } - if err := tc.run(); err != nil { + if err := runLLVMRuntimeTest(t, common, name); err != nil { t.Fatal(err) } }) } }) + t.Run("runtime-graylist", func(t *testing.T) { + names := sortedLLVMTests(t, runtimeCandidates, policy.Runtime, llvmTestGray) + failed := 0 + for _, name := range names { + t.Run(name, func(t *testing.T) { + if err := runLLVMRuntimeTest(t, common, name); err != nil { + failed++ + t.Logf("LLVM graylist failure (allowed): %v", err) + } + }) + } + t.Logf("LLVM runtime graylist: ran %d tests, %d failed", len(names), failed) + }) + t.Run("writebarrier-ir", runLLVMWriteBarrierIRTests) }) } +func runLLVMRuntimeTest(t *testing.T, common testCommon, name string) error { + dir, file := path.Split(name) + tc := test{ + testCommon: common, + T: t, + dir: strings.TrimSuffix(dir, "/"), + goFile: file, + llvm: true, + } + return tc.run() +} + func runLLVMCompileOnlyRegression(t *testing.T, gorootTestDir, name string) { t.Helper() toolexec := llvmToolexec(t, "") @@ -156,7 +201,66 @@ func readLLVMTestPolicy(t *testing.T, gorootTestDir string) llvmTestPolicy { return policy } -func applyLLVMPlatformBlacklist(name, platform string, set *llvmTestSet) error { +func TestLLVMTestPolicy(t *testing.T) { + gorootTestDir := filepath.Join(testenv.GOROOT(t), "test") + if _, err := os.Stat(filepath.Join(gorootTestDir, "llvm_tests.json")); err != nil { + t.Skipf("LLVM test policy is not installed: %v", err) + } + codegenCandidates := llvmTestCandidates(t, gorootTestDir, []string{"codegen"}, "asmcheck") + runtimeCandidates := llvmTestCandidates(t, gorootTestDir, dirs, "run") + for name := range llvmTestCandidates(t, gorootTestDir, dirs, "runoutput") { + runtimeCandidates[name] = true + } + + base := readLLVMTestPolicy(t, gorootTestDir) + platforms := map[string]bool{runtime.GOOS + "/" + runtime.GOARCH: true} + for platform := range base.Codegen.PlatformGraylist { + platforms[platform] = true + } + for platform := range base.Codegen.PlatformBlacklist { + platforms[platform] = true + } + for platform := range base.Runtime.PlatformGraylist { + platforms[platform] = true + } + for platform := range base.Runtime.PlatformBlacklist { + platforms[platform] = true + } + for platform := range platforms { + t.Run(platform, func(t *testing.T) { + policy := readLLVMTestPolicy(t, gorootTestDir) + if err := applyLLVMPlatformPolicy("codegen", platform, &policy.Codegen); err != nil { + t.Fatal(err) + } + if err := applyLLVMPlatformPolicy("runtime", platform, &policy.Runtime); err != nil { + t.Fatal(err) + } + validateLLVMTestSet(t, gorootTestDir, "codegen", codegenCandidates, policy.Codegen, true) + validateLLVMTestSet(t, gorootTestDir, "runtime", runtimeCandidates, policy.Runtime, false) + }) + } +} + +func applyLLVMPlatformPolicy(name, platform string, set *llvmTestSet) error { + if set.Graylist == nil { + set.Graylist = make(map[string]string) + } + if set.Blacklist == nil { + set.Blacklist = make(map[string]string) + } + for target, entries := range set.PlatformGraylist { + if strings.TrimSpace(target) == "" { + return fmt.Errorf("LLVM %s platform graylist has an empty platform", name) + } + for filename, reason := range entries { + if strings.TrimSpace(reason) == "" { + return fmt.Errorf("LLVM %s platform graylist entry %q for %s has no reason", name, filename, target) + } + if _, ok := set.Whitelist[filename]; !ok { + return fmt.Errorf("LLVM %s platform graylist entry %q for %s is not in the common whitelist", name, filename, target) + } + } + } for target, entries := range set.PlatformBlacklist { if strings.TrimSpace(target) == "" { return fmt.Errorf("LLVM %s platform blacklist has an empty platform", name) @@ -165,39 +269,54 @@ func applyLLVMPlatformBlacklist(name, platform string, set *llvmTestSet) error { if strings.TrimSpace(reason) == "" { return fmt.Errorf("LLVM %s platform blacklist entry %q for %s has no reason", name, filename, target) } - if _, ok := set.Whitelist[filename]; !ok { - return fmt.Errorf("LLVM %s platform blacklist entry %q for %s is not in the common whitelist", name, filename, target) + if !validLLVMBlacklistReason(reason) { + return fmt.Errorf("LLVM %s platform blacklist entry %q for %s is not a timeout or OOM", name, filename, target) } } } - for filename := range set.PlatformBlacklist[platform] { + for filename, reason := range set.PlatformGraylist[platform] { + delete(set.Whitelist, filename) + set.Graylist[filename] = reason + } + for filename, reason := range set.PlatformBlacklist[platform] { delete(set.Whitelist, filename) + set.Blacklist[filename] = reason } return nil } -func TestApplyLLVMPlatformBlacklist(t *testing.T) { +func TestApplyLLVMPlatformPolicy(t *testing.T) { set := llvmTestSet{ Whitelist: map[string]string{ "common.go": "common", "linux.go": "linux", "darwin.go": "darwin", + "oom.go": "normally supported", }, - PlatformBlacklist: map[string]map[string]string{ + PlatformGraylist: map[string]map[string]string{ "linux/amd64": {"linux.go": "linux limitation"}, "darwin/arm64": {"darwin.go": "darwin limitation"}, }, + PlatformBlacklist: map[string]map[string]string{ + "linux/amd64": {"oom.go": "OOM: exceeds runner memory"}, + }, } - if err := applyLLVMPlatformBlacklist("runtime", "linux/amd64", &set); err != nil { + if err := applyLLVMPlatformPolicy("runtime", "linux/amd64", &set); err != nil { t.Fatal(err) } if _, ok := set.Whitelist["linux.go"]; ok { - t.Fatal("current-platform exclusion remained in the effective whitelist") + t.Fatal("current-platform graylist entry remained in the effective whitelist") + } + if _, ok := set.Graylist["linux.go"]; !ok { + t.Fatal("current-platform entry was not moved to the effective graylist") + } + if _, ok := set.Blacklist["oom.go"]; !ok { + t.Fatal("current-platform entry was not moved to the effective blacklist") } for _, filename := range []string{"common.go", "darwin.go"} { if _, ok := set.Whitelist[filename]; !ok { - t.Errorf("applyLLVMPlatformBlacklist removed %q for another platform", filename) + t.Errorf("applyLLVMPlatformPolicy removed %q for another platform", filename) } } @@ -209,33 +328,63 @@ func TestApplyLLVMPlatformBlacklist(t *testing.T) { { name: "empty platform", set: llvmTestSet{ - Whitelist: map[string]string{"test.go": "test"}, - PlatformBlacklist: map[string]map[string]string{"": {"test.go": "reason"}}, + Whitelist: map[string]string{"test.go": "test"}, + PlatformGraylist: map[string]map[string]string{"": {"test.go": "reason"}}, }, want: "empty platform", }, { name: "empty reason", set: llvmTestSet{ - Whitelist: map[string]string{"test.go": "test"}, - PlatformBlacklist: map[string]map[string]string{"linux/amd64": {"test.go": " "}}, + Whitelist: map[string]string{"test.go": "test"}, + PlatformGraylist: map[string]map[string]string{"linux/amd64": {"test.go": " "}}, }, want: "has no reason", }, { name: "not in common whitelist", set: llvmTestSet{ - Whitelist: map[string]string{"test.go": "test"}, - PlatformBlacklist: map[string]map[string]string{"linux/amd64": {"missing.go": "reason"}}, + Whitelist: map[string]string{"test.go": "test"}, + PlatformGraylist: map[string]map[string]string{"linux/amd64": {"missing.go": "reason"}}, }, want: "is not in the common whitelist", }, + { + name: "blacklist ordinary failure", + set: llvmTestSet{ + Whitelist: map[string]string{"test.go": "test"}, + PlatformBlacklist: map[string]map[string]string{"linux/amd64": {"test.go": "ordinary failure"}}, + }, + want: "is not a timeout or OOM", + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - err := applyLLVMPlatformBlacklist("runtime", "linux/amd64", &tc.set) + err := applyLLVMPlatformPolicy("runtime", "linux/amd64", &tc.set) if err == nil || !strings.Contains(err.Error(), tc.want) { - t.Fatalf("applyLLVMPlatformBlacklist error = %v, want substring %q", err, tc.want) + t.Fatalf("applyLLVMPlatformPolicy error = %v, want substring %q", err, tc.want) + } + }) + } +} + +func TestClassifyLLVMTest(t *testing.T) { + set := llvmTestSet{ + Whitelist: map[string]string{"white.go": "must pass", "black.go": "normally white"}, + Graylist: map[string]string{"*": "run speculatively"}, + Blacklist: map[string]string{"black.go": "timeout: does not terminate"}, + } + for _, tc := range []struct { + name string + want llvmTestClass + }{ + {"white.go", llvmTestWhite}, + {"gray.go", llvmTestGray}, + {"black.go", llvmTestBlack}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := classifyLLVMTest(t, set, tc.name); got != tc.want { + t.Fatalf("classifyLLVMTest(%q) = %v, want %v", tc.name, got, tc.want) } }) } @@ -290,7 +439,11 @@ func llvmTestAction(t *testing.T, filename string) string { func validateLLVMTestSet(t *testing.T, gorootTestDir, name string, candidates map[string]bool, set llvmTestSet, requireChecks bool) { t.Helper() failed := false - for filename := range set.Whitelist { + for filename, reason := range set.Whitelist { + if strings.TrimSpace(reason) == "" { + t.Errorf("LLVM %s whitelist entry %q has no reason", name, filename) + failed = true + } if !candidates[filename] { t.Errorf("LLVM %s whitelist entry %q is not a %s test", name, filename, name) failed = true @@ -307,33 +460,39 @@ func validateLLVMTestSet(t *testing.T, gorootTestDir, name string, candidates ma } } - for pattern := range set.Blacklist { - matched := false - for filename := range candidates { - if llvmPathMatch(t, pattern, filename) { - matched = true - break + for _, entries := range []struct { + class string + items map[string]string + }{ + {"graylist", set.Graylist}, + {"blacklist", set.Blacklist}, + } { + for pattern, reason := range entries.items { + if strings.TrimSpace(reason) == "" { + t.Errorf("LLVM %s %s pattern %q has no reason", name, entries.class, pattern) + failed = true + } + if entries.class == "blacklist" && !validLLVMBlacklistReason(reason) { + t.Errorf("LLVM %s blacklist pattern %q is not a timeout or OOM", name, pattern) + failed = true + } + matched := false + for filename := range candidates { + if llvmPathMatch(t, pattern, filename) { + matched = true + break + } + } + if !matched { + t.Errorf("LLVM %s %s pattern %q matches no tests", name, entries.class, pattern) + failed = true } - } - if !matched { - t.Errorf("LLVM %s blacklist pattern %q matches no tests", name, pattern) - failed = true } } for filename := range candidates { - if _, ok := set.Whitelist[filename]; ok { - continue - } - classified := false - for pattern := range set.Blacklist { - if llvmPathMatch(t, pattern, filename) { - classified = true - break - } - } - if !classified { - t.Errorf("LLVM %s test %q is in neither whitelist nor blacklist", name, filename) + if classifyLLVMTest(t, set, filename) == llvmTestUnclassified { + t.Errorf("LLVM %s test %q is not classified as white, gray, or black", name, filename) failed = true } } @@ -342,37 +501,75 @@ func validateLLVMTestSet(t *testing.T, gorootTestDir, name string, candidates ma } } +func validLLVMBlacklistReason(reason string) bool { + reason = strings.ToLower(reason) + return strings.Contains(reason, "timeout") || + strings.Contains(reason, "out of memory") || + strings.Contains(reason, "oom") +} + +func classifyLLVMTest(t *testing.T, set llvmTestSet, filename string) llvmTestClass { + t.Helper() + for pattern := range set.Blacklist { + if llvmPathMatch(t, pattern, filename) { + return llvmTestBlack + } + } + if _, ok := set.Whitelist[filename]; ok { + return llvmTestWhite + } + for pattern := range set.Graylist { + if llvmPathMatch(t, pattern, filename) { + return llvmTestGray + } + } + return llvmTestUnclassified +} + +func logLLVMTestPolicy(t *testing.T, name string, candidates map[string]bool, set llvmTestSet) { + t.Helper() + counts := make(map[llvmTestClass]int) + for filename := range candidates { + counts[classifyLLVMTest(t, set, filename)]++ + } + t.Logf("LLVM %s policy: %d white, %d gray, %d black (%d files)", + name, counts[llvmTestWhite], counts[llvmTestGray], counts[llvmTestBlack], len(candidates)) +} + func llvmPathMatch(t *testing.T, pattern, filename string) bool { t.Helper() matched, err := path.Match(pattern, filename) if err != nil { - t.Fatalf("invalid LLVM test blacklist pattern %q: %v", pattern, err) + t.Fatalf("invalid LLVM test policy pattern %q: %v", pattern, err) } if matched || strings.Contains(pattern, "/") { return matched } matched, err = path.Match(pattern, path.Base(filename)) if err != nil { - t.Fatalf("invalid LLVM test blacklist pattern %q: %v", pattern, err) + t.Fatalf("invalid LLVM test policy pattern %q: %v", pattern, err) } return matched } -func sortedLLVMWhitelist(entries map[string]string) []string { - names := make([]string, 0, len(entries)) - for name := range entries { - names = append(names, name) +func sortedLLVMTests(t *testing.T, candidates map[string]bool, set llvmTestSet, class llvmTestClass) []string { + t.Helper() + names := make([]string, 0, len(candidates)) + for name := range candidates { + if classifyLLVMTest(t, set, name) == class { + names = append(names, name) + } } sort.Strings(names) return names } -func runLLVMCodegenTest(t *testing.T, gorootTestDir, name string) { +func runLLVMCodegenTest(t *testing.T, gorootTestDir, name string) error { t.Helper() source := filepath.Join(gorootTestDir, filepath.FromSlash(name)) src, err := os.ReadFile(source) if err != nil { - t.Fatal(err) + return err } header, _, _ := strings.Cut(string(src), "\npackage") if ok, why := shouldTest(header, goos, goarch); !ok { @@ -392,29 +589,29 @@ func runLLVMCodegenTest(t *testing.T, gorootTestDir, name string) { cmd.Env = append(os.Environ(), "GOENV=off", "GOFLAGS=") out, err := cmd.CombinedOutput() if err != nil { - t.Fatalf("LLVM compilation failed: %v\n%s", err, out) + return fmt.Errorf("LLVM compilation failed: %v\n%s", err, out) } irBytes, err := os.ReadFile(archive + ".ll") if err != nil { - t.Fatal(err) + return err } opt := llvmToolPath(t, "opt", "GOALLC_OPT") cmd = exec.Command(opt, "-passes=verify", "-disable-output") cmd.Stdin = bytes.NewReader(irBytes) cmd.Env = append(os.Environ(), "GOENV=off", "GOFLAGS=") if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("LLVM verifier failed: %v\n%s", err, out) + return fmt.Errorf("LLVM verifier failed: %v\n%s", err, out) } fileCheck := llvmToolPath(t, "FileCheck", "GOALLC_FILECHECK") cmd = exec.Command(fileCheck, "--check-prefixes="+llvmFileCheckPrefixes("LLVM", src), source) cmd.Stdin = bytes.NewReader(irBytes) cmd.Env = append(os.Environ(), "GOENV=off", "GOFLAGS=") if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("FileCheck failed: %v\n%s", err, out) + return fmt.Errorf("FileCheck failed: %v\n%s", err, out) } if !bytes.Contains(src, []byte("// LLVM-OPT")) { - return + return nil } cmd = exec.Command(opt, "-passes=default", "-S") cmd.Stdin = bytes.NewReader(irBytes) @@ -423,14 +620,15 @@ func runLLVMCodegenTest(t *testing.T, gorootTestDir, name string) { cmd.Stderr = &stderr optimizedIR, err := cmd.Output() if err != nil { - t.Fatalf("LLVM optimization failed: %v\n%s", err, stderr.Bytes()) + return fmt.Errorf("LLVM optimization failed: %v\n%s", err, stderr.Bytes()) } cmd = exec.Command(fileCheck, "--check-prefixes="+llvmFileCheckPrefixes("LLVM-OPT", src), source) cmd.Stdin = bytes.NewReader(optimizedIR) cmd.Env = append(os.Environ(), "GOENV=off", "GOFLAGS=") if out, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("optimized LLVM FileCheck failed: %v\n%s", err, out) + return fmt.Errorf("optimized LLVM FileCheck failed: %v\n%s", err, out) } + return nil } func llvmFileCheckPrefixes(base string, source []byte) string { diff --git a/test/llvm_tests.json b/test/llvm_tests.json index 7d70aff124c4d5..8940cf5cb0e6e5 100644 --- a/test/llvm_tests.json +++ b/test/llvm_tests.json @@ -74,10 +74,11 @@ "codegen/type_descriptor_methods.go": "uncommon type data, concrete method tables, wrappers, and R_METHODOFF", "codegen/mathbits.go": "trailing-zero and population-count intrinsics plus complete integer bit-operation lowering" }, - "blacklist": { + "graylist": { "codegen/*": "LLVM lowering support has not reached this complete source file yet" }, - "platform_blacklist": { + "blacklist": {}, + "platform_graylist": { "linux/amd64": { "codegen/llvm_abs.go": "the native AMD64 frontend does not form OpAbs for math.Abs", "codegen/llvm_trunc.go": "GOAMD64=v1 guards OpTrunc with unsupported HasCPUFeature lowering" @@ -185,8 +186,6 @@ "llvm_statepoint_gc.go": "heap pointer liveness across runtime.GC", "llvm_statepoint_nested_gc.go": "pointer relocation through nested conditional control flow", "chan/nonblock.go": "non-blocking channel send and receive after zero-sized GoObj relocation targets are preserved", - "chan/powser1.go": "upstream regression repeatedly qualified through LLVM compile, link, and execution", - "chan/powser2.go": "upstream regression repeatedly qualified through LLVM compile, link, and execution", "chan/zerosize.go": "upstream regression repeatedly qualified through LLVM compile, link, and execution", "convT2X.go": "concrete values converted to empty and non-empty interfaces with zero-sized symbol data", "fixedbugs/bug000.go": "upstream regression repeatedly qualified through LLVM compile, link, and execution", @@ -1164,10 +1163,14 @@ "typeparam/list2.go": "zero-length pointer-array stack layout qualified through LLVM compile, GoObj link, and execution", "zerodivide.go": "Darwin/arm64 blacklist probe qualified through LLVM compile, GoObj link, and execution" }, - "blacklist": { + "graylist": { "*": "LLVM lowering or runtime ABI support has not reached this test yet" }, - "platform_blacklist": { + "blacklist": { + "chan/powser1.go": "timeout: exhaustive channel powerset test does not complete within the per-case limit", + "chan/powser2.go": "timeout: exhaustive channel powerset test does not complete within the per-case limit" + }, + "platform_graylist": { "linux/amd64": { "abi/method_wrapper.go": "x86 GoObj traceback does not yet unwind the panic path through the method wrapper", "abi/wrapdefer_largetmp.go": "x86 GoObj traceback does not yet unwind the large deferred-wrapper frame during stack growth", From 059e8a104601d9799b088576058f7833c8688f30 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Thu, 6 Aug 2026 09:24:29 +0800 Subject: [PATCH 5/7] cmd/compile: configure Linux arm64 LLVM GoObj target --- src/cmd/compile/internal/ssa/ssa2llvm.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cmd/compile/internal/ssa/ssa2llvm.go b/src/cmd/compile/internal/ssa/ssa2llvm.go index b9d883a6ce3e6f..f8dc9cdb6aabd8 100644 --- a/src/cmd/compile/internal/ssa/ssa2llvm.go +++ b/src/cmd/compile/internal/ssa/ssa2llvm.go @@ -2672,6 +2672,8 @@ func goObjTargetTriple() string { switch buildcfg.GOOS + "/" + buildcfg.GOARCH { case "darwin/arm64": return "aarch64-apple-darwin-goobj" + case "linux/arm64": + return "aarch64-unknown-linux-goobj" case "linux/amd64": return "x86_64-unknown-linux-goobj" default: From a014f2a96774df9015e592e0b295c326d1aa490c Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Thu, 6 Aug 2026 09:39:56 +0800 Subject: [PATCH 6/7] cmd/internal/testdir: report LLVM policy results --- doc/goallc-llvm-goobj.md | 5 +- src/cmd/internal/testdir/llvm_alloca_test.go | 7 +- src/cmd/internal/testdir/llvm_test.go | 80 ++++++++++++++++---- test/llvm_tests.json | 1 + 4 files changed, 73 insertions(+), 20 deletions(-) diff --git a/doc/goallc-llvm-goobj.md b/doc/goallc-llvm-goobj.md index cffe0d3411d80c..dfd4726b3bb1ea 100644 --- a/doc/goallc-llvm-goobj.md +++ b/doc/goallc-llvm-goobj.md @@ -431,9 +431,10 @@ GoALLC 不维护一套与 Go 仓库重复的测试源码。LLVM 测试由 `opt -passes=default`,并用 `LLVM-OPT` prefix 检查优化后的 IR; - 白名单是当前必须通过的用例,任何失败都会使 CI 失败; - 灰名单是可以执行但尚未要求通过的用例,runner 会记录每个失败和汇总, - 但不会因此使 CI 失败;验证稳定后通过把精确条目加入白名单来提升覆盖; + 并为每个用例明确输出 `PASS`、`FAIL (allowed)` 或 `SKIP`,但不会因此使 + CI 失败;验证稳定后通过把精确条目加入白名单来提升覆盖; - 黑名单完全不执行,只能用于已知会超时或耗尽内存的用例;runner 会校验 - blacklist reason 明确包含 timeout 或 OOM; + blacklist reason 明确包含 timeout 或 OOM,并在日志中输出 `NOT RUN`; - 三类的匹配优先级为黑名单、精确白名单、灰名单。灰名单可以用 glob 覆盖 尚未支持的范围,黑名单中的精确超时/OOM 条目仍能阻止执行; - `platform_graylist` 把公共白名单项在指定平台降为灰名单,普通编译或运行失败 diff --git a/src/cmd/internal/testdir/llvm_alloca_test.go b/src/cmd/internal/testdir/llvm_alloca_test.go index b311234ce2e4c2..88028937b3e4de 100644 --- a/src/cmd/internal/testdir/llvm_alloca_test.go +++ b/src/cmd/internal/testdir/llvm_alloca_test.go @@ -85,8 +85,9 @@ func runLLVMAllocaStatepointTest(t *testing.T, gorootTestDir string) { } // LLVM compilation runs before native register allocation turns - // OpKeepAlive of a stack address into OpVarLive. Preserve the value use - // explicitly so statepoint liveness extends through the preceding calls. + // OpKeepAlive of a stack address into OpVarLive. Preserve the value in the + // go.keepalive operand bundle so statepoint liveness extends through the + // preceding calls. keepAliveArchive := filepath.Join(dir, "keepalive.a") keepAliveSource := filepath.Join(gorootTestDir, "fixedbugs", "issue30476.go") runLLVMABICommand(t, nil, goTool, "tool", "compile", @@ -101,7 +102,7 @@ func runLLVMAllocaStatepointTest(t *testing.T, gorootTestDir string) { `call goabiinternal void @runtime\.GC\(\).*` + `call goabiinternal void @runtime\.GC\(\).*` + `call goabiinternal void @runtime\.GC\(\).*` + - `call void \(\.\.\.\) @llvm\.fake\.use\(ptr %v[0-9]+\)`) + `call void @llvm\.donothing\(\) \[ "go\.keepalive"\(ptr %v[0-9]+\) \]`) if !keepAlivePattern.Match(keepAliveFunction) { t.Fatalf("OpKeepAlive is not preserved after the GC calls\n%s", keepAliveFunction) } diff --git a/src/cmd/internal/testdir/llvm_test.go b/src/cmd/internal/testdir/llvm_test.go index 9c492220c9f6dc..10c1bc66a06de5 100644 --- a/src/cmd/internal/testdir/llvm_test.go +++ b/src/cmd/internal/testdir/llvm_test.go @@ -76,6 +76,8 @@ func runLLVMTests(t *testing.T, common testCommon) { logLLVMTestPolicy(t, "codegen", codegenCandidates, policy.Codegen) logLLVMTestPolicy(t, "runtime", runtimeCandidates, policy.Runtime) + logLLVMBlacklist(t, "codegen", codegenCandidates, policy.Codegen) + logLLVMBlacklist(t, "runtime", runtimeCandidates, policy.Runtime) t.Run("codegen", func(t *testing.T) { names := sortedLLVMTests(t, codegenCandidates, policy.Codegen, llvmTestWhite) @@ -90,16 +92,31 @@ func runLLVMTests(t *testing.T, common testCommon) { t.Run("codegen-graylist", func(t *testing.T) { names := sortedLLVMTests(t, codegenCandidates, policy.Codegen, llvmTestGray) - failed := 0 + passed, failed, skipped := 0, 0, 0 for _, name := range names { t.Run(name, func(t *testing.T) { - if err := runLLVMCodegenTest(t, common.gorootTestDir, name); err != nil { - failed++ - t.Logf("LLVM graylist failure (allowed): %v", err) + var runErr error + t.Cleanup(func() { + switch { + case t.Skipped(): + skipped++ + t.Log("LLVM graylist result: SKIP") + case runErr != nil: + failed++ + t.Log("LLVM graylist result: FAIL (allowed)") + default: + passed++ + t.Log("LLVM graylist result: PASS") + } + }) + runErr = runLLVMCodegenTest(t, common.gorootTestDir, name) + if runErr != nil { + t.Logf("LLVM graylist failure detail: %v", runErr) } }) } - t.Logf("LLVM codegen graylist: ran %d tests, %d failed", len(names), failed) + t.Logf("LLVM codegen graylist summary: %d passed, %d failed (allowed), %d skipped", + passed, failed, skipped) }) t.Run("abi-differential", func(t *testing.T) { @@ -140,16 +157,31 @@ func runLLVMTests(t *testing.T, common testCommon) { t.Run("runtime-graylist", func(t *testing.T) { names := sortedLLVMTests(t, runtimeCandidates, policy.Runtime, llvmTestGray) - failed := 0 + passed, failed, skipped := 0, 0, 0 for _, name := range names { t.Run(name, func(t *testing.T) { - if err := runLLVMRuntimeTest(t, common, name); err != nil { - failed++ - t.Logf("LLVM graylist failure (allowed): %v", err) + var runErr error + t.Cleanup(func() { + switch { + case t.Skipped(): + skipped++ + t.Log("LLVM graylist result: SKIP") + case runErr != nil: + failed++ + t.Log("LLVM graylist result: FAIL (allowed)") + default: + passed++ + t.Log("LLVM graylist result: PASS") + } + }) + runErr = runLLVMRuntimeTest(t, common, name) + if runErr != nil { + t.Logf("LLVM graylist failure detail: %v", runErr) } }) } - t.Logf("LLVM runtime graylist: ran %d tests, %d failed", len(names), failed) + t.Logf("LLVM runtime graylist summary: %d passed, %d failed (allowed), %d skipped", + passed, failed, skipped) }) t.Run("writebarrier-ir", runLLVMWriteBarrierIRTests) @@ -536,6 +568,20 @@ func logLLVMTestPolicy(t *testing.T, name string, candidates map[string]bool, se name, counts[llvmTestWhite], counts[llvmTestGray], counts[llvmTestBlack], len(candidates)) } +func logLLVMBlacklist(t *testing.T, name string, candidates map[string]bool, set llvmTestSet) { + t.Helper() + for _, filename := range sortedLLVMTests(t, candidates, set, llvmTestBlack) { + reason := "" + for pattern, candidateReason := range set.Blacklist { + if llvmPathMatch(t, pattern, filename) { + reason = candidateReason + break + } + } + t.Logf("LLVM %s blacklist result: NOT RUN %s: %s", name, filename, reason) + } +} + func llvmPathMatch(t *testing.T, pattern, filename string) bool { t.Helper() matched, err := path.Match(pattern, filename) @@ -712,7 +758,7 @@ func runLLVMWriteBarrierIRTests(t *testing.T) { } func (t test) runLLVMCase(tempDir string, flags, args []string, runcmd runCmd) error { - out, err := t.runLLVMProgram(tempDir, "test.exe", t.goFileName(), flags, args, runcmd) + out, err := t.runLLVMProgram(tempDir, "test.exe", []string{t.goFileName()}, flags, args, runcmd) if err != nil { return err } @@ -720,7 +766,11 @@ func (t test) runLLVMCase(tempDir string, flags, args []string, runcmd runCmd) e } func (t test) runLLVMRunoutputCase(tempDir string, args []string, runcmd runCmd) error { - out, err := t.runLLVMProgram(tempDir, "generator.exe", t.goFileName(), nil, args, runcmd) + // The arguments in a runoutput recipe are additional generator source + // files, just as they are for "go run file.go args..." in testdir's native + // path. They are not arguments to the generator executable. + generatorSources := append([]string{t.goFileName()}, args...) + out, err := t.runLLVMProgram(tempDir, "generator.exe", generatorSources, nil, nil, runcmd) if err != nil { return err } @@ -728,14 +778,14 @@ func (t test) runLLVMRunoutputCase(tempDir string, args []string, runcmd runCmd) if err := os.WriteFile(generated, out, 0o666); err != nil { return err } - out, err = t.runLLVMProgram(tempDir, "generated.exe", generated, nil, nil, runcmd) + out, err = t.runLLVMProgram(tempDir, "generated.exe", []string{generated}, nil, nil, runcmd) if err != nil { return err } return t.checkExpectedOutput(out) } -func (t test) runLLVMProgram(tempDir, executable, source string, flags, args []string, runcmd runCmd) ([]byte, error) { +func (t test) runLLVMProgram(tempDir, executable string, sources, flags, args []string, runcmd runCmd) ([]byte, error) { if goos != runtime.GOOS || goarch != runtime.GOARCH { t.Skip("LLVM execution tests do not support cross compilation") } @@ -746,7 +796,7 @@ func (t test) runLLVMProgram(tempDir, executable, source string, flags, args []s // execution support instead of failing in the linker's DWARF writer. cmd := []string{goTool, "build", t.goGcflags(), "-ldflags=-w", "-toolexec=" + toolexec, "-o", exe} cmd = append(cmd, flags...) - cmd = append(cmd, source) + cmd = append(cmd, sources...) if _, err := runcmd(cmd...); err != nil { return nil, err } diff --git a/test/llvm_tests.json b/test/llvm_tests.json index 8940cf5cb0e6e5..c6b90c75cea427 100644 --- a/test/llvm_tests.json +++ b/test/llvm_tests.json @@ -81,6 +81,7 @@ "platform_graylist": { "linux/amd64": { "codegen/llvm_abs.go": "the native AMD64 frontend does not form OpAbs for math.Abs", + "codegen/mathbits.go": "the AMD64 frontend forms unsupported Div128u while compiling the full source file", "codegen/llvm_trunc.go": "GOAMD64=v1 guards OpTrunc with unsupported HasCPUFeature lowering" } } From 9b18a8bebccc99302da6210b3d95842a7421da13 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Thu, 6 Aug 2026 09:52:35 +0800 Subject: [PATCH 7/7] cmd/internal/testdir: clarify LLVM blacklist output --- src/cmd/internal/testdir/llvm_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/internal/testdir/llvm_test.go b/src/cmd/internal/testdir/llvm_test.go index 10c1bc66a06de5..8c5614931e6f91 100644 --- a/src/cmd/internal/testdir/llvm_test.go +++ b/src/cmd/internal/testdir/llvm_test.go @@ -578,7 +578,7 @@ func logLLVMBlacklist(t *testing.T, name string, candidates map[string]bool, set break } } - t.Logf("LLVM %s blacklist result: NOT RUN %s: %s", name, filename, reason) + t.Logf("LLVM %s blacklist result: NOT RUN test=%q reason=%q", name, filename, reason) } }