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"""SBOM generation""" 15 16load( 17 "@rules_license//rules_gathering:gather_metadata.bzl", 18 "gather_metadata_info", 19 "gather_metadata_info_and_write", 20 "write_metadata_info", 21) 22load( 23 "@rules_license//rules_gathering:gathering_providers.bzl", 24 "TransitiveLicensesInfo", 25) 26 27# This rule is proof of concept, and may not represent the final 28# form of a rule for compliance validation. 29def _generate_sbom_impl(ctx): 30 # Gather all licenses and write information to one place 31 32 licenses_file = ctx.actions.declare_file("_%s_licenses_info.json" % ctx.label.name) 33 write_metadata_info(ctx, ctx.attr.deps, licenses_file) 34 35 # Now turn the big blob of data into something consumable. 36 inputs = [licenses_file] 37 outputs = [ctx.outputs.out] 38 args = ctx.actions.args() 39 args.add("--licenses_info", licenses_file.path) 40 args.add("--out", ctx.outputs.out.path) 41 ctx.actions.run( 42 mnemonic = "CreateSBOM", 43 progress_message = "Creating SBOM for %s" % ctx.label, 44 inputs = inputs, 45 outputs = outputs, 46 executable = ctx.executable._sbom_generator, 47 arguments = [args], 48 ) 49 return [ 50 DefaultInfo(files = depset(outputs)), 51 OutputGroupInfo(licenses_file = depset([licenses_file])), 52 ] 53 54_generate_sbom = rule( 55 implementation = _generate_sbom_impl, 56 attrs = { 57 "deps": attr.label_list( 58 aspects = [gather_metadata_info], 59 ), 60 "out": attr.output(mandatory = True), 61 "_sbom_generator": attr.label( 62 default = Label("@rules_license//tools:write_sbom"), 63 executable = True, 64 allow_files = True, 65 cfg = "exec", 66 ), 67 }, 68) 69 70def generate_sbom(**kwargs): 71 _generate_sbom(**kwargs) 72 73def _manifest_impl(ctx): 74 # Gather all licenses and make it available as deps for downstream rules 75 # Additionally write the list of license filenames to a file that can 76 # also be used as an input to downstream rules. 77 licenses_file = ctx.actions.declare_file(ctx.attr.out.name) 78 mappings = get_licenses_mapping(ctx.attr.deps, ctx.attr.warn_on_legacy_licenses) 79 ctx.actions.write( 80 output = licenses_file, 81 content = "\n".join([",".join([f.path, p]) for (f, p) in mappings.items()]), 82 ) 83 return [DefaultInfo(files = depset(mappings.keys()))] 84 85_manifest = rule( 86 implementation = _manifest_impl, 87 doc = """Internal tmplementation method for manifest().""", 88 attrs = { 89 "deps": attr.label_list( 90 doc = """List of targets to collect license files for.""", 91 aspects = [gather_metadata_info], 92 ), 93 "out": attr.output( 94 doc = """Output file.""", 95 mandatory = True, 96 ), 97 "warn_on_legacy_licenses": attr.bool(default = False), 98 }, 99) 100 101def manifest(name, deps, out = None, **kwargs): 102 if not out: 103 out = name + ".manifest" 104 105 _manifest(name = name, deps = deps, out = out, **kwargs) 106 107def get_licenses_mapping(deps, warn = False): 108 """Creates list of entries representing all licenses for the deps. 109 110 Args: 111 112 deps: a list of deps which should have TransitiveLicensesInfo providers. 113 This requires that you have run the gather_licenses_info 114 aspect over them 115 116 warn: boolean, if true, display output about legacy targets that need 117 update 118 119 Returns: 120 {File:package_name} 121 """ 122 tls = [] 123 for dep in deps: 124 lds = dep[TransitiveLicensesInfo].licenses 125 tls.append(lds) 126 127 ds = depset(transitive = tls) 128 129 # Ignore any legacy licenses that may be in the report 130 mappings = {} 131 for lic in ds.to_list(): 132 if type(lic.license_text) == "File": 133 mappings[lic.license_text] = lic.package_name 134 elif warn: 135 # buildifier: disable=print 136 print("Legacy license %s not included, rule needs updating" % lic.license_text) 137 138 return mappings 139