diff --git a/.circleci/config.yml b/.circleci/config.yml index d0d6af5..8b39596 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -124,6 +124,26 @@ jobs: docker login -u "$DOCKER_USER" -p "$DOCKER_PASS" make -C ./docker version=${CIRCLE_TAG} alias=latest push + test-bazel: + executor: bionic + steps: + - checkout + - run: apt-get install -q -y curl gnupg + - run: curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor > bazel.gpg + - run: mv bazel.gpg /etc/apt/trusted.gpg.d/ + - run: echo "deb [arch=amd64] https://storage.googleapis.com/bazel-apt stable jdk1.8" | tee /etc/apt/sources.list.d/bazel.list + - run: + name: install pip + command: | + apt-get update -q + apt-get install -q -y python3-pip + - run: pip3 install absl-py + - run: + name: install bazel + command: | + apt-get install -q -y bazel + - run: scripts/test_bazel.sh + workflows: flake8: jobs: @@ -146,3 +166,6 @@ workflows: ignore: /.*/ tags: only: /.*/ + test-bazel: + jobs: + - test-bazel diff --git a/bazel/README.md b/bazel/README.md new file mode 100644 index 0000000..17e9264 --- /dev/null +++ b/bazel/README.md @@ -0,0 +1,63 @@ +# Bazel Emscripten toolchain + +## Setup Instructions + +1. Merge the `WORKSPACE` file in with your own at the root of your bazel +directory structure. If you don't have one, simply copy the file. +2. Merge the `bazelrc` file in with your `.bazelrc` file at the root of your +bazel directory structure. If you don't have one, simply copy the file and +rename it to `.bazelrc`. (Note the `.`) +3. Copy the `emscripten_toolchain` folder along with its contents to the root of +your bazel directory. + +Your directory structure should look like this: +``` +bazel_root/ +├── .bazelrc +├── WORKSPACE +├── emscripten_toolchain/ +│ ├── BUILD.bazel +│ ├── builddefs.bzl +│ ├── crosstool.bzl +│ ├── emar.sh +│ ├── emcc.sh +│ ├── emcc_link.sh +│ ├── emscripten.BUILD +│ ├── emscripten_config +│ ├── env.sh +│ ├── link_wrapper.py +│ ├── wasm_binary.py +│ ├── wasm_cc_binary.bzl +│ ├── wasm_rules.bzl +├── your_project_folder/ +│ ├── your_project.file +``` + +## Building + +### Using --config=wasm +Simply pass `--config=wasm` when building a normal `cc_binary`. The result of +this build will be a tar archive containing any files produced by emscripten. + +### Using wasm_cc_binary +First, write a new rule wrapping your `cc_binary`. + +``` +load("@rules_cc//cc:defs.bzl", "cc_binary") +load("//emscripten_toolchain:wasm_rules.bzl", "wasm_cc_binary") + +cc_binary( + name = "hello-world", + srcs = ["hello-world.cc"], +) + +wasm_cc_binary( + name = "hello-world-wasm", + cc_target = ":hello-world", +) +``` + +Now you can run `bazel build :hello-world-wasm`. The result of this build will +be the individual files produced by emscripten. Note that some of these files +may be empty. This is because bazel has no concept of optional outputs for +rules. diff --git a/bazel/WORKSPACE b/bazel/WORKSPACE new file mode 100644 index 0000000..971c714 --- /dev/null +++ b/bazel/WORKSPACE @@ -0,0 +1,25 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "build_bazel_rules_nodejs", + sha256 = "0f2de53628e848c1691e5729b515022f5a77369c76a09fbe55611e12731c90e3", + urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/2.0.1/rules_nodejs-2.0.1.tar.gz"], +) + +load("@build_bazel_rules_nodejs//:index.bzl", "npm_install") + +# emscripten 2.0.2 +http_archive( + name = "emscripten", + sha256 = "9febbc252372efee82e62d548f510dfc85ade6661f5f3be28823848d4757d614", + strip_prefix = "install", + url = "https://storage.googleapis.com/webassembly/emscripten-releases-builds/linux/ede25d889a0abe63360d4c5d420087c8753b8bbe/wasm-binaries.tbz2", + build_file = "//emscripten_toolchain:emscripten.BUILD", + type = "tar.bz2", +) + +npm_install( + name = "npm", + package_json = "@emscripten//:emscripten/package.json", + package_lock_json = "@emscripten//:emscripten/package-lock.json", +) diff --git a/bazel/bazelrc b/bazel/bazelrc new file mode 100644 index 0000000..85801e8 --- /dev/null +++ b/bazel/bazelrc @@ -0,0 +1,5 @@ +build:wasm --crosstool_top=//emscripten_toolchain:everything + +build:wasm --cpu=wasm + +build:wasm --host_crosstool_top=@bazel_tools//tools/cpp:toolchain diff --git a/bazel/emscripten_toolchain/BUILD.bazel b/bazel/emscripten_toolchain/BUILD.bazel new file mode 100644 index 0000000..89fc320 --- /dev/null +++ b/bazel/emscripten_toolchain/BUILD.bazel @@ -0,0 +1,80 @@ +load(":crosstool.bzl", "emscripten_cc_toolchain_config_rule") + +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "common-script-includes", + srcs = [ + "emar.sh", + "emcc.sh", + "emscripten_config", + "env.sh", + "@emscripten//:all", + "@nodejs//:node_files", + "@npm//:node_modules", + ], +) + +filegroup( + name = "compile-emscripten", + srcs = [":common-script-includes"], +) + +filegroup( + name = "link-emscripten", + srcs = [ + "emcc_link.sh", + "link_wrapper.py", + ":common-script-includes", + "@emscripten//:all", + "@nodejs//:node_files", + ], +) + +filegroup( + name = "every-file", + srcs = [ + ":compile-emscripten", + ":link-emscripten", + "@emscripten//:all", + "@nodejs//:node_files", + ], +) + +filegroup(name = "empty") + +# dlmalloc.bc is implictly added by the emscripten toolchain +cc_library(name = "malloc") + +emscripten_cc_toolchain_config_rule( + name = "wasm", + cpu = "wasm", + emscripten_version = "emscripten", +) + +cc_toolchain( + name = "cc-compiler-wasm", + all_files = ":every-file", + ar_files = ":common-script-includes", + as_files = ":empty", + compiler_files = ":compile-emscripten", + dwp_files = ":empty", + linker_files = ":link-emscripten", + objcopy_files = ":empty", + strip_files = ":empty", + toolchain_config = "wasm", + toolchain_identifier = "emscripten-wasm", +) + +cc_toolchain_suite( + name = "everything", + toolchains = { + "wasm": ":cc-compiler-wasm", + "wasm|emscripten": ":cc-compiler-wasm", + }, +) + +py_binary( + name = "wasm_binary", + srcs = ["wasm_binary.py"], +) diff --git a/bazel/emscripten_toolchain/crosstool.bzl b/bazel/emscripten_toolchain/crosstool.bzl new file mode 100644 index 0000000..31b516e --- /dev/null +++ b/bazel/emscripten_toolchain/crosstool.bzl @@ -0,0 +1,1126 @@ +"""This module encapsulates logic to create emscripten_cc_toolchain_config rule.""" + +load( + "@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl", + "action_config", + "env_entry", + "env_set", + "feature", + "feature_set", + "flag_group", + "tool", + "tool_path", + "variable_with_value", + "with_feature_set", + _flag_set = "flag_set", +) +load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES") + +def flag_set(flags = None, features = None, not_features = None, **kwargs): + """Extension to flag_set which allows for a "simple" form. + + The simple form allows specifying flags as a simple list instead of a flag_group + if enable_if or expand_if semantics are not required. + + Similarly, the simple form allows passing features/not_features if they are a simple + list of semantically "and" features. + (i.e. "asan" and "dbg", rather than "asan" or "dbg") + + Args: + flags: list, set of flags + features: list, set of features required to be enabled. + not_features: list, set of features required to not be enabled. + **kwargs: The rest of the args for flag_set. + + Returns: + flag_set + """ + if flags: + if kwargs.get("flag_groups"): + fail("Cannot set flags and flag_groups") + else: + kwargs["flag_groups"] = [flag_group(flags = flags)] + + if features or not_features: + if kwargs.get("with_features"): + fail("Cannot set features/not_feature and with_features") + kwargs["with_features"] = [with_feature_set( + features = features or [], + not_features = not_features or [], + )] + return _flag_set(**kwargs) + +CROSSTOOL_DEFAULT_WARNINGS = [ + "-Wall", +] + +def _impl(ctx): + target_cpu = ctx.attr.cpu + toolchain_identifier = "emscripten-" + target_cpu + target_system_name = target_cpu + "-unknown-emscripten" + + host_system_name = "i686-unknown-linux-gnu" + + target_libc = "musl/js" + + abi_version = "emscripten_syscalls" + + compiler = "emscripten" + abi_libc_version = "default" + + cc_target_os = "emscripten" + emscripten_version = ctx.attr.emscripten_version + emscripten_root = "external/emscripten/" + emscripten_version + + builtin_sysroot = None + + ################################################################ + # Tools + ################################################################ + clang_tool = tool(path = "emcc.sh") + clif_match_tool = tool(path = "dummy_clif_matcher") + link_tool = tool(path = "emcc_link.sh") + archive_tool = tool(path = "emar.sh") + strip_tool = tool(path = "NOT_USED_STRIP_TOOL") + + #### Legacy tool paths (much of this is redundant with action_configs, but + #### these are still used for some things) + tool_paths = [ + tool_path(name = "ar", path = "emar.sh"), + tool_path(name = "cpp", path = "/bin/false"), + tool_path(name = "gcc", path = "emcc.sh"), + tool_path(name = "gcov", path = "/bin/false"), + tool_path(name = "ld", path = "emcc_link.sh"), + tool_path(name = "nm", path = "NOT_USED"), + tool_path(name = "objdump", path = "/bin/false"), + tool_path(name = "strip", path = "NOT_USED"), + ] + + ################################################################ + # Action Configs + ################################################################ + + cpp_compile_action = action_config( + action_name = ACTION_NAMES.cpp_compile, + tools = [clang_tool], + ) + + cpp_module_compile_action = action_config( + action_name = ACTION_NAMES.cpp_module_compile, + tools = [clang_tool], + ) + + cpp_module_codegen_action = action_config( + action_name = ACTION_NAMES.cpp_module_codegen, + tools = [clang_tool], + ) + + clif_match_action = action_config( + action_name = ACTION_NAMES.clif_match, + tools = [clif_match_tool], + ) + + cpp_link_dynamic_library_action = action_config( + action_name = ACTION_NAMES.cpp_link_dynamic_library, + tools = [link_tool], + ) + + strip_action = action_config( + action_name = ACTION_NAMES.strip, + tools = [strip_tool], + ) + + preprocess_assemble_action = action_config( + action_name = ACTION_NAMES.preprocess_assemble, + tools = [clang_tool], + ) + + cpp_header_parsing_action = action_config( + action_name = ACTION_NAMES.cpp_header_parsing, + tools = [clang_tool], + ) + + cpp_link_static_library_action = action_config( + action_name = ACTION_NAMES.cpp_link_static_library, + enabled = True, + flag_sets = [ + flag_set( + flag_groups = [ + flag_group( + flags = ["rcsD", "%{output_execpath}"], + expand_if_available = "output_execpath", + ), + ], + ), + flag_set( + flag_groups = [ + flag_group( + iterate_over = "libraries_to_link", + flag_groups = [ + flag_group( + flags = ["%{libraries_to_link.name}"], + expand_if_equal = variable_with_value( + name = "libraries_to_link.type", + value = "object_file", + ), + ), + flag_group( + flags = ["%{libraries_to_link.object_files}"], + iterate_over = "libraries_to_link.object_files", + expand_if_equal = variable_with_value( + name = "libraries_to_link.type", + value = "object_file_group", + ), + ), + ], + expand_if_available = "libraries_to_link", + ), + ], + ), + flag_set( + flag_groups = [ + flag_group( + flags = ["@%{linker_param_file}"], + expand_if_available = "linker_param_file", + ), + ], + ), + ], + tools = [archive_tool], + ) + + c_compile_action = action_config( + action_name = ACTION_NAMES.c_compile, + tools = [clang_tool], + ) + + linkstamp_compile_action = action_config( + action_name = ACTION_NAMES.linkstamp_compile, + tools = [clang_tool], + ) + + assemble_action = action_config( + action_name = ACTION_NAMES.assemble, + tools = [clang_tool], + ) + + cpp_link_executable_action = action_config( + action_name = ACTION_NAMES.cpp_link_executable, + tools = [link_tool], + ) + + cpp_link_nodeps_dynamic_library_action = action_config( + action_name = ACTION_NAMES.cpp_link_nodeps_dynamic_library, + tools = [link_tool], + ) + + action_configs = [ + strip_action, + c_compile_action, + cpp_compile_action, + linkstamp_compile_action, + assemble_action, + preprocess_assemble_action, + cpp_header_parsing_action, + cpp_module_compile_action, + cpp_module_codegen_action, + cpp_link_executable_action, + cpp_link_dynamic_library_action, + cpp_link_nodeps_dynamic_library_action, + cpp_link_static_library_action, + clif_match_action, + ] + + all_compile_actions = [ + ACTION_NAMES.c_compile, + ACTION_NAMES.cpp_compile, + ACTION_NAMES.linkstamp_compile, + ACTION_NAMES.assemble, + ACTION_NAMES.preprocess_assemble, + ACTION_NAMES.cpp_header_parsing, + ACTION_NAMES.cpp_module_compile, + ACTION_NAMES.cpp_module_codegen, + ACTION_NAMES.clif_match, + ACTION_NAMES.lto_backend, + ] + + all_cpp_compile_actions = [ + ACTION_NAMES.cpp_compile, + ACTION_NAMES.linkstamp_compile, + ACTION_NAMES.cpp_header_parsing, + ACTION_NAMES.cpp_module_compile, + ACTION_NAMES.cpp_module_codegen, + ACTION_NAMES.clif_match, + ] + + preprocessor_compile_actions = [ + ACTION_NAMES.c_compile, + ACTION_NAMES.cpp_compile, + ACTION_NAMES.linkstamp_compile, + ACTION_NAMES.preprocess_assemble, + ACTION_NAMES.cpp_header_parsing, + ACTION_NAMES.cpp_module_compile, + ACTION_NAMES.clif_match, + ] + + all_link_actions = [ + ACTION_NAMES.cpp_link_executable, + ACTION_NAMES.cpp_link_dynamic_library, + ACTION_NAMES.cpp_link_nodeps_dynamic_library, + ] + + ################################################################ + # Features + ################################################################ + + features = [ + # This set of magic "feature"s are important configuration information for blaze. + feature(name = "no_legacy_features", enabled = True), + feature( + name = "has_configured_linker_path", + enabled = True, + ), + + # Blaze requests this feature by default, but we don't care. + feature(name = "dependency_file"), + + # Blaze requests this feature by default, but we don't care. + feature(name = "random_seed"), + + # Formerly "needsPic" attribute + feature(name = "supports_pic", enabled = False), + + # Blaze requests this feature by default. + # Blaze also tests if this feature is supported, before setting the "pic" build-variable. + feature(name = "pic"), + + # Blaze requests this feature if fission is requested + # Blaze also tests if it's supported to see if we support fission. + feature(name = "per_object_debug_info"), + + # Blaze requests this feature by default. + # Blaze also tests if this feature is supported before setting preprocessor_defines + # (...but why?) + feature(name = "preprocessor_defines"), + + # Blaze requests this feature by default. + # Blaze also tests if this feature is supported before setting includes. (...but why?) + feature(name = "include_paths"), + + # Blaze tests if this feature is enabled in order to create implicit + # "nodeps" .so outputs from cc_library rules. + feature(name = "supports_dynamic_linker", enabled = False), + + # Blaze requests this feature when linking a cc_binary which is + # "dynamic" aka linked against nodeps-dynamic-library cc_library + # outputs. + feature(name = "dynamic_linking_mode"), + + #### Configuration features + feature( + name = "crosstool_cpu", + enabled = True, + implies = ["crosstool_cpu_" + target_cpu], + ), + feature( + name = "crosstool_cpu_asmjs", + provides = ["variant:crosstool_cpu"], + ), + feature( + name = "crosstool_cpu_wasm", + provides = ["variant:crosstool_cpu"], + ), + + # These 3 features will be automatically enabled by blaze in the + # corresponding build mode. + feature( + name = "opt", + provides = ["variant:crosstool_build_mode"], + ), + feature( + name = "dbg", + provides = ["variant:crosstool_build_mode"], + ), + feature( + name = "fastbuild", + provides = ["variant:crosstool_build_mode"], + ), + + #### User-settable features + + # Set if enabling exceptions. + feature(name = "exceptions"), + + # This feature overrides the default optimization to prefer execution speed + # over binary size (like clang -O3). + feature( + name = "optimized_for_speed", + provides = ["variant:crosstool_optimization_mode"], + ), + + # This feature overrides the default optimization to prefer binary size over + # execution speed (like clang -Oz). + feature( + name = "optimized_for_size", + provides = ["variant:crosstool_optimization_mode"], + ), + + # Convenience aliases / alt-spellings. + feature( + name = "optimize_for_speed", + implies = ["optimized_for_speed"], + ), + feature( + name = "optimize_for_size", + implies = ["optimized_for_size"], + ), + + # This feature allows easier use of profiling tools by preserving mangled + # C++ names. This does everything profiling_funcs does and more. + feature(name = "profiling"), + + # This feature emits only enough debug info for function names to appear + # in profiles. + feature(name = "profiling_funcs"), + + # This feature allows source maps to be generated. + feature( + name = "source_maps", + implies = ["full_debug_info"], + ), + feature( + name = "dwarf_debug_info", + implies = ["profiling"], + ), + + # Turns on full debug info (-g4). + feature(name = "full_debug_info"), + + # Enables the use of "Emscripten" Pthread implementation. + # https://kripken.github.io/emscripten-site/docs/porting/pthreads.html + # https://github.com/kripken/emscripten/wiki/Pthreads-with-WebAssembly + feature(name = "use_pthreads"), + + # If enabled, the runtime will exit when main() completes. + feature(name = "exit_runtime"), + + # Primarily for toolchain maintainers: + feature(name = "emcc_debug"), + feature(name = "emcc_debug_link"), + feature( + name = "llvm_backend", + requires = [feature_set(features = ["crosstool_cpu_wasm"])], + enabled = True, + ), + + # Remove once flag is flipped. + # See https://github.com/bazelbuild/bazel/issues/7687 + feature( + name = "do_not_split_linking_cmdline", + ), + + # Adds simd support, only available with the llvm backend. + feature( + name = "wasm_simd", + requires = [feature_set(features = ["llvm_backend"])], + ), + feature( + name = "precise_long_double_printf", + enabled = True, + ), + feature( + name = "wasm_warnings_as_errors", + enabled = True, + ), + + # ASan and UBSan. See also: + # https://emscripten.org/docs/debugging/Sanitizers.html + feature(name = "wasm_asan"), + feature(name = "wasm_ubsan"), + ] + + crosstool_default_flag_sets = [ + # Compile, Link, and CC_FLAGS make variable + flag_set( + actions = [ + ACTION_NAMES.c_compile, + ACTION_NAMES.cpp_compile, + ACTION_NAMES.linkstamp_compile, + ACTION_NAMES.assemble, + ACTION_NAMES.preprocess_assemble, + ACTION_NAMES.cpp_header_parsing, + ACTION_NAMES.cpp_module_compile, + ACTION_NAMES.cpp_module_codegen, + ACTION_NAMES.clif_match, + ACTION_NAMES.cpp_link_executable, + ACTION_NAMES.cpp_link_dynamic_library, + ACTION_NAMES.cpp_link_nodeps_dynamic_library, + ], + flag_groups = [ + flag_group( + flags = ["--sysroot=%{sysroot}"], + expand_if_available = "sysroot", + ), + ], + ), + # Compile + Link + flag_set( + actions = [ + ACTION_NAMES.c_compile, + ACTION_NAMES.cpp_compile, + ACTION_NAMES.linkstamp_compile, + ACTION_NAMES.assemble, + ACTION_NAMES.preprocess_assemble, + ACTION_NAMES.cpp_header_parsing, + ACTION_NAMES.cpp_module_compile, + ACTION_NAMES.cpp_module_codegen, + ACTION_NAMES.clif_match, + ACTION_NAMES.cpp_link_executable, + ACTION_NAMES.cpp_link_dynamic_library, + ACTION_NAMES.cpp_link_nodeps_dynamic_library, + ], + # This forces color diagnostics even on Forge (where we don't have an + # attached terminal). + flags = [ + "-fdiagnostics-color", + ], + ), + # C++ compiles (and implicitly link) + flag_set( + actions = all_cpp_compile_actions, + flags = [ + "-fno-exceptions", + ], + not_features = ["exceptions"], + ), + flag_set( + actions = all_cpp_compile_actions, + flags = [ + "-fexceptions", + ], + features = ["exceptions"], + ), + # All compiles (and implicitly link) + flag_set( + actions = all_compile_actions + + all_link_actions, + flags = [ + "-fno-strict-aliasing", + "-funsigned-char", + "-no-canonical-prefixes", + ], + ), + # Language Features + flag_set( + actions = all_cpp_compile_actions, + flags = [ + "-std=gnu++17", + "-nostdinc", + "-Xclang", + "-nobuiltininc", + "-Xclang", + "-nostdsysteminc", + ], + ), + + # Emscripten-specific settings: + flag_set( + actions = all_compile_actions + all_link_actions, + flags = ["-s", "WASM=0"], + features = ["crosstool_cpu_asmjs"], + ), + flag_set( + actions = all_compile_actions + + all_link_actions, + flags = ["-s", "USE_PTHREADS=1"], + features = ["use_pthreads"], + ), + flag_set( + actions = all_link_actions, + flags = ["-s", "EXIT_RUNTIME=1"], + features = ["exit_runtime"], + ), + flag_set( + actions = all_compile_actions + all_link_actions, + flags = ["-pthread"], + features = ["llvm_backend", "use_pthreads"], + ), + flag_set( + actions = all_compile_actions + all_link_actions, + flags = ["-msimd128"], + features = ["wasm_simd"], + ), + flag_set( + actions = all_link_actions, + flags = ["-s", "PRINTF_LONG_DOUBLE=1"], + features = ["precise_long_double_printf"], + ), + + # Opt + flag_set( + actions = preprocessor_compile_actions, + flags = ["-DNDEBUG"], + features = ["opt"], + ), + flag_set( + actions = all_compile_actions, + flags = ["-fomit-frame-pointer"], + features = ["opt"], + ), + flag_set( + actions = all_compile_actions + + all_link_actions, + flags = ["-O3"], + features = ["opt"], + ), + # Users can override opt-level with semantic names... + flag_set( + actions = all_compile_actions + + all_link_actions, + flags = ["-Oz"], + features = ["optimized_for_size", "opt"], + ), + flag_set( + actions = all_compile_actions + + all_link_actions, + flags = ["-O3"], + features = ["optimized_for_speed", "opt"], + ), + + # Fastbuild + flag_set( + actions = all_compile_actions, + flags = ["-fomit-frame-pointer"], + features = ["fastbuild"], + ), + flag_set( + actions = all_compile_actions + + all_link_actions, + flags = ["-O2"], + features = ["fastbuild"], + ), + + # Dbg + flag_set( + actions = all_compile_actions, + flags = ["-fno-omit-frame-pointer"], + features = ["dbg"], + ), + flag_set( + actions = all_compile_actions + + all_link_actions, + flags = ["-g", "-O0"], + features = ["dbg"], + ), + flag_set( + actions = all_compile_actions + + all_link_actions, + flags = [ + "-g4", + "-fsanitize=address", + "-O1", + "-DADDRESS_SANITIZER=1", + "-fno-omit-frame-pointer", + ], + features = ["wasm_asan"], + ), + flag_set( + actions = all_compile_actions + + all_link_actions, + flags = [ + "-g4", + "-fsanitize=undefined", + "-O1", + "-DUNDEFINED_BEHAVIOR_SANITIZER=1", + "-fno-omit-frame-pointer", + "-fno-sanitize=vptr", + ], + features = ["wasm_ubsan"], + ), + + # Profiling provides full debug info and a special --profiling flag + # to control name mangling + flag_set( + actions = all_link_actions, + flags = ["--profiling"], + features = ["profiling"], + ), + flag_set( + actions = all_link_actions, + flags = ["--profiling_funcs"], + features = ["profiling_funcs"], + ), + flag_set( + actions = all_compile_actions + + all_link_actions, + flags = ["-g4"], + features = ["full_debug_info"], + ), + flag_set( + actions = all_link_actions, + flags = ["-gseparate-dwarf"], + features = ["dwarf_debug_info"], + ), + flag_set( + actions = all_compile_actions + + all_link_actions, + flags = ["-fdebug-compilation-dir=."], + features = ["dwarf_debug_info"], + ), + # Generic warning flag list + flag_set( + actions = all_compile_actions, + flags = CROSSTOOL_DEFAULT_WARNINGS, + ), + + # Defines and Includes and Paths and such + flag_set( + actions = all_compile_actions, + flag_groups = [ + flag_group(flags = ["-fPIC"], expand_if_available = "pic"), + ], + ), + flag_set( + actions = preprocessor_compile_actions, + flag_groups = [ + flag_group( + flags = ["-D%{preprocessor_defines}"], + iterate_over = "preprocessor_defines", + ), + ], + ), + flag_set( + actions = preprocessor_compile_actions, + flag_groups = [ + flag_group( + flags = ["-include", "%{includes}"], + iterate_over = "includes", + expand_if_available = "includes", + ), + ], + ), + flag_set( + actions = preprocessor_compile_actions, + flag_groups = [ + flag_group( + flags = ["-iquote", "%{quote_include_paths}"], + iterate_over = "quote_include_paths", + ), + flag_group( + flags = ["-I%{include_paths}"], + iterate_over = "include_paths", + ), + flag_group( + flags = ["-isystem", "%{system_include_paths}"], + iterate_over = "system_include_paths", + ), + ], + ), + + ## Linking options (not libs -- those go last) + + # Generic link options + flag_set( + actions = [ + ACTION_NAMES.cpp_link_dynamic_library, + ACTION_NAMES.cpp_link_nodeps_dynamic_library, + ], + flags = ["-shared"], + ), + + # Linker search paths and objects: + flag_set( + actions = all_link_actions, + flag_groups = [ + flag_group( + iterate_over = "runtime_library_search_directories", + flag_groups = [ + flag_group( + flags = [ + "-Wl,-rpath,$EXEC_ORIGIN/%{runtime_library_search_directories}", + ], + expand_if_true = "is_cc_test", + ), + flag_group( + flags = [ + "-Wl,-rpath,$ORIGIN/%{runtime_library_search_directories}", + ], + expand_if_false = "is_cc_test", + ), + ], + expand_if_available = "runtime_library_search_directories", + ), + ], + ), + flag_set( + actions = all_link_actions, + flag_groups = [ + flag_group( + flags = ["-L%{library_search_directories}"], + iterate_over = "library_search_directories", + expand_if_available = "library_search_directories", + ), + ], + ), + flag_set( + actions = all_link_actions, + flag_groups = [ + flag_group( + # This is actually a list of object files from the linkstamp steps + flags = ["%{linkstamp_paths}"], + iterate_over = "linkstamp_paths", + expand_if_available = "linkstamp_paths", + ), + ], + ), + flag_set( + actions = all_link_actions, + flag_groups = [ + flag_group( + flags = ["@%{thinlto_param_file}"], + expand_if_available = "libraries_to_link", + expand_if_true = "thinlto_param_file", + ), + flag_group( + iterate_over = "libraries_to_link", + flag_groups = [ + flag_group( + flags = ["-Wl,--start-lib"], + expand_if_equal = variable_with_value( + name = "libraries_to_link.type", + value = "object_file_group", + ), + ), + flag_group( + flags = ["-Wl,-whole-archive"], + expand_if_true = "libraries_to_link.is_whole_archive", + ), + flag_group( + flags = ["%{libraries_to_link.object_files}"], + iterate_over = "libraries_to_link.object_files", + expand_if_equal = variable_with_value( + name = "libraries_to_link.type", + value = "object_file_group", + ), + ), + flag_group( + flags = ["%{libraries_to_link.name}"], + expand_if_equal = variable_with_value( + name = "libraries_to_link.type", + value = "object_file", + ), + ), + flag_group( + flags = ["%{libraries_to_link.name}"], + expand_if_equal = variable_with_value( + name = "libraries_to_link.type", + value = "interface_library", + ), + ), + flag_group( + flags = ["%{libraries_to_link.name}"], + expand_if_equal = variable_with_value( + name = "libraries_to_link.type", + value = "static_library", + ), + ), + flag_group( + flags = ["-l%{libraries_to_link.name}"], + expand_if_equal = variable_with_value( + name = "libraries_to_link.type", + value = "dynamic_library", + ), + ), + flag_group( + flags = ["-l:%{libraries_to_link.name}"], + expand_if_equal = variable_with_value( + name = "libraries_to_link.type", + value = "versioned_dynamic_library", + ), + ), + flag_group( + flags = ["-Wl,-no-whole-archive"], + expand_if_true = "libraries_to_link.is_whole_archive", + ), + flag_group( + flags = ["-Wl,--end-lib"], + expand_if_equal = variable_with_value( + name = "libraries_to_link.type", + value = "object_file_group", + ), + ), + ], + expand_if_available = "libraries_to_link", + ), + ], + ), + + # Configure the header parsing and preprocessing. + flag_set( + actions = [ACTION_NAMES.cpp_header_parsing], + flags = ["-xc++-header", "-fsyntax-only"], + features = ["parse_headers"], + ), + + # Note: user compile flags should be nearly last -- you probably + # don't want to put any more features after this! + flag_set( + actions = [ + ACTION_NAMES.c_compile, + ACTION_NAMES.cpp_compile, + ACTION_NAMES.linkstamp_compile, + ACTION_NAMES.assemble, + ACTION_NAMES.preprocess_assemble, + ACTION_NAMES.cpp_header_parsing, + ACTION_NAMES.cpp_module_compile, + ACTION_NAMES.cpp_module_codegen, + ACTION_NAMES.clif_match, + ], + flag_groups = [ + flag_group( + flags = ["%{user_compile_flags}"], + iterate_over = "user_compile_flags", + expand_if_available = "user_compile_flags", + ), + ], + ), + flag_set( + actions = all_link_actions, + flag_groups = [ + flag_group( + flags = ["%{user_link_flags}"], + iterate_over = "user_link_flags", + expand_if_available = "user_link_flags", + ), + ], + ), + ## Options which need to go late -- after all the user options -- go here. + flag_set( + # One might hope that these options would only be needed for C++ + # compiles. But, sadly, users compile ".c" files with custom + # copts=["-x", "c++"], and expect that to be able to find C++ stdlib + # headers. It might be worth pondering how blaze could support this sort + # of use-case better. + actions = preprocessor_compile_actions + + [ACTION_NAMES.cc_flags_make_variable], + flags = [ + "-isystem", + emscripten_root + "/system/lib/libc/musl/arch/emscripten", + "-isystem", + emscripten_root + "/system/lib/libc/musl/arch/js", + "-isystem", + emscripten_root + "/system/local/include", + "-isystem", + emscripten_root + "/system/include/compat", + "-isystem", + emscripten_root + "/system/include", + "-isystem", + emscripten_root + "/system/include/libcxx", + "-isystem", + emscripten_root + "/system/lib/libcxxabi/include", + "-isystem", + emscripten_root + "/system/lib/compiler-rt/include", + "-isystem", + emscripten_root + "/system/include/libc", + "-isystem", + emscripten_root + "/system/include/gfx", + "-isystem", + emscripten_root + "/system/include/SDL", + "-isystem", + emscripten_root + "/lib/clang/12.0.0/include", + ], + ), + # Inputs and outputs + flag_set( + actions = [ + ACTION_NAMES.c_compile, + ACTION_NAMES.cpp_compile, + ACTION_NAMES.linkstamp_compile, + ACTION_NAMES.assemble, + ACTION_NAMES.preprocess_assemble, + ACTION_NAMES.cpp_header_parsing, + ACTION_NAMES.cpp_module_compile, + ACTION_NAMES.cpp_module_codegen, + ACTION_NAMES.clif_match, + ], + flag_groups = [ + flag_group( + flags = ["-MD", "-MF", "%{dependency_file}"], + expand_if_available = "dependency_file", + ), + ], + ), + flag_set( + actions = [ + ACTION_NAMES.c_compile, + ACTION_NAMES.cpp_compile, + ACTION_NAMES.linkstamp_compile, + ACTION_NAMES.assemble, + ACTION_NAMES.preprocess_assemble, + ACTION_NAMES.cpp_header_parsing, + ACTION_NAMES.cpp_module_compile, + ACTION_NAMES.cpp_module_codegen, + ACTION_NAMES.clif_match, + ], + flag_groups = [ + flag_group( + flags = ["-c", "%{source_file}"], + expand_if_available = "source_file", + ), + ], + ), + flag_set( + actions = [ + ACTION_NAMES.c_compile, + ACTION_NAMES.cpp_compile, + ACTION_NAMES.linkstamp_compile, + ACTION_NAMES.assemble, + ACTION_NAMES.preprocess_assemble, + ACTION_NAMES.cpp_header_parsing, + ACTION_NAMES.cpp_module_compile, + ACTION_NAMES.cpp_module_codegen, + ACTION_NAMES.clif_match, + ], + flag_groups = [ + flag_group( + flags = ["-S"], + expand_if_available = "output_assembly_file", + ), + flag_group( + flags = ["-E"], + expand_if_available = "output_preprocess_file", + ), + flag_group( + flags = ["-o", "%{output_file}"], + expand_if_available = "output_file", + ), + ], + ), + flag_set( + actions = all_link_actions, + flag_groups = [ + flag_group( + flags = ["-o", "%{output_execpath}"], + expand_if_available = "output_execpath", + ), + ], + ), + # And finally, the params file! + flag_set( + actions = all_link_actions, + flag_groups = [ + flag_group( + flags = ["@%{linker_param_file}"], + expand_if_available = "linker_param_file", + ), + ], + ), + flag_set( + actions = all_compile_actions, + flags = [ + "-Wno-builtin-macro-redefined", + # Genrules may not escape quotes enough for these, so + # don't put them into $(CC_FLAGS): + '-D__DATE__="redacted"', + '-D__TIMESTAMP__="redacted"', + '-D__TIME__="redacted"', + ], + ), + flag_set( + actions = all_compile_actions, + flags = ["-Werror"], + features = ["wasm_warnings_as_errors"], + ), + ] + + crosstool_default_env_sets = [ + # Use llvm backend. Off by default, enabled via --features=llvm_backend + env_set( + actions = all_compile_actions + + all_link_actions + + [ACTION_NAMES.cpp_link_static_library], + env_entries = [env_entry(key = "EMCC_WASM_BACKEND", value = "1")], + with_features = [with_feature_set(features = ["llvm_backend"])], + ), + # Debug compile and link. Off by default, enabled via --features=emcc_debug + env_set( + actions = all_compile_actions, + env_entries = [env_entry(key = "EMCC_DEBUG", value = "1")], + with_features = [with_feature_set(features = ["emcc_debug"])], + ), + + # Debug only link step. Off by default, enabled via --features=emcc_debug_link + env_set( + actions = all_link_actions, + env_entries = [env_entry(key = "EMCC_DEBUG", value = "1")], + with_features = [ + with_feature_set(features = ["emcc_debug"]), + with_feature_set(features = ["emcc_debug_link"]), + ], + ), + ] + + crosstool_default_flags_feature = feature( + name = "crosstool_default_flags", + enabled = True, + flag_sets = crosstool_default_flag_sets, + env_sets = crosstool_default_env_sets, + ) + + features.append(crosstool_default_flags_feature) + + cxx_builtin_include_directories = [ + emscripten_version + "/system/lib/libc/musl/arch/emscripten", + emscripten_version + "/system/lib/libc/musl/arch/js", + emscripten_version + "/system/local/include", + emscripten_version + "/system/include/compat", + emscripten_version + "/system/include", + emscripten_version + "/system/include/libcxx", + emscripten_version + "/system/lib/compiler-rt/include", + emscripten_version + "/system/lib/libcxxabi/include", + emscripten_version + "/system/include/libc", + emscripten_version + "/system/include/gfx", + emscripten_version + "/system/include/SDL", + emscripten_version + "/lib/clang/12.0.0/include", + ] + + artifact_name_patterns = [] + + make_variables = [] + + out = ctx.actions.declare_file(ctx.label.name) + ctx.actions.write(out, "Fake executable") + return [ + cc_common.create_cc_toolchain_config_info( + ctx = ctx, + features = features, + action_configs = action_configs, + artifact_name_patterns = artifact_name_patterns, + cxx_builtin_include_directories = cxx_builtin_include_directories, + toolchain_identifier = toolchain_identifier, + host_system_name = host_system_name, + target_system_name = target_system_name, + target_cpu = target_cpu, + target_libc = target_libc, + compiler = compiler, + abi_version = abi_version, + abi_libc_version = abi_libc_version, + tool_paths = tool_paths, + make_variables = make_variables, + builtin_sysroot = builtin_sysroot, + cc_target_os = cc_target_os, + ), + DefaultInfo( + executable = out, + ), + ] + +emscripten_cc_toolchain_config_rule = rule( + implementation = _impl, + attrs = { + "cpu": attr.string(mandatory = True, values = ["asmjs", "wasm"]), + "emscripten_version": attr.string(mandatory = True), + }, + provides = [CcToolchainConfigInfo], + executable = True, +) diff --git a/bazel/emscripten_toolchain/emar.sh b/bazel/emscripten_toolchain/emar.sh new file mode 100755 index 0000000..965442e --- /dev/null +++ b/bazel/emscripten_toolchain/emar.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +source emscripten_toolchain/env.sh + +exec python3 $EMSCRIPTEN/emar.py "$@" diff --git a/bazel/emscripten_toolchain/emcc.sh b/bazel/emscripten_toolchain/emcc.sh new file mode 100755 index 0000000..66d00b6 --- /dev/null +++ b/bazel/emscripten_toolchain/emcc.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +source emscripten_toolchain/env.sh + +exec python3 external/emscripten/emscripten/emcc.py "$@" diff --git a/bazel/emscripten_toolchain/emcc_link.sh b/bazel/emscripten_toolchain/emcc_link.sh new file mode 100755 index 0000000..9d0f8e2 --- /dev/null +++ b/bazel/emscripten_toolchain/emcc_link.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +source emscripten_toolchain/env.sh + +exec python3 emscripten_toolchain/link_wrapper.py "$@" diff --git a/bazel/emscripten_toolchain/emscripten.BUILD b/bazel/emscripten_toolchain/emscripten.BUILD new file mode 100644 index 0000000..6f11852 --- /dev/null +++ b/bazel/emscripten_toolchain/emscripten.BUILD @@ -0,0 +1,6 @@ +package(default_visibility = ['//visibility:public']) + +filegroup( + name = "all", + srcs = glob(["**"]), +) diff --git a/bazel/emscripten_toolchain/emscripten_config b/bazel/emscripten_toolchain/emscripten_config new file mode 100644 index 0000000..4a77344 --- /dev/null +++ b/bazel/emscripten_toolchain/emscripten_config @@ -0,0 +1,9 @@ +import os + +ROOT_DIR = os.environ["ROOT_DIR"] +EMSCRIPTEN_ROOT = os.environ["EMSCRIPTEN"] + +LLVM_ROOT = ROOT_DIR + "/external/emscripten/bin" +EMSCRIPTEN_NATIVE_OPTIMIZER = LLVM_ROOT + "/optimizer" +NODE_JS = ROOT_DIR + "/external/nodejs_linux_amd64/bin/node" +BINARYEN_ROOT = ROOT_DIR + "/external/emscripten" diff --git a/bazel/emscripten_toolchain/env.sh b/bazel/emscripten_toolchain/env.sh new file mode 100755 index 0000000..005fa87 --- /dev/null +++ b/bazel/emscripten_toolchain/env.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +export ROOT_DIR=`(pwd -P)` +export EMSCRIPTEN=${ROOT_DIR}/external/emscripten/emscripten + +export EM_CONFIG=${ROOT_DIR}/emscripten_toolchain/emscripten_config +export EM_CACHE=${ROOT_DIR}/emscripten_toolchain/cache diff --git a/bazel/emscripten_toolchain/link_wrapper.py b/bazel/emscripten_toolchain/link_wrapper.py new file mode 100644 index 0000000..a746ae8 --- /dev/null +++ b/bazel/emscripten_toolchain/link_wrapper.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +"""wrapper around emcc link step. + +This wrapper currently serves the following purposes. + +1. Ensures we always link to file with .js extension. The upstream default + it to link to an llvm bitcode file which is never (AFAICT) want to do that. + +2. When building with --config=wasm the final output is multiple files, usually + at least one .js and one .wasm file. Since the cc_binary link step only + allows a single output, we must tar up the outputs into a single file. + +3. Add quotes around arguments that need them in the response file to work + around a bazel quirk. +""" + +from __future__ import print_function + +import os +import subprocess +import sys + +# Only argument should be @path/to/parameter/file +assert sys.argv[1][0] == '@' +param_filename = sys.argv[1][1:] +param_file_args = [l.strip() for l in open(param_filename, 'r').readlines()] + +output_index = param_file_args.index('-o') + 1 +orig_output = js_output = param_file_args[output_index] +outdir = os.path.dirname(orig_output) + +# google3-only(TODO(b/139440956): Default to False once the bug is fixed) +replace_response_file = any(' ' in a for a in param_file_args) + +if not os.path.splitext(orig_output)[1]: + js_output = orig_output + '.js' + param_file_args[output_index] = js_output + replace_response_file = True + +# Re-write response file if needed. +if replace_response_file: + new_param_filename = param_filename + '.modified' + with open(new_param_filename, 'w') as f: + for param in param_file_args: + if ' ' in param: + f.write('"%s"' % param) + else: + f.write(param) + f.write('\n') + sys.argv[1] = '@' + new_param_filename + +emcc_py = os.path.join(os.environ['EMSCRIPTEN'], 'emcc.py') +rtn = subprocess.call(['python3', emcc_py] + sys.argv[1:]) +if rtn != 0: + sys.exit(1) + +js_name = os.path.basename(js_output) +base_name = os.path.splitext(js_name)[0] + +files = [] +extensions = [ + '.js', + '.wasm', + '.wasm.map', + '.js.mem', + '.fetch.js', + '.worker.js', + '.data', + '.js.symbols', + '.wasm.debug.wasm' +] + +for ext in extensions: + filename = base_name + ext + if os.path.exists(os.path.join(outdir, filename)): + files.append(filename) + +wasm_base = os.path.join(outdir, base_name + '.wasm') +if os.path.exists(wasm_base + '.debug.wasm') and os.path.exists(wasm_base): + # If we have a .wasm.debug.wasm file and a .wasm file, we need to rewrite the + # section in the .wasm file that refers to it. The path that's in there + # is the blaze output path; we want it to be just the filename. + + llvm_objcopy = os.path.join( + os.environ['EMSCRIPTEN'], 'llvm-bin/llvm-objcopy') + # First, check to make sure the .wasm file has the header that needs to be + # rewritten. + rtn = subprocess.call([ + llvm_objcopy, + '--dump-section=external_debug_info=/dev/null', + wasm_base], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if rtn == 0: + # If llvm-objcopy did not return an error, the external_debug_info section + # must exist, so we're good to continue. + + # Next we need to convert length of the filename to LEB128. + # Start by converting the length of the filename to a bit string. + bit_string = '{0:b}'.format(len(base_name + '.wasm.debug.wasm')) + + # Pad the bit string with 0s so that its length is a multiple of 7. + while len(bit_string) % 7 != 0: + bit_string = '0' + bit_string + + # Break up our bit string into chunks of 7. + # We do this backwards because the final format is little-endian. + final_bytes = bytearray() + for i in reversed(range(0, len(bit_string), 7)): + binary_part = bit_string[i:i + 7] + if i != 0: + # Every chunk except the last one needs to be prepended with '1'. + # The length of each chunk is 7, so that one has an implicit '0'. + binary_part = '1' + binary_part + final_bytes.append(int(binary_part, 2)) + # Finally, add the actual filename. + final_bytes.extend(base_name + '.wasm.debug.wasm') + + # Write our length + filename bytes to a temp file. + with open('debugsection.tmp', 'wb+') as f: + f.write(final_bytes) + f.close() + + # First delete the old section. + subprocess.check_call([ + llvm_objcopy, + wasm_base, + '--remove-section=external_debug_info']) + # Rewrite section with the new size and filename from the temp file. + subprocess.check_call([ + llvm_objcopy, + wasm_base, + '--add-section=external_debug_info=debugsection.tmp']) + +# If we have more than one output file then create tarball +if len(files) > 1: + cmd = ['tar', 'cf', 'tmp.tar'] + files + subprocess.check_call(cmd, cwd=outdir) + os.rename(os.path.join(outdir, 'tmp.tar'), orig_output) +elif len(files) == 1: + # Otherwise, if only have a single output than move it to the expected name + if files[0] != os.path.basename(orig_output): + os.rename(os.path.join(outdir, files[0]), orig_output) +else: + print('emcc.py did not appear to output any known files!') + sys.exit(1) + +sys.exit(0) diff --git a/bazel/emscripten_toolchain/wasm_binary.py b/bazel/emscripten_toolchain/wasm_binary.py new file mode 100644 index 0000000..6bb30db --- /dev/null +++ b/bazel/emscripten_toolchain/wasm_binary.py @@ -0,0 +1,84 @@ +"""Unpackages a bazel emscripten archive for use in a bazel BUILD rule. + +This script will take a tar archive containing the output of the emscripten +toolchain. This file contains any output files produced by a wasm_cc_binary or a +cc_binary built with --config=wasm. The files are extracted into the given +output path. + +The name of archive is expected to be of the format `foo` or `foo.XXX` and +the contents are expected to be foo.js and foo.wasm. + +Several optional files may also be in the archive, including but not limited to +foo.js.mem, pthread-main.js, and foo.wasm.map. + +If the file is not a tar archive, the passed file will simply be copied to its +destination. + +This script and its accompanying Bazel rule should allow you to extract a +WebAssembly binary into a larger web application. +""" + +import os +import subprocess +import sys + +from absl import app +from absl import flags + +FLAGS = flags.FLAGS +flags.DEFINE_string('archive', None, 'The the archive to extract from.') +flags.DEFINE_string('output_path', None, 'The path to extract into.') + + +def ensure(f): + if not os.path.exists(f): + with open(f, 'w'): + pass + + +def check(f): + if not os.path.exists(f): + raise Exception('Expected file in archive: %s' % f) + + +def main(argv): + basename = os.path.basename(FLAGS.archive) + stem = basename.split('.')[0] + + # Check the type of the input file + mimetype_bytes = subprocess.check_output(['file', '-Lib', FLAGS.archive]) + mimetype = mimetype_bytes.decode(sys.stdout.encoding) + + # If we have a tar, extract all files. If we have just a single file, copy it. + if 'tar' in mimetype: + subprocess.check_call( + ['tar', 'xf', FLAGS.archive, '-C', FLAGS.output_path]) + elif 'binary' in mimetype: + subprocess.check_call([ + 'cp', + FLAGS.archive, + os.path.join(FLAGS.output_path, stem + '.wasm')]) + elif 'text' in mimetype: + subprocess.check_call([ + 'cp', + FLAGS.archive, + os.path.join(FLAGS.output_path, stem + '.js')]) + else: + subprocess.check_call(['cp', FLAGS.archive, FLAGS.output_path]) + + # At least one of these two files should exist at this point. + ensure(os.path.join(FLAGS.output_path, stem + '.js')) + ensure(os.path.join(FLAGS.output_path, stem + '.wasm')) + + # And can optionally contain these extra files. + ensure(os.path.join(FLAGS.output_path, stem + '.wasm.map')) + ensure(os.path.join(FLAGS.output_path, stem + '.worker.js')) + ensure(os.path.join(FLAGS.output_path, stem + '.js.mem')) + ensure(os.path.join(FLAGS.output_path, stem + '.data')) + ensure(os.path.join(FLAGS.output_path, stem + '.fetch.js')) + ensure(os.path.join(FLAGS.output_path, stem + '.js.symbols')) + ensure(os.path.join(FLAGS.output_path, stem + '.wasm.debug.wasm')) + + +if __name__ == '__main__': + app.run(main) diff --git a/bazel/emscripten_toolchain/wasm_cc_binary.bzl b/bazel/emscripten_toolchain/wasm_cc_binary.bzl new file mode 100644 index 0000000..01780e2 --- /dev/null +++ b/bazel/emscripten_toolchain/wasm_cc_binary.bzl @@ -0,0 +1,150 @@ +"""wasm_cc_binary rule for compiling C++ targets to WebAssembly. +""" + +def _wasm_transition_impl(settings, attr): + _ignore = (settings, attr) + + features = list(settings["//command_line_option:features"]) + linkopts = list(settings["//command_line_option:linkopt"]) + + if attr.threads == "emscripten": + # threads enabled + features.append("use_pthreads") + elif attr.threads == "off": + # threads disabled + features.append("-use_pthreads") + + if attr.exit_runtime == True: + features.append("exit_runtime") + + if attr.backend == "llvm": + features.append("llvm_backend") + elif attr.backend == "emscripten": + features.append("-llvm_backend") + + if attr.simd: + features.append("wasm_simd") + + return { + "//command_line_option:compiler": "emscripten", + "//command_line_option:crosstool_top": "//emscripten_toolchain:everything", + "//command_line_option:cpu": "wasm", + "//command_line_option:features": features, + "//command_line_option:dynamic_mode": "off", + "//command_line_option:linkopt": linkopts, + "//command_line_option:platforms": [], + "//command_line_option:custom_malloc": "//emscripten_toolchain:malloc", + } + +_wasm_transition = transition( + implementation = _wasm_transition_impl, + inputs = [ + "//command_line_option:features", + "//command_line_option:linkopt", + ], + outputs = [ + "//command_line_option:compiler", + "//command_line_option:cpu", + "//command_line_option:crosstool_top", + "//command_line_option:features", + "//command_line_option:dynamic_mode", + "//command_line_option:linkopt", + "//command_line_option:platforms", + "//command_line_option:custom_malloc", + ], +) + +def _wasm_binary_impl(ctx): + cc_target = ctx.attr.cc_target[0] + + args = [ + "--output_path={}".format(ctx.outputs.loader.dirname), + ] + [ + ctx.expand_location("--archive=$(location {})".format( + cc_target.label, + ), [cc_target]), + ] + outputs = [ + ctx.outputs.loader, + ctx.outputs.wasm, + ctx.outputs.map, + ctx.outputs.mem, + ctx.outputs.fetch, + ctx.outputs.worker, + ctx.outputs.data, + ctx.outputs.symbols, + ctx.outputs.dwarf, + ] + + ctx.actions.run( + inputs = ctx.files.cc_target, + outputs = outputs, + arguments = args, + executable = ctx.executable._wasm_binary_extractor, + ) + + return DefaultInfo( + files = depset(outputs), + # This is needed since rules like web_test usually have a data + # dependency on this target. + data_runfiles = ctx.runfiles(transitive_files = depset(outputs)), + ) + +def _wasm_binary_outputs(name, cc_target): + basename = cc_target.name + basename = basename.split(".")[0] + outputs = { + "loader": "{}/{}.js".format(name, basename), + "wasm": "{}/{}.wasm".format(name, basename), + "map": "{}/{}.wasm.map".format(name, basename), + "mem": "{}/{}.js.mem".format(name, basename), + "fetch": "{}/{}.fetch.js".format(name, basename), + "worker": "{}/{}.worker.js".format(name, basename), + "data": "{}/{}.data".format(name, basename), + "symbols": "{}/{}.js.symbols".format(name, basename), + "dwarf": "{}/{}.wasm.debug.wasm".format(name, basename), + } + + return outputs + +# Wraps a C++ Blaze target, extracting the appropriate files. +# +# This rule will transition to the emscripten toolchain in order +# to build the the cc_target as a WebAssembly binary. +# +# Args: +# name: The name of the rule. +# cc_target: The cc_binary or cc_library to extract files from. +wasm_cc_binary = rule( + implementation = _wasm_binary_impl, + attrs = { + "backend": attr.string( + default = "_default", + values = ["_default", "emscripten", "llvm"], + ), + "cc_target": attr.label( + cfg = _wasm_transition, + mandatory = True, + ), + "exit_runtime": attr.bool( + default = False, + ), + "threads": attr.string( + default = "_default", + values = ["_default", "emscripten", "off"], + ), + "simd": attr.bool( + default = False, + ), + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), + "_wasm_binary_extractor": attr.label( + executable = True, + allow_files = True, + cfg = "exec", + default = Label("//emscripten_toolchain:wasm_binary"), + ), + }, + outputs = _wasm_binary_outputs, +) diff --git a/bazel/emscripten_toolchain/wasm_rules.bzl b/bazel/emscripten_toolchain/wasm_rules.bzl new file mode 100644 index 0000000..1c1c409 --- /dev/null +++ b/bazel/emscripten_toolchain/wasm_rules.bzl @@ -0,0 +1,6 @@ +"""Rules related to C++ and WebAssembly. +""" + +load("//emscripten_toolchain:wasm_cc_binary.bzl", _wasm_cc_binary = "wasm_cc_binary") + +wasm_cc_binary = _wasm_cc_binary diff --git a/bazel/hello-world/BUILD b/bazel/hello-world/BUILD new file mode 100644 index 0000000..d0ef7cd --- /dev/null +++ b/bazel/hello-world/BUILD @@ -0,0 +1,12 @@ +load("@rules_cc//cc:defs.bzl", "cc_binary") +load("//emscripten_toolchain:wasm_rules.bzl", "wasm_cc_binary") + +cc_binary( + name = "hello-world", + srcs = ["hello-world.cc"], +) + +wasm_cc_binary( + name = "hello-world-wasm", + cc_target = ":hello-world", +) diff --git a/bazel/hello-world/hello-world.cc b/bazel/hello-world/hello-world.cc new file mode 100644 index 0000000..ee72c53 --- /dev/null +++ b/bazel/hello-world/hello-world.cc @@ -0,0 +1,6 @@ +#include + +int main(int argc, char** argv) { + std::cout << "hello world!" << std::endl; + return 0; +} diff --git a/scripts/test_bazel.sh b/scripts/test_bazel.sh new file mode 100755 index 0000000..f2a5e3c --- /dev/null +++ b/scripts/test_bazel.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +echo "test bazel" + +set -x +set -e + +cd bazel +bazel build //hello-world:hello-world-wasm diff --git a/scripts/update_bazel_workspace.sh b/scripts/update_bazel_workspace.sh new file mode 100755 index 0000000..fb58f04 --- /dev/null +++ b/scripts/update_bazel_workspace.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# This script will update emsdk/bazel/WORKSPACE to the latest version of +# emscripten. It reads emsdk/emscripten-releases-tags.txt to get the latest +# version number. Then, it downloads the prebuilts for that version and computes +# the sha256sum for the archive. It then puts all this information into the +# emsdk/bazel/WORKSPACE file. + +ERR=0 +# Attempt to change to the emsdk root directory +cd $(dirname $0)/.. + +# If the previous command succeeded. We are in the emsdk root. Check to make +# sure the files and directories we need are present. +if [[ $? = 0 ]]; then + if [[ ! -f emscripten-releases-tags.txt ]]; then + echo "Cannot find emscripten-releases-tags.txt." + ERR=1 + fi + + if [[ ! -d bazel ]]; then + echo "Cannot find the bazel directory." + ERR=1 + elif [[ ! -f bazel/WORKSPACE ]]; then + echo "Cannot find bazel/WORKSPACE." + ERR=1 + fi +else + ERR=1 +fi + +if [[ $ERR = 1 ]]; then + echo "Unable to cd into the emsdk root directory." + exit 1 +fi + +URL1=https://storage.googleapis.com/webassembly/emscripten-releases-builds/linux/ +URL2=/wasm-binaries.tbz2 + +# Get the latest version number from emscripten-releases-tag.txt. +VER=$(grep -oP '(?<=latest\": \")([\d\.]+)(?=\")' \ + emscripten-releases-tags.txt \ + | sed --expression "s/\./\\\./g") +# Based on the latest version number, get the commit hash for that version. +HASH=$(grep "${VER}" emscripten-releases-tags.txt \ + | grep -v latest \ + | cut -f4 -d\") +# Download and compute the sha256sum for the archive with the prebuilts. +SHA=$(curl "${URL1}${HASH}${URL2}" 2>/dev/null \ + | sha256sum \ + | awk '{print $1}') +# Get the line number on which the sha256 sum lives for emscripten. +# This will always be one line after the name of the rule. +SHALINE=$(($(grep -n 'name = "emscripten"' bazel/WORKSPACE \ + | sed 's/^\([[:digit:]]*\).*$/\1/')+1)) + +# Insert the new commit hash into the url. +sed -i "s!\(${URL1}\)\([[:alnum:]]*\)\(${URL2}\)!\1${HASH}\3!" bazel/WORKSPACE +# Insert the new version number. +sed -i "s!\(# emscripten \)\(.*\)!\1${VER}!" bazel/WORKSPACE +# Insert the new sha256 sum. +sed -i "${SHALINE}s!\"[[:alnum:]]*\"!\"${SHA}\"!" bazel/WORKSPACE + +echo "Done!"