Add initial generic bind gn template & bound asan wrapper script.

Bug: 816629,861983
Change-Id: I2f6ce50bab8410679e8e5e7255349e087e62373d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1320207
Reviewed-by: Andrew Grieve <agrieve@chromium.org>
Reviewed-by: Dirk Pranke <dpranke@chromium.org>
Commit-Queue: John Budorick <jbudorick@chromium.org>
Cr-Commit-Position: refs/heads/master@{#641654}
diff --git a/build/util/generate_wrapper.gni b/build/util/generate_wrapper.gni
new file mode 100644
index 0000000..51658e2
--- /dev/null
+++ b/build/util/generate_wrapper.gni
@@ -0,0 +1,97 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Wraps a target and any of its arguments to an executable script.
+#
+# Many executable targets have build-time-constant arguments. This
+# template allows those to be wrapped into a single, user- or bot-friendly
+# script at build time.
+#
+# Paths to be wrapped should be relative to root_build_dir and should be
+# wrapped in "@WrappedPath(...)"; see Example below.
+#
+# Variables:
+#   generator_script: Path to the script to use to perform the wrapping.
+#     Defaults to //build/util/generate_wrapper.py. Generally should only
+#     be set by other templates.
+#   wrapper_script: Output path.
+#   executable: Path to the executable to wrap. Can be a script or a
+#     build product. Paths can be relative to the containing gn file
+#     or source-absolute.
+#   executable_args: List of arguments to write into the wrapper.
+#
+# Example wrapping a checked-in script:
+#   generate_wrapper("sample_wrapper") {
+#     executable = "//for/bar/sample.py"
+#     wrapper_script = "$root_build_dir/bin/run_sample"
+#
+#     _sample_argument_path = "//sample/$target_cpu/lib/sample_lib.so"
+#     _rebased_sample_argument_path = rebase_path(
+#         _sample_argument_path,
+#         root_build_dir)
+#     executable_args = [
+#       "--sample-lib", "@WrappedPath(${_rebased_sample_argument_path})",
+#     ]
+#   }
+#
+# Example wrapping a build product:
+#   generate_wrapper("sample_wrapper") {
+#     executable = "$root_build_dir/sample_build_product"
+#     wrapper_script = "$root_build_dir/bin/run_sample_build_product"
+#   }
+template("generate_wrapper") {
+  _generator_script = "//build/util/generate_wrapper.py"
+  if (defined(invoker.generator_script)) {
+    _generator_script = invoker.generator_script
+  }
+  _executable_to_wrap = invoker.executable
+  _wrapper_script = invoker.wrapper_script
+  if (is_win) {
+    _wrapper_script += ".bat"
+  }
+  if (defined(invoker.executable_args)) {
+    _wrapped_arguments = invoker.executable_args
+  } else {
+    _wrapped_arguments = []
+  }
+
+  action(target_name) {
+    forward_variables_from(invoker,
+                           [
+                             "data",
+                             "data_deps",
+                             "deps",
+                             "testonly",
+                           ])
+    script = _generator_script
+    if (!defined(data)) {
+      data = []
+    }
+    data += [ _wrapper_script ]
+    outputs = [
+      _wrapper_script,
+    ]
+
+    _rebased_executable_to_wrap =
+        rebase_path(_executable_to_wrap, root_build_dir)
+    _rebased_wrapper_script = rebase_path(_wrapper_script, root_build_dir)
+    if (is_win) {
+      _script_language = "batch"
+    } else {
+      _script_language = "bash"
+    }
+    args = [
+      "--executable",
+      "@WrappedPath(${_rebased_executable_to_wrap})",
+      "--wrapper-script",
+      _rebased_wrapper_script,
+      "--output-directory",
+      rebase_path(root_build_dir, root_build_dir),
+      "--script-language",
+      _script_language,
+      "--",
+    ]
+    args += _wrapped_arguments
+  }
+}
diff --git a/build/util/generate_wrapper.py b/build/util/generate_wrapper.py
new file mode 100755
index 0000000..b89f9fe
--- /dev/null
+++ b/build/util/generate_wrapper.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env vpython
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Wraps an executable and any provided arguments into an executable script."""
+
+import argparse
+import os
+import sys
+import textwrap
+
+
+BASH_TEMPLATE = textwrap.dedent(
+    """\
+    #!/usr/bin/env bash
+    python - <<END $@
+    _SCRIPT_LOCATION = "${{BASH_SOURCE[0]}}"
+    {script}
+    END
+    """)
+
+
+BATCH_TEMPLATE = textwrap.dedent(
+    """\
+    @SETLOCAL ENABLEDELAYEDEXPANSION \
+        & python -x "%~f0" %* \
+        & EXIT /B !ERRORLEVEL!
+    _SCRIPT_LOCATION = __file__
+    {script}
+    """)
+
+
+SCRIPT_TEMPLATES = {
+    'bash': BASH_TEMPLATE,
+    'batch': BATCH_TEMPLATE,
+}
+
+
+PY_TEMPLATE = textwrap.dedent(
+    """\
+    import os
+    import re
+    import subprocess
+    import sys
+
+    _WRAPPED_PATH_RE = re.compile(r'@WrappedPath\(([^)]+)\)')
+    _PATH_TO_OUTPUT_DIR = '{path_to_output_dir}'
+    _SCRIPT_DIR = os.path.dirname(os.path.realpath(_SCRIPT_LOCATION))
+
+
+    def ExpandWrappedPath(arg):
+      m = _WRAPPED_PATH_RE.match(arg)
+      if m:
+        return os.path.join(
+            os.path.relpath(_SCRIPT_DIR), _PATH_TO_OUTPUT_DIR, m.group(1))
+      return arg
+
+
+    def ExpandWrappedPaths(args):
+      for i, arg in enumerate(args):
+        args[i] = ExpandWrappedPath(arg)
+      return args
+
+
+    def main(raw_args):
+      executable_path = ExpandWrappedPath('{executable_path}')
+      executable_args = ExpandWrappedPaths({executable_args})
+
+      return subprocess.call([executable_path] + executable_args + raw_args)
+
+
+    if __name__ == '__main__':
+      sys.exit(main(sys.argv[1:]))
+    """)
+
+
+def Wrap(args):
+  """Writes a wrapped script according to the provided arguments.
+
+  Arguments:
+    args: an argparse.Namespace object containing command-line arguments
+      as parsed by a parser returned by CreateArgumentParser.
+  """
+  path_to_output_dir = os.path.relpath(
+      args.output_directory,
+      os.path.dirname(args.wrapper_script))
+
+  with open(args.wrapper_script, 'w') as wrapper_script:
+    py_contents = PY_TEMPLATE.format(
+        path_to_output_dir=path_to_output_dir,
+        executable_path=str(args.executable),
+        executable_args=str(args.executable_args))
+    template = SCRIPT_TEMPLATES[args.script_language]
+    wrapper_script.write(template.format(
+        script=py_contents))
+  os.chmod(args.wrapper_script, 0750)
+
+  return 0
+
+
+def CreateArgumentParser():
+  """Creates an argparse.ArgumentParser instance."""
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--executable',
+      help='Executable to wrap.')
+  parser.add_argument(
+      '--wrapper-script',
+      help='Path to which the wrapper script will be written.')
+  parser.add_argument(
+      '--output-directory',
+      help='Path to the output directory.')
+  parser.add_argument(
+      '--script-language',
+      choices=SCRIPT_TEMPLATES.keys(),
+      help='Language in which the warpper script will be written.')
+  parser.add_argument(
+      'executable_args', nargs='*',
+      help='Arguments to wrap into the executable.')
+  return parser
+
+
+def main(raw_args):
+  parser = CreateArgumentParser()
+  args = parser.parse_args(raw_args)
+  return Wrap(args)
+
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv[1:]))
diff --git a/tools/android/BUILD.gn b/tools/android/BUILD.gn
index 2327972..ab51dc0 100644
--- a/tools/android/BUILD.gn
+++ b/tools/android/BUILD.gn
@@ -2,6 +2,8 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//build/config/sanitizers/sanitizers.gni")
+
 # Intermediate target grouping the android tools needed to run native
 # unittests and instrumentation test apks.
 group("android_tools") {
@@ -19,6 +21,9 @@
     "//tools/perf:run_benchmark_wrapper",
     "//tools/perf/clear_system_cache",
   ]
+  if (is_asan) {
+    deps += [ "//tools/android/asan/third_party:asan_device_setup" ]
+  }
 }
 
 group("memdump") {
diff --git a/tools/android/asan/third_party/BUILD.gn b/tools/android/asan/third_party/BUILD.gn
new file mode 100644
index 0000000..ecb998b
--- /dev/null
+++ b/tools/android/asan/third_party/BUILD.gn
@@ -0,0 +1,41 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/config.gni")
+import("//build/config/clang/clang.gni")
+import("//build/util/generate_wrapper.gni")
+
+generate_wrapper("asan_device_setup") {
+  executable = "with_asan.py"
+  wrapper_script = "$root_out_dir/bin/run_with_asan"
+
+  if (target_cpu == "arm") {
+    _lib_arch = "arm"
+  } else if (target_cpu == "arm64") {
+    _lib_arch = "aarch64"
+  } else if (target_cpu == "x86") {
+    _lib_arch = "i686"
+  } else {
+    assert(false, "No ASAN library available for $target_cpu")
+  }
+
+  _adb_path = "${android_sdk_root}/platform-tools/adb"
+  _lib_path = "${clang_base_path}/lib/clang/${clang_version}/lib/linux/libclang_rt.asan-${_lib_arch}-android.so"
+  data = [
+    "asan_device_setup.sh",
+    "with_asan.py",
+    _adb_path,
+    _lib_path,
+  ]
+
+  _rebased_lib_path = rebase_path(_lib_path, root_build_dir)
+  _rebased_adb_path = rebase_path(_adb_path, root_build_dir)
+
+  executable_args = [
+    "--adb",
+    "@WrappedPath(${_rebased_adb_path})",
+    "--lib",
+    "@WrappedPath(${_rebased_lib_path})",
+  ]
+}
diff --git a/tools/android/asan/third_party/with_asan.py b/tools/android/asan/third_party/with_asan.py
new file mode 100755
index 0000000..0c93a98
--- /dev/null
+++ b/tools/android/asan/third_party/with_asan.py
@@ -0,0 +1,62 @@
+#!/usr/bin/env vpython
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+
+import argparse
+import contextlib
+import os
+import subprocess
+import sys
+
+
+_SCRIPT_PATH = os.path.abspath(
+    os.path.join(
+        os.path.dirname(__file__),
+        'asan_device_setup.sh'))
+
+
+@contextlib.contextmanager
+def Asan(args):
+  env = os.environ.copy()
+  env['ADB'] = args.adb
+
+  try:
+    setup_cmd = [_SCRIPT_PATH, '--lib', args.lib]
+    if args.device:
+      setup_cmd += ['--device', args.device]
+    subprocess.check_call(setup_cmd, env=env)
+    yield
+  finally:
+    teardown_cmd = [_SCRIPT_PATH, '--revert']
+    if args.device:
+      teardown_cmd += ['--device', args.device]
+    subprocess.check_call(teardown_cmd, env=env)
+
+
+def main(raw_args):
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--adb', type=os.path.realpath, required=True,
+      help='Path to adb binary.')
+  parser.add_argument(
+      '--device',
+      help='Device serial.')
+  parser.add_argument(
+      '--lib', type=os.path.realpath, required=True,
+      help='Path to asan library.')
+  parser.add_argument(
+      'command', nargs='*',
+      help='Command to run with ASAN installed.')
+  args = parser.parse_args()
+
+  with Asan(args):
+    if args.command:
+      return subprocess.call(args.command)
+
+  return 0
+
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv[1:]))