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