1# Copyright 2022 Google LLC 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# https://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"""Rules and macros for collecting LicenseInfo providers.""" 15 16load( 17 "@rules_license//rules:licenses_core.bzl", 18 "TraceInfo", 19 "gather_metadata_info_common", 20 "should_traverse", 21) 22load( 23 "@rules_license//rules:providers.bzl", 24 "ExperimentalMetadataInfo", 25 "PackageInfo", 26) 27load( 28 "@rules_license//rules/private:gathering_providers.bzl", 29 "TransitiveMetadataInfo", 30) 31 32# Definition for compliance namespace, used for filtering licenses 33# based on the namespace to which they belong. 34NAMESPACES = ["compliance"] 35 36def _strip_null_repo(label): 37 """Removes the null repo name (e.g. @//) from a string. 38 39 The is to make str(label) compatible between bazel 5.x and 6.x 40 """ 41 s = str(label) 42 if s.startswith('@//'): 43 return s[1:] 44 elif s.startswith('@@//'): 45 return s[2:] 46 return s 47 48def _bazel_package(label): 49 clean_label = _strip_null_repo(label) 50 return clean_label[0:-(len(label.name) + 1)] 51 52def _gather_metadata_info_impl(target, ctx): 53 return gather_metadata_info_common( 54 target, 55 ctx, 56 TransitiveMetadataInfo, 57 NAMESPACES, 58 [ExperimentalMetadataInfo, PackageInfo], 59 should_traverse) 60 61gather_metadata_info = aspect( 62 doc = """Collects LicenseInfo providers into a single TransitiveMetadataInfo provider.""", 63 implementation = _gather_metadata_info_impl, 64 attr_aspects = ["*"], 65 attrs = { 66 "_trace": attr.label(default = "@rules_license//rules:trace_target"), 67 }, 68 provides = [TransitiveMetadataInfo], 69 apply_to_generating_rules = True, 70) 71 72def _write_metadata_info_impl(target, ctx): 73 """Write transitive license info into a JSON file 74 75 Args: 76 target: The target of the aspect. 77 ctx: The aspect evaluation context. 78 79 Returns: 80 OutputGroupInfo 81 """ 82 83 if not TransitiveMetadataInfo in target: 84 return [OutputGroupInfo(licenses = depset())] 85 info = target[TransitiveMetadataInfo] 86 outs = [] 87 88 # If the result doesn't contain licenses, we simply return the provider 89 if not hasattr(info, "target_under_license"): 90 return [OutputGroupInfo(licenses = depset())] 91 92 # Write the output file for the target 93 name = "%s_metadata_info.json" % ctx.label.name 94 content = "[\n%s\n]\n" % ",\n".join(metadata_info_to_json(info)) 95 out = ctx.actions.declare_file(name) 96 ctx.actions.write( 97 output = out, 98 content = content, 99 ) 100 outs.append(out) 101 102 if ctx.attr._trace[TraceInfo].trace: 103 trace = ctx.actions.declare_file("%s_trace_info.json" % ctx.label.name) 104 ctx.actions.write(output = trace, content = "\n".join(info.traces)) 105 outs.append(trace) 106 107 return [OutputGroupInfo(licenses = depset(outs))] 108 109gather_metadata_info_and_write = aspect( 110 doc = """Collects TransitiveMetadataInfo providers and writes JSON representation to a file. 111 112 Usage: 113 bazel build //some:target \ 114 --aspects=@rules_license//rules:gather_metadata_info.bzl%gather_metadata_info_and_write 115 --output_groups=licenses 116 """, 117 implementation = _write_metadata_info_impl, 118 attr_aspects = ["*"], 119 attrs = { 120 "_trace": attr.label(default = "@rules_license//rules:trace_target"), 121 }, 122 provides = [OutputGroupInfo], 123 requires = [gather_metadata_info], 124 apply_to_generating_rules = True, 125) 126 127def write_metadata_info(ctx, deps, json_out): 128 """Writes TransitiveMetadataInfo providers for a set of targets as JSON. 129 130 TODO(aiuto): Document JSON schema. But it is under development, so the current 131 best place to look is at tests/hello_licenses.golden. 132 133 Usage: 134 write_metadata_info must be called from a rule implementation, where the 135 rule has run the gather_metadata_info aspect on its deps to 136 collect the transitive closure of LicenseInfo providers into a 137 LicenseInfo provider. 138 139 foo = rule( 140 implementation = _foo_impl, 141 attrs = { 142 "deps": attr.label_list(aspects = [gather_metadata_info]) 143 } 144 ) 145 146 def _foo_impl(ctx): 147 ... 148 out = ctx.actions.declare_file("%s_licenses.json" % ctx.label.name) 149 write_metadata_info(ctx, ctx.attr.deps, metadata_file) 150 151 Args: 152 ctx: context of the caller 153 deps: a list of deps which should have TransitiveMetadataInfo providers. 154 This requires that you have run the gather_metadata_info 155 aspect over them 156 json_out: output handle to write the JSON info 157 """ 158 licenses = [] 159 for dep in deps: 160 if TransitiveMetadataInfo in dep: 161 licenses.extend(metadata_info_to_json(dep[TransitiveMetadataInfo])) 162 ctx.actions.write( 163 output = json_out, 164 content = "[\n%s\n]\n" % ",\n".join(licenses), 165 ) 166 167def metadata_info_to_json(metadata_info): 168 """Render a single LicenseInfo provider to JSON 169 170 Args: 171 metadata_info: A LicenseInfo. 172 173 Returns: 174 [(str)] list of LicenseInfo values rendered as JSON. 175 """ 176 177 main_template = """ {{ 178 "top_level_target": "{top_level_target}", 179 "dependencies": [{dependencies} 180 ], 181 "licenses": [{licenses} 182 ], 183 "packages": [{packages} 184 ]\n }}""" 185 186 dep_template = """ 187 {{ 188 "target_under_license": "{target_under_license}", 189 "licenses": [ 190 {licenses} 191 ] 192 }}""" 193 194 license_template = """ 195 {{ 196 "label": "{label}", 197 "bazel_package": "{bazel_package}", 198 "license_kinds": [{kinds} 199 ], 200 "copyright_notice": "{copyright_notice}", 201 "package_name": "{package_name}", 202 "package_url": "{package_url}", 203 "package_version": "{package_version}", 204 "license_text": "{license_text}", 205 "used_by": [ 206 {used_by} 207 ] 208 }}""" 209 210 kind_template = """ 211 {{ 212 "target": "{kind_path}", 213 "name": "{kind_name}", 214 "conditions": {kind_conditions} 215 }}""" 216 217 package_info_template = """ 218 {{ 219 "target": "{label}", 220 "bazel_package": "{bazel_package}", 221 "package_name": "{package_name}", 222 "package_url": "{package_url}", 223 "package_version": "{package_version}" 224 }}""" 225 226 # Build reverse map of license to user 227 used_by = {} 228 for dep in metadata_info.deps.to_list(): 229 # Undo the concatenation applied when stored in the provider. 230 dep_licenses = dep.licenses.split(",") 231 for license in dep_licenses: 232 if license not in used_by: 233 used_by[license] = [] 234 used_by[license].append(_strip_null_repo(dep.target_under_license)) 235 236 all_licenses = [] 237 for license in sorted(metadata_info.licenses.to_list(), key = lambda x: x.label): 238 kinds = [] 239 for kind in sorted(license.license_kinds, key = lambda x: x.name): 240 kinds.append(kind_template.format( 241 kind_name = kind.name, 242 kind_path = kind.label, 243 kind_conditions = kind.conditions, 244 )) 245 246 if license.license_text: 247 # Special handling for synthetic LicenseInfo 248 text_path = (license.license_text.package + "/" + license.license_text.name if type(license.license_text) == "Label" else license.license_text.path) 249 all_licenses.append(license_template.format( 250 copyright_notice = license.copyright_notice, 251 kinds = ",".join(kinds), 252 license_text = text_path, 253 package_name = license.package_name, 254 package_url = license.package_url, 255 package_version = license.package_version, 256 label = _strip_null_repo(license.label), 257 bazel_package = _bazel_package(license.label), 258 used_by = ",\n ".join(sorted(['"%s"' % x for x in used_by[str(license.label)]])), 259 )) 260 261 all_deps = [] 262 for dep in sorted(metadata_info.deps.to_list(), key = lambda x: x.target_under_license): 263 # Undo the concatenation applied when stored in the provider. 264 dep_licenses = dep.licenses.split(",") 265 all_deps.append(dep_template.format( 266 target_under_license = _strip_null_repo(dep.target_under_license), 267 licenses = ",\n ".join(sorted(['"%s"' % _strip_null_repo(x) for x in dep_licenses])), 268 )) 269 270 all_packages = [] 271 # We would use this if we had distinct depsets for every provider type. 272 #for package in sorted(metadata_info.package_info.to_list(), key = lambda x: x.label): 273 # all_packages.append(package_info_template.format( 274 # label = _strip_null_repo(package.label), 275 # package_name = package.package_name, 276 # package_url = package.package_url, 277 # package_version = package.package_version, 278 # )) 279 280 for mi in sorted(metadata_info.other_metadata.to_list(), key = lambda x: x.label): 281 # Maybe use a map of provider class to formatter. A generic dict->json function 282 # in starlark would help 283 284 # This format is for using distinct providers. I like the compile time safety. 285 if mi.type == "package_info": 286 all_packages.append(package_info_template.format( 287 label = _strip_null_repo(mi.label), 288 bazel_package = _bazel_package(mi.label), 289 package_name = mi.package_name, 290 package_url = mi.package_url, 291 package_version = mi.package_version, 292 )) 293 # experimental: Support the ExperimentalMetadataInfo bag of data 294 if mi.type == "package_info_alt": 295 all_packages.append(package_info_template.format( 296 label = _strip_null_repo(mi.label), 297 bazel_package = _bazel_package(mi.label), 298 # data is just a bag, so we need to use get() or "" 299 package_name = mi.data.get("package_name") or "", 300 package_url = mi.data.get("package_url") or "", 301 package_version = mi.data.get("package_version") or "", 302 )) 303 304 return [main_template.format( 305 top_level_target = _strip_null_repo(metadata_info.target_under_license), 306 dependencies = ",".join(all_deps), 307 licenses = ",".join(all_licenses), 308 packages = ",".join(all_packages), 309 )] 310