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