1# Copyright (C) 2022 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15load("@bazel_skylib//lib:paths.bzl", "paths")
16
17AidlGenInfo = provider(
18    fields = [
19        "srcs",
20        "hdrs",
21        "hash_file",
22        "transitive_srcs",
23        "transitive_include_dirs",
24        "flags",
25    ],
26)
27
28def _symlink_aidl_srcs(ctx, srcs, strip_import_prefix):
29    virtual_imports = paths.join("_virtual_imports", ctx.label.name)
30    include_path = paths.join(ctx.genfiles_dir.path, ctx.label.package, virtual_imports)
31    workspace_root_strip_import_prefix = paths.join(ctx.label.package, strip_import_prefix)
32
33    direct_srcs = []
34    for src in srcs:
35        src_path = src.short_path
36
37        if not paths.normalize(src_path).startswith(paths.normalize(workspace_root_strip_import_prefix)):
38            fail(".aidl file '%s' is not under the specified strip prefix '%s'" %
39                 (src_path, workspace_root_strip_import_prefix))
40
41        import_path = paths.relativize(src_path, workspace_root_strip_import_prefix)
42        virtual_src = ctx.actions.declare_file(paths.join(virtual_imports, import_path))
43        ctx.actions.symlink(
44            output = virtual_src,
45            target_file = src,
46            progress_message = "Symlinking virtual .aidl sources for %{label}",
47        )
48        direct_srcs.append(virtual_src)
49    return include_path, direct_srcs
50
51# https://cs.android.com/android/platform/system/tools/aidl/+/master:build/aidl_api.go;l=718-724;drc=87bcb923b4ed9cf6e6837f4cc02d954f211c0b12
52def _version_for_hash_gen(version):
53    if int(version) > 1:
54        return int(version) - 1
55    return "latest-version"
56
57def _get_aidl_interface_name(versioned_name):
58    parts = versioned_name.split("-V")
59    if len(parts) == 1 or not parts[-1].isdigit():
60        fail("{}'s version is not parsable", versioned_name)
61
62    return parts[0]
63
64def _verify_hash_file(ctx):
65    timestamp = ctx.actions.declare_file(ctx.label.name + "_checkhash_" + ctx.attr.version + ".timestamp")
66
67    api_dir = "{package_dir}/aidl_api/{aidl_interface_name}/{version}".format(
68        package_dir = paths.dirname(ctx.build_file_path),
69        aidl_interface_name = _get_aidl_interface_name(ctx.label.name),
70        version = ctx.attr.version,
71    )
72
73    shell_command = """
74        cd {api_dir}
75        aidl_files_checksums=$(find ./ -name "*.aidl" -print0 | LC_ALL=C sort -z | xargs -0 sha1sum && echo {version})
76        cd -
77
78        if [[ $(echo "$aidl_files_checksums" | sha1sum | cut -d " " -f 1) = $(tail -1 {hash_file}) ]]; then
79            touch {timestamp};
80        else
81            cat "{message_check_equality}"
82            exit 1;
83        fi;
84    """.format(
85        api_dir = api_dir,
86        aidl_files = " ".join([src.path for src in ctx.files.srcs]),
87        version = _version_for_hash_gen(ctx.attr.version),
88        hash_file = ctx.file.hash_file.path,
89        timestamp = timestamp.path,
90        message_check_equality = ctx.file._message_check_equality.path,
91    )
92
93    ctx.actions.run_shell(
94        inputs = ctx.files.srcs + [ctx.file.hash_file, ctx.file._message_check_equality],
95        outputs = [timestamp],
96        command = shell_command,
97        mnemonic = "AidlHashValidation",
98        progress_message = "Validating AIDL .hash file",
99    )
100
101    return timestamp
102
103def _aidl_library_rule_impl(ctx):
104    transitive_srcs = []
105    transitive_include_dirs = []
106
107    validation_output = []
108
109    if ctx.attr.hash_file and ctx.attr.version:
110        # if the aidl_library represents an aidl_interface frozen version,
111        # hash_file and version attributes are set
112        validation_output.append(_verify_hash_file(ctx))
113
114    aidl_import_infos = [d[AidlGenInfo] for d in ctx.attr.deps]
115    for info in aidl_import_infos:
116        transitive_srcs.append(info.transitive_srcs)
117        transitive_include_dirs.append(info.transitive_include_dirs)
118
119    include_path, srcs = _symlink_aidl_srcs(ctx, ctx.files.srcs, ctx.attr.strip_import_prefix)
120    _, hdrs = _symlink_aidl_srcs(ctx, ctx.files.hdrs, ctx.attr.strip_import_prefix)
121
122    return [
123        DefaultInfo(files = depset(ctx.files.srcs)),
124        OutputGroupInfo(
125            _validation = depset(direct = validation_output),
126        ),
127        AidlGenInfo(
128            srcs = depset(srcs),
129            hdrs = depset(hdrs),
130            hash_file = ctx.file.hash_file,
131            transitive_srcs = depset(
132                direct = srcs + hdrs,
133                transitive = transitive_srcs,
134            ),
135            transitive_include_dirs = depset(
136                direct = [include_path],
137                transitive = transitive_include_dirs,
138                # build with preorder so that transitive_include_dirs.to_list()
139                # return direct include path in the first element
140                order = "preorder",
141            ),
142            flags = ctx.attr.flags,
143        ),
144    ]
145
146aidl_library = rule(
147    implementation = _aidl_library_rule_impl,
148    attrs = {
149        "srcs": attr.label_list(
150            allow_files = [".aidl"],
151            doc = "AIDL source files that contain StructuredParcelable" +
152                  " AIDL defintions. These files can be compiled to language" +
153                  " bindings.",
154        ),
155        "hdrs": attr.label_list(
156            allow_files = [".aidl"],
157            doc = "AIDL source files that contain UnstructuredParcelable" +
158                  " AIDL defintions. These files cannot be compiled to language" +
159                  " bindings, but can be referenced by other AIDL sources.",
160        ),
161        "version": attr.string(
162            doc = "The version of the upstream aidl_interface that" +
163                  " the aidl_library is created for",
164        ),
165        "hash_file": attr.label(
166            doc = "The .hash file in the api directory of an aidl_interface frozen version",
167            allow_single_file = [".hash"],
168        ),
169        "_message_check_equality": attr.label(
170            allow_single_file = [".txt"],
171            default = "//system/tools/aidl/build:message_check_equality.txt",
172        ),
173        "deps": attr.label_list(
174            providers = [AidlGenInfo],
175            doc = "Targets listed here provide AIDL sources referenced" +
176                  "by this library.",
177        ),
178        "strip_import_prefix": attr.string(
179            doc = "The prefix to strip from the paths of the .aidl files in " +
180                  "this rule. When set, aidl source files in the srcs " +
181                  "attribute of this rule are accessible at their path with " +
182                  "this prefix cut off.",
183        ),
184        "flags": attr.string_list(
185            doc = "Flags to pass to AIDL tool",
186        ),
187    },
188    provides = [AidlGenInfo],
189)
190
191def _generate_aidl_bindings(ctx, lang, aidl_info):
192    """ Utility function for creating AIDL bindings from aidl_libraries.
193
194    Args:
195      ctx: context, used for declaring actions and new files and providing _aidl_tool
196      lang: string, defines the language of the generated binding code
197      aidl_src_infos: AidlGenInfo, list of sources to provide to AIDL compiler
198
199    Returns:
200        list of output files
201    """
202
203    #TODO(b/235113507) support C++ AIDL binding
204    ext = ""
205    if lang == "java":
206        ext = ".java"
207    else:
208        fail("Cannot generate AIDL language bindings for `{}`.".format(lang))
209
210    out_files = []
211    for aidl_file in aidl_info.srcs.to_list():
212        out_filename = paths.replace_extension(aidl_file.basename, ext)
213        out_file = ctx.actions.declare_file(out_filename, sibling = aidl_file)
214        out_files.append(out_file)
215
216        args = ctx.actions.args()
217        args.add_all(aidl_info.flags)
218
219        #TODO(b/241139797) allow this flag to be controlled by an attribute
220        args.add("--structured")
221
222        args.add_all([
223            "-I {}".format(i)
224            for i in aidl_info.transitive_include_dirs.to_list()
225        ])
226        args.add(aidl_file.path)
227        args.add(out_file)
228
229        ctx.actions.run(
230            inputs = aidl_info.transitive_srcs,
231            outputs = [out_file],
232            arguments = [args],
233            progress_message = "Generating {} AIDL binding from {}".format(lang, aidl_file.short_path),
234            executable = ctx.executable._aidl_tool,
235        )
236
237    return out_files
238
239aidl_file_utils = struct(
240    generate_aidl_bindings = _generate_aidl_bindings,
241)
242