diff options
author | Bill Neubauer <wcn@google.com> | 2022-10-24 19:52:53 -0400 |
---|---|---|
committer | Tony Aiuto <aiuto@google.com> | 2022-11-01 00:32:59 -0400 |
commit | d8c455ebb46b0f489a4a83367e4833fda5a131db (patch) | |
tree | 15432eb15d61d926fadba52d89209698ea4bd324 | |
parent | 84c8fe9fabbd149dff42f854c07fabbe286f93a8 (diff) | |
download | bazelbuild-rules_license-d8c455ebb46b0f489a4a83367e4833fda5a131db.tar.gz |
Mega merge from Google
The core of the PR is an expert from Google, but it applies
several changes to account for Bazel differences.
- deal with bazel 5.x 6.x @// handling
- restore package_url and package_version. This is temporary
they will move to other providers.
PiperOrigin-RevId: 483521567
52 files changed, 1650 insertions, 569 deletions
@@ -1,8 +1,5 @@ # rules_license -CI: -[![Build status](https://badge.buildkite.com/e12f23186aa579f1e20fcb612a22cd799239c3134bc38e1aff.svg)](https://buildkite.com/bazel/rules-license) - This repository contains a set of rules and tools for - declaring metadata about packages, such as - the licenses the package is available under diff --git a/distro/BUILD b/distro/BUILD index 0231f13..a79d54b 100644 --- a/distro/BUILD +++ b/distro/BUILD @@ -17,7 +17,7 @@ load("@rules_pkg//pkg:pkg.bzl", "pkg_tar") load("@rules_pkg//pkg/releasing:defs.bzl", "print_rel_notes") package( - default_visibility = ["//visibility:private"], + default_visibility = ["//visibility:public"], default_applicable_licenses = ["//:license"], ) diff --git a/examples/README.md b/examples/README.md index 0afb5ca..fede271 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,7 +9,7 @@ Terminology - SCM: source code management system. These examples assume that an organization has a SCM that can enforce ownership restrictions on specific folder trees. Targets are divided into BUILD files that are - reviewed by engineers vs. those that are reviewed by an organization's + reviewed by engineers vs. those that are reviewed by an organizations compliance team. ## Overview diff --git a/examples/manifest/BUILD b/examples/manifest/BUILD new file mode 100644 index 0000000..d308a59 --- /dev/null +++ b/examples/manifest/BUILD @@ -0,0 +1,41 @@ +load(":android_mock.bzl", "android_binary", "android_library") +load("@rules_license//tools:test_helpers.bzl", "golden_cmd_test") + + +# These two rules today capture what an android_binary would look like. +# This rule represents the Android specific code that displays licenses +# on the display. Note that it does not depend on anything to get the +# license contents; the implementation of these rules macros handle that +# detail. +android_library( + name = "licenses", + srcs = [ + "license_display.sh", + ], + data = [ + "@rules_license//distro:distro", + ], +) + +# This captures how the application would be built. The dependencies of this +# rule are crawled to identify third-party licenses in use. The macro definition +# of this rule creates a graph to capture that process of identifying licenses, +# building the licenses target, and finally invoking the "real" android_binary +# rule to build the final output with the injected license content. +android_binary( + name = "main", + srcs = ["main.sh"], + deps = [ + ], + data = [ + ":licenses", + ], +) + +golden_cmd_test( + name = "main_test", + srcs = [], + cmd = "$(location :main)", + tools = [":main"], + golden = "main_golden.txt", +) diff --git a/examples/manifest/android_mock.bzl b/examples/manifest/android_mock.bzl new file mode 100644 index 0000000..0dee3c9 --- /dev/null +++ b/examples/manifest/android_mock.bzl @@ -0,0 +1,61 @@ +load("@rules_license//rules:compliance.bzl", "manifest") + +"""This is a proof of concept to show how to modify a macro definition to +create a sub-graph allowing for build time injection of license information. We +use Android-inspired rule names since these are a likely candidate for this +sort of injection.""" + +def android_library(name, **kwargs): + # This is an approximation for demo purposes. + + data = kwargs.pop("data", []) + native.filegroup( + name = name, + srcs = data + kwargs.get("srcs", []), + ) + + # Inject the data dependency into the library, preserving any other data it has. + native.sh_library( + name = name + "_w_licenses", + data = data + [name + "_manifest.txt"], + **kwargs + ) + +def android_binary(name, **kwargs): + # Same observation about not being sloppy with mapping deps, but I think the only important attribute + # in android_binary is deps, but need to double-check. + native.filegroup( + name = name + "_no_licenses", + srcs = kwargs.get("data", []), + ) + + mf_name = name + "_manifest" + manifest( + name = mf_name, + deps = [":" + name + "_no_licenses"], + ) + + # This uses the conditions tool to generate an approximation of a compliance report + # to demonstrate how license data can be plumbed and made available at build time. + native.genrule( + name = "gen_" + name + "_manifest", + srcs = [":" + mf_name], + outs = ["licenses_manifest.txt"], + cmd = "cat $(locations :%s) > $@" % mf_name, + ) + + # Swap out the :licenses dep for our new :licenses_w_licenses dep + newdeps = [] + deps = kwargs.get("data", []) + for dep in deps: + if dep == ":licenses": + newdeps.append(":licenses_w_licenses") + else: + newdeps.append(dep) + kwargs["data"] = newdeps + + # Compile the executable with the user's originally supplied name, but with the new content. + native.sh_binary( + name = name, + **kwargs + ) diff --git a/examples/manifest/license_display.sh b/examples/manifest/license_display.sh new file mode 100644 index 0000000..f96b3ba --- /dev/null +++ b/examples/manifest/license_display.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +function display_licenses { + echo -n "Licenses: " + cat "$0.runfiles/rules_license/examples/manifest/licenses_manifest.txt" + echo +} diff --git a/examples/manifest/main.sh b/examples/manifest/main.sh new file mode 100755 index 0000000..4f80a5c --- /dev/null +++ b/examples/manifest/main.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +#source gbash.sh || exit + +#source "$RUNFILES/google3/tools/build_defs/license/examples/manifest/license_display.sh" +source "$0.runfiles/rules_license/examples/manifest/license_display.sh" +#source module google3/tools/build_defs/license/examples/manifest/license_display.sh + +echo "I am a program that uses open source code." +display_licenses diff --git a/examples/manifest/main_golden.txt b/examples/manifest/main_golden.txt new file mode 100644 index 0000000..4469cce --- /dev/null +++ b/examples/manifest/main_golden.txt @@ -0,0 +1,204 @@ +I am a program that uses open source code. +Licenses: + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/examples/my_org/compliance/BUILD b/examples/my_org/compliance/BUILD deleted file mode 100644 index 074b21e..0000000 --- a/examples/my_org/compliance/BUILD +++ /dev/null @@ -1,31 +0,0 @@ -# Example license policy definitions. - -load("@rules_license//rules:license_policy.bzl", "license_policy") - -package(default_visibility = ["//examples:__subpackages__"]) - -# license_policy rules generally appear in a central location per workspace. They -# are intermingled with normal target build rules -license_policy( - name = "production_service", - conditions = [ - "notice", - "restricted_if_statically_linked", - ], -) - -license_policy( - name = "mobile_application", - conditions = [ - "notice", - ], -) - -license_policy( - name = "special_whitelisted_app", - # There could be a whitelist of targets here. - conditions = [ - "notice", - "whitelist:acme_corp_paid", - ], -) diff --git a/examples/my_org/licenses/BUILD b/examples/my_org/licenses/BUILD index d9d5c25..f17bfa3 100644 --- a/examples/my_org/licenses/BUILD +++ b/examples/my_org/licenses/BUILD @@ -1,3 +1,16 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # Example license kind definitions. # We expect that all license_kind rules used by an organization exist in a diff --git a/examples/src/BUILD b/examples/src/BUILD index 29b6803..ecab5da 100644 --- a/examples/src/BUILD +++ b/examples/src/BUILD @@ -1,17 +1,25 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # Examples of applications and interactions with licenses +load("@rules_license//rules:compliance.bzl", "check_license", "licenses_used") load("@rules_license//examples/vendor/constant_gen:defs.bzl", "constant_gen") -load("@rules_license//rules:compliance.bzl", "licenses_used") -load("@rules_license//rules:license_policy_check.bzl", "license_policy_check") -load("@rules_license//tools:test_helpers.bzl", "golden_test") cc_binary( name = "my_server", srcs = ["server.cc"], - deps = [ - ":message", - "@rules_license//examples/vendor/libhhgttg", - ], + deps = [":message"], ) # Sample @@ -21,32 +29,17 @@ constant_gen( var = "server_message", ) -license_policy_check( +# TODO(aiuto): Turn this strictly into a compliance test. +check_license( name = "check_server", - policy = "@rules_license//examples/my_org/compliance:production_service", - target = ":my_server", -) - -cc_binary( - name = "my_violating_server", - srcs = ["server.cc"], + check_conditions = False, + license_texts = "server_licenses.txt", + report = "server_report.txt", deps = [ - ":message", - "@rules_license//examples/vendor/acme", - "@rules_license//examples/vendor/libhhgttg", + ":my_server", ], ) -license_policy_check( - name = "check_violating_server", - policy = "@rules_license//examples/my_org/compliance:production_service", - tags = [ - "manual", - "notap", - ], - target = ":my_violating_server", -) - # # Verify the licenses are what we expect. The golden output shows that # :my_server only uses the unencumbered license type. @@ -57,8 +50,12 @@ licenses_used( deps = [":my_server"], ) -golden_test( - name = "verify_server_licenses_test", - golden = "server_licenses.golden", - subject = ":server_licenses.json", +py_test( + name = "server_licenses_test", + srcs = ["server_licenses_test.py"], + data = [":server_licenses.json"], + python_version = "PY3", + deps = [ + "@rules_license//tests:license_test_utils", + ], ) diff --git a/examples/src/mobile.cc b/examples/src/mobile.cc deleted file mode 100644 index d15090f..0000000 --- a/examples/src/mobile.cc +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -#include <iostream> - -int main(int argc, char* argv[]) { - std::cout << "Hello world" << std::endl; -} diff --git a/examples/src/server.cc b/examples/src/server.cc index 8f7990e..8229fc1 100644 --- a/examples/src/server.cc +++ b/examples/src/server.cc @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,8 +11,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - #include <iostream> +#include <ostream> extern const char* server_message; diff --git a/examples/src/server_licenses.golden b/examples/src/server_licenses.golden deleted file mode 100644 index 57df1b3..0000000 --- a/examples/src/server_licenses.golden +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "rule": "//examples/vendor/constant_gen:license_for_emitted_code", - "license_kinds": [ - { - "target": "@//examples/my_org/licenses:unencumbered", - "name": "unencumbered", - "conditions": [] - } - ], - "copyright_notice": "", - "package_name": "Trivial Code Generator Output", - "package_url": null, - "package_version": null, - "license_text": "examples/vendor/constant_gen/LICENSE" - }, - { - "rule": "//examples/vendor/libhhgttg:license", - "license_kinds": [ - { - "target": "@//examples/my_org/licenses:generic_notice", - "name": "generic_notice", - "conditions": ["notice"] - } - ], - "copyright_notice": "", - "package_name": "", - "package_url": null, - "package_version": null, - "license_text": "examples/vendor/libhhgttg/LICENSE" - } -] diff --git a/examples/src/server_licenses_test.py b/examples/src/server_licenses_test.py new file mode 100644 index 0000000..c7c30da --- /dev/null +++ b/examples/src/server_licenses_test.py @@ -0,0 +1,46 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for license/examples/src.""" + +import os + +import unittest +from tests import license_test_utils + + +class ServerLicensesTest(unittest.TestCase): + + def test_has_expected_licenses(self): + package_base = license_test_utils.LICENSE_PACKAGE_BASE + licenses_info = license_test_utils.load_licenses_info( + os.path.join(os.path.dirname(__file__), "server_licenses.json")) + licenses_info = license_test_utils.filter_dependencies( + licenses_info, + target_filter=lambda targ: targ.startswith(package_base), + licenses_filter=lambda lic: lic.startswith(package_base)) + + expected = { + "/examples/src:message_src_": [ + "/examples/vendor/constant_gen:license_for_emitted_code" + ], + "/examples/src:message": [ + "/examples/vendor/constant_gen:license_for_emitted_code" + ], + } + license_test_utils.check_licenses_of_dependencies( + self, licenses_info, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/examples/src/server_report.golden b/examples/src/server_report.golden index 6d322b1..8be07a0 100644 --- a/examples/src/server_report.golden +++ b/examples/src/server_report.golden @@ -1,3 +1,3 @@ -= @rules_license//examples/vendor/constant_gen:license_for_emitted_code - kind: @@rules_license//examples/my_org/licenses:unencumbered += //examples/vendor/constant_gen:license_for_emitted_code + kind: @//examples/my_org/licenses:unencumbered conditions: [] diff --git a/examples/vendor/acme/BUILD b/examples/vendor/acme/BUILD index 488af62..814957d 100644 --- a/examples/vendor/acme/BUILD +++ b/examples/vendor/acme/BUILD @@ -1,11 +1,21 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # A package with a commercial license. load("@rules_license//rules:license.bzl", "license") -package( - default_applicable_licenses = [":license"], - default_visibility = ["//visibility:public"], -) +package(default_applicable_licenses = [":license"]) # The default license for an entire package is typically named "license". license( diff --git a/examples/vendor/acme/coyote.cc b/examples/vendor/acme/coyote.cc index e1e8083..d637855 100644 --- a/examples/vendor/acme/coyote.cc +++ b/examples/vendor/acme/coyote.cc @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. - bool caught_road_runner() { return false; } diff --git a/examples/vendor/constant_gen/BUILD b/examples/vendor/constant_gen/BUILD index a81885c..e70f489 100644 --- a/examples/vendor/constant_gen/BUILD +++ b/examples/vendor/constant_gen/BUILD @@ -1,8 +1,21 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # An example of a code generator with a distinct license for the generated code. load("@rules_license//rules:compliance.bzl", "licenses_used") -load("@rules_license//rules:license.bzl", "license") load("@rules_license//tools:test_helpers.bzl", "golden_test") +load("@rules_license//rules:license.bzl", "license") load(":defs.bzl", "constant_gen") package( @@ -13,22 +26,20 @@ package( # The default license for an entire package is typically named "license". license( name = "license", + package_name = "Trivial Code Generator", license_kinds = [ "@rules_license//examples/my_org/licenses:generic_restricted", ], license_text = "LICENSE", - package_name = "Trivial Code Generator", - package_url = "http://github.com/tgc-fake/tgc.tgz", - package_version = "3.14", ) license( name = "license_for_emitted_code", package_name = "Trivial Code Generator Output", - license = "LICENSE.on_output", license_kinds = [ "@rules_license//examples/my_org/licenses:unencumbered", ], + license_text = "LICENSE.on_output", ) # The generator itself will be licensed under :license @@ -69,3 +80,4 @@ golden_test( golden = "generated_code_licenses.golden", subject = ":generated_code_licenses.json", ) + diff --git a/examples/vendor/constant_gen/constant_generator.py b/examples/vendor/constant_gen/constant_generator.py index 6bd92b2..432b6be 100644 --- a/examples/vendor/constant_gen/constant_generator.py +++ b/examples/vendor/constant_gen/constant_generator.py @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,8 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -# Lint as: python3 """A trivial tool to turn a string into a C++ constant. This is not meant to be useful. It is only to provide an example of a tool that diff --git a/examples/vendor/constant_gen/defs.bzl b/examples/vendor/constant_gen/defs.bzl index e54fcee..f3eb715 100644 --- a/examples/vendor/constant_gen/defs.bzl +++ b/examples/vendor/constant_gen/defs.bzl @@ -1,19 +1,5 @@ """A trivial rule to turn a string into a C++ constant.""" -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - def _constant_gen_impl(ctx): # Turn text into a C++ constant. outputs = [ctx.outputs.src_out] diff --git a/examples/vendor/constant_gen/generated_code_licenses.golden b/examples/vendor/constant_gen/generated_code_licenses.golden index 6aef78a..c4c8ef1 100644 --- a/examples/vendor/constant_gen/generated_code_licenses.golden +++ b/examples/vendor/constant_gen/generated_code_licenses.golden @@ -1,17 +1,41 @@ [ { - "rule": "//examples/vendor/constant_gen:license_for_emitted_code", - "license_kinds": [ + "top_level_target": "//examples/vendor/constant_gen:libhello", + "dependencies": [ { - "target": "@//examples/my_org/licenses:unencumbered", - "name": "unencumbered", - "conditions": [] + "target_under_license": "//examples/vendor/constant_gen:libhello", + "licenses": [ + "//examples/vendor/constant_gen:license_for_emitted_code" + ] + }, + { + "target_under_license": "//examples/vendor/constant_gen:libhello_src_", + "licenses": [ + "//examples/vendor/constant_gen:license_for_emitted_code" + ] } ], - "copyright_notice": "", - "package_name": "Trivial Code Generator Output", - "package_url": null, - "package_version": null, - "license_text": "examples/vendor/constant_gen/LICENSE" + "licenses": [ + { + "label": "//examples/vendor/constant_gen:license_for_emitted_code", + "rule": "//examples/vendor/constant_gen:license_for_emitted_code", + "license_kinds": [ + { + "target": "@//examples/my_org/licenses:unencumbered", + "name": "unencumbered", + "conditions": [] + } + ], + "copyright_notice": "", + "package_name": "Trivial Code Generator Output", + "package_url": "", + "package_version": "", + "license_text": "examples/vendor/constant_gen/LICENSE.on_output", + "used_by": [ + "//examples/vendor/constant_gen:libhello", + "//examples/vendor/constant_gen:libhello_src_" + ] + } + ] } ] diff --git a/examples/vendor/constant_gen/generator_licenses.golden b/examples/vendor/constant_gen/generator_licenses.golden index f6fa349..4b2f175 100644 --- a/examples/vendor/constant_gen/generator_licenses.golden +++ b/examples/vendor/constant_gen/generator_licenses.golden @@ -1,17 +1,34 @@ [ { - "rule": "//examples/vendor/constant_gen:license", - "license_kinds": [ + "top_level_target": "//examples/vendor/constant_gen:constant_generator", + "dependencies": [ { - "target": "@//examples/my_org/licenses:generic_restricted", - "name": "generic_restricted", - "conditions": ["restricted"] + "target_under_license": "//examples/vendor/constant_gen:constant_generator", + "licenses": [ + "//examples/vendor/constant_gen:license" + ] } ], - "copyright_notice": "", - "package_name": "Trivial Code Generator", - "package_url": "http://github.com/tgc-fake/tgc.tgz", - "package_version": "3.14", - "license_text": "examples/vendor/constant_gen/LICENSE" + "licenses": [ + { + "label": "//examples/vendor/constant_gen:license", + "rule": "//examples/vendor/constant_gen:license", + "license_kinds": [ + { + "target": "@//examples/my_org/licenses:generic_restricted", + "name": "generic_restricted", + "conditions": ["restricted"] + } + ], + "copyright_notice": "", + "package_name": "Trivial Code Generator", + "package_url": "", + "package_version": "", + "license_text": "examples/vendor/constant_gen/LICENSE", + "used_by": [ + "//examples/vendor/constant_gen:constant_generator" + ] + } + ] } ] diff --git a/examples/vendor/libhhgttg/BUILD b/examples/vendor/libhhgttg/BUILD index c5da389..44d2d61 100644 --- a/examples/vendor/libhhgttg/BUILD +++ b/examples/vendor/libhhgttg/BUILD @@ -1,3 +1,16 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # A package with all code under a single license. This is the most common case # we expect to see. @@ -5,10 +18,7 @@ load("@rules_license//rules:license.bzl", "license") # Using a package wide default ensure that all targets are associated with the # license. -package( - default_applicable_licenses = [":license"], - default_visibility = ["//visibility:public"], -) +package(default_applicable_licenses = [":license"]) # The default license for an entire package is typically named "license". license( diff --git a/examples/vendor/libhhgttg/answer.cc b/examples/vendor/libhhgttg/answer.cc index 8b78f90..440bc62 100644 --- a/examples/vendor/libhhgttg/answer.cc +++ b/examples/vendor/libhhgttg/answer.cc @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/licenses/generic/BUILD b/licenses/generic/BUILD index 71880cf..def2334 100644 --- a/licenses/generic/BUILD +++ b/licenses/generic/BUILD @@ -86,6 +86,14 @@ license_kind( ], ) +# See: https://opensource.google/docs/thirdparty/licenses/#restricted +license_kind( + name = "restricted", + conditions = [ + "restricted", + ], +) + # See: https://opensource.google/docs/thirdparty/licenses/#ByExceptionOnly license_kind( name = "by_exception_only", diff --git a/rules/BUILD b/rules/BUILD index b4dde6f..1d67059 100644 --- a/rules/BUILD +++ b/rules/BUILD @@ -15,6 +15,8 @@ # limitations under the License. """Rules for making license declarations.""" +load("@rules_license//rules:licenses_core.bzl", "trace") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], @@ -22,6 +24,15 @@ package( licenses(["notice"]) +# This target controls the value of the traced target used during dependency collection. +# This value should always be the empty string! +# Specify this value with a flag, like --@rules_license//rules:trace_target=//target/to:trace +trace( + name = "trace_target", + build_setting_default = "", # TRACE-TARGET-SHOULD-BE-EMPTY + visibility = ["//visibility:public"], +) + filegroup( name = "standard_package", srcs = glob(["**"]), diff --git a/rules/check_licenses_shim.bzl b/rules/check_licenses_shim.bzl new file mode 100644 index 0000000..3dcfe2c --- /dev/null +++ b/rules/check_licenses_shim.bzl @@ -0,0 +1,30 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This module provides a custom Starlark rule used to create wrappers for targets that +can have blaze build --check_licenses executed against them.""" + +def _shim_rule_impl(ctx): + # This rule doesn't need to return anything. It only exists to propagate the dependency supplied + # by the label_flag + return [] + +shim_rule = rule( + doc = """This rule exists to configure a dependent target via label. An instantiation of this + rule is then used as a dependency for the legacy_check_target rule, which can be built with --check_licenses + to get the effect of running --check_licenses on an arbitrary target which may or may not have a distribs + attribute""", + implementation = _shim_rule_impl, + # The definition of this attribute creates a dependency relationship on the manually provided label. + attrs = {"target": attr.label(default = ":check_licenses_target")}, +) diff --git a/rules/compliance.bzl b/rules/compliance.bzl index 343759a..1792454 100644 --- a/rules/compliance.bzl +++ b/rules/compliance.bzl @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,40 +11,30 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -"""Proof of concept. License compliance checking.""" +"""License compliance checking.""" load( "@rules_license//rules:gather_licenses_info.bzl", "gather_licenses_info", + "gather_licenses_info_and_write", "write_licenses_info", ) load( "@rules_license//rules:providers.bzl", - "LicensesInfo", + "TransitiveLicensesInfo", ) -# Debugging verbosity -_VERBOSITY = 0 - -def _debug(loglevel, msg): - if _VERBOSITY > loglevel: - print(msg) # buildifier: disable=print - +# This rule is proof of concept, and may not represent the final +# form of a rule for compliance validation. def _check_license_impl(ctx): # Gather all licenses and write information to one place - _debug(0, "Check license: %s" % ctx.label) - licenses_file = ctx.actions.declare_file("_%s_licenses_info.json" % ctx.label.name) write_licenses_info(ctx, ctx.attr.deps, licenses_file) license_files = [] if ctx.outputs.license_texts: - for dep in ctx.attr.deps: - if LicensesInfo in dep: - for license in dep[LicensesInfo].licenses.to_list(): - license_files.append(license.license_text) + license_files = get_licenses_mapping(ctx.attr.deps).keys() # Now run the checker on it inputs = [licenses_file] @@ -75,11 +65,11 @@ def _check_license_impl(ctx): _check_license = rule( implementation = _check_license_impl, attrs = { - "check_conditions": attr.bool(default = True, mandatory = False), - "copyright_notices": attr.output(mandatory = False), "deps": attr.label_list( aspects = [gather_licenses_info], ), + "check_conditions": attr.bool(default = True, mandatory = False), + "copyright_notices": attr.output(mandatory = False), "license_texts": attr.output(mandatory = False), "report": attr.output(mandatory = True), "_checker": attr.label( @@ -91,21 +81,56 @@ _check_license = rule( }, ) +# TODO(b/152546336): Update the check to take a pointer to a condition list. def check_license(**kwargs): _check_license(**kwargs) +def _manifest_impl(ctx): + # Gather all licenses and make it available as deps for downstream rules + # Additionally write the list of license filenames to a file that can + # also be used as an input to downstream rules. + licenses_file = ctx.actions.declare_file(ctx.attr.out.name) + mappings = get_licenses_mapping(ctx.attr.deps, ctx.attr.warn_on_legacy_licenses) + ctx.actions.write( + output = licenses_file, + content = "\n".join([",".join([f.path, p]) for (f, p) in mappings.items()]), + ) + return [DefaultInfo(files = depset(mappings.keys()))] + +_manifest = rule( + implementation = _manifest_impl, + doc = """Internal tmplementation method for manifest().""", + attrs = { + "deps": attr.label_list( + doc = """List of targets to collect license files for.""", + aspects = [gather_licenses_info], + ), + "out": attr.output( + doc = """Output file.""", + mandatory = True, + ), + "warn_on_legacy_licenses": attr.bool(default = False), + }, +) + +def manifest(name, deps, out = None, **kwargs): + if not out: + out = name + ".manifest" + + _manifest(name = name, deps = deps, out = out, **kwargs) + def _licenses_used_impl(ctx): - """Gather all licenses and make it available as JSON.""" + # Gather all licenses and make it available as JSON write_licenses_info(ctx, ctx.attr.deps, ctx.outputs.out) return [DefaultInfo(files = depset([ctx.outputs.out]))] _licenses_used = rule( implementation = _licenses_used_impl, - doc = """Internal implementation method for licenses_used().""", + doc = """Internal tmplementation method for licenses_used().""", attrs = { "deps": attr.label_list( doc = """List of targets to collect LicenseInfo for.""", - aspects = [gather_licenses_info], + aspects = [gather_licenses_info_and_write], ), "out": attr.output( doc = """Output file.""", @@ -114,6 +139,38 @@ _licenses_used = rule( }, ) +def get_licenses_mapping(deps, warn = False): + """Creates list of entries representing all licenses for the deps. + + Args: + + deps: a list of deps which should have TransitiveLicensesInfo providers. + This requires that you have run the gather_licenses_info + aspect over them + + warn: boolean, if true, display output about legacy targets that need + update + + Returns: + {File:package_name} + """ + tls = [] + for dep in deps: + lds = dep[TransitiveLicensesInfo].licenses + tls.append(lds) + + ds = depset(transitive = tls) + + # Ignore any legacy licenses that may be in the report + mappings = {} + for lic in ds.to_list(): + if type(lic.license_text) == "File": + mappings[lic.license_text] = lic.package_name + elif warn: + print("Legacy license %s not included, rule needs updating" % lic.license_text) + + return mappings + def licenses_used(name, deps, out = None, **kwargs): """Collects LicensedInfo providers for a set of targets and writes as JSON. diff --git a/rules/default_license.bzl b/rules/default_license.bzl deleted file mode 100644 index 57a7147..0000000 --- a/rules/default_license.bzl +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Proof of concept. License restriction.""" - -load( - "@rules_license//rules:providers.bzl", - "LicenseInfo", - "LicensesInfo", -) - -# An experiment to provide license defaults via a rule. This is far from -# working and should not be considered part of the current design. -# - -def _default_licenses_impl(ctx): - licenses = [] - for dep in ctx.attr.deps: - if LicenseInfo in dep: - licenses.append(dep[LicenseInfo]) - return [LicensesInfo(licenses = licenses)] - -_default_licenses = rule( - implementation = _default_licenses_impl, - attrs = { - "conditions": attr.string_list( - doc = "TBD", - ), - "deps": attr.label_list( - mandatory = True, - doc = "Licenses", - providers = [LicenseInfo], - cfg = "exec", - ), - }, -) - -# buildifier: disable=unnamed-macro -def default_licenses(licenses, conditions = None): - _default_licenses( - name = "__default_licenses", - deps = ["%s_license" % license for license in licenses], - conditions = conditions, - ) diff --git a/rules/filtered_rule_kinds.bzl b/rules/filtered_rule_kinds.bzl new file mode 100644 index 0000000..1d6f01c --- /dev/null +++ b/rules/filtered_rule_kinds.bzl @@ -0,0 +1,47 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Filtered rule kinds for aspect inspection. +The format of this dictionary is: + + rule_name: [attr, attr, ...] + +Only filters for rules that are part of the Bazel distribution should be added +to this file. Other filters should be added in user_filtered_rule_kinds.bzl + +Attributes are either the explicit list of attributes to filter, or '_*' which +would ignore all attributes prefixed with a _. +""" + +# Rule kinds with attributes the aspect currently needs to ignore +aspect_filters = { + "*": ["linter"], + "_constant_gen": ["_generator"], + "cc_binary": ["_*"], + "cc_embed_data": ["_*"], + "cc_grpc_library": ["_*"], + "cc_library": ["_*"], + "cc_toolchain_alias": ["_cc_toolchain"], + "genrule": ["tools", "exec_tools", "toolchains"], + "genyacc": ["_*"], + "go_binary": ["_*"], + "go_library": ["_*"], + "go_wrap_cc": ["_*"], + "java_binary": ["_*", "plugins", "exported_plugins"], + "java_library": ["plugins", "exported_plugins"], + "java_wrap_cc": ["_cc_toolchain", "swig_top"], + "py_binary": ["_*"], + "py_extension": ["_cc_toolchain"], + "sh_binary": ["_bash_binary"], +} diff --git a/rules/gather_licenses_info.bzl b/rules/gather_licenses_info.bzl index d47a821..d2d9df5 100644 --- a/rules/gather_licenses_info.bzl +++ b/rules/gather_licenses_info.bzl @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,66 +11,113 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Rules and macros for collecting LicenseInfo providers.""" load( + "@rules_license//rules:licenses_core.bzl", + "TraceInfo", + "gather_licenses_info_common", + "should_traverse", +) +load( "@rules_license//rules:providers.bzl", - "LicenseInfo", - "LicensesInfo", + "TransitiveLicensesInfo", ) -# Debugging verbosity -_VERBOSITY = 0 +# Definition for compliance namespace, used for filtering licenses +# based on the namespace to which they belong. +NAMESPACES = ["compliance"] -def _debug(loglevel, msg): - if _VERBOSITY > loglevel: - print(msg) # buildifier: disable=print +def _strip_null_repo(label): + """Removes the null repo name (e.g. @//) from a string. -def _get_transitive_licenses(deps, licenses, trans): - for dep in deps: - if LicenseInfo in dep: - license = dep[LicenseInfo] - _debug(1, " depends on license: %s" % license.rule) - licenses.append(license) - if LicensesInfo in dep: - license_list = dep[LicensesInfo].licenses - if license_list: - _debug(1, " transitively depends on: %s" % licenses) - trans.append(license_list) + The is to make str(label) compatible between bazel 5.x and 6.x + """ + s = str(label) + if s.startswith('@//'): + return s[1:] + return s def _gather_licenses_info_impl(target, ctx): - licenses = [] - trans = [] - if hasattr(ctx.rule.attr, "applicable_licenses"): - _get_transitive_licenses(ctx.rule.attr.applicable_licenses, licenses, trans) - if hasattr(ctx.rule.attr, "deps"): - _get_transitive_licenses(ctx.rule.attr.deps, licenses, trans) - if hasattr(ctx.rule.attr, "srcs"): - _get_transitive_licenses(ctx.rule.attr.srcs, licenses, trans) - return [LicensesInfo(licenses = depset(tuple(licenses), transitive = trans))] + return gather_licenses_info_common(target, ctx, TransitiveLicensesInfo, NAMESPACES, should_traverse) gather_licenses_info = aspect( - doc = """Collects LicenseInfo providers into a single LicensesInfo provider.""", + doc = """Collects LicenseInfo providers into a single TransitiveLicensesInfo provider.""", implementation = _gather_licenses_info_impl, - attr_aspects = ["applicable_licenses", "deps", "srcs"], + attr_aspects = ["*"], + attrs = { + "_trace": attr.label(default = "@rules_license//rules:trace_target"), + }, + provides = [TransitiveLicensesInfo], apply_to_generating_rules = True, ) -def _quotes_or_null(s): - if not s: - return "null" - return '"%s"' % s +def _write_licenses_info_impl(target, ctx): + """Write transitive license info into a JSON file + + Args: + target: The target of the aspect. + ctx: The aspect evaluation context. + + Returns: + OutputGroupInfo + """ + + if not TransitiveLicensesInfo in target: + return [OutputGroupInfo(licenses = depset())] + info = target[TransitiveLicensesInfo] + outs = [] + + # If the result doesn't contain licenses, we simply return the provider + if not hasattr(info, "target_under_license"): + return [OutputGroupInfo(licenses = depset())] + + # Write the output file for the target + name = "%s_licenses_info.json" % ctx.label.name + content = "[\n%s\n]\n" % ",\n".join(licenses_info_to_json(info)) + out = ctx.actions.declare_file(name) + ctx.actions.write( + output = out, + content = content, + ) + outs.append(out) + + if ctx.attr._trace[TraceInfo].trace: + trace = ctx.actions.declare_file("%s_trace_info.json" % ctx.label.name) + ctx.actions.write(output = trace, content = "\n".join(info.traces)) + outs.append(trace) + + return [OutputGroupInfo(licenses = depset(outs))] + +gather_licenses_info_and_write = aspect( + doc = """Collects TransitiveLicensesInfo providers and writes JSON representation to a file. + + Usage: + blaze build //some:target \ + --aspects=@rules_license//rules:gather_licenses_info.bzl%gather_licenses_info_and_write + --output_groups=licenses + """, + implementation = _write_licenses_info_impl, + attr_aspects = ["*"], + attrs = { + "_trace": attr.label(default = "@rules_license//rules:trace_target"), + }, + provides = [OutputGroupInfo], + requires = [gather_licenses_info], + apply_to_generating_rules = True, +) def write_licenses_info(ctx, deps, json_out): - """Writes LicensesInfo providers for a set of targets as JSON. + """Writes TransitiveLicensesInfo providers for a set of targets as JSON. - TODO(aiuto): Document JSON schema. + TODO(aiuto): Document JSON schema. But it is under development, so the current + best place to look is at tests/hello_licenses.golden. Usage: write_licenses_info must be called from a rule implementation, where the - rule has run the gather_licenses_info aspect on its deps to collect the - transitive closure of LicenseInfo providers into a LicenseInfo provider. + rule has run the gather_licenses_info aspect on its deps to + collect the transitive closure of LicenseInfo providers into a + LicenseInfo provider. foo = rule( implementation = _foo_impl, @@ -86,51 +133,116 @@ def write_licenses_info(ctx, deps, json_out): Args: ctx: context of the caller - deps: a list of deps which should have LicensesInfo providers. + deps: a list of deps which should have TransitiveLicensesInfo providers. This requires that you have run the gather_licenses_info aspect over them json_out: output handle to write the JSON info """ - - rule_template = """ {{ - "rule": "{rule}", - "license_kinds": [{kinds} - ], - "copyright_notice": "{copyright_notice}", - "package_name": "{package_name}", - "package_url": {package_url}, - "package_version": {package_version}, - "license_text": "{license_text}"\n }}""" - - kind_template = """ - {{ - "target": "{kind_path}", - "name": "{kind_name}", - "conditions": {kind_conditions} - }}""" - licenses = [] for dep in deps: - if LicensesInfo in dep: - for license in dep[LicensesInfo].licenses.to_list(): - _debug(0, " Requires license: %s" % license) - kinds = [] - for kind in license.license_kinds: - kinds.append(kind_template.format( - kind_name = kind.name, - kind_path = kind.label, - kind_conditions = kind.conditions, - )) - licenses.append(rule_template.format( - rule = license.rule, - copyright_notice = license.copyright_notice, - package_name = license.package_name, - package_url = _quotes_or_null(license.package_url), - package_version = _quotes_or_null(license.package_version), - license_text = license.license_text.path, - kinds = ",\n".join(kinds), - )) + if TransitiveLicensesInfo in dep: + licenses.extend(licenses_info_to_json(dep[TransitiveLicensesInfo])) ctx.actions.write( output = json_out, content = "[\n%s\n]\n" % ",\n".join(licenses), ) + +def licenses_info_to_json(licenses_info): + """Render a single LicenseInfo provider to JSON + + Args: + licenses_info: A LicenseInfo. + + Returns: + [(str)] list of LicenseInfo values rendered as JSON. + """ + + main_template = """ {{ + "top_level_target": "{top_level_target}", + "dependencies": [{dependencies} + ], + "licenses": [{licenses} + ]\n }}""" + + dep_template = """ + {{ + "target_under_license": "{target_under_license}", + "licenses": [ + {licenses} + ] + }}""" + + # TODO(aiuto): 'rule' is a duplicate of 'label' until old users are transitioned + license_template = """ + {{ + "label": "{label}", + "rule": "{label}", + "license_kinds": [{kinds} + ], + "copyright_notice": "{copyright_notice}", + "package_name": "{package_name}", + "package_url": "{package_url}", + "package_version": "{package_version}", + "license_text": "{license_text}", + "used_by": [ + {used_by} + ] + }}""" + + kind_template = """ + {{ + "target": "{kind_path}", + "name": "{kind_name}", + "conditions": {kind_conditions} + }}""" + + # Build reverse map of license to user + used_by = {} + for dep in licenses_info.deps.to_list(): + # Undo the concatenation applied when stored in the provider. + dep_licenses = dep.licenses.split(",") + for license in dep_licenses: + if license not in used_by: + used_by[license] = [] + used_by[license].append(_strip_null_repo(dep.target_under_license)) + + all_licenses = [] + for license in sorted(licenses_info.licenses.to_list(), key = lambda x: x.label): + kinds = [] + for kind in sorted(license.license_kinds, key = lambda x: x.name): + kinds.append(kind_template.format( + kind_name = kind.name, + kind_path = kind.label, + kind_conditions = kind.conditions, + )) + + if license.license_text: + # Special handling for synthetic LicenseInfo + text_path = (license.license_text.package + "/" + license.license_text.name if type(license.license_text) == "Label" else license.license_text.path) + all_licenses.append(license_template.format( + copyright_notice = license.copyright_notice, + kinds = ",".join(kinds), + license_text = text_path, + package_name = license.package_name, + package_url = license.package_url, + package_version = license.package_version, + label = _strip_null_repo(license.label), + used_by = ",\n ".join(sorted(['"%s"' % x for x in used_by[str(license.label)]])), + )) + + all_deps = [] + for dep in sorted(licenses_info.deps.to_list(), key = lambda x: x.target_under_license): + licenses_used = [] + + # Undo the concatenation applied when stored in the provider. + dep_licenses = dep.licenses.split(",") + all_deps.append(dep_template.format( + target_under_license = _strip_null_repo(dep.target_under_license), + licenses = ",\n ".join(sorted(['"%s"' % _strip_null_repo(x) for x in dep_licenses])), + )) + + return [main_template.format( + top_level_target = _strip_null_repo(licenses_info.target_under_license), + dependencies = ",".join(all_deps), + licenses = ",".join(all_licenses), + )] diff --git a/rules/license.bzl b/rules/license.bzl index f726be1..183e106 100644 --- a/rules/license.bzl +++ b/rules/license.bzl @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,47 +11,25 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +"""Rules for declaring the compliance licenses used by a package. -"""Rules for declaring the licenses used by a package.""" +See: go/license-checking-v2 +""" load( "@rules_license//rules:providers.bzl", - "LicenseInfo", "LicenseKindInfo", ) - -# Debugging verbosity -_VERBOSITY = 0 - -def _debug(loglevel, msg): - if _VERBOSITY > loglevel: - print(msg) # buildifier: disable=print - -# -# license() -# - -def _license_impl(ctx): - provider = LicenseInfo( - license_kinds = tuple([k[LicenseKindInfo] for k in ctx.attr.license_kinds]), - copyright_notice = ctx.attr.copyright_notice, - package_name = ctx.attr.package_name, - package_url = ctx.attr.package_url, - package_version = ctx.attr.package_version, - license_text = ctx.file.license_text, - rule = ctx.label, - ) - _debug(0, provider) - return [provider] +load( + "@rules_license//rules:license_impl.bzl", + "license_rule_impl", +) _license = rule( - implementation = _license_impl, + implementation = license_rule_impl, attrs = { - "copyright_notice": attr.string( - doc = "Copyright notice.", - ), "license_kinds": attr.label_list( - mandatory = True, + mandatory = False, doc = "License kind(s) of this license. If multiple license kinds are" + " listed in the LICENSE file, and they all apply, then all" + " should be listed here. If the user can choose a single one" + @@ -59,6 +37,9 @@ _license = rule( providers = [LicenseKindInfo], cfg = "exec", ), + "copyright_notice": attr.string( + doc = "Copyright notice.", + ), "license_text": attr.label( allow_single_file = True, default = "LICENSE", @@ -80,51 +61,66 @@ _license = rule( " by an applicatation. It should be a value that" + " increases over time, rather than a commit hash." ), + "namespace": attr.string( + doc = "A human readable name used to organize licenses into categories." + + " This is used in google3 to differentiate third party licenses used" + + " for compliance versus internal licenses used by SLAsan for internal" + + " teams' SLAs.", + ), }, ) # buildifier: disable=function-docstring-args -def license(name, - copyright_notice = None, - license_kinds = None, - license_text = None, - package_name = None, - package_url = None, - package_version = None, - tags = None, - **kwargs): +def license( + name, + license_text = "LICENSE", + visibility = ["//visibility:public"], + license_kind = None, + license_kinds = None, + copyright_notice = None, + package_name = None, + package_url = None, + package_version = None, + namespace = "compliance", + tags = []): """Wrapper for license rule. Args: name: str target name. + license_text: str Filename of the license file + visibility: list(label) visibility spec + license_kind: label a single license_kind. Only one of license_kind or license_kinds may + be specified license_kinds: list(label) list of license_kind targets. - license_kind: label a single license_kind. Only one of license_kind or - license_kinds may be specified copyright_notice: str Copyright notice associated with this package. - package_name: str A human readable name identifying this package. This - may be used to produce an index of OSS packages used by - an applicatation. - package_url: The URL this instance was downloaded from. - package_version: The version number of this package. This should be a - value that increases over time, rather than a commit - hash. - kwargs: Other things may be specified, but they are explicitly ignored. + package_name : str A human readable name identifying this package. This + may be used to produce an index of OSS packages used by + an application. + tags: list(str) tags applied to the rule """ - single_kind = kwargs.pop("license_kind", default = None) - if single_kind: + if license_kind: if license_kinds: fail("Can not use both license_kind and license_kinds") - license_kinds = [single_kind] - tags = tags or [] + license_kinds = [license_kind] + + # Make sure the file exists as named in the rule. A glob expression that + # expands to the name of the file is not acceptable. + srcs = native.glob([license_text]) + if len(srcs) != 1 or srcs[0] != license_text: + fail("Specified license file doesn't exist: %s" % license_text) + + _license( name = name, license_kinds = license_kinds, - license_text = license_text or "LICENSE", + license_text = license_text, copyright_notice = copyright_notice, package_name = package_name, package_url = package_url, package_version = package_version, + namespace = namespace, applicable_licenses = [], + visibility = visibility, tags = tags, - visibility = ["//visibility:public"], + testonly = 0, ) diff --git a/rules/license_impl.bzl b/rules/license_impl.bzl new file mode 100644 index 0000000..c506953 --- /dev/null +++ b/rules/license_impl.bzl @@ -0,0 +1,82 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Rules for declaring the licenses used by a package. + +See: go/license-checking-v2 +""" + +load( + "@rules_license//rules:providers.bzl", + "LicenseInfo", + "LicenseKindInfo", +) + +# Debugging verbosity +_VERBOSITY = 0 + +def _debug(loglevel, msg): + if _VERBOSITY > loglevel: + print(msg) # buildifier: disable=print + +# +# license() +# + +def license_rule_impl(ctx): + provider = LicenseInfo( + license_kinds = tuple([k[LicenseKindInfo] for k in ctx.attr.license_kinds]), + copyright_notice = ctx.attr.copyright_notice, + package_name = ctx.attr.package_name or ctx.build_file_path.rstrip("/BUILD"), + package_url = ctx.attr.package_url, + package_version = ctx.attr.package_version, + license_text = ctx.file.license_text, + label = ctx.label, + namespace = ctx.attr.namespace, + ) + _debug(0, provider) + return [provider] + +license_impl = rule( + implementation = license_rule_impl, + attrs = { + "license_kinds": attr.label_list( + mandatory = False, + doc = "License kind(s) of this license. If multiple license kinds are" + + " listed in the LICENSE file, and they all apply, then all" + + " should be listed here. If the user can choose a single one" + + " of many, then only list one here.", + providers = [LicenseKindInfo], + cfg = "exec", + ), + "copyright_notice": attr.string( + doc = "Copyright notice.", + ), + "license_text": attr.label( + allow_single_file = True, + default = "LICENSE", + doc = "The license file.", + ), + "package_name": attr.string( + doc = "A human readable name identifying this package." + + " This may be used to produce an index of OSS packages used by" + + " an applicatation.", + ), + "namespace": attr.string( + doc = "A human readable name used to organize licenses into categories." + + " This is used in google3 to differentiate third party licenses used" + + " for compliance versus internal licenses used by SLAsan for internal" + + " teams' SLAs.", + ), + }, +) diff --git a/rules/license_kind.bzl b/rules/license_kind.bzl index 47b7639..5c8022e 100644 --- a/rules/license_kind.bzl +++ b/rules/license_kind.bzl @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Proof of concept. License restriction.""" load("@rules_license//rules:providers.bzl", "LicenseKindInfo") @@ -30,6 +29,7 @@ def _license_kind_impl(ctx): ctx.label.package, ctx.label.name, ), + long_name = ctx.attr.long_name, conditions = ctx.attr.conditions, ) return [provider] @@ -37,15 +37,16 @@ def _license_kind_impl(ctx): _license_kind = rule( implementation = _license_kind_impl, attrs = { - "canonical_text": attr.label( - doc = "File containing the canonical text for this license. Must be UTF-8 encoded.", - allow_single_file = True, - ), "conditions": attr.string_list( doc = "Conditions to be met when using software under this license." + " Conditions are defined by the organization using this license.", mandatory = True, ), + "canonical_text": attr.label( + doc = "File containing the canonical text for this license. Must be UTF-8 encoded.", + allow_single_file = True, + ), + "long_name": attr.string(doc = "Human readable long name of license."), "url": attr.string(doc = "URL pointing to canonical license definition"), }, ) @@ -53,6 +54,8 @@ _license_kind = rule( def license_kind(name, **kwargs): if "conditions" not in kwargs: kwargs["conditions"] = [] + if "long_name" not in kwargs: + kwargs["long_name"] = name _license_kind( name = name, applicable_licenses = [], diff --git a/rules/license_policy.bzl b/rules/license_policy.bzl deleted file mode 100644 index 0539301..0000000 --- a/rules/license_policy.bzl +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""license_policy rule. - -A license_policy names a set of conditions allowed in the union of all -license_kinds use by a target. The name of the rule is typically an -application type (e.g. production_server, mobile_application, ...) - -""" - -load("@rules_license//rules:license_policy_provider.bzl", "LicensePolicyInfo") - -def _license_policy_impl(ctx): - provider = LicensePolicyInfo( - name = ctx.attr.name, - label = "@%s//%s:%s" % ( - ctx.label.workspace_name, - ctx.label.package, - ctx.label.name, - ), - conditions = ctx.attr.conditions, - ) - return [provider] - -_license_policy = rule( - implementation = _license_policy_impl, - attrs = { - "conditions": attr.string_list( - doc = "Conditions to be met when using software under this license." + - " Conditions are defined by the organization using this license.", - mandatory = True, - ), - }, -) - -def license_policy(name, conditions): - _license_policy( - name = name, - conditions = conditions, - applicable_licenses = [], - ) diff --git a/rules/license_policy_check.bzl b/rules/license_policy_check.bzl deleted file mode 100644 index 49ab207..0000000 --- a/rules/license_policy_check.bzl +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""License compliance checking at analysis time.""" - -load( - "@rules_license//rules:gather_licenses_info.bzl", - "gather_licenses_info", -) -load( - "@rules_license//rules:license_policy_provider.bzl", - "LicensePolicyInfo", -) -load( - "@rules_license//rules:providers.bzl", - "LicensesInfo", -) - -def _license_policy_check_impl(ctx): - policy = ctx.attr.policy[LicensePolicyInfo] - allowed_conditions = policy.conditions - if LicensesInfo in ctx.attr.target: - for license in ctx.attr.target[LicensesInfo].licenses.to_list(): - for kind in license.license_kinds: - # print(kind.conditions) - for condition in kind.conditions: - if condition not in allowed_conditions: - fail("Condition %s violates policy %s" % ( - condition, - policy.label, - )) - return [DefaultInfo()] - -_license_policy_check = rule( - implementation = _license_policy_check_impl, - doc = """Internal implementation method for license_policy_check().""", - attrs = { - "policy": attr.label( - doc = """Policy definition.""", - mandatory = True, - providers = [LicensePolicyInfo], - ), - "target": attr.label( - doc = """Target to collect LicenseInfo for.""", - aspects = [gather_licenses_info], - mandatory = True, - allow_single_file = True, - ), - }, -) - -def license_policy_check(name, target, policy, **kwargs): - """Checks a target against a policy. - - Args: - name: The target. - target: A target to test for compliance with a policy - policy: A rule providing LicensePolicyInfo. - **kwargs: other args. - - Usage: - - license_policy_check( - name = "license_info", - target = ":my_app", - policy = "//my_org/compliance/policies:mobile_application", - ) - """ - _license_policy_check(name = name, target = target, policy = policy, **kwargs) diff --git a/rules/licenses_core.bzl b/rules/licenses_core.bzl new file mode 100644 index 0000000..42702bd --- /dev/null +++ b/rules/licenses_core.bzl @@ -0,0 +1,187 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Rules and macros for collecting LicenseInfo providers.""" + +load("@rules_license//rules:filtered_rule_kinds.bzl", "aspect_filters") +load("@rules_license//rules:user_filtered_rule_kinds.bzl", "user_aspect_filters") +load( + "@rules_license//rules:providers.bzl", + "LicenseInfo", + "LicensedTargetInfo", +) + + +TraceInfo = provider( + doc = """Provides a target (as a string) to assist in debugging dependency issues.""", + fields = { + "trace": "String: a target to trace dependency edges to.", + }, +) + +def _trace_impl(ctx): + return TraceInfo(trace = ctx.build_setting_value) + +trace = rule( + doc = """Used to allow the specification of a target to trace while collecting license dependencies.""", + implementation = _trace_impl, + build_setting = config.string(flag = True), +) + +def should_traverse(ctx, attr): + """Checks if the dependent attribute should be traversed. + + Args: + ctx: The aspect evaluation context. + attr: The name of the attribute to be checked. + + Returns: + True iff the attribute should be traversed. + """ + k = ctx.rule.kind + + for filters in [aspect_filters, user_aspect_filters]: + always_ignored = filters.get("*", []) + if k in filters: + attr_matches = filters[k] + if (attr in attr_matches or + "*" in attr_matches or + ("_*" in attr_matches and attr.startswith("_")) or + attr in always_ignored): + return False + + for m in attr_matches: + if attr == m: + return False + + return True + +def _get_transitive_licenses(ctx, trans_licenses, trans_deps, traces, provider, filter_func): + attrs = [a for a in dir(ctx.rule.attr)] + for name in attrs: + if not filter_func(ctx, name): + continue + a = getattr(ctx.rule.attr, name) + + # Make anything singleton into a list for convenience. + if type(a) != type([]): + a = [a] + for dep in a: + # Ignore anything that isn't a target + if type(dep) != "Target": + continue + + # Targets can also include things like input files that won't have the + # aspect, so we additionally check for the aspect rather than assume + # it's on all targets. Even some regular targets may be synthetic and + # not have the aspect. This provides protection against those outlier + # cases. + if provider in dep: + info = dep[provider] + if info.licenses: + trans_licenses.append(info.licenses) + if info.deps: + trans_deps.append(info.deps) + if info.traces: + for trace in info.traces: + traces.append("(" + ", ".join([str(ctx.label), ctx.rule.kind, name]) + ") -> " + trace) + +def gather_licenses_info_common(target, ctx, provider_factory, namespaces, filter_func): + """Collect license info from myself and my deps. + + Any single target might directly depend on a license, or depend on + something that transitively depends on a license, or neither. + This aspect bundles all those into a single provider. At each level, we add + in new direct license deps found and forward up the transitive information + collected so far. + + This is a common abstraction for crawling the dependency graph. It is parameterized + to allow specifying the provider that is populated with results. It is + configurable to select only licenses matching a certain namespace. It is also + configurable to specify which dependency edges should not be traced for the + purpose of tracing the graph. + + Args: + target: The target of the aspect. + ctx: The aspect evaluation context. + provider_factory: abstracts the provider returned by this aspect + namespaces: a list of namespaces licenses must match to be included + filter_func: a function that returns true iff the dep edge should be ignored + + Returns: + provider of parameterized type + """ + + # First we gather my direct license attachments + licenses = [] + if ctx.rule.kind == "_license": + # Don't try to gather licenses from the license rule itself. We'll just + # blunder into the text file of the license and pick up the default + # attribute of the package, which we don't want. + pass + else: + if hasattr(ctx.rule.attr, "applicable_licenses"): + for dep in ctx.rule.attr.applicable_licenses: + if LicenseInfo in dep: + lic = dep[LicenseInfo] + + # This check shouldn't be necessary since any license created + # by the official code will have this set. However, one of the + # tests has its own implementation of license that had to be fixed + # so this is just a conservative safety check. + if hasattr(lic, "namespace"): + if lic.namespace in namespaces: + licenses.append(lic) + else: + fail("should have a namespace") + + + # Now gather transitive collection of providers from the targets + # this target depends upon. + trans_licenses = [] + trans_deps = [] + traces = [] + _get_transitive_licenses(ctx, trans_licenses, trans_deps, traces, provider_factory, filter_func) + + if not licenses and not trans_licenses: + return [provider_factory(deps = depset(), licenses = depset(), traces = [])] + + # If this is the target, start the sequence of traces. + if ctx.attr._trace[TraceInfo].trace and ctx.attr._trace[TraceInfo].trace in str(ctx.label): + traces = [ctx.attr._trace[TraceInfo].trace] + + # Trim the number of traces accumulated since the output can be quite large. + # A few representative traces are generally sufficient to identify why a dependency + # is incorrectly incorporated. + if len(traces) > 10: + traces = traces[0:10] + + if licenses: + # At this point we have a target and a list of directly used licenses. + # Bundle those together so we can report the exact targets that cause the + # dependency on each license. Since a list cannot be stored in a + # depset, even inside a provider, the list is concatenated into a + # string and will be unconcatenated in the output phase. + direct_license_uses = [LicensedTargetInfo( + target_under_license = target.label, + licenses = ",".join([str(x.label) for x in licenses]), + )] + else: + direct_license_uses = None + + return [provider_factory( + target_under_license = target.label, + licenses = depset(tuple(licenses), transitive = trans_licenses), + deps = depset(direct = direct_license_uses, transitive = trans_deps), + traces = traces, + )] diff --git a/rules/providers.bzl b/rules/providers.bzl index 9e830ce..8778fd7 100644 --- a/rules/providers.bzl +++ b/rules/providers.bzl @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,34 +11,51 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Providers for license rules.""" LicenseKindInfo = provider( - doc = """Provides information about a license kind.""", + doc = """Provides information about a license_kind instance.""", fields = { - "conditions": "List of conditions to be met when using this software.", - "label": "The full path to the license kind definition.", - "name": "License Name", + "conditions": "list(string): List of conditions to be met when using this packages under this license.", + "label": "Label: The full path to the license kind definition.", + "long_name": "string: Human readable license name", + "name": "string: Canonical license name", }, ) LicenseInfo = provider( - doc = """Provides information about an instance of a license.""", + doc = """Provides information about a license instance.""", fields = { - "copyright_notice": "Human readable short copyright notice", - "license_kinds": "License kinds", - "license_text": "License file", - "package_name": "Human readable package name", + "copyright_notice": "string: Human readable short copyright notice", + "label": "Label: label of the license rule", + "license_kinds": "list(LicenseKindInfo): License kinds ", + "license_text": "string: The license file path", + "namespace": "string: namespace of the license rule", + # TODO(aiuto): move to PackageInfo + "package_name": "string: Human readable package name", "package_url": "URL from which this package was downloaded.", "package_version": "Human readable version string", - "rule": "From whence this came", }, ) -LicensesInfo = provider( - doc = """The set of license instances used in a target.""", +LicensedTargetInfo = provider( + doc = """Lists the licenses directly used by a single target.""", fields = { - "licenses": "list(LicenseInfo).", + "target_under_license": "Label: The target label", + "licenses": "list(label of a license rule)", }, ) + +def licenses_info(): + return provider( + doc = """The transitive set of licenses used by a target.""", + fields = { + "target_under_license": "Label: The top level target label.", + "deps": "depset(LicensedTargetInfo): The transitive list of dependencies that have licenses.", + "licenses": "depset(LicenseInfo)", + "traces": "list(string) - diagnostic for tracing a dependency relationship to a target.", + }, + ) + +# This provider is used by the aspect that is used by manifest() rules. +TransitiveLicensesInfo = licenses_info() diff --git a/rules/license_policy_provider.bzl b/rules/user_filtered_rule_kinds.bzl index caecce8..a099794 100644 --- a/rules/license_policy_provider.bzl +++ b/rules/user_filtered_rule_kinds.bzl @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,14 +11,18 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +"""Filtered rule kinds for aspect inspection. -"""LicensePolicyProvider.""" +The format of this dictionary is: + rule_name: [attr, attr, ...] -LicensePolicyInfo = provider( - doc = """Declares a policy name and the license conditions allowable under it.""", - fields = { - "conditions": "List of conditions to be met when using this software.", - "label": "The full path to the license policy definition.", - "name": "License policy name", - }, -) +Filters for rules that are not part of the Bazel distribution should be added +to this file. + +Attributes are either the explicit list of attributes to filter, or '_*' which +would ignore all attributes prefixed with a _. +""" + +# Rule kinds with attributes the aspect currently needs to ignore +user_aspect_filters = { +} diff --git a/tests/BUILD b/tests/BUILD index 256386a..10b8560 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -1,13 +1,17 @@ -"""Test cases for license rules.""" +# Test cases for license rules. -load("@rules_license//rules:compliance.bzl", "check_license") +load("@rules_license//tools:test_helpers.bzl", "golden_test") +load("@rules_license//rules:compliance.bzl", "check_license", "licenses_used") load("@rules_license//rules:license.bzl", "license") load("@rules_license//rules:license_kind.bzl", "license_kind") -load("@rules_license//tools:test_helpers.bzl", "golden_test") - -package(default_applicable_licenses = [":license"]) -licenses(["notice"]) +package( + default_applicable_licenses = [":license"], + default_visibility = [ + "//examples:__subpackages__", + "//tests:__subpackages__", + ], +) # license_kind rules generally appear in a central location per workspace. They # are intermingled with normal target build rules @@ -41,8 +45,16 @@ license( license( name = "license_for_extra_feature", package_name = "A test case package", - license = "LICENSE.extra", license_kinds = [":generic_restricted_license"], + license_text = "LICENSE.extra", +) + +# This license is not in the "compliance" namespace and +# therefore should not show up in the report verified by +# :verify_cc_app_test +license( + name = "internal_non_compliance_license", + namespace = "test_namespace", ) cc_binary( @@ -55,7 +67,18 @@ cc_binary( cc_library( name = "c_bar", - srcs = ["bar.cc"], + srcs = [ + "bar.cc", + ], + applicable_licenses = [ + ":license", + ":license_for_extra_feature", + ":internal_non_compliance_license", + ], + #deps = [ + # "@rules_license//rules/tests/legacy:another_library_with_legacy_license_clause", + # "@rules_license//rules/tests/legacy:library_with_legacy_license_clause", + #], ) java_binary( @@ -87,6 +110,28 @@ check_license( ], ) +licenses_used( + name = "hello_licenses", + out = "hello_licenses.json", + deps = [":hello"], +) + +py_test( + name = "hello_licenses_test", + srcs = ["hello_licenses_test.py"], + data = [":hello_licenses.json"], + python_version = "PY3", + deps = [ + ":license_test_utils", + ], +) + +py_library( + name = "license_test_utils", + srcs = ["license_test_utils.py"], + srcs_version = "PY3", +) + check_license( name = "check_java_app", check_conditions = False, @@ -109,3 +154,7 @@ golden_test( golden = "hello_java_copyrights.golden", subject = ":hello_java_copyrights.txt", ) + +exports_files([ + "hello_licenses.golden", +]) diff --git a/tests/apps/BUILD b/tests/apps/BUILD new file mode 100644 index 0000000..2c0778f --- /dev/null +++ b/tests/apps/BUILD @@ -0,0 +1,70 @@ +# Test cases for license rules: Sample app + +load("@rules_license//rules:compliance.bzl", "licenses_used") + +package(default_visibility = ["//examples:__subpackages__"]) + +# Note that the app explicitly depends only on a library and some legacy +# style licensed code. +cc_binary( + name = "an_app", + srcs = ["an_app.cc"], + deps = [ + ":level4", + # "@rules_license//rules/tests/legacy:another_library_with_legacy_license_clause", + # "@rules_license//rules/tests/legacy:library_with_legacy_license_clause", + ], +) + +# pointless chain of libraries to show transitive rule gathering, culminating +# in a diamond dependency on a library under license. +# Note that the lowest level depends on some third party code +[ + genrule( + name = "level_%d_src" % level, + outs = ["level_%d.cc" % level], + # Note to reviewers: This should use string format, but format + # is broken when + cmd = """cat >$@ <<END + #include <iostream> + extern void {lower}(); + void lib_level_{level}() {{ + std::cout << "This is level {level}" << std::endl; + {lower}(); + }} +END + """.format( + level = level, + lower = "lib_level_%d" % (level - 1) if level > 0 else "new_lib_func", + ), + ) + for level in range(5) +] + +[ + cc_library( + name = "level%d" % level, + srcs = [":level_%d.cc" % level], + deps = [ + (":level%d" % (level - 1) if level > 0 else "@rules_license//tests/thrdparty:new_style_lib"), + ], + ) + for level in range(5) +] + +licenses_used( + name = "an_app_licenses", + out = "an_app_licenses.json", + deps = [":an_app"], +) + +# Examining the golden file shows that we depend on both kinds of license. +py_test( + name = "an_app_licenses_test", + srcs = ["an_app_licenses_test.py"], + data = [":an_app_licenses.json"], + python_version = "PY3", + deps = [ + "@rules_license//tests:license_test_utils", + ], +) diff --git a/tests/apps/an_app.cc b/tests/apps/an_app.cc new file mode 100644 index 0000000..410f986 --- /dev/null +++ b/tests/apps/an_app.cc @@ -0,0 +1,11 @@ +#include <iostream> +#include <ostream> + +extern const char* server_message; +extern void lib_level_4(); + +int main(int argc, char* argv[]) { + std::cout << "main" << std::endl; + lib_level_4(); + return 0; +} diff --git a/tests/apps/an_app_licenses_test.py b/tests/apps/an_app_licenses_test.py new file mode 100644 index 0000000..9899e6c --- /dev/null +++ b/tests/apps/an_app_licenses_test.py @@ -0,0 +1,30 @@ +"""Tests for google3.tools.build_defs.license.tests.apps.an_app_licenses.""" + +import os + +import unittest +from tests import license_test_utils + + +class AnAppLicensesTest(unittest.TestCase): + + def test_has_expected_licenses(self): + package_base = license_test_utils.LICENSE_PACKAGE_BASE + licenses_info = license_test_utils.load_licenses_info( + os.path.join(os.path.dirname(__file__), "an_app_licenses.json")) + licenses_info = license_test_utils.filter_dependencies( + licenses_info, + target_filter=lambda targ: targ.startswith(package_base), + licenses_filter=lambda lic: lic.startswith(package_base)) + + expected = { + "/tests/thrdparty:new_style_lib": [ + "/tests/thrdparty:license", + ], + } + license_test_utils.check_licenses_of_dependencies( + self, licenses_info, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/hello_cc_copyrights.golden b/tests/hello_cc_copyrights.golden index ac28bab..38f67ad 100755 --- a/tests/hello_cc_copyrights.golden +++ b/tests/hello_cc_copyrights.golden @@ -1 +1,3 @@ package(A test case package/0.0.4), copyright(Copyright © 2019 Uncle Toasty) +package(A test case package), copyright() + diff --git a/tests/hello_licenses_test.py b/tests/hello_licenses_test.py new file mode 100644 index 0000000..465688f --- /dev/null +++ b/tests/hello_licenses_test.py @@ -0,0 +1,34 @@ +"""Tests for google3.tools.build_defs.license.tests.hello_licenses.""" + +import os + +import unittest +from tests import license_test_utils + + +class HelloLicensesTest(unittest.TestCase): + + def test_has_expected_licenses(self): + package_base = license_test_utils.LICENSE_PACKAGE_BASE + licenses_info = license_test_utils.load_licenses_info( + os.path.join(os.path.dirname(__file__), "hello_licenses.json")) + licenses_info = license_test_utils.filter_dependencies( + licenses_info, + target_filter=lambda targ: targ.startswith(package_base), + licenses_filter=lambda lic: lic.startswith(package_base)) + + expected = { + "/tests:hello": [ + "/tests:license", + ], + "/tests:c_bar": [ + "/tests:license", + "/tests:license_for_extra_feature", + ], + } + license_test_utils.check_licenses_of_dependencies( + self, licenses_info, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/license_test_utils.py b/tests/license_test_utils.py new file mode 100644 index 0000000..2c5a18a --- /dev/null +++ b/tests/license_test_utils.py @@ -0,0 +1,72 @@ +"""Utilities for writing tests of license rules.""" + +import codecs +import json + + +# This is extracted out to make it easier to keep test equivalence between +# the OSS version and Google. +LICENSE_PACKAGE_BASE = "/" + + +def load_licenses_info(info_path): + """Loads the licenses_info() JSON format.""" + with codecs.open(info_path, encoding="utf-8") as licenses_file: + return json.loads(licenses_file.read()) + + +def filter_dependencies(licenses_info, target_filter=None, + licenses_filter=None): + """Filters licenses_info to only include dependencies of interest. + + Args: + licenses_info: (dict) licenses info. + target_filter: (function): function which returns true if we should include + the target. + licenses_filter: (function): function which returns true if we should + include the license. + Returns: + (dict) a valid licenses_info dict. + """ + top_target = licenses_info[0] + new_top_target = dict(top_target) + new_deps = [] + for dep in top_target["dependencies"]: + target_name = dep["target_under_license"] + if target_filter and not target_filter(target_name): + continue + licenses = dep["licenses"] + if licenses_filter: + licenses = [lic for lic in licenses if licenses_filter(lic)] + new_deps.append({ + "target_under_license": target_name, + "licenses": licenses}) + new_top_target["dependencies"] = new_deps + return [new_top_target] + + +def check_licenses_of_dependencies(test_case, licenses_info, expected, + path_prefix=LICENSE_PACKAGE_BASE): + """Checks that licenses_info contains an expected set of licenses. + + Args: + test_case: (TestCase) the test. + licenses_info: (dict) licenses info. + expected: (dict) map of target names to the licenses they are under. Names + must be relative to the licenses package, not absolute. + path_prefix: (str) prefix to prepend to targets and licenses in expected. + This turns the relative target names to absolute ones. + """ + + # Turn the list of deps into a dict by target for easier comparison. + print(licenses_info) + deps_to_licenses = { + x["target_under_license"].lstrip('@'): set(l.strip('@') for l in x["licenses"]) + for x in licenses_info[0]["dependencies"]} + print(deps_to_licenses) + + for target, licenses in expected.items(): + got_licenses = set(deps_to_licenses[path_prefix + target]) + for lic in licenses: + test_case.assertIn(path_prefix + lic, got_licenses) + # future: Maybe check that deps is not larger than expected. diff --git a/tests/thrdparty/BUILD b/tests/thrdparty/BUILD new file mode 100644 index 0000000..2fa9752 --- /dev/null +++ b/tests/thrdparty/BUILD @@ -0,0 +1,26 @@ +# A sample library using new license rules. + +load("@rules_license//rules:license.bzl", "license") + +package( + default_applicable_licenses = [":license"], + default_visibility = [ + "//examples:__subpackages__", + "//tests:__subpackages__", + ], +) + +# The default license for an entire package is typically named "license". +license( + name = "license", + package_name = "migrated package", + license_kinds = ["//licenses/generic:restricted"], + license_text = "LICENSE", +) + +cc_library( + name = "new_style_lib", + srcs = [ + "new_style_lib.cc", + ], +) diff --git a/tests/thrdparty/LICENSE b/tests/thrdparty/LICENSE new file mode 100644 index 0000000..e5bcd1f --- /dev/null +++ b/tests/thrdparty/LICENSE @@ -0,0 +1 @@ +I am restricted in some way. diff --git a/tests/thrdparty/new_style_lib.cc b/tests/thrdparty/new_style_lib.cc new file mode 100644 index 0000000..545e5b4 --- /dev/null +++ b/tests/thrdparty/new_style_lib.cc @@ -0,0 +1,8 @@ + +#include <iostream> +#include <ostream> + +void new_lib_func() { + std::cout << "This is restricted code" << std::endl; +} + diff --git a/tools/checker_demo.py b/tools/checker_demo.py index 73042aa..1075621 100644 --- a/tools/checker_demo.py +++ b/tools/checker_demo.py @@ -23,14 +23,19 @@ import codecs import json # Conditions allowed for all applications -_ALWAYS_ALLOWED_CONDITIONS = frozenset(['notice', 'permissive', 'unencumberd']) +_ALWAYS_ALLOWED_CONDITIONS = frozenset(['notice', 'permissive', 'unencumbered']) -def _get_licenses(licenses_info): +def _load_license_data(licenses_info): with codecs.open(licenses_info, encoding='utf-8') as licenses_file: return json.loads(licenses_file.read()) +def unique_licenses(licenses): + for target in licenses: + for lic in target.get('licenses') or []: + yield lic + def _do_report(out, licenses): """Produce a report showing the set of licenses being used. @@ -41,11 +46,14 @@ def _do_report(out, licenses): Returns: 0 for no restricted licenses. """ - for lic in licenses: # using strange name lic because license is built-in - rule = lic['rule'] - for kind in lic['license_kinds']: - out.write('= %s\n kind: %s\n' % (rule, kind['target'])) - out.write(' conditions: %s\n' % kind['conditions']) + + for target in unique_licenses(licenses): + for lic in target.get('licenses') or []: + print("lic:", lic) + rule = lic['rule'] + for kind in lic['license_kinds']: + out.write('= %s\n kind: %s\n' % (rule, kind['target'])) + out.write(' conditions: %s\n' % kind['conditions']) def _check_conditions(out, licenses, allowed_conditions): @@ -82,11 +90,12 @@ def _do_copyright_notices(out, licenses): if l.get('package_version'): name = name + "/" + l['package_version'] # IGNORE_COPYRIGHT: Not a copyright notice. It is a variable holding one. + print(l) out.write('package(%s), copyright(%s)\n' % (name, l['copyright_notice'])) def _do_licenses(out, licenses): - for lic in licenses: + for lic in unique_licenses(licenses): path = lic['license_text'] with codecs.open(path, encoding='utf-8') as license_file: out.write('= %s\n' % path) @@ -107,7 +116,14 @@ def main(): help='check that the dep only includes allowed license conditions') args = parser.parse_args() - licenses = _get_licenses(args.licenses_info) + license_data = _load_license_data(args.licenses_info) + target = license_data[0] # we assume only one target for the demo + + top_level_target = target['top_level_target'] + dependencies = target['dependencies'] + licenses = target['licenses'] + print(licenses) + err = 0 with codecs.open(args.report, mode='w', encoding='utf-8') as rpt: _do_report(rpt, licenses) diff --git a/tools/test_helpers.bzl b/tools/test_helpers.bzl index 30f1980..3ffb9b7 100644 --- a/tools/test_helpers.bzl +++ b/tools/test_helpers.bzl @@ -38,3 +38,50 @@ def golden_test( golden, ], ) + +def golden_cmd_test( + name, + cmd, + golden, # Required + toolchains = [], + tools = None, + exec_tools = None, + srcs = [], # Optional + **kwargs): # Rest + """Compares cmd output to golden output, passes if they are identical. + + Args: + name: Name of the build rule. + cmd: The command to run to generate output. + golden: The golden file to be compared. + toolchains: List of toolchains needed to run the command, passed to genrule. + tools: List of tools needed to run the command, passed to genrule. + exec_tools: List of tools needed to run the command, passed to genrule. + srcs: List of sources needed as input to the command, passed to genrule. + **kwargs: Any additional parameters for the generated golden_test. + """ + actual = name + ".output" + + # There are some cases where tools are provided and exec_tools are provided. + # Specifying both in the same genrule, confuses the host vs exec rules, + # which prevents python3 from execution. + if tools and exec_tools: + fail("Only set one: tools or exec_tools. " + + "Setting both confuses python execution mode (host vs exec).") + native.genrule( + name = name + "_output", + srcs = srcs, + outs = [actual], + cmd = cmd + " > '$@'", # Redirect to collect output + toolchains = toolchains, + tools = tools, + exec_tools = exec_tools, + testonly = True, + ) + + golden_test( + name = name, + subject = actual, + golden = golden, + **kwargs + ) |