1# Copyright (C) 2021 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 15"""Rule used to import artifacts prebuilt by Soong into the Bazel workspace. 16 17The rule returns a DefaultInfo provider with all artifacts and runtime dependencies, 18and a SoongPrebuiltInfo provider with the original Soong module name, artifacts, 19runtime dependencies and data dependencies. 20""" 21 22load("//bazel/rules:platform_transitions.bzl", "device_transition") 23load("//bazel/rules:common_settings.bzl", "BuildSettingInfo") 24 25SoongPrebuiltInfo = provider( 26 doc = "Info about a prebuilt Soong build module", 27 fields = { 28 "module_name": "Name of the original Soong build module", 29 # This field contains this target's outputs and all runtime dependency 30 # outputs. 31 "transitive_runtime_outputs": "Files required in the runtime environment", 32 "transitive_test_files": "Files of test modules", 33 "platform_flavor": "The platform flavor that this target will be built on", 34 }, 35) 36 37def _soong_prebuilt_impl(ctx): 38 files = ctx.files.files 39 40 # Ensure that soong_prebuilt targets always have at least one file to avoid 41 # evaluation errors when running Bazel cquery on a clean tree to find 42 # dependencies. 43 # 44 # This happens because soong_prebuilt dependency target globs don't match 45 # any files when the workspace symlinks are broken and point to build 46 # artifacts that still don't exist. This in turn causes errors in rules 47 # that reference these targets via attributes with allow_single_file=True 48 # and which expect a file to be present. 49 # 50 # Note that the below action is never really executed during cquery 51 # evaluation but fails when run as part of a test execution to signal that 52 # prebuilts were not correctly imported. 53 if not files: 54 placeholder_file = ctx.actions.declare_file(ctx.label.name + ".missing") 55 56 progress_message = ( 57 "Attempting to import missing artifacts for Soong module '%s'; " + 58 "please make sure that the module is built with Soong before " + 59 "running Bazel" 60 ) % ctx.attr.module_name 61 62 # Note that we don't write the file for the action to always be 63 # executed and display the warning message. 64 ctx.actions.run_shell( 65 outputs = [placeholder_file], 66 command = "/bin/false", 67 progress_message = progress_message, 68 ) 69 files = [placeholder_file] 70 71 runfiles = ctx.runfiles(files = files).merge_all([ 72 dep[DefaultInfo].default_runfiles 73 for dep in ctx.attr.runtime_deps + ctx.attr.data + ctx.attr.device_data 74 ]) 75 76 # We exclude the outputs of static dependencies from the runfiles since 77 # they're already embedded in this target's output. Note that this is done 78 # recursively such that only transitive runtime dependency outputs are 79 # included. For example, in a chain A -> B -> C -> D where B and C are 80 # statically linked, only A's and D's outputs would remain in the runfiles. 81 runfiles = runfiles.merge_all([ 82 ctx.runfiles( 83 files = _exclude_files( 84 dep[DefaultInfo].default_runfiles.files, 85 dep[DefaultInfo].files, 86 ).to_list(), 87 ) 88 for dep in ctx.attr.static_deps 89 ]) 90 91 return [ 92 _make_soong_prebuilt_info( 93 ctx.attr.module_name, 94 ctx.attr._platform_flavor[BuildSettingInfo].value, 95 files = files, 96 runtime_deps = ctx.attr.runtime_deps, 97 static_deps = ctx.attr.static_deps, 98 data = ctx.attr.data, 99 device_data = ctx.attr.device_data, 100 suites = ctx.attr.suites, 101 ), 102 DefaultInfo( 103 files = depset(files), 104 runfiles = runfiles, 105 ), 106 ] 107 108soong_prebuilt = rule( 109 attrs = { 110 "module_name": attr.string(), 111 # Artifacts prebuilt by Soong. 112 "files": attr.label_list(allow_files = True), 113 # Targets that are needed by this target during runtime. 114 "runtime_deps": attr.label_list(), 115 # Note that while the outputs of static deps are not required for test 116 # execution we include them since they have their own runtime 117 # dependencies. 118 "static_deps": attr.label_list(), 119 "data": attr.label_list(), 120 "device_data": attr.label_list( 121 cfg = device_transition, 122 ), 123 "suites": attr.string_list(), 124 "_platform_flavor": attr.label(default = "//bazel/rules:platform_flavor"), 125 # This attribute is required to use Starlark transitions. It allows 126 # allowlisting usage of this rule. For more information, see 127 # https://docs.bazel.build/versions/master/skylark/config.html#user-defined-transitions 128 "_allowlist_function_transition": attr.label( 129 default = "@bazel_tools//tools/allowlists/function_transition_allowlist", 130 ), 131 }, 132 implementation = _soong_prebuilt_impl, 133 doc = "A rule that imports artifacts prebuilt by Soong into the Bazel workspace", 134) 135 136def _soong_uninstalled_prebuilt_impl(ctx): 137 runfiles = ctx.runfiles().merge_all([ 138 dep[DefaultInfo].default_runfiles 139 for dep in ctx.attr.runtime_deps 140 ]) 141 142 return [ 143 _make_soong_prebuilt_info( 144 ctx.attr.module_name, 145 ctx.attr._platform_flavor[BuildSettingInfo].value, 146 runtime_deps = ctx.attr.runtime_deps, 147 ), 148 DefaultInfo( 149 runfiles = runfiles, 150 ), 151 ] 152 153soong_uninstalled_prebuilt = rule( 154 attrs = { 155 "module_name": attr.string(), 156 "runtime_deps": attr.label_list(), 157 "_platform_flavor": attr.label(default = "//bazel/rules:platform_flavor"), 158 }, 159 implementation = _soong_uninstalled_prebuilt_impl, 160 doc = "A rule for targets with no runtime outputs", 161) 162 163def _make_soong_prebuilt_info( 164 module_name, 165 platform_flavor, 166 files = [], 167 runtime_deps = [], 168 static_deps = [], 169 data = [], 170 device_data = [], 171 suites = []): 172 """Build a SoongPrebuiltInfo based on the given information. 173 174 Args: 175 runtime_deps: List of runtime dependencies required by this target. 176 static_deps: List of static dependencies required by this target. 177 data: List of data required by this target. 178 device_data: List of data on device variant required by this target. 179 suites: List of test suites this target belongs to. 180 181 Returns: 182 An instance of SoongPrebuiltInfo. 183 """ 184 transitive_runtime_outputs = [ 185 dep[SoongPrebuiltInfo].transitive_runtime_outputs 186 for dep in runtime_deps 187 ] 188 189 # We exclude the outputs of static dependencies and data dependencies from 190 # the transitive runtime outputs since static dependencies are already 191 # embedded in this target's output and the data dependencies shouldn't be 192 # present in the runtime paths. Note that this is done recursively such that 193 # only transitive runtime dependency outputs are included. For example, in a 194 # chain A -> B -> C -> D where B and C are statically linked or data 195 # dependencies, only A's and D's outputs would remain in the transitive 196 # runtime outputs. 197 transitive_runtime_outputs.extend([ 198 _exclude_files( 199 dep[SoongPrebuiltInfo].transitive_runtime_outputs, 200 dep[DefaultInfo].files, 201 ) 202 for dep in static_deps + data 203 ]) 204 return SoongPrebuiltInfo( 205 module_name = module_name, 206 platform_flavor = platform_flavor, 207 transitive_runtime_outputs = depset(files, transitive = transitive_runtime_outputs), 208 transitive_test_files = depset( 209 # Note that `suites` is never empty for test files. This because 210 # test build modules that do not explicitly specify a `test_suites` 211 # Soong attribute belong to `null-suite`. 212 files if suites else [], 213 transitive = [ 214 dep[SoongPrebuiltInfo].transitive_test_files 215 for dep in data + device_data + runtime_deps 216 ], 217 ), 218 ) 219 220def _exclude_files(all_files, files_to_exclude): 221 files = [] 222 files_to_exclude = {f: None for f in files_to_exclude.to_list()} 223 for f in all_files.to_list(): 224 if f not in files_to_exclude: 225 files.append(f) 226 return depset(files) 227