Extract the Bazel `cc_import` rule from `@_builtins` to `@rules_cc`

PiperOrigin-RevId: 786644184
Change-Id: I83991e4d83c8700aade3cfee454ef94b8c6ea155
diff --git a/cc/common/cc_helper.bzl b/cc/common/cc_helper.bzl
index 35f6812..7eb24ef 100644
--- a/cc/common/cc_helper.bzl
+++ b/cc/common/cc_helper.bzl
@@ -15,6 +15,15 @@
 
 load("//cc:find_cc_toolchain.bzl", "CC_TOOLCHAIN_TYPE")
 load(":cc_common.bzl", "cc_common")
+load(
+    ":cc_helper_internal.bzl",
+    "get_relative_path",
+    "is_versioned_shared_library_extension_valid",
+    "path_contains_up_level_references",
+    _package_source_root = "package_source_root",
+    _repository_exec_path = "repository_exec_path",
+)
+load(":cc_info.bzl", "CcInfo")
 load(":visibility.bzl", "INTERNAL_VISIBILITY")
 
 visibility(INTERNAL_VISIBILITY)
@@ -27,6 +36,62 @@
 # LINT.ThenChange(https://github.com/bazelbuild/bazel/blob/master/src/main/starlark/builtins_bzl/common/cc/cc_helper.bzl:linker_mode)
 
 # LINT.IfChange(forked_exports)
+
+def _get_compilation_contexts_from_deps(deps):
+    compilation_contexts = []
+    for dep in deps:
+        if CcInfo in dep:
+            compilation_contexts.append(dep[CcInfo].compilation_context)
+    return compilation_contexts
+
+def _tool_path(cc_toolchain, tool):
+    return cc_toolchain._tool_paths.get(tool, None)
+
+def _get_toolchain_global_make_variables(cc_toolchain):
+    result = {
+        "CC": _tool_path(cc_toolchain, "gcc"),
+        "AR": _tool_path(cc_toolchain, "ar"),
+        "NM": _tool_path(cc_toolchain, "nm"),
+        "LD": _tool_path(cc_toolchain, "ld"),
+        "STRIP": _tool_path(cc_toolchain, "strip"),
+        "C_COMPILER": cc_toolchain.compiler,
+    }  # buildifier: disable=unsorted-dict-items
+
+    obj_copy_tool = _tool_path(cc_toolchain, "objcopy")
+    if obj_copy_tool != None:
+        # objcopy is optional in Crostool.
+        result["OBJCOPY"] = obj_copy_tool
+    gcov_tool = _tool_path(cc_toolchain, "gcov-tool")
+    if gcov_tool != None:
+        # gcovtool is optional in Crostool.
+        result["GCOVTOOL"] = gcov_tool
+
+    libc = cc_toolchain.libc
+    if libc.startswith("glibc-"):
+        # Strip "glibc-" prefix.
+        result["GLIBC_VERSION"] = libc[6:]
+    else:
+        result["GLIBC_VERSION"] = libc
+
+    abi_glibc_version = cc_toolchain._abi_glibc_version
+    if abi_glibc_version != None:
+        result["ABI_GLIBC_VERSION"] = abi_glibc_version
+
+    abi = cc_toolchain._abi
+    if abi != None:
+        result["ABI"] = abi
+
+    result["CROSSTOOLTOP"] = cc_toolchain._crosstool_top_path
+    return result
+
+_SHARED_LIBRARY_EXTENSIONS = ["so", "dll", "dylib", "wasm"]
+
+def _is_valid_shared_library_artifact(shared_library):
+    if (shared_library.extension in _SHARED_LIBRARY_EXTENSIONS):
+        return True
+
+    return is_versioned_shared_library_extension_valid(shared_library.basename)
+
 def _get_static_mode_params_for_dynamic_library_libraries(libs):
     linker_inputs = []
     for lib in libs.to_list():
@@ -285,11 +350,79 @@
         )
     )
 
