Add C++ runtime libs to cgo action inputs (#4594)

With @llvm toolchain, libc++ is provided via cc_toolchain's
`static_runtime_library`/`dynamic_runtime_library`. These groups should
be added to link actions otherwise cgo code that attempts to link libc++
will not work properly. It seems there were previous attempts to drop
libc++ - I'm not sure the best way to harmonize these, some tweaking of
this PR may be necessary. @jayconrod @fmeum wdyt?
diff --git a/go/private/actions/archive.bzl b/go/private/actions/archive.bzl
index 47fed32..3a5a76b 100644
--- a/go/private/actions/archive.bzl
+++ b/go/private/actions/archive.bzl
@@ -140,7 +140,7 @@
             cxxopts = cgo.cxxopts,
             objcopts = cgo.objcopts,
             objcxxopts = cgo.objcxxopts,
-            clinkopts = cgo.clinkopts,
+            ldflags = cgo.ldflags,
             testfilter = testfilter,
             is_external_pkg = is_external_pkg,
         )
diff --git a/go/private/actions/compilepkg.bzl b/go/private/actions/compilepkg.bzl
index b0769cc..3b8d775 100644
--- a/go/private/actions/compilepkg.bzl
+++ b/go/private/actions/compilepkg.bzl
@@ -66,7 +66,7 @@
         cxxopts = [],
         objcopts = [],
         objcxxopts = [],
-        clinkopts = [],
+        ldflags = None,
         out_lib = None,
         out_export = None,
         out_facts = None,
@@ -191,19 +191,21 @@
             compile_args.add("-objcflags", quote_opts(objcopts))
         if objcxxopts:
             compile_args.add("-objcxxflags", quote_opts(objcxxopts))
-        if clinkopts:
-            compile_args.add("-ldflags", quote_opts(clinkopts))
 
     if go.mode.pgoprofile:
         compile_args.add("-pgoprofile", go.mode.pgoprofile)
         inputs_direct.append(go.mode.pgoprofile)
 
+    arguments = ["compilepkg", shared_args, compile_args]
+    if ldflags:
+        arguments.append(ldflags)
+
     go.actions.run(
         inputs = depset(inputs_direct, transitive = inputs_transitive),
         outputs = outputs,
         mnemonic = "GoCompilePkgExternal" if is_external_pkg else "GoCompilePkg",
         executable = go.toolchain._builder,
-        arguments = ["compilepkg", shared_args, compile_args],
+        arguments = arguments,
         env = env,
         toolchain = GO_TOOLCHAIN_LABEL,
         execution_requirements = execution_requirements,
diff --git a/go/private/context.bzl b/go/private/context.bzl
index d669983..d23f0c6 100644
--- a/go/private/context.bzl
+++ b/go/private/context.bzl
@@ -57,8 +57,11 @@
 )
 load(
     ":mode.bzl",
+    "LINKMODE_C_SHARED",
     "LINKMODE_NORMAL",
     "LINKMODE_PIE",
+    "LINKMODE_PLUGIN",
+    "LINKMODE_SHARED",
     "installsuffix",
     "validate_mode",
 )
@@ -521,6 +524,11 @@
     export_stdlib = False,
 )
 
+def _cc_runtime_libs_for_mode(mode, cgo_tools):
+    if mode.linkmode in (LINKMODE_SHARED, LINKMODE_PLUGIN, LINKMODE_C_SHARED):
+        return cgo_tools.cc_toolchain.dynamic_runtime_lib(feature_configuration = cgo_tools.feature_configuration)
+    return cgo_tools.cc_toolchain.static_runtime_lib(feature_configuration = cgo_tools.feature_configuration)
+
 def _defaults_to_pie(goos, race):
     # based on DefaultPIE in src/internal/platform/supported.go
     if goos in ["android", "darwin", "ios"]:
@@ -650,8 +658,11 @@
 
     if cgo_context_info:
         env.update(cgo_context_info.env)
-        cc_toolchain_files = cgo_context_info.cc_toolchain_files
         cgo_tools = cgo_context_info.cgo_tools
+        cc_toolchain_files = depset(transitive = [
+            cgo_context_info.cc_toolchain_files,
+            _cc_runtime_libs_for_mode(mode, cgo_tools),
+        ])
     else:
         cc_toolchain_files = depset()
         cgo_tools = None
diff --git a/go/private/mode.bzl b/go/private/mode.bzl
index dbe36ac..4a74dc3 100644
--- a/go/private/mode.bzl
+++ b/go/private/mode.bzl
@@ -168,6 +168,16 @@
         # in each package. We use the executable options for this.
         return go.cgo_tools.ld_executable_options
 
