Merge pull request #8260 from Rylan12/allow-AND-and-WITH-in-licenses

audit: allow AND and WITH in licenses
This commit is contained in:
Rylan Polster 2020-08-19 13:14:57 -04:00 committed by GitHub
commit 778bd543c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1236 additions and 107 deletions

View File

@ -26,7 +26,7 @@ jobs:
run: | run: |
cd "$GITHUB_WORKSPACE/Library/Homebrew" cd "$GITHUB_WORKSPACE/Library/Homebrew"
if brew update-license-data --commit --fail-if-not-changed; then 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 if ! git ls-remote --exit-code --heads origin "spdx-$SPDX_VERSION"; then
git checkout -b "spdx-$SPDX_VERSION" git checkout -b "spdx-$SPDX_VERSION"
git push origin "spdx-$SPDX_VERSION" git push origin "spdx-$SPDX_VERSION"

View File

@ -68,6 +68,10 @@ Style/HashTransformKeys:
Style/HashTransformValues: Style/HashTransformValues:
Enabled: true Enabled: true
# Allow for license expressions
Style/HashAsLastArrayItem:
Enabled: false
# Enabled now LineLength is lowish. # Enabled now LineLength is lowish.
Style/IfUnlessModifier: Style/IfUnlessModifier:
Enabled: true Enabled: true

View File

@ -61,6 +61,8 @@ Metrics/MethodLength:
Metrics/ModuleLength: Metrics/ModuleLength:
Enabled: true Enabled: true
Max: 600 Max: 600
Exclude:
- 'test/**/*'
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Enabled: true Enabled: true
Max: 90 Max: 90
@ -143,3 +145,9 @@ Style/GuardClause:
# so many of these in formulae but none in here # so many of these in formulae but none in here
Style/StringConcatenation: Style/StringConcatenation:
Enabled: true Enabled: true
# don't want this for formulae but re-enabled for Library/Homebrew
Style/HashAsLastArrayItem:
Enabled: true
Exclude:
- 'test/utils/spdx_spec.rb'

View File

@ -8,6 +8,7 @@ require "formula"
require "keg" require "keg"
require "tab" require "tab"
require "json" require "json"
require "utils/spdx"
module Homebrew module Homebrew
module_function module_function
@ -211,13 +212,7 @@ module Homebrew
puts "From: #{Formatter.url(github_info(f))}" puts "From: #{Formatter.url(github_info(f))}"
if f.license.present? puts "License: #{SPDX.license_expression_to_string f.license}" if f.license.present?
licenses = f.license
.map(&:to_s)
.join(", ")
.sub("public_domain", "Public Domain")
puts "License: #{licenses}"
end
unless f.deps.empty? unless f.deps.empty?
ohai "Dependencies" ohai "Dependencies"

View File

@ -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 # Where shim scripts for various build and SCM tools are stored
HOMEBREW_SHIMS_PATH = (HOMEBREW_LIBRARY/"Homebrew/shims").freeze 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 # Where we store symlinks to currently linked kegs
HOMEBREW_LINKED_KEGS = (HOMEBREW_PREFIX/"var/homebrew/linked").freeze HOMEBREW_LINKED_KEGS = (HOMEBREW_PREFIX/"var/homebrew/linked").freeze

View File

@ -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"
}
]
}

View File