+SYSROOT_FLAG = "--sysroot="
+
+def _contains_sysroot(original_cc_flags, feature_config_cc_flags):
+    if SYSROOT_FLAG in original_cc_flags:
+        return True
+    for flag in feature_config_cc_flags:
+        if SYSROOT_FLAG in flag:
+            return True
+
+    return False
+
+def _get_cc_flags_make_variable(_ctx, feature_configuration, cc_toolchain):
+    original_cc_flags = cc_toolchain._legacy_cc_flags_make_variable
+    sysroot_cc_flag = ""
+    if cc_toolchain.sysroot != None:
+        sysroot_cc_flag = SYSROOT_FLAG + cc_toolchain.sysroot
+
+    build_vars = cc_toolchain._build_variables
+    feature_config_cc_flags = cc_common.get_memory_inefficient_command_line(
+        feature_configuration = feature_configuration,
+        action_name = "cc-flags-make-variable",
+        variables = build_vars,
+    )
+    cc_flags = [original_cc_flags]
+
+    # Only add sysroots flag if nothing else adds sysroot, BUT it must appear
+    # before the feature config flags.
+    if not _contains_sysroot(original_cc_flags, feature_config_cc_flags):
+        cc_flags.append(sysroot_cc_flag)
+    cc_flags.extend(feature_config_cc_flags)
+    return {"CC_FLAGS": " ".join(cc_flags)}
+
+def _package_exec_path(ctx, package, sibling_repository_layout):
+    return get_relative_path(_repository_exec_path(ctx.label.workspace_name, sibling_repository_layout), package)
+
+def _system_include_dirs(ctx, additional_make_variable_substitutions):
+    result = []
+    sibling_repository_layout = ctx.configuration.is_sibling_repository_layout()
+    package = ctx.label.package
+    package_exec_path = _package_exec_path(ctx, package, sibling_repository_layout)
+    package_source_root = _package_source_root(ctx.label.workspace_name, package, sibling_repository_layout)
+    for include in ctx.attr.includes:
+        includes_attr = _expand(ctx, include, additional_make_variable_substitutions)
+        if includes_attr.startswith("/"):
+            continue
+        includes_path = get_relative_path(package_exec_path, includes_attr)
+        if not sibling_repository_layout and path_contains_up_level_references(includes_path):
+            fail("Path references a path above the execution root.", attr = "includes")
+
+        if includes_path == ".":
+            fail("'" + includes_attr + "' resolves to the workspace root, which would allow this rule and all of its " +
+                 "transitive dependents to include any file in your workspace. Please include only" +
+                 " what you need", attr = "includes")
+        result.append(includes_path)
+
+        # We don't need to perform the above checks against out_includes_path again since any errors
+        # must have manifested in includesPath already.
+        out_includes_path = get_relative_path(package_source_root, includes_attr)
+        if (ctx.configuration.has_separate_genfiles_directory()):
+            result.append(get_relative_path(ctx.genfiles_dir.path, out_includes_path))
+        result.append(get_relative_path(ctx.bin_dir.path, out_includes_path))
+    return result
+
 cc_helper = struct(
     create_strip_action = _create_strip_action,
     get_expanded_env = _get_expanded_env,
     get_static_mode_params_for_dynamic_library_libraries = _get_static_mode_params_for_dynamic_library_libraries,
     should_use_pic = _should_use_pic,
     tokenize = _tokenize,
+    is_valid_shared_library_artifact = _is_valid_shared_library_artifact,
+    get_toolchain_global_make_variables = _get_toolchain_global_make_variables,
+    get_cc_flags_make_variable = _get_cc_flags_make_variable,
+    get_compilation_contexts_from_deps = _get_compilation_contexts_from_deps,
+    system_include_dirs = _system_include_dirs,
 )
 # LINT.ThenChange(https://github.com/bazelbuild/bazel/blob/master/src/main/starlark/builtins_bzl/common/cc/cc_helper.bzl:forked_exports)
