blob: faf0db02e988479807556404728be89ab2b0011a [file]
# Copyright 2026 The IREE Authors
#
# Licensed under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
# Wasm cross-compilation toolchains using wasi-sdk.
#
# Two toolchains are provided:
#
# 1. wasm32 (freestanding): for production runtime builds.
# --target=wasm32-unknown-unknown, uses IREE's minimal libc headers,
# no system libraries. bazel build --config=wasm32 //some:target
#
# 2. wasm32_wasi (hosted): for building with full C/C++ stdlib.
# --target=wasm32-wasi, uses wasi-sdk's sysroot (musl + libc++).
# bazel build --config=wasm32_wasi //some:target
#
# Both toolchains are downloaded automatically from GitHub releases.
# Normal host builds are completely unaffected.
load("@rules_cc//cc/toolchains:args.bzl", "cc_args")
load("@rules_cc//cc/toolchains:tool.bzl", "cc_tool")
load("@rules_cc//cc/toolchains:tool_map.bzl", "cc_tool_map")
load("@rules_cc//cc/toolchains:toolchain.bzl", "cc_toolchain")
load("@wasi_sdk//:paths.bzl", "WASI_RESOURCE_DIR", "WASI_SYSROOT_PATH")
load(":wasm_clang_linker.bzl", "wasm_clang_linker")
package(default_visibility = ["//visibility:public"])
#===------------------------------------------------------------------------===#
# Platform definitions
#===------------------------------------------------------------------------===#
platform(
name = "wasm32",
constraint_values = [
"@platforms//cpu:wasm32",
"@platforms//os:none",
],
)
# WASI is a custom OS constraint — @platforms doesn't define it.
constraint_value(
name = "wasi",
constraint_setting = "@platforms//os:os",
)
platform(
name = "wasm32_wasi",
constraint_values = [
"@platforms//cpu:wasm32",
":wasi",
],
)
#===------------------------------------------------------------------------===#
# Freestanding wasm32 toolchain (for production runtime builds)
#===------------------------------------------------------------------------===#
cc_tool(
name = "clang_tool",
src = "@wasi_sdk//:clang",
)
# Clang-based linker driver: invokes clang with --target=wasm32 and
# -fuse-ld=lld, so Bazel's GCC driver conventions (-Wl,X, -shared,
# @paramfile) pass through without translation. A wasm-ld symlink is
# co-located with clang via -B for linker discovery.
wasm_clang_linker(
name = "wasm_link_driver",
clang = "@wasi_sdk//:clang",
extra_flags = [
"--target=wasm32-unknown-unknown",
"-nostdlib",
],
lld = "@wasi_sdk//:lld",
)
cc_tool(
name = "wasm_link_tool",
src = ":wasm_link_driver",
)
cc_tool(
name = "ar_tool",
src = "@wasi_sdk//:llvm-ar",
)
cc_tool_map(
name = "wasm32_tool_map",
tools = {
"@rules_cc//cc/toolchains/actions:assembly_actions": ":clang_tool",
"@rules_cc//cc/toolchains/actions:c_compile": ":clang_tool",
"@rules_cc//cc/toolchains/actions:cpp_compile_actions": ":clang_tool",
"@rules_cc//cc/toolchains/actions:link_executable_actions": ":wasm_link_tool",
"@rules_cc//cc/toolchains/actions:dynamic_library_link_actions": ":wasm_link_tool",
"@rules_cc//cc/toolchains/actions:ar_actions": ":ar_tool",
},
)
cc_args(
name = "wasm32_compile_args",
actions = [
"@rules_cc//cc/toolchains/actions:c_compile",
"@rules_cc//cc/toolchains/actions:cpp_compile_actions",
"@rules_cc//cc/toolchains/actions:assembly_actions",
],
args = [
# Target triple.
"--target=wasm32-unknown-unknown",
# Freestanding: no system headers, no libc assumptions.
# -nostdinc disables all default include paths. By default we provide
# IREE's libc headers below; users embedding IREE into another wasm
# environment can pass --define=iree_wasm_libc=custom and provide
# their own declared headers and include paths.
"-nostdinc",
"-nostdlib",
"-ffreestanding",
] + select({
"//build_tools/bazel:iree_wasm_libc_custom": [],
"//conditions:default": [
# Wasm libc headers (freestanding + hosted shims).
# Path is relative to the execroot, where source files live.
"-isystem",
"build_tools/wasm/libc/include",
],
}) + [
# C/C++ settings.
"-std=c17",
"-fno-exceptions",
"-fno-rtti",
"-fvisibility=hidden",
"-fno-short-wchar",
# Wasm feature flags: these correspond to widely-supported proposals
# that are baseline in all modern engines (Chrome 91+, Firefox 89+,
# Safari 15+, Node 16+).
"-mbulk-memory",
"-msign-ext",
"-mnontrapping-fptoint",
# Freestanding wasm is currently single-mutator. Disable IREE threading
# and synchronization primitives until the libc heap, errno, and worker
# lifecycle have a real pthread/TLS contract.
"-DIREE_SYNCHRONIZATION_DISABLE_UNSAFE=1",
"-DIREE_THREADING_ENABLE=0",
# Web platform define (injected by toolchain, not auto-detected).
"-DIREE_PLATFORM_WEB=1",
],
data = select({
"//build_tools/bazel:iree_wasm_libc_custom": [],
"//conditions:default": ["//build_tools/wasm/libc:headers"],
}),
)
cc_args(
name = "wasm32_link_args",
actions = [
"@rules_cc//cc/toolchains/actions:link_executable_actions",
"@rules_cc//cc/toolchains/actions:dynamic_library_link_actions",
],
args = [
# Import-memory model: the embedder creates WebAssembly.Memory and
# passes it to the module so it can control memory sizing.
"-Wl,--import-memory",
# No _start entry point — Wasm modules are libraries invoked
# by the JS host, not standalone executables.
"-Wl,--no-entry",
# Export all non-hidden symbols so the JS host can call them.
"-Wl,--export-dynamic",
# Allow undefined symbols — they become Wasm imports that the JS host
# must provide (csprng, topology, clock, etc.). --allow-undefined is
# kept for compatibility; --import-undefined makes the import contract
# explicit so bridge functions are preserved as imports at link time.
"-Wl,--allow-undefined",
"-Wl,--import-undefined",
# Max memory: 4GB (the wasm32 address space limit).
# Initial memory is determined by the module's data segments.
"-Wl,--max-memory=4294967296",
],
)
cc_toolchain(
name = "wasm32_cc_toolchain",
args = [
":wasm32_compile_args",
":wasm32_link_args",
],
compiler = "clang",
enabled_features = [
"@rules_cc//cc/toolchains/args:experimental_replace_legacy_action_config_features",
],
known_features = [
"@rules_cc//cc/toolchains/args:experimental_replace_legacy_action_config_features",
],
# No standard includes — we provide everything via our libc shim.
supports_header_parsing = False,
supports_param_files = True,
tool_map = ":wasm32_tool_map",
)
# Register for each host platform where wasi-sdk is available.
[toolchain(
name = "wasm32_toolchain_" + os + "_" + cpu,
exec_compatible_with = [
"@platforms//os:" + os,
"@platforms//cpu:" + cpu,
],
target_compatible_with = [
"@platforms//cpu:wasm32",
"@platforms//os:none",
],
toolchain = ":wasm32_cc_toolchain",
toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
) for os, cpu in [
("linux", "x86_64"),
("linux", "aarch64"),
("macos", "x86_64"),
("macos", "aarch64"),
]]
#===------------------------------------------------------------------------===#
# WASI-hosted wasm32 toolchain (for tests with gtest/gbenchmark)
#===------------------------------------------------------------------------===#
#
# Uses the same clang binary as the freestanding toolchain, but with
# --sysroot and -resource-dir pointing at the wasi-sdk's sysroot (musl +
# libc++ + compiler-rt) using execroot-relative paths from paths.bzl.
#
# -no-canonical-prefixes prevents clang from resolving symlinks and
# producing absolute paths, which would fail Bazel's header inclusion check.
# Linker driver for WASI: same co-location trick as freestanding, but
# targeting wasm32-wasi (with system libraries, no -nostdlib).
wasm_clang_linker(
name = "wasm_wasi_link_driver",
clang = "@wasi_sdk//:clang",
extra_flags = [
"--target=wasm32-wasi",
# Force __main_argc_argv as a strong undefined from the start.
# wasi-libc's crt1-command.o calls __main_void, and libc.a provides
# a weak __main_void that dispatches to __main_argc_argv (also weak).
# But libc.a is linked implicitly by clang AFTER user archives, so
# when the linker processes gtest_main.a (which defines
# __main_argc_argv via int main(int, char**)), the symbol isn't
# yet needed and the archive member isn't extracted. Making it
# strong undefined up front forces extraction during the first pass.
"-Wl,--undefined=__main_argc_argv",
],
# Strip host-oriented flags that Bazel's built-in features inject into
# params files: -pthread enables --shared-memory (requires atomics-
# compiled sysroot libraries), -pie is meaningless for wasm.
filter_flags = [
"-pthread",
"-pie",
"-fsanitize=address",
],
lld = "@wasi_sdk//:lld",
# System libraries appended AFTER all Bazel-generated arguments ("$@").
# Bazel places toolchain cc_args before user objects/archives, but the
# linker's single-pass archive extraction requires system libraries to
# come after user archives: libc's __main_void creates a weak ref to
# __main_argc_argv, which gtest_main (a user archive) defines. If -lc++
# comes before user archives, the linker never resolves this forward ref.
suffix_flags = [
"-lwasi-emulated-signal",
"-lwasi-emulated-process-clocks",
"-lc++",
"-lc++abi",
],
)
cc_tool(
name = "wasi_link_tool",
src = ":wasm_wasi_link_driver",
)
cc_tool_map(
name = "wasm32_wasi_tool_map",
tools = {
# Same clang as freestanding — sysroot is passed via cc_args.
"@rules_cc//cc/toolchains/actions:assembly_actions": ":clang_tool",
"@rules_cc//cc/toolchains/actions:c_compile": ":clang_tool",
"@rules_cc//cc/toolchains/actions:cpp_compile_actions": ":clang_tool",
"@rules_cc//cc/toolchains/actions:link_executable_actions": ":wasi_link_tool",
"@rules_cc//cc/toolchains/actions:dynamic_library_link_actions": ":wasi_link_tool",
"@rules_cc//cc/toolchains/actions:ar_actions": ":ar_tool",
},
)
cc_args(
name = "wasm32_wasi_compile_args",
actions = [
"@rules_cc//cc/toolchains/actions:c_compile",
"@rules_cc//cc/toolchains/actions:cpp_compile_actions",
"@rules_cc//cc/toolchains/actions:assembly_actions",
],
args = [
"--target=wasm32-wasi",
# Sysroot: wasi-libc (musl) + libc++ headers.
# Path is execroot-relative (from @wasi_sdk//:paths.bzl).
"--sysroot=" + WASI_SYSROOT_PATH,
# Resource directory: clang builtins (stddef.h, stdarg.h, etc.)
# and compiler-rt. Overrides clang's default of resolving through
# symlinks to find its resource dir (which produces absolute paths).
"-resource-dir=" + WASI_RESOURCE_DIR,
# Prevent clang from canonicalizing include paths to absolute.
# Without this, clang resolves symlinks in --sysroot and
# -resource-dir, producing absolute paths that fail Bazel's
# header inclusion check.
"-no-canonical-prefixes",
# IREE code style: no exceptions, no RTTI.
"-fno-exceptions",
"-fno-rtti",
"-fvisibility=hidden",
"-fno-short-wchar",
# Wasm feature flags (same as freestanding).
"-mbulk-memory",
"-msign-ext",
"-mnontrapping-fptoint",
# Web platform define.
"-DIREE_PLATFORM_WEB=1",
# WASI preview1 is single-threaded: no pthreads, no shared memory,
# no atomics instructions. Disable synchronization primitives (turns
# mutexes/notifications/futexes into no-ops) and threading support.
"-DIREE_SYNCHRONIZATION_DISABLE_UNSAFE=1",
"-DIREE_THREADING_ENABLE=0",
# WASI emulation layers.
# Signal: gtest includes <csignal> for its test runner.
"-D_WASI_EMULATED_SIGNAL",
# Process clocks: Google Benchmark uses getrusage(RUSAGE_SELF)
# for CPU timing. WASI emulates this using wall clock time, which
# is equivalent for single-threaded wasm (wall time ≈ CPU time).
"-D_WASI_EMULATED_PROCESS_CLOCKS",
# Google Benchmark compatibility: benchmark doesn't detect WASI as a
# platform and #errors on missing cycle counter and thread CPU time.
# Thread CPU time: wasi-libc defines CLOCK_THREAD_CPUTIME_ID as
# integer 3 in <time.h> but clockid_t is an opaque pointer type
# (const struct __clockid*) and no extern symbol exists for it.
# Aliasing to CLOCK_MONOTONIC gives wall-clock time, identical to
# CPU time in single-threaded wasm.
"-DCLOCK_THREAD_CPUTIME_ID=CLOCK_MONOTONIC",
# Cycle counter: benchmark's cycleclock.h has no __wasm__ branch.
# NaCl's path uses clock_gettime(CLOCK_MONOTONIC) which is exactly
# what we want, and NaCl is a dead platform (no collision risk).
# The only other effect is HOST_NAME_MAX=64 in sysinfo.cc.
"-DBENCHMARK_OS_NACL",
# WASI lacks dup/dup2/mkstemp/fork — disable gtest features that
# require them. gtest's auto-detection correctly skips death tests
# (no GTEST_OS_* matches WASI) and POSIX regex (unknown platform
# defaults to 0). But stream redirection auto-detects to 1 for any
# platform not in the explicit exclusion list, so we override it.
"-DGTEST_HAS_STREAM_REDIRECTION=0",
],
data = [
"@wasi_sdk//:clang_resources",
"@wasi_sdk//:sysroot",
],
)
cc_args(
name = "wasm32_wasi_cxx_args",
actions = [
"@rules_cc//cc/toolchains/actions:cpp_compile_actions",
],
args = [
# IREE requires C++17 (matching host builds).
"-std=c++17",
# Explicit C++ standard library include paths.
#
# Clang's addLibCxxIncludePaths auto-detection probes the filesystem
# relative to its InstalledDir (resolved from the binary's symlink
# chain). In Bazel's processwrapper-sandbox the symlink layout differs
# from the real execroot, so the probing fails and clang silently
# skips the C++ include directories. Providing the paths explicitly
# via -isystem makes the build sandbox-independent.
#
# Target-specific headers first (wasm32-wasi overrides), then generic.
"-isystem",
WASI_SYSROOT_PATH + "/include/wasm32-wasi/c++/v1",
"-isystem",
WASI_SYSROOT_PATH + "/include/c++/v1",
],
)
cc_args(
name = "wasm32_wasi_link_args",
actions = [
"@rules_cc//cc/toolchains/actions:link_executable_actions",
"@rules_cc//cc/toolchains/actions:dynamic_library_link_actions",
],
args = [
# Target triple and sysroot for finding crt1.o, libc.a, libc++.a.
"--target=wasm32-wasi",
"--sysroot=" + WASI_SYSROOT_PATH,
"-resource-dir=" + WASI_RESOURCE_DIR,
"-no-canonical-prefixes",
# System libraries (-lc++, -lc++abi, -lwasi-emulated-signal) are in
# the linker wrapper's suffix_flags, not here. Bazel places cc_args
# before user objects/archives, but the linker's single-pass archive
# extraction requires system libraries after user archives.
],
data = [
"@wasi_sdk//:clang_resources",
"@wasi_sdk//:sysroot",
],
)
cc_toolchain(
name = "wasm32_wasi_cc_toolchain",
args = [
":wasm32_wasi_compile_args",
":wasm32_wasi_cxx_args",
":wasm32_wasi_link_args",
],
compiler = "clang",
enabled_features = [
"@rules_cc//cc/toolchains/args:experimental_replace_legacy_action_config_features",
],
known_features = [
"@rules_cc//cc/toolchains/args:experimental_replace_legacy_action_config_features",
],
supports_header_parsing = False,
supports_param_files = True,
tool_map = ":wasm32_wasi_tool_map",
)
[toolchain(
name = "wasm32_wasi_toolchain_" + os + "_" + cpu,
exec_compatible_with = [
"@platforms//os:" + os,
"@platforms//cpu:" + cpu,
],
target_compatible_with = [
"@platforms//cpu:wasm32",
":wasi",
],
toolchain = ":wasm32_wasi_cc_toolchain",
toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
) for os, cpu in [
("linux", "x86_64"),
("linux", "aarch64"),
("macos", "x86_64"),
("macos", "aarch64"),
]]