Skip to content

build: prepare the LLVM 21 upgrade - #2334

Open
zhouguangyuan0718 wants to merge 6 commits into
xgo-dev:mainfrom
zhouguangyuan0718:codex/llvm21-upgrade
Open

build: prepare the LLVM 21 upgrade#2334
zhouguangyuan0718 wants to merge 6 commits into
xgo-dev:mainfrom
zhouguangyuan0718:codex/llvm21-upgrade

Conversation

@zhouguangyuan0718

@zhouguangyuan0718 zhouguangyuan0718 commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add explicit LLVM 21 build configuration while retaining LLVM 19 as the default
  • fail fast when the linked LLVM library and command-line tools have different major versions
  • make semantic IR tests tolerate LLVM 21 spelling-only changes and allow the LTO plugin to build with LLVM 19 or 21
  • centralize the revision-locked ESP LLVM payload manifest, verify SHA-256 before extraction, and derive release linker flags from the manifest
  • include the compiler version, payload manifest (including patch provenance), target CPU/features, optimization level, and LTO mode in compiled cross-library cache identity
  • keep named-target CPU/features on the external clang/lld path, remove feature names no longer accepted by LLVM 21, and include those settings in cache identity
  • keep source downloads shared while isolating compiled newlib/compiler-rt archives by effective LLVM toolchain
  • normalize Os/Oz to the numeric optimization level accepted by LLD's LTO option

This PR is the compatibility and payload-contract layer only. It intentionally does not switch the default LLVM major or reference an unpublished LLVM 21 payload.

Validation

  • exact Espressif LLVM 21 payload: go test for internal/llvmpayload, internal/crosscompile, internal/build, and ssa
  • LLVM 19 compatibility: go test -tags=llvm19 for internal/crosscompile, internal/build, and ssa
  • clean Xtensa newlib/compiler-rt build with the patched LLVM 21 payload
  • ESP32 and ESP32-C3 executable links with non-LTO and full LTO configurations
  • verified that all tested target feature lists produce zero unknown-feature diagnostics
  • verified the intended split between generic host LLVM 21 for in-process IR generation and the exact Espressif LLVM 21 payload for physical target code generation
  • configured and built ltoplugin against LLVM 19.1.7 and LLVM 21.1.8
  • shell syntax/lint, workflow YAML parse, and git diff --check

The branch is rebased on current main (4a5bd7615). A separate follow-up will switch the default only after the LLVM binding and all four payload assets have tagged releases.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: LLVM 21 upgrade

Solid, well-tested change. The new internal/llvmpayload package cleanly centralizes the previously-scattered ESP Clang constants, SHA256 verification (Go and bash, verified before extraction, with cleanup on mismatch and a dedicated negative test) is a real integrity improvement, and the bash hardening (set -euo pipefail, trap cleanup, download-then-verify-then-extract) is correct and consistent with the Go path.

A few items are worth addressing before merge — most importantly whether the LLVM 21 payload/toolchain path is actually functional, and the platform-specific linker/compiler names in the new toolchain validator. See inline comments.