diff --git a/cc/common/cc_helper_internal.bzl b/cc/common/cc_helper_internal.bzl
new file mode 100644
index 0000000..ae25bb7
--- /dev/null
+++ b/cc/common/cc_helper_internal.bzl
@@ -0,0 +1,98 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Utility functions for C++ rules that don't depend on cc_common.
+
+Only use those within C++ implementation. The others need to go through cc_common.
+"""
+
+load("@bazel_skylib//lib:paths.bzl", "paths")
+
+# LINT.IfChange(forked_exports)
+
+def is_versioned_shared_library_extension_valid(shared_library_name):
+    """Validates the name against the regex "^.+\\.((so)|(dylib))(\\.\\d\\w*)+$",
+
+    Args:
+        shared_library_name: (str) the name to validate
+
+    Returns:
+        (bool)
+    """
+
+    # must match VERSIONED_SHARED_LIBRARY.
+    for ext in (".so.", ".dylib."):
+        name, _, version = shared_library_name.rpartition(ext)
+        if name and version:
+            version_parts = version.split(".")
+            for part in version_parts:
+                if not part[0].isdigit():
+                    return False
+                for c in part[1:].elems():
+                    if not (c.isalnum() or c == "_"):
+                        return False
+            return True
+    return False
+
+def _is_repository_main(repository):
+    return repository == ""
+
+def package_source_root(repository, package, sibling_repository_layout):
+    """
+    Determines the source root for a given repository and package.
+
+    Args:
+      repository: The repository to get the source root for.
+      package: The package to get the source root for.
+      sibling_repository_layout: Whether the repository layout is a sibling repository layout.
+
+    Returns:
+      The source root for the given repository and package.
+    """
+    if _is_repository_main(repository) or sibling_repository_layout:
+        return package
+    if repository.startswith("@"):
+        repository = repository[1:]
+    return paths.get_relative(paths.get_relative("external", repository), package)
+
+def repository_exec_path(repository, sibling_repository_layout):
+    """
+    Determines the exec path for a given repository.
+
+    Args:
+      repository: The repository to get the exec path for.
+      sibling_repository_layout: Whether the repository layout is a sibling repository layout.
+
+    Returns:
+      The exec path for the given repository.
+    """
+    if _is_repository_main(repository):
+        return ""
+    prefix = "external"
+    if sibling_repository_layout:
+        prefix = ".."
+    if repository.startswith("@"):
+        repository = repository[1:]
+    return paths.get_relative(prefix, repository)
+
+# LINT.ThenChange(https://github.com/bazelbuild/bazel/blob/master/src/main/starlark/builtins_bzl/common/cc/cc_helper_internal.bzl:forked_exports)
+
+def get_relative_path(path_a, path_b):
+    if paths.is_absolute(path_b):
+        return path_b
+    return paths.normalize(paths.join(path_a, path_b))
+
+def path_contains_up_level_references(path):
+    return path.startswith("..") and (len(path) == 2 or path[2] == "/")
diff --git a/cc/common/semantics.bzl b/cc/common/semantics.bzl
new file mode 100644
index 0000000..121142f
--- /dev/null
+++ b/cc/common/semantics.bzl
@@ -0,0 +1,205 @@
+# Copyright 2020 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Semantics for Bazel cc rules"""
+
+# Point virtual includes symlinks to the source root for better IDE integration.
+# See https://github.com/bazelbuild/bazel/pull/20540.
+# TODO: b/320980684 - Add a test that fails if this is flipped to True.
+USE_EXEC_ROOT_FOR_VIRTUAL_INCLUDES_SYMLINKS = False
+
+def _get_proto_aspects():
+    return []
+
+def _should_create_empty_archive():
+    return False
+
+def _validate_attributes(_ctx):
+    pass
+
+def _get_stl():
+    return attr.label()
+
+def _get_repo():
+    return "bazel_tools"
+
+def _get_platforms_root():
+    return "platforms//"
+
+def _additional_fragments():
+    return []
+
+def _get_licenses_attr():
+    # TODO(b/182226065): Change to applicable_licenses
+    return {}
+
+def _def_parser_computed_default(name, tags):
+    # This is needed to break the dependency cycle.
+    if "__DONT_DEPEND_ON_DEF_PARSER__" in tags or "def_parser" in name:
+        return None
+    else:
+        return Label("@bazel_tools//tools/def_parser:def_parser")
+
+def _get_def_parser():
+    return attr.label(
+        default = _def_parser_computed_default,
+        allow_single_file = True,
+        cfg = "exec",
+    )
+
+def _get_grep_includes():
+    return attr.label()
+
+def _get_runtimes_toolchain():
+    return []
+
+def _get_test_malloc_attr():
+    return {}
+
+def _get_coverage_attrs():
+    return {
+        "_lcov_merger": attr.label(
+            default = configuration_field(fragment = "coverage", name = "output_generator"),
+            executable = True,
+            cfg = config.exec(exec_group = "test"),
+        ),
+        "_collect_cc_coverage": attr.label(
+            default = "@bazel_tools//tools/test:collect_cc_coverage",
+            executable = True,
+            cfg = config.exec(exec_group = "test"),
+        ),
+    }  # buildifier: disable=unsorted-dict-items
+
+def _get_coverage_env(ctx):
+    return ctx.runfiles(), {}
+
+def _get_cc_runtimes(ctx, is_library):
+    if is_library:
+        return []
+
+    runtimes = [ctx.attr.link_extra_lib]
+
+    if ctx.fragments.cpp.custom_malloc != None:
+        runtimes.append(ctx.attr._default_malloc)
+    else:
+        runtimes.append(ctx.attr.malloc)
+
+    return runtimes
+
+def _get_cc_runtimes_copts(_ctx):
+    return []
+
+def _get_implementation_deps_allowed_attr():
+    return {}
+
+def _check_can_use_implementation_deps(ctx):
+    experimental_cc_implementation_deps = ctx.fragments.cpp.experimental_cc_implementation_deps()
+    if (not experimental_cc_implementation_deps and ctx.attr.implementation_deps):
+        fail("requires --experimental_cc_implementation_deps", attr = "implementation_deps")
+
+_WINDOWS_PLATFORM = Label("@platforms//os:windows")  # Resolve the label within builtins context
+
+def _get_linkstatic_default_for_test():
+    return select({
+        _WINDOWS_PLATFORM: True,
+        "//conditions:default": False,
+    })
+
+def _get_cc_link_memlimit(_compilation_mode, exec_info):
+    return exec_info
+
+def _get_nocopts_attr():
+    return {}
+
+def _get_experimental_link_static_libraries_once(ctx):
+    return ctx.fragments.cpp.experimental_link_static_libraries_once()
+
+def _check_cc_shared_library_tags(_ctx):
+    pass
+
+def _cpp_modules_tools():
+    return {
+        "_aggregate_ddi": attr.label(
+            executable = True,
+            cfg = "exec",
+            default = "@" + _get_repo() + "//tools/cpp:aggregate-ddi",
+        ),
+        "_generate_modmap": attr.label(
+            executable = True,
+            cfg = "exec",
+            default = "@" + _get_repo() + "//tools/cpp:generate-modmap",
+        ),
+    }
+
+def _validate(_ctx, _rule_name):
+    pass
+
+semantics = struct(
+    toolchain = "@bazel_tools//tools/cpp:toolchain_type",
+    validate = _validate,
+    allowlist_attrs = {},
+    ALLOWED_RULES_IN_DEPS = [
+        "cc_library",
+        "objc_library",
+        "cc_proto_library",
+        "cc_import",
+    ],
+    ALLOWED_FILES_IN_DEPS = [
+        ".ld",
+        ".lds",
+        ".ldscript",
+    ],
+    ALLOWED_RULES_WITH_WARNINGS_IN_DEPS = [],
+    validate_attributes = _validate_attributes,
+    get_repo = _get_repo,
+    get_platforms_root = _get_platforms_root,
+    additional_fragments = _additional_fragments,
+    get_licenses_attr = _get_licenses_attr,
+    get_def_parser = _get_def_parser,
+    get_stl = _get_stl,
+    should_create_empty_archive = _should_create_empty_archive,
+    get_grep_includes = _get_grep_includes,
+    get_implementation_deps_allowed_attr = _get_implementation_deps_allowed_attr,
+    check_can_use_implementation_deps = _check_can_use_implementation_deps,
+    get_linkstatic_default_for_test = _get_linkstatic_default_for_test,
+    get_cc_link_memlimit = _get_cc_link_memlimit,
+    get_runtimes_toolchain = _get_runtimes_toolchain,
+    get_test_malloc_attr = _get_test_malloc_attr,
+    get_cc_runtimes = _get_cc_runtimes,
+    get_cc_runtimes_copts = _get_cc_runtimes_copts,
+    get_coverage_attrs = _get_coverage_attrs,
+    get_coverage_env = _get_coverage_env,
+    get_proto_aspects = _get_proto_aspects,
+    get_nocopts_attr = _get_nocopts_attr,
+    get_experimental_link_static_libraries_once = _get_experimental_link_static_libraries_once,
+    cpp_modules_tools = _cpp_modules_tools,
+    check_cc_shared_library_tags = _check_cc_shared_library_tags,
+    BUILD_INFO_TRANLATOR_LABEL = "@bazel_tools//tools/build_defs/build_info:cc_build_info",
+    CC_PROTO_TOOLCHAIN = "@rules_cc//cc/proto:toolchain_type",
+    is_bazel = True,
+    extra_exec_groups = {},
+    stamp_extra_docs = "",
+    malloc_docs = """
+ Override the default dependency on malloc.
+ <p>
+   By default, C++ binaries are linked against <code>//tools/cpp:malloc</code>,
+   which is an empty library so the binary ends up using libc malloc.
+   This label must refer to a <code>cc_library</code>. If compilation is for a non-C++
+   rule, this option has no effect. The value of this attribute is ignored if
+   <code>linkshared=True</code> is specified.
+ </p>
+""",
+    cc_binary_extra_docs = "",
+    cc_test_extra_docs = "",
+)
diff --git a/cc/private/rules_impl/cc_import.bzl b/cc/private/rules_impl/cc_import.bzl
index 05c3f6b..418facd 100644
--- a/cc/private/rules_impl/cc_import.bzl
+++ b/cc/private/rules_impl/cc_import.bzl
@@ -11,7 +11,433 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-"""cc_import rule without a macro"""
 
