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_gathering:gathering_providers.bzl", 23 "TransitiveLicensesInfo", 24) 25load("@rules_license//rules_gathering:trace.bzl", "TraceInfo") 26 27def _strip_null_repo(label): 28 """Removes the null repo name (e.g. @//) from a string. 29 30 The is to make str(label) compatible between bazel 5.x and 6.x 31 """ 32 s = str(label) 33 if s.startswith('@//'): 34 return s[1:] 35 elif s.startswith('@@//'): 36 return s[2:] 37 return s 38 39def _gather_licenses_info_impl(target, ctx): 40 return gather_metadata_info_common(target, ctx, TransitiveLicensesInfo, [], should_traverse) 41 42gather_licenses_info = aspect( 43 doc = """Collects LicenseInfo providers into a single TransitiveLicensesInfo provider.""", 44 implementation = _gather_licenses_info_impl, 45 attr_aspects = ["*"], 46 attrs = { 47 "_trace": attr.label(default = "@rules_license//rules:trace_target"), 48 }, 49 provides = [TransitiveLicensesInfo], 50 apply_to_generating_rules = True, 51) 52 53def _write_licenses_info_impl(target, ctx): 54 """Write transitive license info into a JSON file 55 56 Args: 57 target: The target of the aspect. 58 ctx: The aspect evaluation context. 59 60 Returns: 61 OutputGroupInfo 62 """ 63 64 if not TransitiveLicensesInfo in target: 65 return [OutputGroupInfo(licenses = depset())] 66 info = target[TransitiveLicensesInfo] 67 outs = [] 68 69 # If the result doesn't contain licenses, we simply return the provider 70 if not hasattr(info, "target_under_license"): 71 return [OutputGroupInfo(licenses = depset())] 72 73 # Write the output file for the target 74 name = "%s_licenses_info.json" % ctx.label.name 75 lic_info, _ = licenses_info_to_json(info) 76 content = "[\n%s\n]\n" % ",\n".join(lic_info) 77 out = ctx.actions.declare_file(name) 78 ctx.actions.write( 79 output = out, 80 content = content, 81 ) 82 outs.append(out) 83 84 if ctx.attr._trace[TraceInfo].trace: 85 trace = ctx.actions.declare_file("%s_trace_info.json" % ctx.label.name) 86 ctx.actions.write(output = trace, content = "\n".join(info.traces)) 87 outs.append(trace) 88 89 return [OutputGroupInfo(licenses = depset(outs))] 90 91gather_licenses_info_and_write = aspect( 92 doc = """Collects TransitiveLicensesInfo providers and writes JSON representation to a file. 93 94 Usage: 95 blaze build //some:target \ 96 --aspects=@rules_license//rules:gather_licenses_info.bzl%gather_licenses_info_and_write 97 --output_groups=licenses 98 """, 99 implementation = _write_licenses_info_impl, 100 attr_aspects = ["*"], 101 attrs = { 102 "_trace": attr.label(default = "@rules_license//rules:trace_target"), 103 }, 104 provides = [OutputGroupInfo], 105 requires = [gather_licenses_info], 106 apply_to_generating_rules = True, 107) 108 109def write_licenses_info(ctx, deps, json_out): 110 """Writes TransitiveLicensesInfo providers for a set of targets as JSON. 111 112 TODO(aiuto): Document JSON schema. But it is under development, so the current 113 best place to look is at tests/hello_licenses.golden. 114 115 Usage: 116 write_licenses_info must be called from a rule implementation, where the 117 rule has run the gather_licenses_info aspect on its deps to 118 collect the transitive closure of LicenseInfo providers into a 119 LicenseInfo provider. 120 121 foo = rule( 122 implementation = _foo_impl, 123 attrs = { 124 "deps": attr.label_list(aspects = [gather_licenses_info]) 125 } 126 ) 127 128 def _foo_impl(ctx): 129 ... 130 json_file = ctx.actions.declare_file("%s_licenses.json" % ctx.label.name) 131 license_files = write_licenses_info(ctx, ctx.attr.deps, json_file) 132 133 // process the json file and the license_files referenced by it 134 ctx.actions.run( 135 inputs = [json_file] + license_files 136 executable = ... 137 ) 138 139 Args: 140 ctx: context of the caller 141 deps: a list of deps which should have TransitiveLicensesInfo providers. 142 This requires that you have run the gather_licenses_info 143 aspect over them 144 json_out: output handle to write the JSON info 145 146 Returns: 147 A list of License File objects for each of the license paths referenced in the json. 148 """ 149 licenses_json = [] 150 licenses_files = [] 151 for dep in deps: 152 if TransitiveLicensesInfo in dep: 153 transitive_licenses_info = dep[TransitiveLicensesInfo] 154 lic_info, _ = licenses_info_to_json(transitive_licenses_info) 155 licenses_json.extend(lic_info) 156 for info in transitive_licenses_info.licenses.to_list(): 157 if info.license_text: 158 licenses_files.append(info.license_text) 159 160 ctx.actions.write( 161 output = json_out, 162 content = "[\n%s\n]\n" % ",\n".join(licenses_json), 163 ) 164 return licenses_files 165 166def licenses_info_to_json(licenses_info): 167 """Render a single LicenseInfo provider to JSON 168 169 Args: 170 licenses_info: A LicenseInfo. 171 172 Returns: 173 [(str)] list of LicenseInfo values rendered as JSON. 174 [(File)] list of Files containing license texts. 175 """ 176 177 main_template = """ {{ 178 "top_level_target": "{top_level_target}", 179 "dependencies": [{dependencies} 180 ], 181 "licenses": [{licenses} 182 ]\n }}""" 183 184 dep_template = """ 185 {{ 186 "target_under_license": "{target_under_license}", 187 "licenses": [ 188 {licenses} 189 ] 190 }}""" 191 192 # TODO(aiuto): 'rule' is a duplicate of 'label' until old users are transitioned 193 license_template = """ 194 {{ 195 "label": "{label}", 196 "rule": "{label}", 197 "license_kinds": [{kinds} 198 ], 199 "copyright_notice": "{copyright_notice}", 200 "package_name": "{package_name}", 201 "package_url": "{package_url}", 202 "package_version": "{package_version}", 203 "license_text": "{license_text}", 204 "used_by": [ 205 {used_by} 206 ] 207 }}""" 208 209 kind_template = """ 210 {{ 211 "target": "{kind_path}", 212 "name": "{kind_name}", 213 "long_name": "{kind_long_name}", 214 "conditions": {kind_conditions} 215 }}""" 216 217 # Build reverse map of license to user 218 used_by = {} 219 for dep in licenses_info.deps.to_list(): 220 # Undo the concatenation applied when stored in the provider. 221 dep_licenses = dep.licenses.split(",") 222 for license in dep_licenses: 223 if license not in used_by: 224 used_by[license] = [] 225 used_by[license].append(_strip_null_repo(dep.target_under_license)) 226 227 all_licenses = [] 228 all_license_text_files = [] 229 for license in sorted(licenses_info.licenses.to_list(), key = lambda x: x.label): 230 kinds = [] 231 for kind in sorted(license.license_kinds, key = lambda x: x.name): 232 if hasattr(kind, "long_name"): 233 long_name = kind.long_name 234 else: 235 long_name = "" 236 kinds.append(kind_template.format( 237 kind_name = kind.name, 238 kind_long_name = long_name, 239 kind_path = kind.label, 240 kind_conditions = kind.conditions, 241 )) 242 243 if license.license_text: 244 # Special handling for synthetic LicenseInfo 245 text_path = (license.license_text.package + "/" + license.license_text.name if type(license.license_text) == "Label" else license.license_text.path) 246 all_licenses.append(license_template.format( 247 copyright_notice = license.copyright_notice, 248 kinds = ",".join(kinds), 249 license_text = text_path, 250 package_name = license.package_name, 251 package_url = license.package_url, 252 package_version = license.package_version, 253 label = _strip_null_repo(license.label), 254 used_by = ",\n ".join(sorted(['"%s"' % x for x in used_by[str(license.label)]])), 255 )) 256 # Additionally return all File references so that other rules invoking 257 # this method can load license text file contents from external repos 258 # using runfiles 259 all_license_text_files.append(license.license_text) 260 all_deps = [] 261 for dep in sorted(licenses_info.deps.to_list(), key = lambda x: x.target_under_license): 262 # Undo the concatenation applied when stored in the provider. 263 dep_licenses = dep.licenses.split(",") 264 all_deps.append(dep_template.format( 265 target_under_license = _strip_null_repo(dep.target_under_license), 266 licenses = ",\n ".join(sorted(['"%s"' % _strip_null_repo(x) for x in dep_licenses])), 267 )) 268 269 return [main_template.format( 270 top_level_target = _strip_null_repo(licenses_info.target_under_license), 271 dependencies = ",".join(all_deps), 272 licenses = ",".join(all_licenses), 273 )], all_license_text_files 274