xref: /aosp_15_r20/external/bazelbuild-rules_license/rules/compliance.bzl (revision f578df4fd057ffe2023728444759535685631548)
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"""License compliance checking."""
15
16load(
17    "@rules_license//rules:gather_licenses_info.bzl",
18    "gather_licenses_info",
19    "gather_licenses_info_and_write",
20    "write_licenses_info",
21)
22load(
23    "@rules_license//rules/private: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 _check_license_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_licenses_info(ctx, ctx.attr.deps, licenses_file)
34
35    license_files = []
36    if ctx.outputs.license_texts:
37        license_files = get_licenses_mapping(ctx.attr.deps).keys()
38
39    # Now run the checker on it
40    inputs = [licenses_file]
41    outputs = [ctx.outputs.report]
42    args = ctx.actions.args()
43    args.add("--licenses_info", licenses_file.path)
44    args.add("--report", ctx.outputs.report.path)
45    if ctx.attr.check_conditions:
46        args.add("--check_conditions")
47    if ctx.outputs.copyright_notices:
48        args.add("--copyright_notices", ctx.outputs.copyright_notices.path)
49        outputs.append(ctx.outputs.copyright_notices)
50    if ctx.outputs.license_texts:
51        args.add("--license_texts", ctx.outputs.license_texts.path)
52        outputs.append(ctx.outputs.license_texts)
53        inputs.extend(license_files)
54    ctx.actions.run(
55        mnemonic = "CheckLicenses",
56        progress_message = "Checking license compliance for %s" % ctx.label,
57        inputs = inputs,
58        outputs = outputs,
59        executable = ctx.executable._checker,
60        arguments = [args],
61    )
62    outputs.append(licenses_file)  # also make the json file available.
63    return [DefaultInfo(files = depset(outputs))]
64
65_check_license = rule(
66    implementation = _check_license_impl,
67    attrs = {
68        "deps": attr.label_list(
69            aspects = [gather_licenses_info],
70        ),
71        "check_conditions": attr.bool(default = True, mandatory = False),
72        "copyright_notices": attr.output(mandatory = False),
73        "license_texts": attr.output(mandatory = False),
74        "report": attr.output(mandatory = True),
75        "_checker": attr.label(
76            default = Label("@rules_license//tools:checker_demo"),
77            executable = True,
78            allow_files = True,
79            cfg = "exec",
80        ),
81    },
82)
83
84# TODO(b/152546336): Update the check to take a pointer to a condition list.
85def check_license(**kwargs):
86    _check_license(**kwargs)
87
88def _manifest_impl(ctx):
89    # Gather all licenses and make it available as deps for downstream rules
90    # Additionally write the list of license filenames to a file that can
91    # also be used as an input to downstream rules.
92    licenses_file = ctx.actions.declare_file(ctx.attr.out.name)
93    mappings = get_licenses_mapping(ctx.attr.deps, ctx.attr.warn_on_legacy_licenses)
94    ctx.actions.write(
95        output = licenses_file,
96        content = "\n".join([",".join([f.path, p]) for (f, p) in mappings.items()]),
97    )
98    return [DefaultInfo(files = depset(mappings.keys()))]
99
100_manifest = rule(
101    implementation = _manifest_impl,
102    doc = """Internal tmplementation method for manifest().""",
103    attrs = {
104        "deps": attr.label_list(
105            doc = """List of targets to collect license files for.""",
106            aspects = [gather_licenses_info],
107        ),
108        "out": attr.output(
109            doc = """Output file.""",
110            mandatory = True,
111        ),
112        "warn_on_legacy_licenses": attr.bool(default = False),
113    },
114)
115
116def manifest(name, deps, out = None, **kwargs):
117    if not out:
118        out = name + ".manifest"
119
120    _manifest(name = name, deps = deps, out = out, **kwargs)
121
122def _licenses_used_impl(ctx):
123    # Gather all licenses and make it available as JSON
124    write_licenses_info(ctx, ctx.attr.deps, ctx.outputs.out)
125    return [DefaultInfo(files = depset([ctx.outputs.out]))]
126
127_licenses_used = rule(
128    implementation = _licenses_used_impl,
129    doc = """Internal tmplementation method for licenses_used().""",
130    attrs = {
131        "deps": attr.label_list(
132            doc = """List of targets to collect LicenseInfo for.""",
133            aspects = [gather_licenses_info_and_write],
134        ),
135        "out": attr.output(
136            doc = """Output file.""",
137            mandatory = True,
138        ),
139    },
140)
141
142def get_licenses_mapping(deps, warn = False):
143    """Creates list of entries representing all licenses for the deps.
144
145    Args:
146
147      deps: a list of deps which should have TransitiveLicensesInfo providers.
148            This requires that you have run the gather_licenses_info
149            aspect over them
150
151      warn: boolean, if true, display output about legacy targets that need
152            update
153
154    Returns:
155      {File:package_name}
156    """
157    tls = []
158    for dep in deps:
159        lds = dep[TransitiveLicensesInfo].licenses
160        tls.append(lds)
161
162    ds = depset(transitive = tls)
163
164    # Ignore any legacy licenses that may be in the report
165    mappings = {}
166    for lic in ds.to_list():
167        if type(lic.license_text) == "File":
168            mappings[lic.license_text] = lic.package_name
169        elif warn:
170            print("Legacy license %s not included, rule needs updating" % lic.license_text)
171
172    return mappings
173
174def licenses_used(name, deps, out = None, **kwargs):
175    """Collects LicensedInfo providers for a set of targets and writes as JSON.
176
177    The output is a single JSON array, with an entry for each license used.
178    See gather_licenses_info.bzl:write_licenses_info() for a description of the schema.
179
180    Args:
181      name: The target.
182      deps: A list of targets to get LicenseInfo for. The output is the union of
183            the result, not a list of information for each dependency.
184      out: The output file name. Default: <name>.json.
185      **kwargs: Other args
186
187    Usage:
188
189      licenses_used(
190          name = "license_info",
191          deps = [":my_app"],
192          out = "license_info.json",
193      )
194    """
195    if not out:
196        out = name + ".json"
197    _licenses_used(name = name, deps = deps, out = out, **kwargs)
198