-# buildifier: disable=native-cc-import
-cc_import = native.cc_import
+"""Starlark implementation of cc_import.
+
+We may change the implementation at any moment or even delete this file. Do not
+rely on this. Pass the flag --experimental_starlark_cc_import
+"""
+
+load("//cc:find_cc_toolchain.bzl", "CC_TOOLCHAIN_TYPE", "find_cc_toolchain", "use_cc_toolchain")
+load("//cc/common:cc_common.bzl", "cc_common")
+load("//cc/common:cc_helper.bzl", "cc_helper")
+load("//cc/common:cc_info.bzl", "CcInfo")
+load("//cc/common:semantics.bzl", "semantics")
+
+CPP_LINK_STATIC_LIBRARY_ACTION_NAME = "c++-link-static-library"
+
+def _perform_error_checks(
+        system_provided,
+        shared_library_artifact,
+        interface_library_artifact):
+    # If the shared library will be provided by system during runtime, users are not supposed to
+    # specify shared_library.
+    if system_provided and shared_library_artifact != None:
+        fail("'shared_library' shouldn't be specified when 'system_provided' is true")
+
+    # If a shared library won't be provided by system during runtime and we are linking the shared
+    # library through interface library, the shared library must be specified.
+    if (not system_provided and shared_library_artifact == None and
+        interface_library_artifact != None):
+        fail("'shared_library' should be specified when 'system_provided' is false")
+
+    if (shared_library_artifact != None and
+        not cc_helper.is_valid_shared_library_artifact(shared_library_artifact)):
+        fail("'shared_library' does not produce any cc_import shared_library files (expected .so, .dylib or .dll)")
+
+def _create_archive_action(
+        ctx,
+        feature_configuration,
+        cc_toolchain,
+        output_file,
+        object_files):
+    archiver_path = cc_common.get_tool_for_action(
+        feature_configuration = feature_configuration,
+        action_name = CPP_LINK_STATIC_LIBRARY_ACTION_NAME,
+    )
+    archiver_variables = cc_common.create_link_variables(
+        feature_configuration = feature_configuration,
+        cc_toolchain = cc_toolchain,
+        output_file = output_file.path,
+        is_using_linker = False,
+    )
+    command_line = cc_common.get_memory_inefficient_command_line(
+        feature_configuration = feature_configuration,
+        action_name = CPP_LINK_STATIC_LIBRARY_ACTION_NAME,
+        variables = archiver_variables,
+    )
+    args = ctx.actions.args()
+    args.add_all(command_line)
+    args.add_all(object_files)
+    args.use_param_file("@%s", use_always = True)
+
+    env = cc_common.get_environment_variables(
+        feature_configuration = feature_configuration,
+        action_name = CPP_LINK_STATIC_LIBRARY_ACTION_NAME,
+        variables = archiver_variables,
+    )
+
+    # TODO(bazel-team): PWD=/proc/self/cwd env var is missing, but it is present when an analogous archiving
+    # action is created by cc_library
+    ctx.actions.run(
+        executable = archiver_path,
+        toolchain = CC_TOOLCHAIN_TYPE,
+        arguments = [args],
+        env = env,
+        inputs = depset(
+            direct = object_files,
+            transitive = [
+                cc_toolchain.all_files,
+            ],
+        ),
+        use_default_shell_env = True,
+        outputs = [output_file],
+        mnemonic = "CppArchive",
+    )
+
+def _cc_import_impl(ctx):
+    cc_toolchain = find_cc_toolchain(ctx)
+    feature_configuration = cc_common.configure_features(
+        ctx = ctx,
+        cc_toolchain = cc_toolchain,
+        requested_features = ctx.features,
+        unsupported_features = ctx.disabled_features,
+    )
+
+    _perform_error_checks(
+        ctx.attr.system_provided,
+        ctx.file.shared_library,
+        ctx.file.interface_library,
+    )
+
+    pic_static_library = ctx.file.pic_static_library or None
+    static_library = ctx.file.static_library or None
+
+    if ctx.files.pic_objects and not pic_static_library:
+        lib_name = "lib" + ctx.label.name + ".pic.a"
+        pic_static_library = ctx.actions.declare_file(lib_name)
+        _create_archive_action(ctx, feature_configuration, cc_toolchain, pic_static_library, ctx.files.pic_objects)
+
+    if ctx.files.objects and not static_library:
+        lib_name = "lib" + ctx.label.name + ".a"
+        static_library = ctx.actions.declare_file(lib_name)
+        _create_archive_action(ctx, feature_configuration, cc_toolchain, static_library, ctx.files.objects)
+
+    not_none_artifact_to_link = False
+
+    # Check if there is something to link, if not skip that part.
+    if static_library != None or pic_static_library != None or ctx.file.interface_library != None or ctx.file.shared_library != None:
+        not_none_artifact_to_link = True
+
+    linking_context = None
+    if not_none_artifact_to_link:
+        library_to_link = cc_common.create_library_to_link(
+            actions = ctx.actions,
+            feature_configuration = feature_configuration,
+            cc_toolchain = cc_toolchain,
+            static_library = static_library,
+            pic_static_library = pic_static_library,
+            interface_library = ctx.file.interface_library,
+            dynamic_library = ctx.file.shared_library,
+            pic_objects = ctx.files.pic_objects,
+            objects = ctx.files.objects,
+            alwayslink = ctx.attr.alwayslink,
+        )
+
+        linker_input = cc_common.create_linker_input(
+            libraries = depset([library_to_link]),
+            user_link_flags = depset(ctx.attr.linkopts),
+            owner = ctx.label,
+        )
+
+        linking_context = cc_common.create_linking_context(
+            linker_inputs = depset([linker_input]),
+        )
+
+    additional_make_variable_substitutions = cc_helper.get_toolchain_global_make_variables(cc_toolchain)
+    additional_make_variable_substitutions.update(cc_helper.get_cc_flags_make_variable(ctx, feature_configuration, cc_toolchain))
+
+    runtimes_deps = semantics.get_cc_runtimes(ctx, True)
+    runtimes_copts = semantics.get_cc_runtimes_copts(ctx)
+    compilation_contexts = cc_helper.get_compilation_contexts_from_deps(runtimes_deps)
+    (compilation_context, _) = cc_common.compile(
+        actions = ctx.actions,
+        feature_configuration = feature_configuration,
+        user_compile_flags = runtimes_copts,
+        cc_toolchain = cc_toolchain,
+        compilation_contexts = compilation_contexts,
+        public_hdrs = ctx.files.hdrs,
+        includes = cc_helper.system_include_dirs(ctx, additional_make_variable_substitutions),
+        name = ctx.label.name,
+        strip_include_prefix = ctx.attr.strip_include_prefix,
+    )
+
+    this_cc_info = CcInfo(compilation_context = compilation_context, linking_context = linking_context)
+    cc_infos = [this_cc_info]
+
+    for dep in ctx.attr.deps:
+        cc_infos.append(dep[CcInfo])
+    merged_cc_info = cc_common.merge_cc_infos(direct_cc_infos = [this_cc_info], cc_infos = cc_infos)
+
+    return [merged_cc_info]
+
+cc_import = rule(
+    implementation = _cc_import_impl,
+    doc = """
+<p>
+<code>cc_import</code> rules allows users to import precompiled C/C++ libraries.
+</p>
+
+<p>
+The following are the typical use cases: <br/>
+
+1. Linking a static library
+<pre><code class="lang-starlark">
+cc_import(
+  name = "mylib",
+  hdrs = ["mylib.h"],
+  static_library = "libmylib.a",
+  # If alwayslink is turned on,
+  # libmylib.a will be forcely linked into any binary that depends on it.
+  # alwayslink = True,
+)
+</code></pre>
+
+2. Linking a shared library (Unix)
+<pre><code class="lang-starlark">
+cc_import(
+  name = "mylib",
+  hdrs = ["mylib.h"],
+  shared_library = "libmylib.so",
+)
+</code></pre>
+
+3. Linking a shared library with interface library
+
+<p>On Unix:
+<pre><code class="lang-starlark">
+cc_import(
+  name = "mylib",
+  hdrs = ["mylib.h"],
+  # libmylib.ifso is an interface library for libmylib.so which will be passed to linker
+  interface_library = "libmylib.ifso",
+  # libmylib.so will be available for runtime
+  shared_library = "libmylib.so",
+)
+</code></pre>
+
+<p>On Windows:
+<pre><code class="lang-starlark">
+cc_import(
+  name = "mylib",
+  hdrs = ["mylib.h"],
+  # mylib.lib is an import library for mylib.dll which will be passed to linker
+  interface_library = "mylib.lib",
+  # mylib.dll will be available for runtime
+  shared_library = "mylib.dll",
+)
+</code></pre>
+
+4. Linking a shared library with <code>system_provided=True</code>
+
+<p>On Unix:
+<pre><code class="lang-starlark">
+cc_import(
+  name = "mylib",
+  hdrs = ["mylib.h"],
+  interface_library = "libmylib.ifso", # Or we can also use libmylib.so as its own interface library
+  # libmylib.so is provided by system environment, for example it can be found in LD_LIBRARY_PATH.
+  # This indicates that Bazel is not responsible for making libmylib.so available.
+  system_provided = True,
+)
+</code></pre>
+
+<p>On Windows:
+<pre><code class="lang-starlark">
+cc_import(
+  name = "mylib",
+  hdrs = ["mylib.h"],
+  # mylib.lib is an import library for mylib.dll which will be passed to linker
+  interface_library = "mylib.lib",
+  # mylib.dll is provided by system environment, for example it can be found in PATH.
+  # This indicates that Bazel is not responsible for making mylib.dll available.
+  system_provided = True,
+)
+</code></pre>
+
+5. Linking to static or shared library
+
+<p>On Unix:
+<pre><code class="lang-starlark">
+cc_import(
+  name = "mylib",
+  hdrs = ["mylib.h"],
+  static_library = "libmylib.a",
+  shared_library = "libmylib.so",
+)
+</code></pre>
+
+<p>On Windows:
+<pre><code class="lang-starlark">
+cc_import(
+  name = "mylib",
+  hdrs = ["mylib.h"],
+  static_library = "libmylib.lib", # A normal static library
+  interface_library = "mylib.lib", # An import library for mylib.dll
+  shared_library = "mylib.dll",
+)
+</code></pre>
+
+<p>The remaining is the same on Unix and Windows:
+<pre><code class="lang-starlark">
+# first will link to libmylib.a (or libmylib.lib)
+cc_binary(
+  name = "first",
+  srcs = ["first.cc"],
+  deps = [":mylib"],
+  linkstatic = True, # default value
+)
+
+# second will link to libmylib.so (or libmylib.lib)
+cc_binary(
+  name = "second",
+  srcs = ["second.cc"],
+  deps = [":mylib"],
+  linkstatic = False,
+)
+</code></pre>
+
+<p>
+<code>cc_import</code> supports an include attribute. For example:
+<pre><code class="lang-starlark">
+cc_import(
+  name = "curl_lib",
+  hdrs = glob(["vendor/curl/include/curl/*.h"]),
+  includes = ["vendor/curl/include"],
+  shared_library = "vendor/curl/lib/.libs/libcurl.dylib",
+)
+</code></pre>
+</p>
+""",
+    attrs = {
+        "hdrs": attr.label_list(
+            allow_files = True,
+            flags = ["ORDER_INDEPENDENT", "DIRECT_COMPILE_TIME_INPUT"],
+            doc = """
+The list of header files published by
+this precompiled library to be directly included by sources in dependent rules.""",
+        ),
+        "static_library": attr.label(allow_single_file = [".a", ".lib"], doc = """
+A single precompiled static library.
+<p> Permitted file types:
+  <code>.a</code>,
+  <code>.pic.a</code>
+  or <code>.lib</code>
+</p>"""),
+        "pic_static_library": attr.label(allow_single_file = [".pic.a", ".pic.lib"]),  # TODO: b/320462212 - document this attribute
+        "shared_library": attr.label(allow_single_file = True, doc = """
+A single precompiled shared library. Bazel ensures it is available to the
+binary that depends on it during runtime.
+<p> Permitted file types:
+  <code>.so</code>,
+  <code>.dll</code>
+  or <code>.dylib</code>
+</p>"""),
+        "interface_library": attr.label(
+            allow_single_file = [".ifso", ".tbd", ".lib", ".so", ".dylib"],
+            doc = """
+A single interface library for linking the shared library.
+<p> Permitted file types:
+  <code>.ifso</code>,
+  <code>.tbd</code>,
+  <code>.lib</code>,
+  <code>.so</code>
+  or <code>.dylib</code>
+</p>""",
+        ),
+        "pic_objects": attr.label_list(
+            # TODO: b/320462212 - document this attribute
+            allow_files = [".o", ".pic.o"],
+        ),
+        "objects": attr.label_list(
+            # TODO: b/320462212 - document this attribute
+            allow_files = [".o", ".nopic.o"],
+        ),
+        "system_provided": attr.bool(default = False, doc = """
+If 1, it indicates the shared library required at runtime is provided by the system. In
+this case, <code>interface_library</code> should be specified and
+<code>shared_library</code> should be empty."""),
+        "alwayslink": attr.bool(default = False, doc = """
+If 1, any binary that depends (directly or indirectly) on this C++
+precompiled library will link in all the object files archived in the static library,
+even if some contain no symbols referenced by the binary.
+This is useful if your code isn't explicitly called by code in
+the binary, e.g., if your code registers to receive some callback
+provided by some service.
+
+<p>If alwayslink doesn't work with VS 2017 on Windows, that is due to a
+<a href="https://github.com/bazelbuild/bazel/issues/3949">known issue</a>,
+please upgrade your VS 2017 to the latest version.</p>"""),
+        "linkopts": attr.string_list(doc = """
+Add these flags to the C++ linker command.
+Subject to <a href="make-variables.html">"Make" variable</a> substitution,
+<a href="common-definitions.html#sh-tokenization">
+Bourne shell tokenization</a> and
+<a href="common-definitions.html#label-expansion">label expansion</a>.
+Each string in this attribute is added to <code>LINKOPTS</code> before
+linking the binary target.
+<p>
+  Each element of this list that does not start with <code>$</code> or <code>-</code> is
+  assumed to be the label of a target in <code>deps</code>. The
+  list of files generated by that target is appended to the linker
+  options.  An error is reported if the label is invalid, or is
+  not declared in <code>deps</code>.
+</p>"""),
+        "includes": attr.string_list(doc = """
+List of include dirs to be added to the compile line.
+Subject to <a href="${link make-variables}">"Make variable"</a> substitution.
+Each string is prepended with the package path and passed to the C++ toolchain for
+expansion via the "include_paths" CROSSTOOL feature. A toolchain running on a POSIX system
+with typical feature definitions will produce
+<code>-isystem path_to_package/include_entry</code>.
+This should only be used for third-party libraries that
+do not conform to the Google style of writing #include statements.
+Unlike <a href="#cc_binary.copts">COPTS</a>, these flags are added for this rule
+and every rule that depends on it. (Note: not the rules it depends upon!) Be
+very careful, since this may have far-reaching effects.  When in doubt, add
+"-I" flags to <a href="#cc_binary.copts">COPTS</a> instead.
+<p>
+The default <code>include</code> path doesn't include generated
+files. If you need to <code>#include</code> a generated header
+file, list it in the <code>srcs</code>.
+</p>
+"""),
+        "strip_include_prefix": attr.string(doc = """
+The prefix to strip from the paths of the headers of this rule.
+
+<p>When set, the headers in the <code>hdrs</code> attribute of this rule are accessible
+at their path with this prefix cut off.
+
+<p>If it's a relative path, it's taken as a package-relative one. If it's an absolute one,
+it's understood as a repository-relative path.
+
+<p>The prefix in the <code>include_prefix</code> attribute is added after this prefix is
+stripped.
+
+<p>This attribute is only legal under <code>third_party</code>.
+"""),
+        "deps": attr.label_list(doc = """
+The list of other libraries that the target depends upon.
+See general comments about <code>deps</code>
+at <a href="${link common-definitions#typical-attributes}">Typical attributes defined by
+most build rules</a>."""),
+        "data": attr.label_list(
+            allow_files = True,
+            flags = ["SKIP_CONSTRAINTS_OVERRIDE"],
+        ),
+        "_use_auto_exec_groups": attr.bool(default = True),
+    },  # buildifier: disable=unsorted-dict-items
+    provides = [CcInfo],
+    toolchains = use_cc_toolchain() + semantics.get_runtimes_toolchain(),
+    fragments = ["cpp"],
+)
diff --git a/tests/simple_import/BUILD b/tests/simple_import/BUILD
new file mode 100644
index 0000000..aa50f8c
--- /dev/null
+++ b/tests/simple_import/BUILD
@@ -0,0 +1,27 @@
+# Copyright 2025 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load("//cc:cc_binary.bzl", "cc_binary")
+load("//cc:cc_import.bzl", "cc_import")
+
+cc_import(
+    name = "foo_import",
+    hdrs = ["foo.h"],
+)
+
+cc_binary(
+    name = "foo_bin",
+    srcs = ["foo.cc"],
+    deps = [":foo_import"],
+)
diff --git a/tests/simple_import/foo.cc b/tests/simple_import/foo.cc
new file mode 100644
index 0000000..c3aeba4
--- /dev/null
+++ b/tests/simple_import/foo.cc
@@ -0,0 +1 @@
+int main() { return 0; }
\ No newline at end of file
diff --git a/tests/simple_import/foo.h b/tests/simple_import/foo.h
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/simple_import/foo.h