1load("@rules_license//rules:providers.bzl", "LicenseInfo") 2load("//build/bazel/rules:metadata.bzl", "MetadataFileInfo") 3 4RuleLicensedDependenciesInfo = provider( 5 doc = """Rule's licensed dependencies.""", 6 fields = dict( 7 license_closure = "depset(license) for the rule and its licensed dependencies", 8 ), 9) 10 11def _maybe_expand(rule, transitive_licenses): 12 if not RuleLicensedDependenciesInfo in rule: 13 return 14 dep_info = rule[RuleLicensedDependenciesInfo] 15 if hasattr(dep_info, "license_closure"): 16 transitive_licenses.append(dep_info.license_closure) 17 18def create_metadata_file_info(ctx): 19 if hasattr(ctx.rule.attr, "applicable_licenses"): 20 for lic in ctx.rule.attr.applicable_licenses: 21 files = lic.files.to_list() 22 if len(files) == 1 and files[0].basename == "METADATA": 23 return MetadataFileInfo(metadata_file = files[0]) 24 25 return MetadataFileInfo(metadata_file = None) 26 27def _rule_licenses_aspect_impl(_rule, ctx): 28 if ctx.rule.kind == "_license": 29 return [RuleLicensedDependenciesInfo(), MetadataFileInfo()] 30 31 licenses = [] 32 transitive_licenses = [] 33 if hasattr(ctx.rule.attr, "applicable_licenses"): 34 licenses.extend(ctx.rule.attr.applicable_licenses) 35 36 for a in dir(ctx.rule.attr): 37 # Ignore private attributes 38 if a.startswith("_"): 39 continue 40 value = getattr(ctx.rule.attr, a) 41 vlist = value if type(value) == type([]) else [value] 42 for item in vlist: 43 if type(item) == "Target" and RuleLicensedDependenciesInfo in item: 44 _maybe_expand(item, transitive_licenses) 45 46 return [ 47 RuleLicensedDependenciesInfo(license_closure = depset(licenses, transitive = transitive_licenses)), 48 create_metadata_file_info(ctx), 49 ] 50 51license_aspect = aspect( 52 doc = """Collect transitive license closure.""", 53 implementation = _rule_licenses_aspect_impl, 54 attr_aspects = ["*"], 55 apply_to_generating_rules = True, 56 provides = [RuleLicensedDependenciesInfo, MetadataFileInfo], 57) 58 59_license_kind_template = """ 60 {{ 61 "target": "{kind_path}", 62 "name": "{kind_name}", 63 "conditions": {kind_conditions} 64 }}""" 65 66def _license_kind_to_json(kind): 67 return _license_kind_template.format(kind_name = kind.name, kind_path = kind.label, kind_conditions = kind.conditions) 68 69def _quotes_or_null(s): 70 if not s: 71 return "null" 72 return s 73 74def _license_file(license_rule): 75 file = license_rule[LicenseInfo].license_text 76 return file if file and file.basename != "__NO_LICENSE__" else struct(path = "") 77 78def _divine_package_name(license): 79 if license.package_name: 80 return license.package_name.removeprefix("external").removesuffix("BUILD.bazel").replace("/", " ").strip() 81 return license.rule.name.removeprefix("external_").removesuffix("_license").replace("_", " ") 82 83def license_map(deps): 84 """Collects license to licensees map for the given set of rule targets. 85 86 TODO(asmundak): at the moment licensees lists are all empty because collecting 87 the licensees turned out to be too slow. Restore this later. 88 Args: 89 deps: list of rule targets 90 Returns: 91 dictionary mapping a license to its licensees 92 """ 93 transitive_licenses = [] 94 for d in deps: 95 _maybe_expand(d, transitive_licenses) 96 97 # Each rule provides the closure of its licenses, let us build the 98 # reverse map. A minor quirk is that for some reason there may be 99 # multiple license instances with with the same label. Use the 100 # intermediary dict to map rule's label to its first instance 101 license_by_label = dict() 102 licensees = dict() 103 for lic in depset(transitive = transitive_licenses).to_list(): 104 if not LicenseInfo in lic: 105 continue 106 label = lic[LicenseInfo].label.name 107 if not label in license_by_label: 108 license_by_label[label] = lic 109 licensees[lic] = [] 110 return licensees 111 112_license_template = """ {{ 113 "rule": "{rule}", 114 "license_kinds": [{kinds} 115 ], 116 "copyright_notice": "{copyright_notice}", 117 "package_name": "{package_name}", 118 "package_url": {package_url}, 119 "package_version": {package_version}, 120 "license_text": "{license_text}", 121 "licensees": [ 122 "{licensees}" 123 ] 124 \n }}""" 125 126def _used_license_to_json(license_rule, licensed_rules): 127 license = license_rule[LicenseInfo] 128 return _license_template.format( 129 rule = license.label.name, 130 copyright_notice = license.copyright_notice, 131 package_name = _divine_package_name(license), 132 package_url = _quotes_or_null(license.package_url), 133 package_version = _quotes_or_null(license.package_version), 134 license_text = _license_file(license_rule).path, 135 kinds = ",\n".join([_license_kind_to_json(kind) for kind in license.license_kinds]), 136 licensees = "\",\n \"".join([r for r in licensed_rules]), 137 ) 138 139def license_map_to_json(licensees): 140 """Returns an array of JSON representations of a license and its licensees. """ 141 return [_used_license_to_json(lic, rules) for lic, rules in licensees.items()] 142 143def license_map_notice_files(licensees): 144 """Returns an array of license text files for the given licensee map. 145 146 Args: 147 licensees: dict returned by license_map() call 148 Returns: 149 the list of notice files this licensees map depends on. 150 """ 151 notice_files = [] 152 for lic in licensees.keys(): 153 file = _license_file(lic) 154 if file.path: 155 notice_files.append(file) 156 return notice_files 157