@ -119,18 +119,20 @@ module Homebrew
# Check style in a single batch run up front for performance # Check style in a single batch run up front for performance
style_results = Style.check_style_json(style_files, options) if style_files style_results = Style.check_style_json(style_files, options) if style_files
# load licenses # load licenses
spdx_data = SPDX.spdx_data spdx_license_data = SPDX.license_data
spdx_exception_data = SPDX.exception_data
new_formula_problem_lines = [] new_formula_problem_lines = []
audit_formulae.sort.each do |f| audit_formulae.sort.each do |f|
only = only_cops ? ["style"] : args.only only = only_cops ? ["style"] : args.only
options = { options = {
new_formula: new_formula, new_formula: new_formula,
strict: strict, strict: strict,
online: online, online: online,
git: git, git: git,
only: only, only: only,
except: args.except, except: args.except,
spdx_data: spdx_data, 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[:style_offenses] = style_results.file_offenses(f.path) if style_results
options[:display_cop_names] = args.display_cop_names? options[:display_cop_names] = args.display_cop_names?
@ -228,7 +230,8 @@ module Homebrew
@new_formula_problems = [] @new_formula_problems = []
@text = FormulaText.new(formula.path) @text = FormulaText.new(formula.path)
@specs = %w[stable devel head].map { |s| formula.send(s) }.compact @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 end
def audit_style def audit_style
@ -357,30 +360,36 @@ module Homebrew
def audit_license def audit_license
if formula.license.present? if formula.license.present?
non_standard_licenses = formula.license.map do |license| licenses, exceptions = SPDX.parse_license_expression formula.license
next if license == :public_domain
next if @spdx_data["licenses"].any? { |spdx| spdx["licenseId"] == license }
license
end.compact
non_standard_licenses = licenses.reject { |license| SPDX.valid_license? license }
if non_standard_licenses.present? 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 end
if @strict if @strict
deprecated_licenses = formula.license.map do |license| deprecated_licenses = licenses.select do |license|
next if license == :public_domain SPDX.deprecated_license? license
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}."
end 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 end
return unless @online return unless @online
@ -389,11 +398,11 @@ module Homebrew
return if user.blank? return if user.blank?
github_license = GitHub.get_repo_license(user, repo) github_license = GitHub.get_repo_license(user, repo)
return if github_license && (formula.license + ["NOASSERTION"]).include?(github_license) return if github_license && (licenses + ["NOASSERTION"]).include?(github_license)
return if PERMITTED_LICENSE_MISMATCHES[github_license]&.any? { |license| formula.license.include? license } return if PERMITTED_LICENSE_MISMATCHES[github_license]&.any? { |license| licenses.include? license }
return if PERMITTED_FORMULA_LICENSE_MISMATCHES[formula.name] == formula.version 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 elsif @new_formula && @core_tap
problem "Formulae in homebrew/core must specify a license." problem "Formulae in homebrew/core must specify a license."

View File

@ -28,13 +28,13 @@ module Homebrew
SPDX.download_latest_license_data! 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? return unless args.commit?
ohai "git add" ohai "git add"
safe_system "git", "add", SPDX::JSON_PATH safe_system "git", "add", SPDX::DATA_PATH
ohai "git commit" 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
end end

View File

@ -2219,18 +2219,20 @@ class Formula
# @!attribute [w] # @!attribute [w]
# The SPDX ID of the open-source license that the formula uses. # The SPDX ID of the open-source license that the formula uses.
# Shows when running `brew info`. # Shows when running `brew info`.
# Multiple licenses means that the software is licensed under multiple licenses. # Use `:any`, `:all` or `:with` to describe complex license expressions.
# Do not use multiple licenses if e.g. different parts are under different licenses. # `: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
# <pre>license "BSD-2-Clause"</pre> # <pre>license "BSD-2-Clause"</pre>
# <pre>license ["MIT", "GPL-2.0"]</pre> # <pre>license "EPL-1.0+"</pre>
# <pre>license any_of: ["MIT", "GPL-2.0-only"]</pre>
# <pre>license all_of: ["MIT", "GPL-2.0-only"]</pre>
# <pre>license "GPL-2.0-only" => { with: "LLVM-exception" }</pre>
# <pre>license :public_domain</pre> # <pre>license :public_domain</pre>
def license(args = nil) attr_rw :license
if args.nil?
@licenses
else
@licenses = Array(args)
end
end
# @!attribute [w] homepage # @!attribute [w] homepage
# The homepage for the software. Used by users to get more information # The homepage for the software. Used by users to get more information

View File

@ -19,6 +19,7 @@ require "messages"
require "cask/cask_loader" require "cask/cask_loader"
require "cmd/install" require "cmd/install"
require "find" require "find"
require "utils/spdx"
class FormulaInstaller class FormulaInstaller
include FormulaCellarChecks include FormulaCellarChecks
@ -1130,24 +1131,29 @@ class FormulaInstaller
.to_s .to_s
.sub("Public Domain", "public_domain") .sub("Public Domain", "public_domain")
.split(" ") .split(" ")
.to_h do |license|
[license, SPDX.license_version_info(license)]
end
return if forbidden_licenses.blank? return if forbidden_licenses.blank?
compute_dependencies.each do |dep, _| compute_dependencies.each do |dep, _|
next if @ignore_deps next if @ignore_deps
dep_f = dep.to_formula 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 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 EOS
end end
return if @only_deps 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 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 EOS
end end
end end

View File

@ -3,6 +3,7 @@
require "dev-cmd/audit" require "dev-cmd/audit"
require "formulary" require "formulary"
require "cmd/shared_examples/args_parse" require "cmd/shared_examples/args_parse"
require "utils/spdx"
describe "Homebrew.audit_args" do describe "Homebrew.audit_args" do
it_behaves_like "parseable arguments" it_behaves_like "parseable arguments"
@ -80,20 +81,21 @@ module Homebrew
end end
describe "#audit_license" do describe "#audit_license" do
let(:spdx_data) { let(:spdx_license_data) { SPDX.license_data }
JSON.parse Pathname(File.join(File.dirname(__FILE__), "../../data/spdx.json")).read let(:spdx_exception_data) { SPDX.exception_data }
}
let(:custom_spdx_id) { "zzz" }
let(:deprecated_spdx_id) { "GPL-1.0" } let(:deprecated_spdx_id) { "GPL-1.0" }
let(:standard_mismatch_spdx_id) { "0BSD" } let(:license_all_custom_id) { 'all_of: ["MIT", "zzz"]' }
let(:license_array) { ["0BSD", "GPL-3.0"] } let(:deprecated_spdx_exception) { "Nokia-Qt-exception-1.1" }
let(:license_array_mismatch) { ["0BSD", "MIT"] } let(:license_any) { 'any_of: ["0BSD", "GPL-3.0-only"]' }
let(:license_array_nonstandard) { ["0BSD", "zzz", "MIT"] } let(:license_any_with_plus) { 'any_of: ["0BSD+", "GPL-3.0-only"]' }
let(:license_array_deprecated) { ["0BSD", "GPL-1.0", "MIT"] } 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 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 class Foo < Formula
url "https://brew.sh/foo-1.0.tgz" url "https://brew.sh/foo-1.0.tgz"
end end
@ -104,7 +106,7 @@ module Homebrew
end end
it "detects no license info" do 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 class Foo < Formula
url "https://brew.sh/foo-1.0.tgz" url "https://brew.sh/foo-1.0.tgz"
end end
@ -115,19 +117,22 @@ module Homebrew
end end
it "detects if license is not a standard spdx-id" do 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 class Foo < Formula
url "https://brew.sh/foo-1.0.tgz" url "https://brew.sh/foo-1.0.tgz"
license "#{custom_spdx_id}" license "zzz"
end end
RUBY RUBY
fa.audit_license 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 end
it "detects if license is a deprecated spdx-id" do 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 class Foo < Formula
url "https://brew.sh/foo-1.0.tgz" url "https://brew.sh/foo-1.0.tgz"
license "#{deprecated_spdx_id}" license "#{deprecated_spdx_id}"
@ -135,35 +140,61 @@ module Homebrew
RUBY RUBY
fa.audit_license 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 end
it "detects if license array contains a non-standard spdx-id" do 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 class Foo < Formula
url "https://brew.sh/foo-1.0.tgz" url "https://brew.sh/foo-1.0.tgz"
license #{license_array_nonstandard} license #{license_any_nonstandard}
end end
RUBY RUBY
fa.audit_license 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 end
it "detects if license array contains a deprecated spdx-id" do 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 class Foo < Formula
url "https://brew.sh/foo-1.0.tgz" url "https://brew.sh/foo-1.0.tgz"
license #{license_array_deprecated} license #{license_any_deprecated}
end end
RUBY RUBY
fa.audit_license 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 end
it "verifies that a license info is a standard spdx id" do 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 class Foo < Formula
url "https://brew.sh/foo-1.0.tgz" url "https://brew.sh/foo-1.0.tgz"
license "0BSD" license "0BSD"
@ -174,11 +205,85 @@ module Homebrew
expect(fa.problems).to be_empty expect(fa.problems).to be_empty
end end
it "verifies that a license array contains only standard spdx id" do it "verifies that a license info with plus 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 class Foo < Formula
url "https://brew.sh/foo-1.0.tgz" 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 end
RUBY RUBY
@ -188,21 +293,93 @@ module Homebrew
it "checks online and verifies that a standard license id is the same "\ it "checks online and verifies that a standard license id is the same "\
"as what is indicated on its Github repo" do "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 class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git" head "https://github.com/cask/cask.git"
license "GPL-3.0" license "GPL-3.0"
end end
RUBY RUBY
fa = formula_auditor "cask", formula_text, spdx_license_data: spdx_license_data,
online: true, core_tap: true, new_formula: true
fa.audit_license fa.audit_license
expect(fa.problems).to be_empty expect(fa.problems).to be_empty
end 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" \ 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 "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 class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git" 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" \ 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 "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 class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git" head "https://github.com/cask/cask.git"
license ["GPL-3.0-or-later", "MIT"] license any_of: ["GPL-3.0-or-later", "MIT"]
end end
RUBY RUBY
@ -230,43 +407,48 @@ module Homebrew
it "checks online and detects that a formula-specified license is not "\ it "checks online and detects that a formula-specified license is not "\
"the same as what is indicated on its Github repository" do "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 class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git" head "https://github.com/cask/cask.git"
license "#{standard_mismatch_spdx_id}" license "0BSD"
end end
RUBY RUBY
fa = formula_auditor "cask", formula_text, spdx_license_data: spdx_license_data,
online: true, core_tap: true, new_formula: true
fa.audit_license fa.audit_license
expect(fa.problems.first).to match "Formula license #{Array(standard_mismatch_spdx_id)} "\ expect(fa.problems.first).to match "Formula license [\"0BSD\"] does not match GitHub license [\"GPL-3.0\"]."
"does not match GitHub license [\"GPL-3.0\"]."
end end
it "checks online and detects that an array of license does not contain "\ it "checks online and detects that an array of license does not contain "\
"what is indicated on its Github repository" do "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 class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git" head "https://github.com/cask/cask.git"
license #{license_array_mismatch} license #{license_any_mismatch}
end end
RUBY RUBY
fa = formula_auditor "cask", formula_text, spdx_license_data: spdx_license_data,
online: true, core_tap: true, new_formula: true
fa.audit_license 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\"]." "does not match GitHub license [\"GPL-3.0\"]."
end end
it "checks online and verifies that an array of license contains "\ it "checks online and verifies that an array of license contains "\
"what is indicated on its Github repository" do "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 class Cask < Formula
url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
head "https://github.com/cask/cask.git" head "https://github.com/cask/cask.git"
license #{license_array} license #{license_any}
end end
RUBY RUBY
fa = formula_auditor "cask", formula_text, spdx_license_data: spdx_license_data,
online: true, core_tap: true, new_formula: true
fa.audit_license fa.audit_license
expect(fa.problems).to be_empty expect(fa.problems).to be_empty

View File

@ -19,6 +19,9 @@ end.freeze
# Paths pointing into the Homebrew code base that persist across test runs # Paths pointing into the Homebrew code base that persist across test runs
HOMEBREW_SHIMS_PATH = (HOMEBREW_LIBRARY_PATH/"shims").freeze 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" require "extend/git_repository"
# Paths redirected to a temporary directory and wiped at the end of the test run # Paths redirected to a temporary directory and wiped at the end of the test run

View File

@ -3,30 +3,337 @@
require "utils/spdx" require "utils/spdx"
describe SPDX do describe SPDX do
describe ".spdx_data" do describe ".license_data" do
it "has the license list version" 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 end
it "has the release date" do 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 end
it "has licenses" do 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
end end
describe ".download_latest_license_data!", :needs_network do 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 after do
FileUtils.rm tmp_json_path FileUtils.rm tmp_json_path/"spdx_licenses.json"
FileUtils.rm tmp_json_path/"spdx_exceptions.json"
end end
it "downloads latest license data" do it "downloads latest license data" do
described_class.download_latest_license_data! to: tmp_json_path 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 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"]]
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, "curl", "0BSD", "Zlib"], ["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 "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"
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(: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
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 "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
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
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
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 end
end end

View File

@ -5,19 +5,163 @@ require "utils/github"
module SPDX module SPDX
module_function 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" API_URL = "https://api.github.com/repos/spdx/license-list-data/releases/latest"
def spdx_data def license_data
@spdx_data ||= JSON.parse(JSON_PATH.read) @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 end
def latest_tag def latest_tag
@latest_tag ||= GitHub.open_api(API_URL)["tag_name"] @latest_tag ||= GitHub.open_api(API_URL)["tag_name"]
end end
def download_latest_license_data!(to: JSON_PATH) def download_latest_license_data!(to: DATA_PATH)
data_url = "https://raw.githubusercontent.com/spdx/license-list-data/#{latest_tag}/json/licenses.json" data_url = "https://raw.githubusercontent.com/spdx/license-list-data/#{latest_tag}/json/"
curl_download(data_url, to: to, partial: false) 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, 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
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, Array
license_expression = { any_of: license_expression } if license_expression.is_a? Array
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(/-(?<version>[0-9.]+)(?:-.*?)??(?<or_later>\+|-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, Array
license_expression = { any_of: license_expression } if license_expression.is_a? Array
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
end end