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