diff --git a/.github/workflows/build_linux_ubuntu1804.yml b/.github/workflows/build_linux_ubuntu1804.yml index 3578b19a1..9c4d98a32 100644 --- a/.github/workflows/build_linux_ubuntu1804.yml +++ b/.github/workflows/build_linux_ubuntu1804.yml @@ -44,7 +44,7 @@ jobs: echo $PATH go env -w GOFLAGS="-buildvcs=false" rustup default nightly - tgn gen linux x64 ${{ matrix.build_type }} -- is_clang=false log_level=1 enable_serialized_actions=true ten_enable_integration_tests=false ten_enable_private_tests=false ten_rust_enable_tests=false ten_enable_package_manager=false + tgn gen linux x64 ${{ matrix.build_type }} -- is_clang=false log_level=1 enable_serialized_actions=true ten_enable_integration_tests=false ten_enable_private_tests=false ten_rust_enable_tests=false ten_enable_package_manager=false ten_rust_enable_gen_cargo_config=false tgn build linux x64 ${{ matrix.build_type }} tree -I 'gen|obj' out diff --git a/.github/workflows/build_linux_ubuntu2204.yml b/.github/workflows/build_linux_ubuntu2204.yml index 26d5c942b..1f290e805 100644 --- a/.github/workflows/build_linux_ubuntu2204.yml +++ b/.github/workflows/build_linux_ubuntu2204.yml @@ -44,7 +44,7 @@ jobs: go env -w GOFLAGS="-buildvcs=false" go1.20.12 download rustup default nightly - tgn gen linux x64 ${{ matrix.build_type }} -- is_clang=${{ matrix.compiler == 'gcc' && 'false' || 'true' }} log_level=1 enable_serialized_actions=true ten_enable_integration_tests=false ten_enable_private_tests=false ten_rust_enable_tests=false ten_package_manager_enable_tests=false + tgn gen linux x64 ${{ matrix.build_type }} -- is_clang=${{ matrix.compiler == 'gcc' && 'false' || 'true' }} log_level=1 enable_serialized_actions=true ten_enable_integration_tests=false ten_enable_private_tests=false ten_rust_enable_tests=false ten_package_manager_enable_tests=false ten_rust_enable_gen_cargo_config=false tgn build linux x64 ${{ matrix.build_type }} tree -I 'gen|obj' out diff --git a/.github/workflows/build_mac.yml b/.github/workflows/build_mac.yml index 6130bd7de..7b847d8d4 100644 --- a/.github/workflows/build_mac.yml +++ b/.github/workflows/build_mac.yml @@ -76,7 +76,7 @@ jobs: fi export PATH=$(pwd)/core/ten_gn:$PATH echo $PATH - tgn gen mac arm64 ${{ matrix.build_type }} -- log_level=1 enable_serialized_actions=true ten_enable_integration_tests=false ten_enable_private_tests=false ten_rust_enable_tests=false + tgn gen mac arm64 ${{ matrix.build_type }} -- log_level=1 enable_serialized_actions=true ten_enable_integration_tests=false ten_enable_private_tests=false ten_rust_enable_tests=false ten_rust_enable_gen_cargo_config=false tgn build mac arm64 ${{ matrix.build_type }} tree -I 'gen|obj' out @@ -173,7 +173,7 @@ jobs: fi export PATH=$(pwd)/core/ten_gn:$PATH echo $PATH - tgn gen mac x64 ${{ matrix.build_type }} -- log_level=1 enable_serialized_actions=true ten_enable_integration_tests=false ten_enable_private_tests=false ten_rust_enable_tests=false ten_package_manager_enable_tests=false + tgn gen mac x64 ${{ matrix.build_type }} -- log_level=1 enable_serialized_actions=true ten_enable_integration_tests=false ten_enable_private_tests=false ten_rust_enable_tests=false ten_package_manager_enable_tests=false ten_rust_enable_gen_cargo_config=false tgn build mac x64 ${{ matrix.build_type }} tree -I 'gen|obj' out diff --git a/.github/workflows/build_win.yml b/.github/workflows/build_win.yml index e586d4c5c..7ccec6f71 100644 --- a/.github/workflows/build_win.yml +++ b/.github/workflows/build_win.yml @@ -49,7 +49,7 @@ jobs: - name: Build run: | $ENV:PATH += ";$PWD/core/ten_gn" - tgn gen win x64 ${{ matrix.build_type }} -- vs_version=2022 log_level=1 enable_serialized_actions=true ten_enable_integration_tests=false ten_enable_private_tests=false ten_rust_enable_tests=false ten_package_manager_enable_tests=false + tgn gen win x64 ${{ matrix.build_type }} -- vs_version=2022 log_level=1 enable_serialized_actions=true ten_enable_integration_tests=false ten_enable_private_tests=false ten_rust_enable_tests=false ten_package_manager_enable_tests=false ten_rust_enable_gen_cargo_config=false tgn build win x64 ${{ matrix.build_type }} - name: Run Tests (ten_utils_unit_test) diff --git a/.gitignore b/.gitignore index 239f9db5e..3d1b49daf 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ package-lock.json core/src/ten_manager/target/ core/src/ten_rust/src/schema/bindings.rs core/src/ten_rust/target/ +/.cargo # private modules packages/private_apps diff --git a/.vscode/settings.json b/.vscode/settings.json index fffd5a492..fa28d7473 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,8 +7,13 @@ "editor.wordWrapColumn": 80 }, "[python]": { - "editor.tabSize": 4 + "editor.tabSize": 4, + "editor.defaultFormatter": "ms-python.black-formatter" }, + "black-formatter.args": [ + "--line-length", + "80" + ], "debug.allowBreakpointsEverywhere": true, "doxdocgen.c.commentPrefix": "/// ", "doxdocgen.c.firstLine": "///", diff --git a/build/common/rust/rust.gni b/build/common/rust/rust.gni index 9decfdd36..291b23139 100644 --- a/build/common/rust/rust.gni +++ b/build/common/rust/rust.gni @@ -43,12 +43,31 @@ template("rust_target") { _rustflags = invoker._rustflags } - if (is_debug) { - if (ten_rust_enable_asan) { - if (enable_sanitizer) { - _rustflags = "${_rustflags} -Zsanitizer=address" - } + if (!ten_rust_enable_gen_cargo_config && enable_sanitizer && + ten_rust_enable_asan) { + asan_args = [ + "--action", + "print", + "--compiler", + ] + if (is_clang) { + asan_args += [ "clang" ] + } else { + asan_args += [ "gcc" ] } + + asan_args += [ + "--target-os", + target_os, + "--target-arch", + target_cpu, + ] + + flags = exec_script("//build/common/rust/rust_gen_cargo_config.py", + asan_args, + "trim string") + + _rustflags = "${_rustflags} ${flags}" } if (_rustflags != "") { @@ -209,12 +228,31 @@ template("rust_test") { _rustflags = invoker._rustflags } - if (is_debug) { - if (ten_rust_enable_asan) { - if (enable_sanitizer) { - _rustflags = "${_rustflags} -Zsanitizer=address" - } + if (!ten_rust_enable_gen_cargo_config && enable_sanitizer && + ten_rust_enable_asan) { + asan_args = [ + "--action", + "print", + "--compiler", + ] + if (is_clang) { + asan_args += [ "clang" ] + } else { + asan_args += [ "gcc" ] } + + asan_args += [ + "--target-os", + target_os, + "--target-arch", + target_cpu, + ] + + flags = exec_script("//build/common/rust/rust_gen_cargo_config.py", + asan_args, + "trim string") + + _rustflags = "${_rustflags} ${flags}" } if (_rustflags != "") { @@ -353,3 +391,91 @@ template("rust_cbindgen") { outputs = [ _output ] } } + +template("rust_gen_cargo_config") { + assert(defined(invoker.project_root), "project_root is not defined") + + _project_root = rebase_path(invoker.project_root) + _target_name = target_name + _target_path = target_gen_dir + + action("${_target_name}") { + script = "//build/common/rust/rust_gen_cargo_config.py" + _output = "${_target_path}/${_target_name}_gen_cargo_config" + + args = [ + "--project-root", + _project_root, + "--tg-timestamp-proxy-file", + rebase_path(_output), + ] + + args += [ "--compiler" ] + if (is_clang) { + args += [ "clang" ] + } else { + args += [ "gcc" ] + } + + if (is_win) { + if (target_cpu == "x86") { + target = "i686-pc-windows-msvc" + } else if (target_cpu == "x64") { + target = "x86_64-pc-windows-msvc" + } else if (target_cpu == "arm64") { + target = "aarch64-pc-windows-msvc" + } + } else if (is_linux) { + if (target_cpu == "arm64") { + target = "aarch64-unknown-linux-gnu" + } else if (target_cpu == "arm") { + target = "armv7-unknown-linux-gnueabi" + } else if (target_cpu == "x86") { + target = "i686-unknown-linux-gnu" + } else if (target_cpu == "x64") { + target = "x86_64-unknown-linux-gnu" + } + } else if (is_mac) { + if (target_cpu == "arm64") { + target = "aarch64-apple-darwin" + } else if (target_cpu == "x86") { + target = "i686-apple-darwin" + } else if (target_cpu == "x64") { + target = "x86_64-apple-darwin" + } + } + + args += [ + "--target", + target, + "--target-os", + target_os, + "--target-arch", + target_cpu, + ] + + if (enable_sanitizer && ten_rust_enable_gen_cargo_config) { + args += [ + "--action", + "gen", + ] + } else { + # Delete `.cargo/config.toml` file to remove all configurations related to + # Rust + ASan. + args += [ + "--action", + "delete", + ] + } + + forward_variables_from(invoker, + [ + "deps", + "public_deps", + "data_deps", + "public_configs", + ]) + + outputs = [ _output ] + } +} diff --git a/build/common/rust/rust_gen_cargo_config.py b/build/common/rust/rust_gen_cargo_config.py new file mode 100644 index 000000000..11d8fa7fb --- /dev/null +++ b/build/common/rust/rust_gen_cargo_config.py @@ -0,0 +1,211 @@ +# +# Copyright © 2024 Agora +# This file is part of TEN Framework, an open source project. +# Licensed under the Apache License, Version 2.0, with certain conditions. +# Refer to the "LICENSE" file in the root directory for more information. +# +import argparse +import json +import shutil +import sys +import os +from build.scripts import timestamp_proxy + +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) +from scripts import package_asan_lib + + +# The content of the auto generated .cargo/config.toml file is as follows. +# +# - For gcc compiler: +# +# ```toml +# [target.x86_64-unknown-linux-gnu] +# rustflags = ["-C", "linker=gcc", "-Z", "external-clangrt", "-Z", "sanitizer=address", "-l", "asan"] +# +# [build] +# target = "x86_64-unknown-linux-gnu" +# ``` +# +# - For clang compiler: +# +# ```toml +# [target.x86_64-unknown-linux-gnu] +# rustflags = ["-C", "linker=clang", "-Z", "external-clangrt", "-Z", "sanitizer=address", "-C", "link-args=-fsanitize=address"] +# +# [build] +# target = "x86_64-unknown-linux-gnu" +# ``` + +GCC_ASAN_FLAGS = [ + "-C", + "linker=gcc", + "-Z", + "external-clangrt", + "-Z", + "sanitizer=address", + "-l", + "asan", +] + + +# It is better to use lld as the linker in clang. In Rust, you can specify it +# using ["-C", "link-arg=-fuse-ld=lld"]. However, lld needs to be in the PATH. +# On some CI machines, lld is not in the PATH, so we won't use lld here. +CLANG_ASAN_FLAGS = [ + "-C", + "linker=clang", + "-Z", + "external-clangrt", + "-Z", + "sanitizer=address", + "-C", + "link-args=-fsanitize=address", +] + +ASAN_CONFIG = """[target.{build_target}] +rustflags = {asan_flags} + +[build] +target = "{build_target}" +""" + + +class ArgumentInfo(argparse.Namespace): + def __init__(self): + super().__init__() + self.project_root: str + self.build_type: str + self.compiler: str + self.target: str + self.target_os: str + self.target_arch: str + self.tg_timestamp_proxy_file: str | None = None + self.enable_asan: bool + self.action: str + + +# On macOS, only the Clang compiler is available, and Clang's ASan runtime +# is provided only as a dynamic library. However, the linker flag +# '-shared-libasan' is not supported in Cargo with Clang on macOS. Cargo will +# issue a warning and ignore the flag, e.g., 'clang: warning: argument unused +# during compilation: '-shared-libasan''. This could be a bug in Cargo. To +# resolve this, use the following flag instead. +def special_link_args_on_mac(arch: str) -> str: + asan_lib = package_asan_lib.detect_mac_asan_lib(arch) + return f"link-arg=-Wl,{asan_lib}" + + +def gen_cargo_config(args: ArgumentInfo): + if not os.path.exists(args.project_root): + raise Exception(f"Project root {args.project_root} does not exist.") + + # Create .cargo/ folder if not exist. + cargo_dir = os.path.join(args.project_root, ".cargo") + if not os.path.exists(cargo_dir): + os.mkdir(cargo_dir) + + # Check if .cargo/config.toml exists, and remove it if it does. + cargo_config = os.path.join(cargo_dir, "config.toml") + if os.path.exists(cargo_config): + os.remove(cargo_config) + + flags = [] + if args.compiler == "gcc": + flags = GCC_ASAN_FLAGS + else: + flags = CLANG_ASAN_FLAGS + + if args.target_os == "mac": + flags.extend(["-C", special_link_args_on_mac(args.target_arch)]) + + config_content = ASAN_CONFIG.format( + build_target=args.target, asan_flags=json.dumps(flags) + ) + + with open(cargo_config, "w") as f: + f.write(config_content) + + +def delete_cargo_config(root: str): + cargo_dir = os.path.join(root, ".cargo") + if os.path.exists(cargo_dir): + shutil.rmtree(cargo_dir) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument( + "--action", type=str, required=True, help="gen|delete|print" + ) + parser.add_argument("--project-root", type=str, required=False) + parser.add_argument("--compiler", type=str, required=True) + parser.add_argument("--target", type=str, required=False) + parser.add_argument("--target-os", type=str, required=True) + parser.add_argument("--target-arch", type=str, required=True) + parser.add_argument( + "--tg-timestamp-proxy-file", type=str, default="", required=False + ) + parser.add_argument( + "--enable-asan", action=argparse.BooleanOptionalAction, default=True + ) + + arg_info = ArgumentInfo() + args = parser.parse_args(namespace=arg_info) + + returncode = 0 + if args.action == "gen": + try: + gen_cargo_config(args) + + # Success to gen cargo config, update the stamp file to represent + # this fact. + timestamp_proxy.touch_timestamp_proxy_file( + args.tg_timestamp_proxy_file + ) + except Exception as exc: + returncode = 1 + timestamp_proxy.remove_timestamp_proxy_file( + args.tg_timestamp_proxy_file + ) + print(exc) + + finally: + sys.exit(-1 if returncode != 0 else 0) + + elif args.action == "delete": + try: + delete_cargo_config(args.project_root) + + # Success to delete cargo config, update the stamp file to represent + # this fact. + timestamp_proxy.touch_timestamp_proxy_file( + args.tg_timestamp_proxy_file + ) + except Exception as exc: + returncode = 1 + timestamp_proxy.remove_timestamp_proxy_file( + args.tg_timestamp_proxy_file + ) + print(exc) + + finally: + sys.exit(-1 if returncode != 0 else 0) + + else: + # action = print + # + # Constructs and prints a space-separated string of the necessary ASan + # flags for Clang, taking into account any macOS-specific handling. + flags = [ + CLANG_ASAN_FLAGS[i] + CLANG_ASAN_FLAGS[i + 1] + for i in range(0, len(CLANG_ASAN_FLAGS) - 1, 2) + ] + + if args.target_os == "mac": + asan_flag = special_link_args_on_mac(args.target_arch) + flags.append(f"-C{asan_flag}") + + print(" ".join(flags)) + sys.exit(0) diff --git a/build/ten_rust/options.gni b/build/ten_rust/options.gni index b84655953..b2ed89135 100644 --- a/build/ten_rust/options.gni +++ b/build/ten_rust/options.gni @@ -11,6 +11,86 @@ declare_args() { # rustc finds the asan runtime library in ~/.rustup/toolchains// # lib/rustlib//lib/librustc-nightly_rt.asan.a ten_rust_enable_asan = is_mac || (is_linux && target_cpu == "x64") + + # The rust projects (i.e., ten_rust and ten_manager) depend on the C static + # library (ex: ten_utils), thus the rust projects must be compiled with asan + # enabled if the C library is compiled with asan. There are two ways to + # enable asan in rust: + # + # 1. Using the RUSTFLAGS environment variable. Ex: + # RUSTFLAGS="-Zsanitizer=address" cargo build. + # 2. Using the cargo config file, i.e., create a `config.toml` file in the + # `.cargo` folder in the source tree of the rust project. Ex, we can + # create a `config.toml` file in `ten_framework/.cargo` folder, and add + # the following content: + # + # [target.x86_64-unknown-linux-gnu] + # rustflags = ["-Z", "sanitizer=address"] + # + # [build] + # target = "x86_64-unknown-linux-gnu" + # + # The second way has a limitation: the cargo config file only effects on the + # builds in the subfolder of the location of the config file. Ex: we add the + # cargo config in `ten_framework` as above. If we run `cargo build` in the + # source folder of ten_manager or ten_rust, the cargo config effects. As + # cargo toolchain supports building rust projects outside of the source + # crate, using the `--manifest-path` option. In other words, we can build + # ten_rust in any folder. The cargo config in the `ten_framework` folder will + # not effect if we build ten_rust in some folder outside of ten_framework + # such as a 'out' folder which is a sibling node of ten_framework. In summary, + # the cargo config effect as follows. + # + # - + # - ten_framework <= works under this folder + # - .cargo + # - out <= not works + # - <= not works + # + # Therefore, the first way is more flexible. That is the default way we use + # in TGN toolchain. + # + # However, the first way can not work well in some cases, ex: develop rust + # in an IDE such as VSCode. In VSCode, when we open a rust file including a + # `main` function or a `test` function, there will be some trigger to run the + # functions, ex: a `Run|Debug` button on the main function, or a `Run Test| + # Debug` button on the test function. The trigger is a task in VSCode, we can + # not customize the task to add the RUSTFLAGS environment variable + # automatically. In addition, if we want to manually run cargo commands in the + # terminal to compile the source code or run some tests during development, + # it is not convenient to add the RUSTFLAGS environment variable every time. + # And the content of the RUSTFLAGS environment variable is different between + # different compilers and platforms. + # + # So we add a new option here, to control whether to auto generate the cargo + # config file under ten_framework if asan is enabled by `enable_sanitizer`. + # The combination of the `enable_sanitizer` and `ten_rust_enable_gen_cargo_config` + # is as follows. + # + # - enable_sanitizer = false + # - ten_rust_enable_gen_cargo_config = true or false + # + # => The asan is disabled, and the cargo config file is not generated. + # + # - enable_sanitizer = true + # - ten_rust_enable_gen_cargo_config = false + # + # => The asan is enabled, which means the C library is compiled with asan. + # => The cargo config is not generated. The rust projects will be compiled + # with asan by setting the RUSTFLAGS environment variable if using tgn. + # => Running the trigger button in the VSCode or manually running `cargo + # build` in the terminal will be FAILED. + # + # - ten_rust_enable_gen_cargo_config = true + # + # => The asan is enabled. + # => The cargo config file is generated in ten_framework. + # => Running the trigger button in the VSCode or manully running `cargo + # build` in the source folder of the rust projects will be success. + # + # Setting this option to true by default to make the development more + # convenient, then developers do not need to care about this option. + ten_rust_enable_gen_cargo_config = true } declare_args() { diff --git a/core/src/ten_rust/BUILD.gn b/core/src/ten_rust/BUILD.gn index bb7b6a648..0f5ca1d1d 100644 --- a/core/src/ten_rust/BUILD.gn +++ b/core/src/ten_rust/BUILD.gn @@ -80,6 +80,10 @@ group("ten_rust_build_deps") { deps = [ "//core/src/ten_utils:ten_utils_combined_static" ] } +rust_gen_cargo_config("asan_config") { + project_root = "//" +} + group("ten_rust") { deps = [] public_deps = [] diff --git a/core/src/ten_utils/BUILD.gn b/core/src/ten_utils/BUILD.gn index bc988d341..8f2f9c2af 100644 --- a/core/src/ten_utils/BUILD.gn +++ b/core/src/ten_utils/BUILD.gn @@ -127,5 +127,8 @@ combine_static_library("ten_utils_combined_static") { libuv_static[0], ] - deps = [ ":ten_utils_static" ] + deps = [ + ":ten_utils_static", + "//core/src/ten_rust:asan_config", + ] }