diff --git a/.github/actions/setup-binaryen/action.yml b/.github/actions/setup-binaryen/action.yml index cb2b6e22fb..1779def40d 100644 --- a/.github/actions/setup-binaryen/action.yml +++ b/.github/actions/setup-binaryen/action.yml @@ -2,9 +2,9 @@ name: "Setup Binaryen" description: "Install a pinned Binaryen release" inputs: version: - description: "Binaryen release version" + description: "LLGo Binaryen release tag; update checksums.txt together" required: false - default: "132" + default: "llgo-v132.3" runs: using: "composite" @@ -27,18 +27,30 @@ runs: ;; esac - archive="binaryen-version_${version}-${platform}.tar.gz" - base_url="https://github.com/WebAssembly/binaryen/releases/download/version_${version}" + archive="binaryen-${version}-${platform}.tar.gz" + base_url="https://github.com/xgo-dev/binaryen/releases/download/${version}" + expected="$(awk -v platform="$platform" '$1 == platform { print $2 }' "$GITHUB_ACTION_PATH/checksums.txt")" + if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then + echo "Missing or invalid pinned Binaryen checksum for $platform" >&2 + exit 1 + fi cd "$RUNNER_TEMP" curl --retry 3 --retry-all-errors -fsSLO "${base_url}/${archive}" - curl --retry 3 --retry-all-errors -fsSLO "${base_url}/${archive}.sha256" if command -v sha256sum >/dev/null; then - sha256sum --check "${archive}.sha256" + printf '%s %s\n' "$expected" "$archive" | sha256sum --check - else - shasum -a 256 --check "${archive}.sha256" + printf '%s %s\n' "$expected" "$archive" | shasum -a 256 --check - fi tar -xzf "$archive" -C "$RUNNER_TEMP" - echo "$RUNNER_TEMP/binaryen-version_${version}/bin" >> "$GITHUB_PATH" + binaryen_root="$RUNNER_TEMP/binaryen-${version}" + test -x "$binaryen_root/bin/wasm-opt" + test -x "$binaryen_root/bin/wasm-emscripten-finalize" + echo "Binaryen root: $binaryen_root" + "$binaryen_root/bin/wasm-opt" --version + "$binaryen_root/bin/wasm-emscripten-finalize" --version + echo "EM_BINARYEN_ROOT=$binaryen_root" >> "$GITHUB_ENV" + echo "WASMOPT=$binaryen_root/bin/wasm-opt" >> "$GITHUB_ENV" + echo "$binaryen_root/bin" >> "$GITHUB_PATH" - name: Install Binaryen on Windows if: runner.os == 'Windows' @@ -51,19 +63,34 @@ runs: "ARM64" { "arm64-windows" } default { throw "Unsupported Binaryen Windows host: $env:PROCESSOR_ARCHITECTURE" } } - $archive = "binaryen-version_$version-$platform.tar.gz" - $baseUrl = "https://github.com/WebAssembly/binaryen/releases/download/version_$version" + $archive = "binaryen-$version-$platform.tar.gz" + $baseUrl = "https://github.com/xgo-dev/binaryen/releases/download/$version" $archivePath = Join-Path $env:RUNNER_TEMP $archive - $checksumPath = "$archivePath.sha256" + $checksumRows = @(Get-Content -LiteralPath (Join-Path $env:GITHUB_ACTION_PATH "checksums.txt") | + Where-Object { ($_ -split '\s+')[0] -eq $platform }) + if ($checksumRows.Count -ne 1) { throw "Missing pinned Binaryen checksum for $platform" } + $expected = ($checksumRows[0] -split '\s+')[1] + if ($expected -notmatch '^[0-9a-fA-F]{64}$') { throw "Invalid pinned Binaryen checksum for $platform" } & curl.exe --fail --location --retry 3 --retry-all-errors --output $archivePath "$baseUrl/$archive" if ($LASTEXITCODE -ne 0) { throw "Downloading Binaryen failed with exit code $LASTEXITCODE" } - & curl.exe --fail --location --retry 3 --retry-all-errors --output $checksumPath "$baseUrl/$archive.sha256" - if ($LASTEXITCODE -ne 0) { throw "Downloading the Binaryen checksum failed with exit code $LASTEXITCODE" } - $expected = ((Get-Content -LiteralPath $checksumPath -Raw).Trim() -split '\s+')[0] $actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $archivePath).Hash if (-not $actual.Equals($expected, [StringComparison]::OrdinalIgnoreCase)) { throw "Binaryen archive SHA-256 is $actual, want $expected" } & tar.exe -xzf $archivePath -C $env:RUNNER_TEMP if ($LASTEXITCODE -ne 0) { throw "Extracting Binaryen failed with exit code $LASTEXITCODE" } - Add-Content -Encoding utf8 $env:GITHUB_PATH (Join-Path $env:RUNNER_TEMP "binaryen-version_$version\bin") + $binaryenRoot = Join-Path $env:RUNNER_TEMP "binaryen-$version" + $binaryenBin = Join-Path $binaryenRoot "bin" + $wasmOpt = Join-Path $binaryenBin "wasm-opt.exe" + $finalize = Join-Path $binaryenBin "wasm-emscripten-finalize.exe" + if (-not (Test-Path $wasmOpt) -or -not (Test-Path $finalize)) { + throw "Binaryen archive is missing wasm-opt or wasm-emscripten-finalize in $binaryenBin" + } + Write-Host "Binaryen root: $binaryenRoot" + & $wasmOpt --version + if ($LASTEXITCODE -ne 0) { throw "wasm-opt --version failed with exit code $LASTEXITCODE" } + & $finalize --version + if ($LASTEXITCODE -ne 0) { throw "wasm-emscripten-finalize --version failed with exit code $LASTEXITCODE" } + Add-Content -Encoding utf8 $env:GITHUB_ENV "EM_BINARYEN_ROOT=$binaryenRoot" + Add-Content -Encoding utf8 $env:GITHUB_ENV "WASMOPT=$wasmOpt" + Add-Content -Encoding utf8 $env:GITHUB_PATH $binaryenBin diff --git a/.github/actions/setup-binaryen/checksums.txt b/.github/actions/setup-binaryen/checksums.txt new file mode 100644 index 0000000000..00fab05ff2 --- /dev/null +++ b/.github/actions/setup-binaryen/checksums.txt @@ -0,0 +1,9 @@ +# SHA-256 of xgo-dev/binaryen llgo-v132.3 release archives. +# Format: platform SHA-256. Verified against release assets and .sha256 files. +aarch64-linux d7fe6f6558bd668f5bea3c64fd700885854a4847c023cf7a3ce3286158fd077d +arm64-macos 06427b4fdd36a97d28ac5d7d1826a1aaa8af5393a3e4d50cf4f8017462f38b73 +arm64-windows 58553d04ab8e67e4b741fb946e444ea11622dda53893054513559f21cbb0c7b8 +node bbb11c13202433ddf98ddd84b96d1a74b9ab9a48544a37702f8a53659d7b5691 +x86_64-linux c82c9cbf5f53d17f01ca66b40ddd1f2e87dc34591ca562ef4c411824997e5bf8 +x86_64-macos 0cef8ddc4c59d7819b92c79be01c7655fd5f244494af474a7a31ed29a05a170c +x86_64-windows b20b4c06fb1ae4da6461f5387958979733c741e6e4a437a31a2821ae6e40bb53 diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index d58264fd12..fd69cc7978 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -337,7 +337,7 @@ jobs: - name: Set up Emscripten uses: emscripten-core/setup-emsdk@v15 with: - version: "4.0.21" + version: "6.0.8" - name: Set up Binaryen uses: ./.github/actions/setup-binaryen diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 20457f941c..b1ec5d144b 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -566,12 +566,10 @@ jobs: - name: Set up Emscripten uses: emscripten-core/setup-emsdk@v15 with: - version: "4.0.21" + version: "6.0.8" - name: Set up Node for Emscripten Memory64 - # Emscripten 4.0.21's Memory64 glue rejects Node versions older than - # 23. setup-emsdk currently bundles Node 22, so select an active LTS - # runtime explicitly for execution without changing emcc itself. + # Keep memory64 JavaScript execution on the pinned Node LTS runtime. uses: actions/setup-node@v7 with: node-version: "24" diff --git a/doc/wasm-proposal.md b/doc/wasm-proposal.md index 079ff5631e..dfb47718bb 100644 --- a/doc/wasm-proposal.md +++ b/doc/wasm-proposal.md @@ -1,6 +1,8 @@ # LLGo WebAssembly Proposal -Status: draft. Tracking issue: [xgo-dev/llgo#2152](https://github.com/xgo-dev/llgo/issues/2152). +Status: active. Profile and source-compatibility work is tracked in [xgo-dev/llgo#2152](https://github.com/xgo-dev/llgo/issues/2152); the supported execution and toolchain decisions are tracked in [#2632](https://github.com/xgo-dev/llgo/issues/2632). + +The supported build uses the LLGo-patched Binaryen release. Browser execution keeps Emscripten Fiber/Asyncify and advances to a bounded Web Worker pool. W32 advances to WASI threads on WAMR; single-thread WASI is retired only after threaded GC passes. These execution milestones proceed alongside W1-W3 rather than waiting for W3 to finish. R1 through R3, including R2.1, are merged and remain the delivered foundation of this proposal. W1-W3 build on that foundation to complete the supported WebAssembly profiles before advanced engine features are added. @@ -109,13 +111,15 @@ Run and classify the full applicable `test/**` suite on all four paths, the comp ## Deferred work -W64, WASI Preview 2 and WIT components, threads and Atomics, WasmGC, Exception Handling, JSPI and Stack Switching, multiple workers, and parallel goroutines are later capability work. Unsupported environments continue to use the single-worker scheduler and LLGo linear-memory GC. +W64, WASI Preview 2 and WIT components, a WasmGC heap, and JSPI/stackless execution remain later research. EH encoding is selected through the comparison in [#2632](https://github.com/xgo-dev/llgo/issues/2632); existing validated EH behavior remains supported during that work. Browser multi-worker execution and W32 WASI threads are required milestones of #2632, with the single-worker browser mode retained. --- # LLGo WebAssembly 提案 -状态:草案。跟踪 issue:[xgo-dev/llgo#2152](https://github.com/xgo-dev/llgo/issues/2152)。 +状态:推进中。Profile 与源码兼容性由 [xgo-dev/llgo#2152](https://github.com/xgo-dev/llgo/issues/2152) 跟踪;正式执行架构与工具链决定由 [#2632](https://github.com/xgo-dev/llgo/issues/2632) 跟踪。 + +正式构建使用 LLGo 打补丁的 Binaryen 版本。浏览器保留 Emscripten Fiber/Asyncify,并推进到有上限的 Web Worker 池。W32 以 WAMR 上的 WASI threads 为目标;只有在线程 GC 验收通过后才移除单线程 WASI。这些运行时工作与 W1-W3 并行推进,不等待 W3 结束。 R1 至 R3(包括 R2.1)已经合并,作为本提案已交付的基础继续保留。W1-W3 在此基础上完成当前支持的 WebAssembly profile,再推进高级引擎特性。 @@ -224,4 +228,4 @@ LLGo Core Wasm C ABI 与 WIT Canonical ABI 无关。未来 WASI Preview 2 将在 ## 延期范围 -W64、WASI Preview 2/WIT component、threads/Atomics、WasmGC、Exception Handling、JSPI/Stack Switching、多 worker 和并行 goroutine 都是后续 capability 工作。不支持这些能力的环境继续使用单 worker 调度和 LLGo 线性内存 GC。 +W64、WASI Preview 2/WIT component、WasmGC 堆以及 JSPI/stackless 执行仍属于后续研究。EH 编码按照 [#2632](https://github.com/xgo-dev/llgo/issues/2632) 的对比结果确定;期间保留已验证的 EH 行为。浏览器多 worker 和 W32 WASI threads 是 #2632 的必需里程碑,浏览器仍保留单 worker 模式。 diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 3e9339c873..206306f3fb 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -41,6 +41,13 @@ func TestMain(m *testing.M) { os.Exit(0) } if mode := os.Getenv("LLGO_TEST_WASM_OPT_HELPER"); mode != "" { + if len(os.Args) == 2 && os.Args[1] == "--version" { + if mode == "fail-version" { + os.Exit(7) + } + fmt.Println("wasm-opt version 132 (test helper)") + os.Exit(0) + } if argsFile := os.Getenv("ARGS_FILE"); argsFile != "" { file, err := os.OpenFile(argsFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666) if err == nil { diff --git a/internal/build/wasm_postlink.go b/internal/build/wasm_postlink.go index 3b03c09958..0d828a5640 100644 --- a/internal/build/wasm_postlink.go +++ b/internal/build/wasm_postlink.go @@ -23,6 +23,8 @@ import ( "os" "os/exec" "path/filepath" + "runtime" + "strings" "github.com/xgo-dev/llgo/internal/crosscompile" "github.com/xgo-dev/llgo/internal/optlevel" @@ -101,13 +103,9 @@ func createClosedTemp(dir, pattern string) (string, error) { } func postLinkWasm(ctx *context, input, output string, verbose bool) error { - wasmOpt := os.Getenv("WASMOPT") - if wasmOpt == "" { - wasmOpt = "wasm-opt" - } - resolved, err := exec.LookPath(wasmOpt) + resolved, err := resolveWasmOpt() if err != nil { - return fmt.Errorf("WebAssembly Asyncify requires wasm-opt; install Binaryen or set WASMOPT: %w", err) + return fmt.Errorf("WebAssembly Asyncify requires wasm-opt; install Binaryen or set WASMOPT or EM_BINARYEN_ROOT: %w", err) } preAsyncifyArgs := wasmPreAsyncifyArgs( @@ -118,7 +116,7 @@ func postLinkWasm(ctx *context, input, output string, verbose bool) error { ) if preAsyncifyArgs != nil { if err := runWasmOpt(resolved, preAsyncifyArgs, verbose, ctx); err != nil { - return fmt.Errorf("wasm-opt pre-Asyncify optimization failed: %w", err) + return fmt.Errorf("wasm-opt pre-Asyncify optimization failed using %s: %w", wasmOptIdentity(resolved), err) } } @@ -139,7 +137,7 @@ func postLinkWasm(ctx *context, input, output string, verbose bool) error { ctx.buildConf.OptLevel, ) if err := runWasmOpt(resolved, args, verbose, ctx); err != nil { - return fmt.Errorf("wasm-opt Asyncify failed: %w", err) + return fmt.Errorf("wasm-opt Asyncify failed using %s: %w", wasmOptIdentity(resolved), err) } if err := os.Rename(tmpName, output); err != nil { return err @@ -147,6 +145,31 @@ func postLinkWasm(ctx *context, input, output string, verbose bool) error { return nil } +func resolveWasmOpt() (string, error) { + wasmOpt := os.Getenv("WASMOPT") + if wasmOpt == "" { + if root := os.Getenv("EM_BINARYEN_ROOT"); root != "" { + name := "wasm-opt" + if runtime.GOOS == "windows" { + name += ".exe" + } + wasmOpt = filepath.Join(root, "bin", name) + } else { + wasmOpt = "wasm-opt" + } + } + return exec.LookPath(wasmOpt) +} + +func wasmOptIdentity(path string) string { + output, err := exec.Command(path, "--version").CombinedOutput() + version := strings.TrimSpace(string(output)) + if err != nil || version == "" { + return path + " (version unavailable)" + } + return path + " (" + version + ")" +} + func runWasmOpt(resolved string, args []string, verbose bool, ctx *context) error { if ctx.shouldPrintCommands(verbose) { fmt.Fprintln(os.Stderr, resolved, args) diff --git a/internal/build/wasm_postlink_test.go b/internal/build/wasm_postlink_test.go index 69f0f48b5e..a8e1484508 100644 --- a/internal/build/wasm_postlink_test.go +++ b/internal/build/wasm_postlink_test.go @@ -240,6 +240,9 @@ func TestPostLinkWasmReportsToolFailure(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "wasm-opt Asyncify failed") { t.Fatalf("postLinkWasm() error = %v", err) } + if !strings.Contains(err.Error(), tool) || !strings.Contains(err.Error(), "wasm-opt version 132 (test helper)") { + t.Fatalf("postLinkWasm() error omits selected Binaryen tool: %v", err) + } if data, err := os.ReadFile(output); err != nil || string(data) != "old" { t.Fatalf("failed post-link changed final output: %q, %v", data, err) } @@ -263,6 +266,9 @@ func TestPostLinkWasmReportsPreAsyncifyFailure(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "wasm-opt pre-Asyncify optimization failed") { t.Fatalf("postLinkWasm() error = %v", err) } + if !strings.Contains(err.Error(), tool) || !strings.Contains(err.Error(), "wasm-opt version 132 (test helper)") { + t.Fatalf("postLinkWasm() error omits selected Binaryen tool: %v", err) + } if data, err := os.ReadFile(output); err != nil || string(data) != "old" { t.Fatalf("failed pre-optimization changed final output: %q, %v", data, err) } @@ -301,6 +307,44 @@ func TestPostLinkWasmReportsMissingTool(t *testing.T) { } } +func TestResolveWasmOptUsesEmscriptenBinaryenRoot(t *testing.T) { + root := t.TempDir() + bin := filepath.Join(root, "bin") + if err := os.Mkdir(bin, 0o755); err != nil { + t.Fatal(err) + } + want := writeWasmOptTestTool(t, bin) + t.Setenv("EM_BINARYEN_ROOT", root) + t.Setenv("WASMOPT", "") + got, err := resolveWasmOpt() + if err != nil || got != want { + t.Fatalf("resolveWasmOpt() = %q, %v; want %q", got, err, want) + } + + override := writeWasmOptTestTool(t, t.TempDir()) + t.Setenv("WASMOPT", override) + got, err = resolveWasmOpt() + if err != nil || got != override { + t.Fatalf("resolveWasmOpt() with WASMOPT = %q, %v; want %q", got, err, override) + } + + t.Setenv("EM_BINARYEN_ROOT", "") + t.Setenv("WASMOPT", "") + t.Setenv("PATH", bin) + got, err = resolveWasmOpt() + if err != nil || got != want { + t.Fatalf("resolveWasmOpt() from PATH = %q, %v; want %q", got, err, want) + } +} + +func TestWasmOptIdentityWhenVersionUnavailable(t *testing.T) { + tool := writeWasmOptTestTool(t, t.TempDir()) + t.Setenv("LLGO_TEST_WASM_OPT_HELPER", "fail-version") + if got, want := wasmOptIdentity(tool), tool+" (version unavailable)"; got != want { + t.Fatalf("wasmOptIdentity() = %q; want %q", got, want) + } +} + func TestPostLinkWasmReportsInvalidOutputDirectory(t *testing.T) { dir := t.TempDir() tool := writeWasmOptTestTool(t, dir)