+def runtime_libs_from_cc_toolchain(go):
+    if not go.cgo_tools:
+        return depset()
+    elif go.mode.linkmode in (LINKMODE_SHARED, LINKMODE_PLUGIN, LINKMODE_C_SHARED):
+        return go.cgo_tools.cc_toolchain.dynamic_runtime_lib(feature_configuration = go.cgo_tools.feature_configuration)
+    else:
+        # Match extldflags_from_cc_toolchain: c-archive still uses executable
+        # link semantics for cgo's per-package link probe.
+        return go.cgo_tools.cc_toolchain.static_runtime_lib(feature_configuration = go.cgo_tools.feature_configuration)
+
 def extld_from_cc_toolchain(go):
     if not go.cgo_tools:
         return []
diff --git a/go/private/rules/cgo.bzl b/go/private/rules/cgo.bzl
index 7ebb9fa..11affdb 100644
--- a/go/private/rules/cgo.bzl
+++ b/go/private/rules/cgo.bzl
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
+load("//go/private/actions:utils.bzl", "quote_opts")
 load(
     "//go/private:common.bzl",
     "get_versioned_shared_lib_extension",
@@ -23,8 +24,22 @@
     "//go/private:mode.bzl",
     "LINKMODE_NORMAL",
     "extldflags_from_cc_toolchain",
+    "runtime_libs_from_cc_toolchain",
 )
 
+_CXX_SOURCE_EXTENSIONS = {
+    "cc": None,
+    "cpp": None,
+    "cxx": None,
+    "mm": None,
+}
+
+def _has_cxx_sources(srcs):
+    for src in srcs:
+        if src.extension in _CXX_SOURCE_EXTENSIONS:
+            return True
+    return False
+
 def cgo_configure(go, srcs, cdeps, cppopts, copts, cxxopts, clinkopts):
     """cgo_configure returns the inputs and compile / link options
     that are required to build a cgo archive.
@@ -49,7 +64,7 @@
         cxxopts: complete list of C++ compiler options.
         objcopts: complete list of Objective-C compiler options.
         objcxxopts: complete list of Objective-C++ compiler options.
-        clinkopts: complete list of linker options.
+        ldflags: Args object containing complete C linker options.
     """
     if not go.cgo_tools:
         fail("Go toolchain does not support cgo")
@@ -59,15 +74,9 @@
     cxxopts = go.cgo_tools.cxx_compile_options + cxxopts
     objcopts = go.cgo_tools.objc_compile_options + copts
     objcxxopts = go.cgo_tools.objcxx_compile_options + cxxopts
-    clinkopts = extldflags_from_cc_toolchain(go) + clinkopts
-
-    # NOTE(#2545): avoid unnecessary dynamic link
-    if "-static-libstdc++" in clinkopts:
-        clinkopts = [
-            option
-            for option in clinkopts
-            if option not in ("-lstdc++", "-lc++")
-        ]
+    toolchain_clinkopts = extldflags_from_cc_toolchain(go)
+    runtime_libs = runtime_libs_from_cc_toolchain(go)
+    needs_cxx_runtime = _has_cxx_sources(srcs)
 
     if go.mode.linkmode != LINKMODE_NORMAL:
         for opt_list in (copts, cxxopts, objcopts, objcxxopts):
@@ -149,6 +158,9 @@
                         # so it can be treated as a simple shared library too.
                         continue
                 lib_opts.append(lib.path)
+                if lib.basename.endswith(".a"):
+                    # Match cgo2's heuristic: static cdeps may need the C++ runtime.
+                    needs_cxx_runtime = True
             clinkopts.extend(cc_link_flags)
 
         elif hasattr(d, "objc"):
@@ -174,7 +186,26 @@
     # specified with -l flags) unless they appear after .o or .a files with
     # undefined symbols they provide. Put all the .a files from cdeps first,
     # so that we actually link with -lstdc++ and others.
-    clinkopts = lib_opts + clinkopts
+    pre_runtime_clinkopts = lib_opts + toolchain_clinkopts
+
+    # NOTE(#2545): avoid unnecessary dynamic link
+    if "-static-libstdc++" in pre_runtime_clinkopts + clinkopts:
+        pre_runtime_clinkopts = [
+            option
+            for option in pre_runtime_clinkopts
+            if option not in ("-lstdc++", "-lc++")
+        ]
+        clinkopts = [
+            option
+            for option in clinkopts
+            if option not in ("-lstdc++", "-lc++")
+        ]
+
+    ldflags = go.actions.args()
+    _add_ldflags(ldflags, pre_runtime_clinkopts)
+    if needs_cxx_runtime:
+        ldflags.add_all(runtime_libs, before_each = "-ldflags")
+    _add_ldflags(ldflags, clinkopts)
 
     return struct(
         inputs = inputs,
@@ -186,9 +217,13 @@
         cxxopts = cxxopts,
         objcopts = objcopts,
         objcxxopts = objcxxopts,
-        clinkopts = clinkopts,
+        ldflags = ldflags,
     )
 