Findings that have no reliable inline location:

  • Docs still pin LLVM 19. ltoplugin/README.md (build instructions, -DLLVM_DIR=/path/to/llvm-19/..., and the "LLVM 19 ld64.lld" wording) still hard-codes LLVM 19 even though CMakeLists.txt now accepts 19.x or 21.x and internal/lto/lto.go was de-versioned to "the bundled ld64.lld". Similarly, README.md (generated from doc/_readme/scripts/install_macos.sh / install_ubuntu.sh) still installs only llvm@19/llvm-19 and does not mention the new toolchain-major validation requirement introduced by validateLLVMToolchain — a user who builds with -tags llvm21 but installs LLVM 19 tools will now hit "LLVM major version mismatch". Consider updating these docs (fix belongs in the doc/_readme/scripts/*.sh sources for the generated README).
  • WASI SDK download remains unverified (pre-existing, low). Now that the checksum plumbing exists (downloadAndExtractArchiveWithChecksum), the WASI SDK download still passes an empty checksum and skips verification. Not introduced here, but a natural candidate to pin next since this PR is about hardening this path.

Comment on lines +33 to +44
var manifests = map[int]manifest{
19: {
llvmMajor: 19,
version: "19.1.2_20250905-3",
sha256: map[string]string{
"aarch64-apple-darwin": "4f15d18c93eabdace3eab901582e528ac334d328fb8f19f153ee55b2208d101b",
"aarch64-linux-gnu": "b2d8e77bbf3394c6a1f0d66e59385d78d2b49b97ebe782e612cba7f93dcb2337",
"x86_64-apple-darwin": "e4f329a911e813ee825984f039578614dc0fe69001c2afe3e61edf27821be3ad",
"x86_64-linux-gnu": "e2e0c48cd76e45ceba910917a2a97988dc80e3bb6040ea262bfe9293d5d9ac57",
},
},
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] No LLVM 21 payload: ESP cross-compile fails under -tags llvm21

This PR adds llvm21 build-tag config files and widens the LTO plugin to accept LLVM 21, but manifests contains only the 19 entry and DefaultMajor is still 19. When LLGo is built with -tags llvm21, gllvm.Version reports 21.x, so getESPClangRoot -> llvmpayload.ForLLVMVersion("21...") -> ForMajor(21) returns "no LLGo LLVM payload for major version 21" (crosscompile.go:141-144), and any ESP/cross-compile build on an LLVM 21 toolchain fails.

If the LLVM 21 ESP payload simply is not published yet and 19-only downloads are intentional for now, that is fine — but it should be documented, and the package doc comment ("toolchains distributed with and downloaded by LLGo", ForLLVMVersion example 21.1.8) overstates availability. As written, the headline "upgrade to LLVM 21" is not functional for the ESP download path.

Comment thread internal/build/build.go
Comment on lines +415 to +417
if err := validateLLVMToolchain(export); err != nil {
return nil, fmt.Errorf("invalid LLVM toolchain: %w", err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] validateLLVMToolchain runs 3 uncached --version subprocesses on every Build

Build now unconditionally calls validateLLVMToolchain, which spawns three subprocesses (llvm-config, clang/export.CC, ld.lld) via exec.Command(tool, "--version"). For a one-shot llgo build this is negligible. But in-process, high-fan-out drivers (the cltest harness, go test suites that call Build/Do once per package) will spawn hundreds x 3 --version processes per run, all producing the same answer since gllvm.Version and the toolchain paths are constant for the process lifetime. Consider memoizing the result per (linkedVersion, toolPath) in ValidateToolchainMajor so repeated Build calls pay the cost once.

Comment on lines +108 to +121
func PlatformSuffix(goos, goarch string) (string, bool) {
switch goos + "/" + goarch {
case "darwin/amd64":
return "x86_64-apple-darwin", true
case "darwin/arm64":
return "aarch64-apple-darwin", true
case "linux/amd64":
return "x86_64-linux-gnu", true
case "linux/arm64":
return "aarch64-linux-gnu", true
default:
return "", false
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] PlatformSuffix drops linux/arm and windows/amd64 mappings

The refactored getESPClangPlatform now delegates to PlatformSuffix, which only maps darwin/linux amd64/arm64. The previous inline switch also handled linux/arm (arm-linux-gnueabihf) and windows/amd64 (x86_64-w64-mingw32). If any target config or CI relied on those, the download path now returns "" -> "platform ... is not supported for download". If the removal is intentional (those payloads never shipped), please confirm; otherwise it is a regression.

Comment on lines +32 to +33
fmt.Printf("LLGO_LLVM_MAJOR=%s\n", fmt.Sprint(manifest.LLVMMajor()))
fmt.Printf("ESP_CLANG_LLVM_MAJOR=%s\n", fmt.Sprint(manifest.LLVMMajor()))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Unnecessary fmt.Sprint on int; use %d

LLVMMajor() returns int; fmt.Printf("...=%s\n", fmt.Sprint(manifest.LLVMMajor())) formats an int to string then re-formats as %s. Use %d directly. Also note the mixed error handling below: a missing platform mapping panics while a missing artifact does os.Exit(1). For a CLI feeding $GITHUB_ENV, prefer the stderr + os.Exit(1) form for both so CI failures are readable without a Go stack trace.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (supplemental)

One additional inline finding from the LLVM 21 review; see comment.

Comment thread internal/build/build.go
Comment on lines +805 to +811
func validateLLVMToolchain(export crosscompile.Export) error {
if export.ClangRoot != "" {
binDir := filepath.Join(export.ClangRoot, "bin")
return envllvm.ValidateToolchainMajor(gllvm.Version,
filepath.Join(binDir, "llvm-config"),
filepath.Join(binDir, "clang"),
filepath.Join(binDir, "ld.lld"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Toolchain validator hardcodes ld.lld/clang (breaks darwin/windows)

validateLLVMToolchain always probes ld.lld, but the linker is platform-specific: darwin toolchains ship ld64.lld and windows uses lld-link (crosscompile.go:290,295). ClangRoot is set to the ESP Clang bundle for darwin and linux hosts, so on macOS filepath.Join(binDir, "ld.lld") does not exist and ValidateToolchainMajor fails with query LLVM tool ... ld.lld ... no such file, aborting the build on exactly the darwin release path this PR cares about.

Also, the ClangRoot branch probes filepath.Join(binDir, "clang"), but the bundle and export.CC only guarantee clang++ (crosscompile.go:217). Prefer validating export.CC and deriving the linker name from the target GOOS (mirroring the crosscompile linker logic) instead of hardcoding ld.lld/clang.

@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

dc4aa1a03968 | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 19248 B +0.0% 390.926 ms +11.9% (worse) 1.282 ms -5.0% (better)
Linux fmtprintf 1879904 B +0.0% 2.760 s +1.6% (worse) 3.245 ms -2.0% (better)
Linux println 68616 B +0.0% 386.168 ms +10.6% (worse) 1.672 ms +2.3% (worse)
macOS cprintf 84624 B +0.0% 517.408 ms -5.9% (better) 3.763 ms -15.4% (better)
macOS fmtprintf 1891424 B +0.0% 4.320 s +45.8% (worse) 15.163 ms +2.3% (worse)
macOS println 121168 B +0.0% 505.513 ms +18.6% (worse) 4.192 ms -1.7% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 13.270 ns/op -0.2% (better)
Linux BenchmarkMergeCompilerFlags 151.500 ns/op -0.3% (better)
Linux BenchmarkMergeLinkerFlags 97.810 ns/op +3.7% (worse)
Linux BenchmarkChannelBuffered 35.310 ns/op +0.2% (worse)
Linux BenchmarkChannelHandoff 26762 ns/op +2.7% (worse)
Linux BenchmarkDefer 45.710 ns/op -2.6% (better)
Linux BenchmarkDirectCall 1.557 ns/op -0.1% (better)
Linux BenchmarkGlobalRead 1.560 ns/op +0.2% (worse)
Linux BenchmarkGlobalWrite 2.487 ns/op -0.0% (better)
Linux BenchmarkGoroutine 31557 ns/op +1.5% (worse)
Linux BenchmarkInterfaceCall 7.784 ns/op +0.0% (worse)
Linux BenchmarkRuntimeGetG 2.495 ns/op +0.1% (worse)
macOS BenchmarkLookupPCRandom 12.780 ns/op -23.7% (better)
macOS BenchmarkMergeCompilerFlags 139.100 ns/op -11.7% (better)
macOS BenchmarkMergeLinkerFlags 93.800 ns/op -8.1% (better)
macOS BenchmarkChannelBuffered 39.690 ns/op +62.8% (worse)
macOS BenchmarkChannelHandoff 5605 ns/op -20.5% (better)
macOS BenchmarkDefer 42.400 ns/op +14.7% (worse)
macOS BenchmarkDirectCall 1.235 ns/op -6.0% (better)
macOS BenchmarkGlobalRead 1.249 ns/op -7.5% (better)
macOS BenchmarkGlobalWrite 1.390 ns/op +8.3% (worse)
macOS BenchmarkGoroutine 55736 ns/op +106.6% (worse)
macOS BenchmarkInterfaceCall 7.865 ns/op +12.2% (worse)
macOS BenchmarkRuntimeGetG 2.837 ns/op +36.1% (worse)

Compared with 7d9f052513f2 measured in the same runner job.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant