kleaf: Add ddk_uapi_headers rule

It's common for external kernel modules to publish UAPI headers which
need to be sanitized prior to delivery to userspace. Currently, the
DDK has no way to package UAPI headers this way.

Add a new rule, ddk_uapi_headers(), which allows external modules
to package their UAPI headers for userspace. For example:

    ddk_uapi_headers(
       name = "my_headers",
       srcs = glob(["include/uapi/**/*.h"]),
       out = "my_headers.tar.gz",
       kernel_build = "//common:kernel_aarch64",
    )

Bug: 313898637
Bug: 326633656
Change-Id: I1b7b59def1bf0b99b1073600e1324c21f82996e8
Signed-off-by: John Moon <quic_johmoo@quicinc.com>
Signed-off-by: Isaac J. Manjarres <isaacmanjarres@google.com>
[isaacmanjarres: Used config_env_and_outputs_info and
get_setup_script, since KernelSerializedEnvInfo is not
available on this branch. Also added module_scripts
and part of the inputs for running the rule.]
diff --git a/kleaf/impl/BUILD.bazel b/kleaf/impl/BUILD.bazel
index a19f652..da0ec45 100644
--- a/kleaf/impl/BUILD.bazel
+++ b/kleaf/impl/BUILD.bazel
@@ -49,6 +49,7 @@
         "ddk/ddk_headers.bzl",
         "ddk/ddk_module.bzl",
         "ddk/ddk_submodule.bzl",
+        "ddk/ddk_uapi_headers.bzl",
         "ddk/makefiles.bzl",
         "debug.bzl",
         "file.bzl",
diff --git a/kleaf/impl/ddk/ddk_uapi_headers.bzl b/kleaf/impl/ddk/ddk_uapi_headers.bzl
new file mode 100644
index 0000000..d5e27d8
--- /dev/null
+++ b/kleaf/impl/ddk/ddk_uapi_headers.bzl
@@ -0,0 +1,118 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# 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.
+
+"""UAPI headers target for DDK."""
+
+load(":common_providers.bzl", "KernelBuildExtModuleInfo")
+load(":debug.bzl", "debug")
+load(":utils.bzl", "utils")
+
+visibility("//build/kernel/kleaf/...")
+
+def _ddk_uapi_headers_impl(ctx):
+    if not ctx.attr.out.endswith(".tar.gz"):
+        fail("{}: out-file name must end with \".tar.gz\"".format(ctx.label.name))
+
+    out_file = ctx.actions.declare_file("{}/{}".format(ctx.label.name, ctx.attr.out))
+    setup_info = ctx.attr.kernel_build[KernelBuildExtModuleInfo].config_env_and_outputs_info
+
+    command = setup_info.get_setup_script(
+        data = setup_info.data,
+        restore_out_dir_cmd = utils.get_check_sandbox_cmd(),
+    )
+
+    command += """
+         # Make the staging directory
+           mkdir -p {kernel_uapi_headers_dir}/usr
+         # Make unifdef, required by scripts/headers_install.sh
+           make -C ${{KERNEL_DIR}} -f /dev/null scripts/unifdef
+         # Install each header individually
+           while read -r hdr; do
+             out_prefix=$(dirname $(echo ${{hdr}} | sed -e 's|.*include/uapi/||g'))
+             mkdir -p {kernel_uapi_headers_dir}/usr/include/${{out_prefix}}
+             base=$(basename ${{hdr}})
+             (
+               cd ${{KERNEL_DIR}}
+               ./scripts/headers_install.sh \
+                 ${{OLDPWD}}/${{hdr}} ${{OLDPWD}}/{kernel_uapi_headers_dir}/usr/include/${{out_prefix}}/${{base}}
+             )
+           done < $1
+         # Create archive
+           tar czf {out_file} --directory={kernel_uapi_headers_dir} usr/
+         # Delete kernel_uapi_headers_dir because it is not declared
+           rm -rf {kernel_uapi_headers_dir}
+    """.format(
+        out_file = out_file.path,
+        kernel_uapi_headers_dir = out_file.path + "_staging",
+    )
+
+    args = ctx.actions.args()
+    args.use_param_file("%s", use_always = True)
+    args.add_all(depset(transitive = [target.files for target in ctx.attr.srcs]))
+
+    inputs = []
+    transitive_inputs = [target.files for target in ctx.attr.srcs]
+    transitive_inputs.append(ctx.attr.kernel_build[KernelBuildExtModuleInfo].module_scripts)
+    transitive_inputs.append(setup_info.inputs)
+    tools = setup_info.tools
+
+    debug.print_scripts(ctx, command)
+    ctx.actions.run_shell(
+        mnemonic = "DdkUapiHeaders",
+        inputs = depset(inputs, transitive = transitive_inputs),
+        tools = tools,
+        outputs = [out_file],
+        progress_message = "Building DDK UAPI headers %s" % ctx.attr.name,
+        command = command,
+        arguments = [args],
+    )
+
+    return [
+        DefaultInfo(files = depset([out_file])),
+    ]
+
+ddk_uapi_headers = rule(
+    implementation = _ddk_uapi_headers_impl,
+    doc = """A rule that generates a sanitized UAPI header tarball.
+
+    Example:
+
+    ```
+    ddk_uapi_headers(
+       name = "my_headers",
+       srcs = glob(["include/uapi/**/*.h"]),
+       out = "my_headers.tar.gz",
+       kernel_build = "//common:kernel_aarch64",
+    )
+    ```
+    """,
+    attrs = {
+        "srcs": attr.label_list(
+            doc = 'UAPI headers files which can be sanitized by "make headers_install"',
+            allow_files = [".h"],
+        ),
+        "out": attr.string(
+            doc = "Name of the output tarball",
+            mandatory = True,
+        ),
+        "kernel_build": attr.label(
+            doc = "[`kernel_build`](#kernel_build).",
+            providers = [
+                KernelBuildExtModuleInfo,
+            ],
+            mandatory = True,
+        ),
+        "_debug_print_scripts": attr.label(default = "//build/kernel/kleaf:debug_print_scripts"),
+    },
+)
diff --git a/kleaf/kernel.bzl b/kleaf/kernel.bzl
index ffae3ad..79a870f 100644
--- a/kleaf/kernel.bzl
+++ b/kleaf/kernel.bzl
@@ -31,6 +31,7 @@
 load("//build/kernel/kleaf/impl:ddk/ddk_headers.bzl", _ddk_headers = "ddk_headers")
 load("//build/kernel/kleaf/impl:ddk/ddk_module.bzl", _ddk_module = "ddk_module")
 load("//build/kernel/kleaf/impl:ddk/ddk_submodule.bzl", _ddk_submodule = "ddk_submodule")
+load("//build/kernel/kleaf/impl:ddk/ddk_uapi_headers.bzl", _ddk_uapi_headers = "ddk_uapi_headers")
 load("//build/kernel/kleaf/impl:gki_artifacts.bzl", _gki_artifacts = "gki_artifacts", _gki_artifacts_prebuilts = "gki_artifacts_prebuilts")
 load("//build/kernel/kleaf/impl:image/kernel_images.bzl", _kernel_images = "kernel_images")
 load("//build/kernel/kleaf/impl:kernel_build.bzl", _kernel_build_macro = "kernel_build")
@@ -52,6 +53,7 @@
 ddk_headers = _ddk_headers
 ddk_module = _ddk_module
 ddk_submodule = _ddk_submodule
+ddk_uapi_headers = _ddk_uapi_headers
 extract_symbols = _extract_symbols
 gki_artifacts = _gki_artifacts
 gki_artifacts_prebuilts = _gki_artifacts_prebuilts
diff --git a/kleaf/tests/ddk_test/BUILD.bazel b/kleaf/tests/ddk_test/BUILD.bazel
index cd2ada2..61b14b7 100644
--- a/kleaf/tests/ddk_test/BUILD.bazel
+++ b/kleaf/tests/ddk_test/BUILD.bazel
@@ -19,6 +19,7 @@
 load(":ddk_images_test.bzl", "ddk_images_test_suite")
 load(":ddk_module_test.bzl", "ddk_module_test_suite")
 load(":ddk_submodule_test.bzl", "ddk_submodule_test")
+load(":ddk_uapi_headers_test.bzl", "ddk_uapi_headers_test_suite")
 load(":makefiles_test.bzl", "makefiles_test_suite")
 
 ddk_headers_test_suite(name = "ddk_headers_test_suite")
@@ -29,6 +30,8 @@
 
 ddk_submodule_test(name = "ddk_submodule_test")
 
+ddk_uapi_headers_test_suite(name = "ddk_uapi_headers_test_suite")
+
 makefiles_test_suite(name = "makefiles_test_suite")
 
 kernel_build(
diff --git a/kleaf/tests/ddk_test/ddk_uapi_headers_test.bzl b/kleaf/tests/ddk_test/ddk_uapi_headers_test.bzl
new file mode 100644
index 0000000..8bd66d2
--- /dev/null
+++ b/kleaf/tests/ddk_test/ddk_uapi_headers_test.bzl
@@ -0,0 +1,123 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# 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.
+"""
+Defines ddk_uapi_headers tests.
+"""
+
+load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
+load("//build/kernel/kleaf/impl:ddk/ddk_uapi_headers.bzl", "ddk_uapi_headers")
+load("//build/kernel/kleaf/impl:kernel_build.bzl", "kernel_build")
+load("//build/kernel/kleaf/tests:failure_test.bzl", "failure_test")
+
+def check_ddk_uapi_headers_info(env):
+    """Check that the number of outputs and the output's format are correct.
+
+    Args:
+        env: The test environment.
+    """
+    target_under_test = analysistest.target_under_test(env)
+    output_files = target_under_test[DefaultInfo].files.to_list()
+    output_file = output_files[0].basename
+    num_output_files = len(output_files)
+
+    asserts.equals(
+        env,
+        num_output_files,
+        1,
+        "Expected 1 output file, but found {} files".format(num_output_files),
+    )
+
+    asserts.true(
+        env,
+        output_file.endswith(".tar.gz"),
+        "Expected GZIP compressed tarball for output, but found {}".format(output_file),
+    )
+
+def _good_uapi_headers_test_impl(ctx):
+    env = analysistest.begin(ctx)
+    check_ddk_uapi_headers_info(env)
+    return analysistest.end(env)
+
+_good_uapi_headers_test = analysistest.make(
+    impl = _good_uapi_headers_test_impl,
+)
+
+def _ddk_uapi_headers_good_headers_test(
+        name,
+        srcs = None):
+    kernel_build(
+        name = name + "_kernel_build",
+        build_config = "build.config.fake",
+        outs = ["vmlinux"],
+        tags = ["manual"],
+    )
+
+    ddk_uapi_headers(
+        name = name + "_headers",
+        srcs = srcs,
+        out = "good_headers.tar.gz",
+        kernel_build = name + "_kernel_build",
+    )
+
+    _good_uapi_headers_test(
+        name = name,
+        target_under_test = name + "_headers",
+    )
+
+def _ddk_uapi_headers_bad_headers_test(name, srcs):
+    kernel_build(
+        name = name + "_kernel_build",
+        build_config = "build.config.fake",
+        outs = ["vmlinux"],
+        tags = ["manual"],
+    )
+
+    ddk_uapi_headers(
+        name = name + "_bad_headers_out_file_name",
+        srcs = srcs,
+        out = "bad-headers.gz",
+        kernel_build = name + "_kernel_build",
+    )
+
+    failure_test(
+        name = name,
+        target_under_test = name + "_bad_headers_out_file_name",
+        error_message_substrs = ["out-file name must end with"],
+    )
+
+def ddk_uapi_headers_test_suite(name):
+    """Defines analysis test for `ddk_uapi_headers`.
+
+    Args:
+        name: rule name
+    """
+
+    tests = []
+
+    _ddk_uapi_headers_good_headers_test(
+        name = name + "_good_headers_test",
+        srcs = ["include/uapi/uapi.h"],
+    )
+    tests.append(name + "_good_headers_test")
+
+    _ddk_uapi_headers_bad_headers_test(
+        name = name + "_bad_headers_test",
+        srcs = ["include/uapi/uapi.h"],
+    )
+    tests.append(name + "_bad_headers_test")
+
+    native.test_suite(
+        name = name,
+        tests = tests,
+    )
diff --git a/kleaf/tests/ddk_test/include/uapi/uapi.h b/kleaf/tests/ddk_test/include/uapi/uapi.h
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/kleaf/tests/ddk_test/include/uapi/uapi.h