+def _add_ldflags(args, opts):
+    if opts:
+        args.add("-ldflags", quote_opts(opts))
+
 def _cc_libs_and_flags(target):
     # Copied from get_libs_for_static_executable in migration instructions
     # from bazelbuild/bazel#7036.
diff --git a/tests/core/starlark/cgo/cgo_test.bzl b/tests/core/starlark/cgo/cgo_test.bzl
index b56accc..b5de750 100644
--- a/tests/core/starlark/cgo/cgo_test.bzl
+++ b/tests/core/starlark/cgo/cgo_test.bzl
@@ -1,6 +1,47 @@
 load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
+load("@rules_cc//cc:cc_toolchain_config_lib.bzl", "feature", "tool_path")  # buildifier: disable=deprecated-function
+load("@rules_cc//cc:defs.bzl", "cc_toolchain")
+load("@rules_cc//cc/common:cc_common.bzl", "cc_common")
 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_cross_binary")
 
+def _test_cc_config_impl(ctx):
+    tool_paths = [
+        tool_path(name = name, path = "/bin/false")
+        for name in [
+            "ar",
+            "cpp",
+            "dwp",
+            "gcc",
+            "gcov",
+            "ld",
+            "nm",
+            "objcopy",
+            "objdump",
+            "strip",
+        ]
+    ]
+
+    return cc_common.create_cc_toolchain_config_info(
+        ctx = ctx,
+        toolchain_identifier = "runtime-libs-test-toolchain",
+        host_system_name = "local",
+        target_system_name = "local",
+        target_cpu = "local",
+        target_libc = "local",
+        compiler = "gcc",
+        abi_version = "local",
+        abi_libc_version = "local",
+        tool_paths = tool_paths,
+        features = [
+            feature(name = "static_link_cpp_runtimes", enabled = True),
+        ],
+    )
+
+test_cc_config = rule(
+    implementation = _test_cc_config_impl,
+    provides = [CcToolchainConfigInfo],
+)
+
 def _missing_cc_toolchain_explicit_pure_off_test(ctx):
     env = analysistest.begin(ctx)
 
@@ -16,7 +57,83 @@
     },
 )
 
+def _runtime_lib_inputs_test_impl(ctx):
+    env = analysistest.begin(ctx)
+    actions = analysistest.target_actions(env)
+    link_actions = [action for action in actions if action.mnemonic == "GoLink"]
+    asserts.equals(env, 1, len(link_actions), "expected exactly one GoLink action")
+
+    inputs = link_actions[0].inputs.to_list()
+    for expected in ctx.attr.expected_inputs:
+        asserts.true(
+            env,
+            any([input.path.endswith("/" + expected) for input in inputs]),
+            "expected '{}' to be in inputs: '{}'".format(expected, inputs),
+        )
+    for unexpected in ctx.attr.unexpected_inputs:
+        asserts.false(
+            env,
+            any([input.path.endswith("/" + unexpected) for input in inputs]),
+            "did not expect '{}' to be in inputs: '{}'".format(unexpected, inputs),
+        )
+
+    compile_actions = [action for action in actions if action.mnemonic == "GoCompilePkg"]
+    asserts.equals(env, 1, len(compile_actions), "expected exactly one GoCompilePkg action")
+    compile_argv = " ".join(compile_actions[0].argv)
+    for expected in ctx.attr.expected_linkopts:
+        asserts.true(
+            env,
+            expected in compile_argv,
+            "expected '{}' to be in compile argv: '{}'".format(expected, compile_argv),
+        )
+    for unexpected in ctx.attr.unexpected_linkopts:
+        asserts.false(
+            env,
+            unexpected in compile_argv,
+            "did not expect '{}' to be in compile argv: '{}'".format(unexpected, compile_argv),
+        )
+
+    return analysistest.end(env)
+
+runtime_lib_inputs_test = analysistest.make(
+    _runtime_lib_inputs_test_impl,
+    attrs = {
+        "expected_inputs": attr.string_list(),
+        "expected_linkopts": attr.string_list(),
+        "unexpected_inputs": attr.string_list(),
+        "unexpected_linkopts": attr.string_list(),
+    },
+    config_settings = {
+        "//command_line_option:extra_toolchains": str(Label("//tests/core/starlark/cgo:runtime_libs_test_cc_toolchain")),
+    },
+)
+
 def cgo_test_suite():
