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