From 90d9454d1e431277bf92e57cecae28f056662a3f Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Tue, 18 Aug 2020 10:56:54 -0400 Subject: [PATCH 1/4] utils/spdx: add support for complex expressions Co-authored-by: Seeker --- .github/workflows/spdx.yml | 2 +- Library/Homebrew/config.rb | 3 + .../Homebrew/data/spdx/spdx_exceptions.json | 466 ++++++++++++++++++ .../{spdx.json => spdx/spdx_licenses.json} | 0 .../Homebrew/dev-cmd/update-license-data.rb | 6 +- Library/Homebrew/test/support/lib/config.rb | 3 + Library/Homebrew/test/utils/spdx_spec.rb | 283 ++++++++++- Library/Homebrew/utils/spdx.rb | 154 +++++- 8 files changed, 900 insertions(+), 17 deletions(-) create mode 100644 Library/Homebrew/data/spdx/spdx_exceptions.json rename Library/Homebrew/data/{spdx.json => spdx/spdx_licenses.json} (100%) diff --git a/.github/workflows/spdx.yml b/.github/workflows/spdx.yml index 6cd9a5739f..1126ced451 100644 --- a/.github/workflows/spdx.yml +++ b/.github/workflows/spdx.yml @@ -26,7 +26,7 @@ jobs: run: | cd "$GITHUB_WORKSPACE/Library/Homebrew" if brew update-license-data --commit --fail-if-not-changed; then - SPDX_VERSION=$(jq -er .licenseListVersion data/spdx.json) + SPDX_VERSION=$(jq -er .licenseListVersion data/spdx/spdx_licenses.json) if ! git ls-remote --exit-code --heads origin "spdx-$SPDX_VERSION"; then git checkout -b "spdx-$SPDX_VERSION" git push origin "spdx-$SPDX_VERSION" diff --git a/Library/Homebrew/config.rb b/Library/Homebrew/config.rb index 30d39094de..e4514dfabd 100644 --- a/Library/Homebrew/config.rb +++ b/Library/Homebrew/config.rb @@ -29,6 +29,9 @@ HOMEBREW_LIBRARY = Pathname.new(get_env_or_raise("HOMEBREW_LIBRARY")).freeze # Where shim scripts for various build and SCM tools are stored HOMEBREW_SHIMS_PATH = (HOMEBREW_LIBRARY/"Homebrew/shims").freeze +# Where external data that has been incorporated into Homebrew is stored +HOMEBREW_DATA_PATH = (HOMEBREW_LIBRARY/"Homebrew/data").freeze + # Where we store symlinks to currently linked kegs HOMEBREW_LINKED_KEGS = (HOMEBREW_PREFIX/"var/homebrew/linked").freeze diff --git a/Library/Homebrew/data/spdx/spdx_exceptions.json b/Library/Homebrew/data/spdx/spdx_exceptions.json new file mode 100644 index 0000000000..9efd4ea6d7 --- /dev/null +++ b/Library/Homebrew/data/spdx/spdx_exceptions.json @@ -0,0 +1,466 @@ +{ + "licenseListVersion": "3.10", + "releaseDate": "2020-08-03", + "exceptions": [ + { + "reference": "./GCC-exception-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/GCC-exception-2.0.json", + "referenceNumber": "1", + "name": "GCC Runtime Library exception 2.0", + "seeAlso": [ + "https://gcc.gnu.org/git/?p\u003dgcc.git;a\u003dblob;f\u003dgcc/libgcc1.c;h\u003d762f5143fc6eed57b6797c82710f3538aa52b40b;hb\u003dcb143a3ce4fb417c68f5fa2691a1b1b1053dfba9#l10" + ], + "licenseExceptionId": "GCC-exception-2.0" + }, + { + "reference": "./openvpn-openssl-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/openvpn-openssl-exception.json", + "referenceNumber": "2", + "name": "OpenVPN OpenSSL Exception", + "seeAlso": [ + "http://openvpn.net/index.php/license.html" + ], + "licenseExceptionId": "openvpn-openssl-exception" + }, + { + "reference": "./Nokia-Qt-exception-1.1.html", + "isDeprecatedLicenseId": true, + "detailsUrl": "http://spdx.org/licenses/Nokia-Qt-exception-1.1.json", + "referenceNumber": "3", + "name": "Nokia Qt LGPL exception 1.1", + "seeAlso": [ + "https://www.keepassx.org/dev/projects/keepassx/repository/revisions/b8dfb9cc4d5133e0f09cd7533d15a4f1c19a40f2/entry/LICENSE.NOKIA-LGPL-EXCEPTION" + ], + "licenseExceptionId": "Nokia-Qt-exception-1.1" + }, + { + "reference": "./GPL-3.0-linking-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/GPL-3.0-linking-exception.json", + "referenceNumber": "4", + "name": "GPL-3.0 Linking Exception", + "seeAlso": [ + "https://www.gnu.org/licenses/gpl-faq.en.html#GPLIncompatibleLibs" + ], + "licenseExceptionId": "GPL-3.0-linking-exception" + }, + { + "reference": "./Fawkes-Runtime-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Fawkes-Runtime-exception.json", + "referenceNumber": "5", + "name": "Fawkes Runtime Exception", + "seeAlso": [ + "http://www.fawkesrobotics.org/about/license/" + ], + "licenseExceptionId": "Fawkes-Runtime-exception" + }, + { + "reference": "./u-boot-exception-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/u-boot-exception-2.0.json", + "referenceNumber": "6", + "name": "U-Boot exception 2.0", + "seeAlso": [ + "http://git.denx.de/?p\u003du-boot.git;a\u003dblob;f\u003dLicenses/Exceptions" + ], + "licenseExceptionId": "u-boot-exception-2.0" + }, + { + "reference": "./PS-or-PDF-font-exception-20170817.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/PS-or-PDF-font-exception-20170817.json", + "referenceNumber": "7", + "name": "PS/PDF font exception (2017-08-17)", + "seeAlso": [ + "https://github.com/ArtifexSoftware/urw-base35-fonts/blob/65962e27febc3883a17e651cdb23e783668c996f/LICENSE" + ], + "licenseExceptionId": "PS-or-PDF-font-exception-20170817" + }, + { + "reference": "./gnu-javamail-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/gnu-javamail-exception.json", + "referenceNumber": "8", + "name": "GNU JavaMail exception", + "seeAlso": [ + "http://www.gnu.org/software/classpathx/javamail/javamail.html" + ], + "licenseExceptionId": "gnu-javamail-exception" + }, + { + "reference": "./LGPL-3.0-linking-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/LGPL-3.0-linking-exception.json", + "referenceNumber": "9", + "name": "LGPL-3.0 Linking Exception", + "seeAlso": [ + "https://raw.githubusercontent.com/go-xmlpath/xmlpath/v2/LICENSE", + "https://github.com/goamz/goamz/blob/master/LICENSE", + "https://github.com/juju/errors/blob/master/LICENSE" + ], + "licenseExceptionId": "LGPL-3.0-linking-exception" + }, + { + "reference": "./DigiRule-FOSS-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/DigiRule-FOSS-exception.json", + "referenceNumber": "10", + "name": "DigiRule FOSS License Exception", + "seeAlso": [ + "http://www.digirulesolutions.com/drupal/foss" + ], + "licenseExceptionId": "DigiRule-FOSS-exception" + }, + { + "reference": "./LLVM-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/LLVM-exception.json", + "referenceNumber": "11", + "name": "LLVM Exception", + "seeAlso": [ + "http://llvm.org/foundation/relicensing/LICENSE.txt" + ], + "licenseExceptionId": "LLVM-exception" + }, + { + "reference": "./Linux-syscall-note.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Linux-syscall-note.json", + "referenceNumber": "12", + "name": "Linux Syscall Note", + "seeAlso": [ + "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/COPYING" + ], + "licenseExceptionId": "Linux-syscall-note" + }, + { + "reference": "./GPL-3.0-linking-source-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/GPL-3.0-linking-source-exception.json", + "referenceNumber": "13", + "name": "GPL-3.0 Linking Exception (with Corresponding Source)", + "seeAlso": [ + "https://www.gnu.org/licenses/gpl-faq.en.html#GPLIncompatibleLibs", + "https://github.com/mirror/wget/blob/master/src/http.c#L20" + ], + "licenseExceptionId": "GPL-3.0-linking-source-exception" + }, + { + "reference": "./Qwt-exception-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Qwt-exception-1.0.json", + "referenceNumber": "14", + "name": "Qwt exception 1.0", + "seeAlso": [ + "http://qwt.sourceforge.net/qwtlicense.html" + ], + "licenseExceptionId": "Qwt-exception-1.0" + }, + { + "reference": "./389-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/389-exception.json", + "referenceNumber": "15", + "name": "389 Directory Server Exception", + "seeAlso": [ + "http://directory.fedoraproject.org/wiki/GPL_Exception_License_Text" + ], + "licenseExceptionId": "389-exception" + }, + { + "reference": "./mif-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/mif-exception.json", + "referenceNumber": "16", + "name": "Macros and Inline Functions Exception", + "seeAlso": [ + "http://www.scs.stanford.edu/histar/src/lib/cppsup/exception", + "http://dev.bertos.org/doxygen/", + "https://www.threadingbuildingblocks.org/licensing" + ], + "licenseExceptionId": "mif-exception" + }, + { + "reference": "./eCos-exception-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/eCos-exception-2.0.json", + "referenceNumber": "17", + "name": "eCos exception 2.0", + "seeAlso": [ + "http://ecos.sourceware.org/license-overview.html" + ], + "licenseExceptionId": "eCos-exception-2.0" + }, + { + "reference": "./CLISP-exception-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/CLISP-exception-2.0.json", + "referenceNumber": "18", + "name": "CLISP exception 2.0", + "seeAlso": [ + "http://sourceforge.net/p/clisp/clisp/ci/default/tree/COPYRIGHT" + ], + "licenseExceptionId": "CLISP-exception-2.0" + }, + { + "reference": "./Bison-exception-2.2.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Bison-exception-2.2.json", + "referenceNumber": "19", + "name": "Bison exception 2.2", + "seeAlso": [ + "http://git.savannah.gnu.org/cgit/bison.git/tree/data/yacc.c?id\u003d193d7c7054ba7197b0789e14965b739162319b5e#n141" + ], + "licenseExceptionId": "Bison-exception-2.2" + }, + { + "reference": "./Libtool-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Libtool-exception.json", + "referenceNumber": "20", + "name": "Libtool Exception", + "seeAlso": [ + "http://git.savannah.gnu.org/cgit/libtool.git/tree/m4/libtool.m4" + ], + "licenseExceptionId": "Libtool-exception" + }, + { + "reference": "./LZMA-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/LZMA-exception.json", + "referenceNumber": "21", + "name": "LZMA exception", + "seeAlso": [ + "http://nsis.sourceforge.net/Docs/AppendixI.html#I.6" + ], + "licenseExceptionId": "LZMA-exception" + }, + { + "reference": "./OpenJDK-assembly-exception-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/OpenJDK-assembly-exception-1.0.json", + "referenceNumber": "22", + "name": "OpenJDK Assembly exception 1.0", + "seeAlso": [ + "http://openjdk.java.net/legal/assembly-exception.html" + ], + "licenseExceptionId": "OpenJDK-assembly-exception-1.0" + }, + { + "reference": "./Font-exception-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Font-exception-2.0.json", + "referenceNumber": "23", + "name": "Font exception 2.0", + "seeAlso": [ + "http://www.gnu.org/licenses/gpl-faq.html#FontException" + ], + "licenseExceptionId": "Font-exception-2.0" + }, + { + "reference": "./OCaml-LGPL-linking-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/OCaml-LGPL-linking-exception.json", + "referenceNumber": "24", + "name": "OCaml LGPL Linking Exception", + "seeAlso": [ + "https://caml.inria.fr/ocaml/license.en.html" + ], + "licenseExceptionId": "OCaml-LGPL-linking-exception" + }, + { + "reference": "./GCC-exception-3.1.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/GCC-exception-3.1.json", + "referenceNumber": "25", + "name": "GCC Runtime Library exception 3.1", + "seeAlso": [ + "http://www.gnu.org/licenses/gcc-exception-3.1.html" + ], + "licenseExceptionId": "GCC-exception-3.1" + }, + { + "reference": "./Bootloader-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Bootloader-exception.json", + "referenceNumber": "26", + "name": "Bootloader Distribution Exception", + "seeAlso": [ + "https://github.com/pyinstaller/pyinstaller/blob/develop/COPYING.txt" + ], + "licenseExceptionId": "Bootloader-exception" + }, + { + "reference": "./SHL-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/SHL-2.0.json", + "referenceNumber": "27", + "name": "Solderpad Hardware License v2.0", + "seeAlso": [ + "https://solderpad.org/licenses/SHL-2.0/" + ], + "licenseExceptionId": "SHL-2.0" + }, + { + "reference": "./Classpath-exception-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Classpath-exception-2.0.json", + "referenceNumber": "28", + "name": "Classpath exception 2.0", + "seeAlso": [ + "http://www.gnu.org/software/classpath/license.html", + "https://fedoraproject.org/wiki/Licensing/GPL_Classpath_Exception" + ], + "licenseExceptionId": "Classpath-exception-2.0" + }, + { + "reference": "./Swift-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Swift-exception.json", + "referenceNumber": "29", + "name": "Swift Exception", + "seeAlso": [ + "https://swift.org/LICENSE.txt", + "https://github.com/apple/swift-package-manager/blob/7ab2275f447a5eb37497ed63a9340f8a6d1e488b/LICENSE.txt#L205" + ], + "licenseExceptionId": "Swift-exception" + }, + { + "reference": "./Autoconf-exception-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Autoconf-exception-2.0.json", + "referenceNumber": "30", + "name": "Autoconf exception 2.0", + "seeAlso": [ + "http://ac-archive.sourceforge.net/doc/copyright.html", + "http://ftp.gnu.org/gnu/autoconf/autoconf-2.59.tar.gz" + ], + "licenseExceptionId": "Autoconf-exception-2.0" + }, + { + "reference": "./FLTK-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/FLTK-exception.json", + "referenceNumber": "31", + "name": "FLTK exception", + "seeAlso": [ + "http://www.fltk.org/COPYING.php" + ], + "licenseExceptionId": "FLTK-exception" + }, + { + "reference": "./freertos-exception-2.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/freertos-exception-2.0.json", + "referenceNumber": "32", + "name": "FreeRTOS Exception 2.0", + "seeAlso": [ + "https://web.archive.org/web/20060809182744/http://www.freertos.org/a00114.html" + ], + "licenseExceptionId": "freertos-exception-2.0" + }, + { + "reference": "./Universal-FOSS-exception-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Universal-FOSS-exception-1.0.json", + "referenceNumber": "33", + "name": "Universal FOSS Exception, Version 1.0", + "seeAlso": [ + "https://oss.oracle.com/licenses/universal-foss-exception/" + ], + "licenseExceptionId": "Universal-FOSS-exception-1.0" + }, + { + "reference": "./WxWindows-exception-3.1.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/WxWindows-exception-3.1.json", + "referenceNumber": "34", + "name": "WxWindows Library Exception 3.1", + "seeAlso": [ + "http://www.opensource.org/licenses/WXwindows" + ], + "licenseExceptionId": "WxWindows-exception-3.1" + }, + { + "reference": "./OCCT-exception-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/OCCT-exception-1.0.json", + "referenceNumber": "35", + "name": "Open CASCADE Exception 1.0", + "seeAlso": [ + "http://www.opencascade.com/content/licensing" + ], + "licenseExceptionId": "OCCT-exception-1.0" + }, + { + "reference": "./Autoconf-exception-3.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Autoconf-exception-3.0.json", + "referenceNumber": "36", + "name": "Autoconf exception 3.0", + "seeAlso": [ + "http://www.gnu.org/licenses/autoconf-exception-3.0.html" + ], + "licenseExceptionId": "Autoconf-exception-3.0" + }, + { + "reference": "./i2p-gpl-java-exception.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/i2p-gpl-java-exception.json", + "referenceNumber": "37", + "name": "i2p GPL+Java Exception", + "seeAlso": [ + "http://geti2p.net/en/get-involved/develop/licenses#java_exception" + ], + "licenseExceptionId": "i2p-gpl-java-exception" + }, + { + "reference": "./GPL-CC-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/GPL-CC-1.0.json", + "referenceNumber": "38", + "name": "GPL Cooperation Commitment 1.0", + "seeAlso": [ + "https://github.com/gplcc/gplcc/blob/master/Project/COMMITMENT", + "https://gplcc.github.io/gplcc/Project/README-PROJECT.html" + ], + "licenseExceptionId": "GPL-CC-1.0" + }, + { + "reference": "./Qt-LGPL-exception-1.1.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Qt-LGPL-exception-1.1.json", + "referenceNumber": "39", + "name": "Qt LGPL exception 1.1", + "seeAlso": [ + "http://code.qt.io/cgit/qt/qtbase.git/tree/LGPL_EXCEPTION.txt" + ], + "licenseExceptionId": "Qt-LGPL-exception-1.1" + }, + { + "reference": "./SHL-2.1.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/SHL-2.1.json", + "referenceNumber": "40", + "name": "Solderpad Hardware License v2.1", + "seeAlso": [ + "https://solderpad.org/licenses/SHL-2.1/" + ], + "licenseExceptionId": "SHL-2.1" + }, + { + "reference": "./Qt-GPL-exception-1.0.html", + "isDeprecatedLicenseId": false, + "detailsUrl": "http://spdx.org/licenses/Qt-GPL-exception-1.0.json", + "referenceNumber": "41", + "name": "Qt GPL exception 1.0", + "seeAlso": [ + "http://code.qt.io/cgit/qt/qtbase.git/tree/LICENSE.GPL3-EXCEPT" + ], + "licenseExceptionId": "Qt-GPL-exception-1.0" + } + ] +} \ No newline at end of file diff --git a/Library/Homebrew/data/spdx.json b/Library/Homebrew/data/spdx/spdx_licenses.json similarity index 100% rename from Library/Homebrew/data/spdx.json rename to Library/Homebrew/data/spdx/spdx_licenses.json diff --git a/Library/Homebrew/dev-cmd/update-license-data.rb b/Library/Homebrew/dev-cmd/update-license-data.rb index 78b9f3e333..dae194f907 100644 --- a/Library/Homebrew/dev-cmd/update-license-data.rb +++ b/Library/Homebrew/dev-cmd/update-license-data.rb @@ -28,13 +28,13 @@ module Homebrew SPDX.download_latest_license_data! - Homebrew.failed = system("git", "diff", "--stat", "--exit-code", SPDX::JSON_PATH) if args.fail_if_not_changed? + Homebrew.failed = system("git", "diff", "--stat", "--exit-code", SPDX::DATA_PATH) if args.fail_if_not_changed? return unless args.commit? ohai "git add" - safe_system "git", "add", SPDX::JSON_PATH + safe_system "git", "add", SPDX::DATA_PATH ohai "git commit" - system "git", "commit", "--message", "data/spdx.json: update to #{SPDX.latest_tag}" + system "git", "commit", "--message", "spdx license data: update to #{SPDX.latest_tag}" end end diff --git a/Library/Homebrew/test/support/lib/config.rb b/Library/Homebrew/test/support/lib/config.rb index a34a660740..a9c6769f70 100644 --- a/Library/Homebrew/test/support/lib/config.rb +++ b/Library/Homebrew/test/support/lib/config.rb @@ -19,6 +19,9 @@ end.freeze # Paths pointing into the Homebrew code base that persist across test runs HOMEBREW_SHIMS_PATH = (HOMEBREW_LIBRARY_PATH/"shims").freeze +# Where external data that has been incorporated into Homebrew is stored +HOMEBREW_DATA_PATH = (HOMEBREW_LIBRARY_PATH/"data").freeze + require "extend/git_repository" # Paths redirected to a temporary directory and wiped at the end of the test run diff --git a/Library/Homebrew/test/utils/spdx_spec.rb b/Library/Homebrew/test/utils/spdx_spec.rb index e99cc5e610..01fb65107e 100644 --- a/Library/Homebrew/test/utils/spdx_spec.rb +++ b/Library/Homebrew/test/utils/spdx_spec.rb @@ -3,30 +3,299 @@ require "utils/spdx" describe SPDX do - describe ".spdx_data" do + describe ".license_data" do it "has the license list version" do - expect(described_class.spdx_data["licenseListVersion"]).not_to eq(nil) + expect(described_class.license_data["licenseListVersion"]).not_to eq(nil) end it "has the release date" do - expect(described_class.spdx_data["releaseDate"]).not_to eq(nil) + expect(described_class.license_data["releaseDate"]).not_to eq(nil) end it "has licenses" do - expect(described_class.spdx_data["licenses"].length).not_to eq(0) + expect(described_class.license_data["licenses"].length).not_to eq(0) + end + end + + describe ".exception_data" do + it "has the license list version" do + expect(described_class.exception_data["licenseListVersion"]).not_to eq(nil) + end + + it "has the release date" do + expect(described_class.exception_data["releaseDate"]).not_to eq(nil) + end + + it "has exceptions" do + expect(described_class.exception_data["exceptions"].length).not_to eq(0) end end describe ".download_latest_license_data!", :needs_network do - let(:tmp_json_path) { Pathname.new("#{TEST_TMPDIR}/spdx.json") } + let(:tmp_json_path) { Pathname.new(TEST_TMPDIR) } after do - FileUtils.rm tmp_json_path + FileUtils.rm tmp_json_path/"spdx_licenses.json" + FileUtils.rm tmp_json_path/"spdx_exceptions.json" end it "downloads latest license data" do described_class.download_latest_license_data! to: tmp_json_path - expect(tmp_json_path).to exist + expect(tmp_json_path/"spdx_licenses.json").to exist + expect(tmp_json_path/"spdx_exceptions.json").to exist + end + end + + describe ".parse_license_expression" do + it "returns a single license" do + expect(described_class.parse_license_expression("MIT").first).to eq ["MIT"] + end + + it "returns a single license with plus" do + expect(described_class.parse_license_expression("Apache-2.0+").first).to eq ["Apache-2.0+"] + end + + it "returns multiple licenses with :any" do + expect(described_class.parse_license_expression(any_of: ["MIT", "0BSD"]).first).to eq ["MIT", "0BSD"] + end + + it "returns multiple licenses with :all" do + expect(described_class.parse_license_expression(all_of: ["MIT", "0BSD"]).first).to eq ["MIT", "0BSD"] + end + + it "returns multiple licenses with plus" do + expect(described_class.parse_license_expression(any_of: ["MIT", "EPL-1.0+"]).first).to eq ["MIT", "EPL-1.0+"] + end + + it "returns license and exception" do + license_expression = { "MIT" => { with: "LLVM-exception" } } + expect(described_class.parse_license_expression(license_expression)).to eq [["MIT"], ["LLVM-exception"]] + end + + it "returns licenses and exceptions for compex license expressions" do + license_expression = { any_of: [ + "MIT", + :public_domain, + all_of: ["0BSD", "Zlib"], + "curl" => { with: "LLVM-exception" }, + ] } + result = [["MIT", :public_domain, "0BSD", "Zlib", "curl"], ["LLVM-exception"]] + expect(described_class.parse_license_expression(license_expression)).to eq result + end + + it "returns :public_domain" do + expect(described_class.parse_license_expression(:public_domain).first).to eq [:public_domain] + end + end + + describe ".valid_license?" do + it "returns true for valid license identifier" do + expect(described_class.valid_license?("MIT")).to eq true + end + + it "returns false for invalid license identifier" do + expect(described_class.valid_license?("foo")).to eq false + end + + it "returns true for deprecated license identifier" do + expect(described_class.valid_license?("GPL-1.0")).to eq true + end + + it "returns true for license identifier with plus" do + expect(described_class.valid_license?("Apache-2.0+")).to eq true + end + + it "returns true for :public_domain" do + expect(described_class.valid_license?(:public_domain)).to eq true + end + end + + describe ".deprecated_license?" do + it "returns true for deprecated license identifier" do + expect(described_class.deprecated_license?("GPL-1.0")).to eq true + end + + it "returns false for non-deprecated license identifier" do + expect(described_class.deprecated_license?("MIT")).to eq false + end + + it "returns false for invalid license identifier" do + expect(described_class.deprecated_license?("foo")).to eq false + end + + it "returns false for :public_domain" do + expect(described_class.deprecated_license?(:public_domain)).to eq false + end + end + + describe ".valid_license_exception?" do + it "returns true for valid license exception identifier" do + expect(described_class.valid_license_exception?("LLVM-exception")).to eq true + end + + it "returns false for invalid license exception identifier" do + expect(described_class.valid_license_exception?("foo")).to eq false + end + + it "returns false for deprecated license exception identifier" do + expect(described_class.valid_license_exception?("Nokia-Qt-exception-1.1")).to eq false + end + end + + describe ".license_expression_to_string" do + it "returns a single license" do + expect(described_class.license_expression_to_string("MIT")).to eq "MIT" + end + + it "returns a single license with plus" do + expect(described_class.license_expression_to_string("Apache-2.0+")).to eq "Apache-2.0+" + end + + it "returns multiple licenses with :any" do + expect(described_class.license_expression_to_string(any_of: ["MIT", "0BSD"])).to eq "MIT or 0BSD" + end + + it "returns multiple licenses with :all" do + expect(described_class.license_expression_to_string(all_of: ["MIT", "0BSD"])).to eq "MIT and 0BSD" + end + + it "returns multiple licenses with plus" do + expect(described_class.license_expression_to_string(any_of: ["MIT", "EPL-1.0+"])).to eq "MIT or EPL-1.0+" + end + + it "returns license and exception" do + license_expression = { "MIT" => { with: "LLVM-exception" } } + expect(described_class.license_expression_to_string(license_expression)).to eq "MIT with LLVM-exception" + end + + it "returns licenses and exceptions for compex license expressions" do + license_expression = { any_of: [ + "MIT", + :public_domain, + all_of: ["0BSD", "Zlib"], + "curl" => { with: "LLVM-exception" }, + ] } + result = "MIT or Public Domain or (0BSD and Zlib) or (curl with LLVM-exception)" + expect(described_class.license_expression_to_string(license_expression)).to eq result + end + + it "returns :public_domain" do + expect(described_class.license_expression_to_string(:public_domain)).to eq "Public Domain" + end + end + + describe ".license_version_info_info" do + it "returns license without version" do + expect(described_class.license_version_info("MIT")).to eq ["MIT"] + end + + it "returns :public_domain without version" do + expect(described_class.license_version_info(:public_domain)).to eq [:public_domain] + end + + it "returns license with version" do + expect(described_class.license_version_info("Apache-2.0")).to eq ["Apache", "2.0", false] + end + + it "returns license with version and plus" do + expect(described_class.license_version_info("Apache-2.0+")).to eq ["Apache", "2.0", true] + end + + it "returns more complicated license with version" do + expect(described_class.license_version_info("CC-BY-3.0-AT")).to eq ["CC-BY", "3.0", false] + end + + it "returns more complicated license with version and plus" do + expect(described_class.license_version_info("CC-BY-3.0-AT+")).to eq ["CC-BY", "3.0", true] + end + + it "returns license with -only" do + expect(described_class.license_version_info("GPL-3.0-only")).to eq ["GPL", "3.0", false] + end + + it "returns license with -or-later" do + expect(described_class.license_version_info("GPL-3.0-or-later")).to eq ["GPL", "3.0", true] + end + end + + describe ".licenses_forbid_installation?" do + let(:mit_forbidden) { { "MIT" => described_class.license_version_info("MIT") } } + let(:epl_1_forbidden) { { "EPL-1.0" => described_class.license_version_info("EPL-1.0") } } + let(:epl_1_plus_forbidden) { { "EPL-1.0+" => described_class.license_version_info("EPL-1.0+") } } + let(:multiple_forbidden) { + { + "MIT" => described_class.license_version_info("MIT"), + "0BSD" => described_class.license_version_info("0BSD"), + } + } + let(:any_of_license) { { any_of: ["MIT", "0BSD"] } } + let(:all_of_license) { { all_of: ["MIT", "0BSD"] } } + let(:license_exception) { { "MIT" => { with: "LLVM-exception" } } } + + it "allows installation with no forbidden licenses" do + expect(described_class.licenses_forbid_installation?("MIT", {})).to eq false + end + + it "allows installation with non-forbidden license" do + expect(described_class.licenses_forbid_installation?("0BSD", mit_forbidden)).to eq false + end + + it "forbids installation with forbidden license" do + expect(described_class.licenses_forbid_installation?("MIT", mit_forbidden)).to eq true + end + + it "allows installation of later license version" do + expect(described_class.licenses_forbid_installation?("EPL-2.0", epl_1_forbidden)).to eq false + end + + it "forbids installation of later license version with plus in forbidden license list" do + expect(described_class.licenses_forbid_installation?("EPL-2.0", epl_1_plus_forbidden)).to eq true + end + + it "allows installation when one of the any_of licenses is allowed" do + expect(described_class.licenses_forbid_installation?(any_of_license, mit_forbidden)).to eq false + end + + it "forbids installation when none of the any_of licenses are allowed" do + expect(described_class.licenses_forbid_installation?(any_of_license, multiple_forbidden)).to eq true + end + + it "forbids installation when one of the all_of licenses is allowed" do + expect(described_class.licenses_forbid_installation?(all_of_license, mit_forbidden)).to eq true + end + + it "allows installation with license + exception that aren't forbidden" do + expect(described_class.licenses_forbid_installation?(license_exception, epl_1_forbidden)).to eq false + end + + it "forbids installation with license + exception that are't forbidden" do + expect(described_class.licenses_forbid_installation?(license_exception, mit_forbidden)).to eq true + end + end + + describe ".forbidden_licenses_include?" do + let(:mit_forbidden) { { "MIT" => described_class.license_version_info("MIT") } } + let(:epl_1_forbidden) { { "EPL-1.0" => described_class.license_version_info("EPL-1.0") } } + let(:epl_1_plus_forbidden) { { "EPL-1.0+" => described_class.license_version_info("EPL-1.0+") } } + + it "returns false with no forbidden licenses" do + expect(described_class.forbidden_licenses_include?("MIT", {})).to eq false + end + + it "returns false with no matching forbidden licenses" do + expect(described_class.forbidden_licenses_include?("MIT", epl_1_forbidden)).to eq false + end + + it "returns true with matching license" do + expect(described_class.forbidden_licenses_include?("MIT", mit_forbidden)).to eq true + end + + it "returns false with later version of forbidden license" do + expect(described_class.forbidden_licenses_include?("EPL-2.0", epl_1_forbidden)).to eq false + end + + it "returns true with later version of forbidden license with later versions forbidden" do + expect(described_class.forbidden_licenses_include?("EPL-2.0", epl_1_plus_forbidden)).to eq true end end end diff --git a/Library/Homebrew/utils/spdx.rb b/Library/Homebrew/utils/spdx.rb index ffa2de8054..a52cf6090c 100644 --- a/Library/Homebrew/utils/spdx.rb +++ b/Library/Homebrew/utils/spdx.rb @@ -5,19 +5,161 @@ require "utils/github" module SPDX module_function - JSON_PATH = (HOMEBREW_LIBRARY_PATH/"data/spdx.json").freeze + DATA_PATH = (HOMEBREW_DATA_PATH/"spdx").freeze API_URL = "https://api.github.com/repos/spdx/license-list-data/releases/latest" - def spdx_data - @spdx_data ||= JSON.parse(JSON_PATH.read) + def license_data + @license_data ||= JSON.parse (DATA_PATH/"spdx_licenses.json").read + end + + def exception_data + @exception_data ||= JSON.parse (DATA_PATH/"spdx_exceptions.json").read end def latest_tag @latest_tag ||= GitHub.open_api(API_URL)["tag_name"] end - def download_latest_license_data!(to: JSON_PATH) - data_url = "https://raw.githubusercontent.com/spdx/license-list-data/#{latest_tag}/json/licenses.json" - curl_download(data_url, to: to, partial: false) + def download_latest_license_data!(to: DATA_PATH) + data_url = "https://raw.githubusercontent.com/spdx/license-list-data/#{latest_tag}/json/" + curl_download("#{data_url}licenses.json", to: to/"spdx_licenses.json", partial: false) + curl_download("#{data_url}exceptions.json", to: to/"spdx_exceptions.json", partial: false) + end + + def parse_license_expression(license_expression) + licenses = [] + exceptions = [] + + case license_expression + when String, Symbol + licenses.push license_expression + when Hash + license_expression.each do |key, value| + if [:any_of, :all_of].include? key + sub_license, sub_exception = parse_license_expression value + licenses += sub_license + exceptions += sub_exception + else + licenses.push key + exceptions.push value[:with] + end + end + when Array + license_expression.each do |license| + sub_license, sub_exception = parse_license_expression license + licenses += sub_license + exceptions += sub_exception + end + end + + [licenses, exceptions] + end + + def valid_license?(license) + return true if license == :public_domain + + license = license.delete_suffix "+" + license_data["licenses"].any? { |spdx_license| spdx_license["licenseId"] == license } + end + + def deprecated_license?(license) + return false if license == :public_domain + return false unless valid_license?(license) + + license_data["licenses"].none? do |spdx_license| + spdx_license["licenseId"] == license && !spdx_license["isDeprecatedLicenseId"] + end + end + + def valid_license_exception?(exception) + exception_data["exceptions"].any? do |spdx_exception| + spdx_exception["licenseExceptionId"] == exception && !spdx_exception["isDeprecatedLicenseId"] + end + end + + def license_expression_to_string(license_expression, bracket: false, hash_type: nil) + case license_expression + when String + license_expression + when :public_domain + "Public Domain" + when Hash + expressions = [] + + if license_expression.keys.length == 1 + hash_type = license_expression.keys.first + if hash_type.is_a? String + expressions.push "#{hash_type} with #{license_expression[hash_type][:with]}" + else + expressions += license_expression[hash_type].map do |license| + license_expression_to_string license, bracket: true, hash_type: hash_type + end + end + else + bracket = false + license_expression.each do |expression| + expressions.push license_expression_to_string(Hash[*expression], bracket: true) + end + end + + operator = if hash_type == :any_of + " or " + else + " and " + end + + if bracket + "(#{expressions.join operator})" + else + expressions.join operator + end + end + end + + def license_version_info(license) + return [license] if license == :public_domain + + match = license.match(/-(?[0-9.]+)(?:-.*?)??(?\+|-only|-or-later)?$/) + return [license] if match.blank? + + license_name = license.split(match[0]).first + or_later = match["or_later"].present? && %w[+ -or-later].include?(match["or_later"]) + + # [name, version, later versions allowed?] + # e.g. GPL-2.0-or-later --> ["GPL", "2.0", true] + [license_name, match["version"], or_later] + end + + def licenses_forbid_installation?(license_expression, forbidden_licenses) + case license_expression + when String, Symbol + forbidden_licenses_include? license_expression.to_s, forbidden_licenses + when Hash + key = license_expression.keys.first + case key + when :any_of + license_expression[key].all? { |license| licenses_forbid_installation? license, forbidden_licenses } + when :all_of + license_expression[key].any? { |license| licenses_forbid_installation? license, forbidden_licenses } + else + forbidden_licenses_include? key, forbidden_licenses + end + end + end + + def forbidden_licenses_include?(license, forbidden_licenses) + return true if forbidden_licenses.key? license + + name, version, = license_version_info license + + forbidden_licenses.each do |_, license_info| + forbidden_name, forbidden_version, forbidden_or_later = *license_info + next unless forbidden_name == name + + return true if forbidden_or_later && forbidden_version <= version + + return true if forbidden_version == version + end + false end end From 60ec30d41eeee3ea71e8d0755f41d2780aae2052 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Tue, 18 Aug 2020 10:58:32 -0400 Subject: [PATCH 2/4] formula: update license specification --- Library/.rubocop.yml | 4 ++++ Library/Homebrew/.rubocop.yml | 8 ++++++++ Library/Homebrew/cmd/info.rb | 9 ++------- Library/Homebrew/formula.rb | 22 ++++++++++++---------- Library/Homebrew/formula_installer.rb | 14 ++++++++++---- 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/Library/.rubocop.yml b/Library/.rubocop.yml index 0c51ee1567..dc1ee29a3c 100644 --- a/Library/.rubocop.yml +++ b/Library/.rubocop.yml @@ -68,6 +68,10 @@ Style/HashTransformKeys: Style/HashTransformValues: Enabled: true +# Allow for license expressions +Style/HashAsLastArrayItem: + Enabled: false + # Enabled now LineLength is lowish. Style/IfUnlessModifier: Enabled: true diff --git a/Library/Homebrew/.rubocop.yml b/Library/Homebrew/.rubocop.yml index 6799841ae1..d517069b81 100644 --- a/Library/Homebrew/.rubocop.yml +++ b/Library/Homebrew/.rubocop.yml @@ -61,6 +61,8 @@ Metrics/MethodLength: Metrics/ModuleLength: Enabled: true Max: 600 + Exclude: + - 'test/**/*' Metrics/PerceivedComplexity: Enabled: true Max: 90 @@ -143,3 +145,9 @@ Style/GuardClause: # so many of these in formulae but none in here Style/StringConcatenation: Enabled: true + +# don't want this for formulae but re-enabled for Library/Homebrew +Style/HashAsLastArrayItem: + Enabled: true + Exclude: + - 'test/utils/spdx_spec.rb' diff --git a/Library/Homebrew/cmd/info.rb b/Library/Homebrew/cmd/info.rb index 42089d4d87..c5884e0e09 100644 --- a/Library/Homebrew/cmd/info.rb +++ b/Library/Homebrew/cmd/info.rb @@ -8,6 +8,7 @@ require "formula" require "keg" require "tab" require "json" +require "utils/spdx" module Homebrew module_function @@ -211,13 +212,7 @@ module Homebrew puts "From: #{Formatter.url(github_info(f))}" - if f.license.present? - licenses = f.license - .map(&:to_s) - .join(", ") - .sub("public_domain", "Public Domain") - puts "License: #{licenses}" - end + puts "License: #{SPDX.license_expression_to_string f.license}" if f.license.present? unless f.deps.empty? ohai "Dependencies" diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 9cdf6324b1..cf1b784e8b 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -2219,18 +2219,20 @@ class Formula # @!attribute [w] # The SPDX ID of the open-source license that the formula uses. # Shows when running `brew info`. - # Multiple licenses means that the software is licensed under multiple licenses. - # Do not use multiple licenses if e.g. different parts are under different licenses. + # Use `:any`, `:all` or `:with` to describe complex license expressions. + # `:any` should be used when the user can choose which license to use. + # `:all` should be used when the user must use all licenses. + # `:with` should be used to specify a valid SPDX exception. + # Add `+` to an identifier to indicate that the formulae can be + # licensed under later versions of the same license. + # @see https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/ SPDX license expression guide #
license "BSD-2-Clause"
- #
license ["MIT", "GPL-2.0"]
+ #
license "EPL-1.0+"
+ #
license any_of: ["MIT", "GPL-2.0-only"]
+ #
license all_of: ["MIT", "GPL-2.0-only"]
+ #
license "GPL-2.0-only" => { with: "LLVM-exception" }
#
license :public_domain
- def license(args = nil) - if args.nil? - @licenses - else - @licenses = Array(args) - end - end + attr_rw :license # @!attribute [w] homepage # The homepage for the software. Used by users to get more information diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index bb207ba2ef..57a62988e1 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -19,6 +19,7 @@ require "messages" require "cask/cask_loader" require "cmd/install" require "find" +require "utils/spdx" class FormulaInstaller include FormulaCellarChecks @@ -1130,24 +1131,29 @@ class FormulaInstaller .to_s .sub("Public Domain", "public_domain") .split(" ") + .to_h do |license| + [license, SPDX.license_version_info(license)] + end + return if forbidden_licenses.blank? compute_dependencies.each do |dep, _| next if @ignore_deps dep_f = dep.to_formula - next unless dep_f.license.all? { |license| forbidden_licenses.include?(license.to_s) } + next unless SPDX.licenses_forbid_installation? dep_f.license, forbidden_licenses raise CannotInstallFormulaError, <<~EOS - The installation of #{formula.name} has a dependency on #{dep.name} where all its licenses are forbidden: #{dep_f.license}. + The installation of #{formula.name} has a dependency on #{dep.name} where all its licenses are forbidden: + #{SPDX.license_expression_to_string dep_f.license}. EOS end return if @only_deps - return unless formula.license.all? { |license| forbidden_licenses.include?(license.to_s) } + return unless SPDX.licenses_forbid_installation? formula.license, forbidden_licenses raise CannotInstallFormulaError, <<~EOS - #{formula.name}'s licenses are all forbidden: #{formula.license}. + #{formula.name}'s licenses are all forbidden: #{SPDX.license_expression_to_string formula.license}. EOS end end From e215b3df75bbc355e0e1872258055ab270db4844 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Tue, 18 Aug 2020 11:00:17 -0400 Subject: [PATCH 3/4] dev-cmd/audit: update license checks to new style --- Library/Homebrew/dev-cmd/audit.rb | 69 +++--- Library/Homebrew/test/dev-cmd/audit_spec.rb | 260 +++++++++++++++++--- 2 files changed, 260 insertions(+), 69 deletions(-) diff --git a/Library/Homebrew/dev-cmd/audit.rb b/Library/Homebrew/dev-cmd/audit.rb index 0cd05bbab1..67f325fbad 100644 --- a/Library/Homebrew/dev-cmd/audit.rb +++ b/Library/Homebrew/dev-cmd/audit.rb @@ -119,18 +119,20 @@ module Homebrew # Check style in a single batch run up front for performance style_results = Style.check_style_json(style_files, options) if style_files # load licenses - spdx_data = SPDX.spdx_data + spdx_license_data = SPDX.license_data + spdx_exception_data = SPDX.exception_data new_formula_problem_lines = [] audit_formulae.sort.each do |f| only = only_cops ? ["style"] : args.only options = { - new_formula: new_formula, - strict: strict, - online: online, - git: git, - only: only, - except: args.except, - spdx_data: spdx_data, + new_formula: new_formula, + strict: strict, + online: online, + git: git, + only: only, + except: args.except, + spdx_license_data: spdx_license_data, + spdx_exception_data: spdx_exception_data, } options[:style_offenses] = style_results.file_offenses(f.path) if style_results options[:display_cop_names] = args.display_cop_names? @@ -228,7 +230,8 @@ module Homebrew @new_formula_problems = [] @text = FormulaText.new(formula.path) @specs = %w[stable devel head].map { |s| formula.send(s) }.compact - @spdx_data = options[:spdx_data] + @spdx_license_data = options[:spdx_license_data] + @spdx_exception_data = options[:spdx_exception_data] end def audit_style @@ -357,30 +360,36 @@ module Homebrew def audit_license if formula.license.present? - non_standard_licenses = formula.license.map do |license| - next if license == :public_domain - next if @spdx_data["licenses"].any? { |spdx| spdx["licenseId"] == license } - - license - end.compact + licenses, exceptions = SPDX.parse_license_expression formula.license + non_standard_licenses = licenses.reject { |license| SPDX.valid_license? license } if non_standard_licenses.present? - problem "Formula #{formula.name} contains non-standard SPDX licenses: #{non_standard_licenses}." + problem <<~EOS + Formula #{formula.name} contains non-standard SPDX licenses: #{non_standard_licenses}. + For a list of valid licenses check: #{Formatter.url("https://spdx.org/licenses/")} + EOS end if @strict - deprecated_licenses = formula.license.map do |license| - next if license == :public_domain - next if @spdx_data["licenses"].any? do |spdx| - spdx["licenseId"] == license && !spdx["isDeprecatedLicenseId"] - end - - license - end.compact - - if deprecated_licenses.present? - problem "Formula #{formula.name} contains deprecated SPDX licenses: #{deprecated_licenses}." + deprecated_licenses = licenses.select do |license| + SPDX.deprecated_license? license end + if deprecated_licenses.present? + problem <<~EOS + Formula #{formula.name} contains deprecated SPDX licenses: #{deprecated_licenses}. + You may need to add `-only` or `-or-later` for GNU licenses (e.g. `GPL`, `LGPL`, `AGPL`, `GFDL`). + For a list of valid licenses check: #{Formatter.url("https://spdx.org/licenses/")} + EOS + end + end + + invalid_exceptions = exceptions.reject { |exception| SPDX.valid_license_exception? exception } + if invalid_exceptions.present? + problem <<~EOS + Formula #{formula.name} contains invalid or deprecated SPDX license exceptions: #{invalid_exceptions}. + For a list of valid license exceptions check: + #{Formatter.url("https://spdx.org/licenses/exceptions-index.html/")} + EOS end return unless @online @@ -389,11 +398,11 @@ module Homebrew return if user.blank? github_license = GitHub.get_repo_license(user, repo) - return if github_license && (formula.license + ["NOASSERTION"]).include?(github_license) - return if PERMITTED_LICENSE_MISMATCHES[github_license]&.any? { |license| formula.license.include? license } + return if github_license && (licenses + ["NOASSERTION"]).include?(github_license) + return if PERMITTED_LICENSE_MISMATCHES[github_license]&.any? { |license| licenses.include? license } return if PERMITTED_FORMULA_LICENSE_MISMATCHES[formula.name] == formula.version - problem "Formula license #{formula.license} does not match GitHub license #{Array(github_license)}." + problem "Formula license #{licenses} does not match GitHub license #{Array(github_license)}." elsif @new_formula && @core_tap problem "Formulae in homebrew/core must specify a license." diff --git a/Library/Homebrew/test/dev-cmd/audit_spec.rb b/Library/Homebrew/test/dev-cmd/audit_spec.rb index a574f0c3d5..c0895c2db0 100644 --- a/Library/Homebrew/test/dev-cmd/audit_spec.rb +++ b/Library/Homebrew/test/dev-cmd/audit_spec.rb @@ -3,6 +3,7 @@ require "dev-cmd/audit" require "formulary" require "cmd/shared_examples/args_parse" +require "utils/spdx" describe "Homebrew.audit_args" do it_behaves_like "parseable arguments" @@ -80,20 +81,21 @@ module Homebrew end describe "#audit_license" do - let(:spdx_data) { - JSON.parse Pathname(File.join(File.dirname(__FILE__), "../../data/spdx.json")).read - } + let(:spdx_license_data) { SPDX.license_data } + let(:spdx_exception_data) { SPDX.exception_data } - let(:custom_spdx_id) { "zzz" } let(:deprecated_spdx_id) { "GPL-1.0" } - let(:standard_mismatch_spdx_id) { "0BSD" } - let(:license_array) { ["0BSD", "GPL-3.0"] } - let(:license_array_mismatch) { ["0BSD", "MIT"] } - let(:license_array_nonstandard) { ["0BSD", "zzz", "MIT"] } - let(:license_array_deprecated) { ["0BSD", "GPL-1.0", "MIT"] } + let(:license_all_custom_id) { 'all_of: ["MIT", "zzz"]' } + let(:deprecated_spdx_exception) { "Nokia-Qt-exception-1.1" } + let(:license_any) { 'any_of: ["0BSD", "GPL-3.0-only"]' } + let(:license_any_with_plus) { 'any_of: ["0BSD+", "GPL-3.0-only"]' } + let(:license_nested_conditions) { 'any_of: ["0BSD", { all_of: ["GPL-3.0-only", "MIT"] }]' } + let(:license_any_mismatch) { 'any_of: ["0BSD", "MIT"]' } + let(:license_any_nonstandard) { 'any_of: ["0BSD", "zzz", "MIT"]' } + let(:license_any_deprecated) { 'any_of: ["0BSD", "GPL-1.0", "MIT"]' } it "does not check if the formula is not a new formula" do - fa = formula_auditor "foo", <<~RUBY, spdx_data: spdx_data, new_formula: false + fa = formula_auditor "foo", <<~RUBY, new_formula: false class Foo < Formula url "https://brew.sh/foo-1.0.tgz" end @@ -104,7 +106,7 @@ module Homebrew end it "detects no license info" do - fa = formula_auditor "foo", <<~RUBY, spdx_data: spdx_data, new_formula: true, core_tap: true + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true, core_tap: true class Foo < Formula url "https://brew.sh/foo-1.0.tgz" end @@ -115,19 +117,22 @@ module Homebrew end it "detects if license is not a standard spdx-id" do - fa = formula_auditor "foo", <<~RUBY, spdx_data: spdx_data, new_formula: true + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true class Foo < Formula url "https://brew.sh/foo-1.0.tgz" - license "#{custom_spdx_id}" + license "zzz" end RUBY fa.audit_license - expect(fa.problems.first).to match "Formula foo contains non-standard SPDX licenses: [\"zzz\"]." + expect(fa.problems.first).to match <<~EOS + Formula foo contains non-standard SPDX licenses: ["zzz"]. + For a list of valid licenses check: https://spdx.org/licenses/ + EOS end it "detects if license is a deprecated spdx-id" do - fa = formula_auditor "foo", <<~RUBY, spdx_data: spdx_data, new_formula: true, strict: true + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true, strict: true class Foo < Formula url "https://brew.sh/foo-1.0.tgz" license "#{deprecated_spdx_id}" @@ -135,35 +140,61 @@ module Homebrew RUBY fa.audit_license - expect(fa.problems.first).to match "Formula foo contains deprecated SPDX licenses: [\"GPL-1.0\"]." + expect(fa.problems.first).to match <<~EOS + Formula foo contains deprecated SPDX licenses: ["GPL-1.0"]. + You may need to add `-only` or `-or-later` for GNU licenses (e.g. `GPL`, `LGPL`, `AGPL`, `GFDL`). + For a list of valid licenses check: https://spdx.org/licenses/ + EOS + end + + it "detects if license with AND contains a non-standard spdx-id" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license #{license_all_custom_id} + end + RUBY + + fa.audit_license + expect(fa.problems.first).to match <<~EOS + Formula foo contains non-standard SPDX licenses: ["zzz"]. + For a list of valid licenses check: https://spdx.org/licenses/ + EOS end it "detects if license array contains a non-standard spdx-id" do - fa = formula_auditor "foo", <<~RUBY, spdx_data: spdx_data, new_formula: true + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true class Foo < Formula url "https://brew.sh/foo-1.0.tgz" - license #{license_array_nonstandard} + license #{license_any_nonstandard} end RUBY fa.audit_license - expect(fa.problems.first).to match "Formula foo contains non-standard SPDX licenses: [\"zzz\"]." + expect(fa.problems.first).to match <<~EOS + Formula foo contains non-standard SPDX licenses: ["zzz"]. + For a list of valid licenses check: https://spdx.org/licenses/ + EOS end it "detects if license array contains a deprecated spdx-id" do - fa = formula_auditor "foo", <<~RUBY, spdx_data: spdx_data, new_formula: true, strict: true + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true, strict: true class Foo < Formula url "https://brew.sh/foo-1.0.tgz" - license #{license_array_deprecated} + license #{license_any_deprecated} end RUBY fa.audit_license - expect(fa.problems.first).to match "Formula foo contains deprecated SPDX licenses: [\"GPL-1.0\"]." + expect(fa.problems.first).to match <<~EOS + Formula foo contains deprecated SPDX licenses: ["GPL-1.0"]. + You may need to add `-only` or `-or-later` for GNU licenses (e.g. `GPL`, `LGPL`, `AGPL`, `GFDL`). + For a list of valid licenses check: https://spdx.org/licenses/ + EOS end it "verifies that a license info is a standard spdx id" do - fa = formula_auditor "foo", <<~RUBY, spdx_data: spdx_data, new_formula: true + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true class Foo < Formula url "https://brew.sh/foo-1.0.tgz" license "0BSD" @@ -174,11 +205,85 @@ module Homebrew expect(fa.problems).to be_empty end - it "verifies that a license array contains only standard spdx id" do - fa = formula_auditor "foo", <<~RUBY, spdx_data: spdx_data, new_formula: true + it "verifies that a license info with plus is a standard spdx id" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true class Foo < Formula url "https://brew.sh/foo-1.0.tgz" - license #{license_array} + license "0BSD+" + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "allows :public_domain license" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license :public_domain + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license info with multiple licenses are standard spdx ids" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license any_of: ["0BSD", "MIT"] + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license info with exceptions are standard spdx ids" do + formula_text = <<~RUBY + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license "Apache-2.0" => { with: "LLVM-exception" } + end + RUBY + fa = formula_auditor "foo", formula_text, new_formula: true, + spdx_license_data: spdx_license_data, spdx_exception_data: spdx_exception_data + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license array contains only standard spdx id" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license #{license_any} + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license array contains only standard spdx id with plus" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license #{license_any_with_plus} + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license array with AND contains only standard spdx ids" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license #{license_nested_conditions} end RUBY @@ -188,21 +293,93 @@ module Homebrew it "checks online and verifies that a standard license id is the same "\ "as what is indicated on its Github repo" do - fa = formula_auditor "cask", <<~RUBY, spdx_data: spdx_data, online: true, core_tap: true, new_formula: true + formula_text = <<~RUBY class Cask < Formula url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" head "https://github.com/cask/cask.git" license "GPL-3.0" end RUBY + fa = formula_auditor "cask", formula_text, spdx_license_data: spdx_license_data, + online: true, core_tap: true, new_formula: true fa.audit_license expect(fa.problems).to be_empty end + it "checks online and verifies that a standard license id with AND is the same "\ + "as what is indicated on its Github repo" do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license all_of: ["GPL-3.0-or-later", "MIT"] + end + RUBY + fa = formula_auditor "cask", formula_text, spdx_license_data: spdx_license_data, + online: true, core_tap: true, new_formula: true + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "checks online and verifies that a standard license id with WITH is the same "\ + "as what is indicated on its Github repo" do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license "GPL-3.0-or-later" => { with: "LLVM-exception" } + end + RUBY + fa = formula_auditor "cask", formula_text, online: true, core_tap: true, new_formula: true, + spdx_license_data: spdx_license_data, spdx_exception_data: spdx_exception_data + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license exception has standard spdx ids" do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license "GPL-3.0-or-later" => { with: "zzz" } + end + RUBY + fa = formula_auditor "cask", formula_text, core_tap: true, new_formula: true, + spdx_license_data: spdx_license_data, spdx_exception_data: spdx_exception_data + + fa.audit_license + expect(fa.problems.first).to match <<~EOS + Formula cask contains invalid or deprecated SPDX license exceptions: ["zzz"]. + For a list of valid license exceptions check: + https://spdx.org/licenses/exceptions-index.html/ + EOS + end + + it "verifies that a license exception has non-deprecated spdx ids" do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license "GPL-3.0-or-later" => { with: "#{deprecated_spdx_exception}" } + end + RUBY + fa = formula_auditor "cask", formula_text, core_tap: true, new_formula: true, + spdx_license_data: spdx_license_data, spdx_exception_data: spdx_exception_data + + fa.audit_license + expect(fa.problems.first).to match <<~EOS + Formula cask contains invalid or deprecated SPDX license exceptions: ["#{deprecated_spdx_exception}"]. + For a list of valid license exceptions check: + https://spdx.org/licenses/exceptions-index.html/ + EOS + end + it "checks online and verifies that a standard license id is in the same exempted license group" \ "as what is indicated on its GitHub repo" do - fa = formula_auditor "cask", <<~RUBY, spdx_data: spdx_data, online: true, new_formula: true + fa = formula_auditor "cask", <<~RUBY, spdx_license_data: spdx_license_data, online: true, new_formula: true class Cask < Formula url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" head "https://github.com/cask/cask.git" @@ -216,11 +393,11 @@ module Homebrew it "checks online and verifies that a standard license array is in the same exempted license group" \ "as what is indicated on its GitHub repo" do - fa = formula_auditor "cask", <<~RUBY, spdx_data: spdx_data, online: true, new_formula: true + fa = formula_auditor "cask", <<~RUBY, spdx_license_data: spdx_license_data, online: true, new_formula: true class Cask < Formula url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" head "https://github.com/cask/cask.git" - license ["GPL-3.0-or-later", "MIT"] + license any_of: ["GPL-3.0-or-later", "MIT"] end RUBY @@ -230,43 +407,48 @@ module Homebrew it "checks online and detects that a formula-specified license is not "\ "the same as what is indicated on its Github repository" do - fa = formula_auditor "cask", <<~RUBY, online: true, spdx_data: spdx_data, core_tap: true, new_formula: true + formula_text = <<~RUBY class Cask < Formula url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" head "https://github.com/cask/cask.git" - license "#{standard_mismatch_spdx_id}" + license "0BSD" end RUBY + fa = formula_auditor "cask", formula_text, spdx_license_data: spdx_license_data, + online: true, core_tap: true, new_formula: true fa.audit_license - expect(fa.problems.first).to match "Formula license #{Array(standard_mismatch_spdx_id)} "\ - "does not match GitHub license [\"GPL-3.0\"]." + expect(fa.problems.first).to match "Formula license [\"0BSD\"] does not match GitHub license [\"GPL-3.0\"]." end it "checks online and detects that an array of license does not contain "\ "what is indicated on its Github repository" do - fa = formula_auditor "cask", <<~RUBY, online: true, spdx_data: spdx_data, core_tap: true, new_formula: true + formula_text = <<~RUBY class Cask < Formula url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" head "https://github.com/cask/cask.git" - license #{license_array_mismatch} + license #{license_any_mismatch} end RUBY + fa = formula_auditor "cask", formula_text, spdx_license_data: spdx_license_data, + online: true, core_tap: true, new_formula: true fa.audit_license - expect(fa.problems.first).to match "Formula license #{license_array_mismatch} "\ + expect(fa.problems.first).to match "Formula license [\"0BSD\", \"MIT\"] "\ "does not match GitHub license [\"GPL-3.0\"]." end it "checks online and verifies that an array of license contains "\ "what is indicated on its Github repository" do - fa = formula_auditor "cask", <<~RUBY, online: true, spdx_data: spdx_data, core_tap: true, new_formula: true + formula_text = <<~RUBY class Cask < Formula url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" head "https://github.com/cask/cask.git" - license #{license_array} + license #{license_any} end RUBY + fa = formula_auditor "cask", formula_text, spdx_license_data: spdx_license_data, + online: true, core_tap: true, new_formula: true fa.audit_license expect(fa.problems).to be_empty From 1a321dab623fe01958e02b6614b0910d12c86ae7 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Wed, 19 Aug 2020 10:25:02 -0400 Subject: [PATCH 4/4] keep license array support --- Library/Homebrew/test/utils/spdx_spec.rb | 40 +++++++++++++++++++++++- Library/Homebrew/utils/spdx.rb | 28 +++++++++-------- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/Library/Homebrew/test/utils/spdx_spec.rb b/Library/Homebrew/test/utils/spdx_spec.rb index 01fb65107e..bf216a60dc 100644 --- a/Library/Homebrew/test/utils/spdx_spec.rb +++ b/Library/Homebrew/test/utils/spdx_spec.rb @@ -67,6 +67,10 @@ describe SPDX do expect(described_class.parse_license_expression(any_of: ["MIT", "EPL-1.0+"]).first).to eq ["MIT", "EPL-1.0+"] end + it "returns multiple licenses with array" do + expect(described_class.parse_license_expression(["MIT", "EPL-1.0+"]).first).to eq ["MIT", "EPL-1.0+"] + end + it "returns license and exception" do license_expression = { "MIT" => { with: "LLVM-exception" } } expect(described_class.parse_license_expression(license_expression)).to eq [["MIT"], ["LLVM-exception"]] @@ -79,7 +83,7 @@ describe SPDX do all_of: ["0BSD", "Zlib"], "curl" => { with: "LLVM-exception" }, ] } - result = [["MIT", :public_domain, "0BSD", "Zlib", "curl"], ["LLVM-exception"]] + result = [["MIT", :public_domain, "curl", "0BSD", "Zlib"], ["LLVM-exception"]] expect(described_class.parse_license_expression(license_expression)).to eq result end @@ -163,6 +167,10 @@ describe SPDX do expect(described_class.license_expression_to_string(any_of: ["MIT", "EPL-1.0+"])).to eq "MIT or EPL-1.0+" end + it "treats array as any_of:" do + expect(described_class.license_expression_to_string(["MIT", "EPL-1.0+"])).to eq "MIT or EPL-1.0+" + end + it "returns license and exception" do license_expression = { "MIT" => { with: "LLVM-exception" } } expect(described_class.license_expression_to_string(license_expression)).to eq "MIT with LLVM-exception" @@ -229,7 +237,17 @@ describe SPDX do } } let(:any_of_license) { { any_of: ["MIT", "0BSD"] } } + let(:license_array) { ["MIT", "0BSD"] } let(:all_of_license) { { all_of: ["MIT", "0BSD"] } } + let(:nested_licenses) { + { + any_of: [ + "MIT", + { "MIT" => { with: "LLVM-exception" } }, + { any_of: ["MIT", "0BSD"] }, + ], + } + } let(:license_exception) { { "MIT" => { with: "LLVM-exception" } } } it "allows installation with no forbidden licenses" do @@ -260,6 +278,14 @@ describe SPDX do expect(described_class.licenses_forbid_installation?(any_of_license, multiple_forbidden)).to eq true end + it "allows installation when one of the array licenses is allowed" do + expect(described_class.licenses_forbid_installation?(license_array, mit_forbidden)).to eq false + end + + it "forbids installation when none of the array licenses are allowed" do + expect(described_class.licenses_forbid_installation?(license_array, multiple_forbidden)).to eq true + end + it "forbids installation when one of the all_of licenses is allowed" do expect(described_class.licenses_forbid_installation?(all_of_license, mit_forbidden)).to eq true end @@ -271,6 +297,18 @@ describe SPDX do it "forbids installation with license + exception that are't forbidden" do expect(described_class.licenses_forbid_installation?(license_exception, mit_forbidden)).to eq true end + + it "allows installation with nested licenses with no forbidden licenses" do + expect(described_class.licenses_forbid_installation?(nested_licenses, epl_1_forbidden)).to eq false + end + + it "allows installation with nested licenses when second hash item matches" do + expect(described_class.licenses_forbid_installation?(nested_licenses, mit_forbidden)).to eq false + end + + it "forbids installation with nested licenses when all licenses are forbidden" do + expect(described_class.licenses_forbid_installation?(nested_licenses, multiple_forbidden)).to eq true + end end describe ".forbidden_licenses_include?" do diff --git a/Library/Homebrew/utils/spdx.rb b/Library/Homebrew/utils/spdx.rb index a52cf6090c..dda9d811d8 100644 --- a/Library/Homebrew/utils/spdx.rb +++ b/Library/Homebrew/utils/spdx.rb @@ -33,18 +33,18 @@ module SPDX case license_expression when String, Symbol licenses.push license_expression - when Hash - license_expression.each do |key, value| - if [:any_of, :all_of].include? key - sub_license, sub_exception = parse_license_expression value - licenses += sub_license - exceptions += sub_exception - else - licenses.push key - exceptions.push value[:with] - end + when Hash, Array + if license_expression.is_a? Hash + license_expression = license_expression.map do |key, value| + if key.is_a? String + licenses.push key + exceptions.push value[:with] + next + end + value + end.compact end - when Array + license_expression.each do |license| sub_license, sub_exception = parse_license_expression license licenses += sub_license @@ -83,7 +83,8 @@ module SPDX license_expression when :public_domain "Public Domain" - when Hash + when Hash, Array + license_expression = { any_of: license_expression } if license_expression.is_a? Array expressions = [] if license_expression.keys.length == 1 @@ -134,7 +135,8 @@ module SPDX case license_expression when String, Symbol forbidden_licenses_include? license_expression.to_s, forbidden_licenses - when Hash + when Hash, Array + license_expression = { any_of: license_expression } if license_expression.is_a? Array key = license_expression.keys.first case key when :any_of