+    test_cc_config(
+        name = "runtime_libs_test_cc_toolchain_config",
+    )
+
+    cc_toolchain(
+        name = "runtime_libs_test_cc_toolchain_impl",
+        all_files = ":empty",
+        compiler_files = ":empty",
+        dwp_files = ":empty",
+        dynamic_runtime_lib = ":dummy.so",
+        linker_files = ":empty",
+        objcopy_files = ":empty",
+        static_runtime_lib = ":dummy.a",
+        strip_files = ":empty",
+        supports_param_files = 0,
+        toolchain_config = ":runtime_libs_test_cc_toolchain_config",
+        toolchain_identifier = "runtime-libs-test-toolchain",
+    )
+
+    native.toolchain(
+        name = "runtime_libs_test_cc_toolchain",
+        toolchain = ":runtime_libs_test_cc_toolchain_impl",
+        toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
+    )
+
     go_binary(
         name = "cross_impure",
         srcs = ["main.go"],
@@ -36,8 +153,53 @@
         target_under_test = ":go_cross_impure_cgo",
     )
 
+    go_binary(
+        name = "runtime_libs_static_binary",
+        srcs = [
+            "runtime_libs.cc",
+            "runtime_libs.go",
+        ],
+        cgo = True,
+        pure = "off",
+        tags = ["manual"],
+    )
+
+    runtime_lib_inputs_test(
+        name = "static_runtime_lib_inputs_test",
+        expected_inputs = ["dummy.a"],
+        expected_linkopts = ["dummy.a"],
+        target_under_test = ":runtime_libs_static_binary",
+        unexpected_inputs = ["dummy.so"],
+        unexpected_linkopts = ["dummy.so"],
+    )
+
+    go_binary(
+        name = "runtime_libs_dynamic_binary",
+        srcs = [
+            "runtime_libs.cc",
+            "runtime_libs.go",
+        ],
+        cgo = True,
+        linkmode = "c-shared",
+        pure = "off",
+        tags = ["manual"],
+    )
+
+    runtime_lib_inputs_test(
+        name = "dynamic_runtime_lib_inputs_test",
+        expected_inputs = ["dummy.so"],
+        expected_linkopts = ["dummy.so"],
+        target_under_test = ":runtime_libs_dynamic_binary",
+        unexpected_inputs = ["dummy.a"],
+        unexpected_linkopts = ["dummy.a"],
+    )
+
     """Creates the test targets and test suite for cgo.bzl tests."""
     native.test_suite(
         name = "cgo_tests",
-        tests = [":missing_cc_toolchain_explicit_pure_off_test"],
+        tests = [
+            ":dynamic_runtime_lib_inputs_test",
+            ":missing_cc_toolchain_explicit_pure_off_test",
+            ":static_runtime_lib_inputs_test",
+        ],
     )
diff --git a/tests/core/starlark/cgo/dummy.a b/tests/core/starlark/cgo/dummy.a
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/tests/core/starlark/cgo/dummy.a
@@ -0,0 +1 @@
+
diff --git a/tests/core/starlark/cgo/dummy.so b/tests/core/starlark/cgo/dummy.so
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/tests/core/starlark/cgo/dummy.so
@@ -0,0 +1 @@
+
diff --git a/tests/core/starlark/cgo/empty b/tests/core/starlark/cgo/empty
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/tests/core/starlark/cgo/empty
@@ -0,0 +1 @@
+
diff --git a/tests/core/starlark/cgo/main.go b/tests/core/starlark/cgo/main.go
new file mode 100644
index 0000000..38dd16d
--- /dev/null
+++ b/tests/core/starlark/cgo/main.go
@@ -0,0 +1,3 @@
+package main
+
+func main() {}
diff --git a/tests/core/starlark/cgo/runtime_libs.cc b/tests/core/starlark/cgo/runtime_libs.cc
new file mode 100644
index 0000000..b2791d0
--- /dev/null
+++ b/tests/core/starlark/cgo/runtime_libs.cc
@@ -0,0 +1 @@
+extern "C" void runtime_libs_test(void) {}
diff --git a/tests/core/starlark/cgo/runtime_libs.go b/tests/core/starlark/cgo/runtime_libs.go
new file mode 100644
index 0000000..5aa8e1f
--- /dev/null
+++ b/tests/core/starlark/cgo/runtime_libs.go
@@ -0,0 +1,8 @@
+package main
+
+// void runtime_libs_test(void);
+import "C"
+
+func main() {
+	C.runtime_libs_test()
+}