| # 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"), | |
| ]] |