blob: 8c68447bb0d8031651666c5465b6f9047262374f [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.packages;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization;
import com.google.devtools.build.lib.skylarkbuildapi.LicenseApi;
import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/** Support for license and distribution checking. */
@Immutable
@ThreadSafe
@AutoCodec
public final class License implements LicenseApi {
private final ImmutableSet<LicenseType> licenseTypes;
private final ImmutableSet<Label> exceptions;
/**
* The error that's thrown if a build file contains an invalid license string.
*/
public static class LicenseParsingException extends Exception {
public LicenseParsingException(String s) {
super(s);
}
}
/**
* LicenseType is the basis of the License lattice - stricter licenses should
* be declared before less-strict licenses in the enum.
*
* <p>Note that the order is important for the purposes of finding the least
* restrictive license.
*/
public enum LicenseType {
BY_EXCEPTION_ONLY,
RESTRICTED,
RESTRICTED_IF_STATICALLY_LINKED,
RECIPROCAL,
NOTICE,
PERMISSIVE,
UNENCUMBERED,
NONE
}
/**
* Gets the least restrictive license type from the list of licenses declared
* for a target. For the purposes of license checking, the license type set of
* a declared license can be reduced to its least restrictive member.
*
* @param types a collection of license types
* @return the least restrictive license type
*/
@VisibleForTesting
static LicenseType leastRestrictive(Collection<LicenseType> types) {
return types.isEmpty() ? LicenseType.BY_EXCEPTION_ONLY : Collections.max(types);
}
/**
* An instance of LicenseType.None with no exceptions, used for packages outside of third_party
* which have no license clause in their BUILD files.
*/
public static final License NO_LICENSE =
new License(ImmutableSet.of(LicenseType.NONE), ImmutableSet.of());
/**
* A default instance of Distributions which is used for packages which
* have no "distribs" declaration. If nothing is declared, we opt for the
* most permissive kind of distribution, which is the internal-only distrib.
*/
public static final Set<DistributionType> DEFAULT_DISTRIB =
Collections.singleton(DistributionType.INTERNAL);
/**
* The types of distribution that are supported.
*/
public enum DistributionType {
INTERNAL,
WEB,
CLIENT,
EMBEDDED
}
/**
* Parses a set of strings declaring distribution types.
*
* @param distStrings strings containing distribution declarations from BUILD
* files
* @return a new, unmodifiable set of DistributionTypes
* @throws LicenseParsingException
*/
public static Set<DistributionType> parseDistributions(Collection<String> distStrings)
throws LicenseParsingException {
if (distStrings.isEmpty()) {
return Collections.unmodifiableSet(EnumSet.of(DistributionType.INTERNAL));
} else {
Set<DistributionType> result = EnumSet.noneOf(DistributionType.class);
for (String distStr : distStrings) {
try {
DistributionType dist = DistributionType.valueOf(distStr.toUpperCase(Locale.ENGLISH));
result.add(dist);
} catch (IllegalArgumentException e) {
throw new LicenseParsingException("Invalid distribution type '" + distStr + "'");
}
}
return Collections.unmodifiableSet(result);
}
}
private static final Object MARKER = new Object();
/**
* The license incompatibility set. This contains the set of
* (Distribution,License) pairs that should generate errors.
*/
private static Table<DistributionType, LicenseType, Object> LICENSE_INCOMPATIBILIES =
createLicenseIncompatibilitySet();
private static Table<DistributionType, LicenseType, Object> createLicenseIncompatibilitySet() {
Table<DistributionType, LicenseType, Object> result = HashBasedTable.create();
result.put(DistributionType.CLIENT, LicenseType.RESTRICTED, MARKER);
result.put(DistributionType.EMBEDDED, LicenseType.RESTRICTED, MARKER);
result.put(DistributionType.INTERNAL, LicenseType.BY_EXCEPTION_ONLY, MARKER);
result.put(DistributionType.CLIENT, LicenseType.BY_EXCEPTION_ONLY, MARKER);
result.put(DistributionType.WEB, LicenseType.BY_EXCEPTION_ONLY, MARKER);
result.put(DistributionType.EMBEDDED, LicenseType.BY_EXCEPTION_ONLY, MARKER);
return ImmutableTable.copyOf(result);
}
/**
* The license warning set. This contains the set of
* (Distribution,License) pairs that should generate warnings when the user
* requests verbose license checking.
*/
private static Table<DistributionType, LicenseType, Object> LICENSE_WARNINGS =
createLicenseWarningsSet();
private static Table<DistributionType, LicenseType, Object> createLicenseWarningsSet() {
Table<DistributionType, LicenseType, Object> result = HashBasedTable.create();
result.put(DistributionType.CLIENT, LicenseType.RECIPROCAL, MARKER);
result.put(DistributionType.EMBEDDED, LicenseType.RECIPROCAL, MARKER);
result.put(DistributionType.CLIENT, LicenseType.NOTICE, MARKER);
result.put(DistributionType.EMBEDDED, LicenseType.NOTICE, MARKER);
return ImmutableTable.copyOf(result);
}
@AutoCodec.Instantiator
@VisibleForSerialization
License(ImmutableSet<LicenseType> licenseTypes, ImmutableSet<Label> exceptions) {
// Defensive copy is done in .of()
this.licenseTypes = licenseTypes;
this.exceptions = exceptions;
}
public static License of(Collection<LicenseType> licenses, Collection<Label> exceptions) {
ImmutableSet<LicenseType> licenseSet = ImmutableSet.copyOf(licenses);
ImmutableSet<Label> exceptionSet = ImmutableSet.copyOf(exceptions);
if (exceptionSet.isEmpty() && licenseSet.equals(ImmutableSet.of(LicenseType.NONE))) {
return License.NO_LICENSE;
}
return new License(licenseSet, exceptionSet);
}
/**
* Computes a license which can be used to check if a package is compatible
* with some kinds of distribution. The list of licenses is scanned for the
* least restrictive, and the exceptions are added.
*
* @param licStrings the list of license strings declared for the package
* @throws LicenseParsingException if there are any parsing problems
*/
public static License parseLicense(List<String> licStrings) throws LicenseParsingException {
/*
* The semantics of comparison for licenses depends on a stable iteration
* order for both license types and exceptions. For licenseTypes, it will be
* the comparison order from the enumerated types; for exceptions, it will
* be lexicographic order achieved using TreeSets.
*/
Set<LicenseType> licenseTypes = EnumSet.noneOf(LicenseType.class);
Set<Label> exceptions = Sets.newTreeSet();
for (String str : licStrings) {
if (str.startsWith("exception=")) {
try {
Label label =
Label.parseAbsolute(str.substring("exception=".length()), ImmutableMap.of());
exceptions.add(label);
} catch (LabelSyntaxException e) {
throw new LicenseParsingException(e.getMessage());
}
} else {
try {
licenseTypes.add(LicenseType.valueOf(str.toUpperCase(Locale.ENGLISH)));
} catch (IllegalArgumentException e) {
throw new LicenseParsingException("invalid license type: '" + str + "'");
}
}
}
return License.of(licenseTypes, exceptions);
}
/**
* Checks if this license is compatible with distributing a particular target
* in some set of distribution modes.
*
* @param dists the modes of distribution
* @param target the target which is being checked, and which will be used for
* checking exceptions
* @param licensedTarget the target which declared the license being checked.
* @param eventHandler a reporter where any licensing issues discovered should be
* reported
* @param staticallyLinked whether the target is statically linked under this command
* @return true if the license is compatible with the distributions
*/
public boolean checkCompatibility(Set<DistributionType> dists,
Target target, Label licensedTarget, EventHandler eventHandler,
boolean staticallyLinked) {
Location location = (target instanceof Rule) ? ((Rule) target).getLocation() : null;
LicenseType leastRestrictiveLicense;
if (licenseTypes.contains(LicenseType.RESTRICTED_IF_STATICALLY_LINKED)) {
Set<LicenseType> tempLicenses = EnumSet.copyOf(licenseTypes);
tempLicenses.remove(LicenseType.RESTRICTED_IF_STATICALLY_LINKED);
if (staticallyLinked) {
tempLicenses.add(LicenseType.RESTRICTED);
} else {
tempLicenses.add(LicenseType.UNENCUMBERED);
}
leastRestrictiveLicense = leastRestrictive(tempLicenses);
} else {
leastRestrictiveLicense = leastRestrictive(licenseTypes);
}
for (DistributionType dt : dists) {
if (LICENSE_INCOMPATIBILIES.contains(dt, leastRestrictiveLicense)) {
if (!exceptions.contains(target.getLabel())) {
eventHandler.handle(Event.error(location, "Build target '" + target.getLabel()
+ "' is not compatible with license '" + this + "' from target '"
+ licensedTarget + "'"));
return false;
}
} else if (LICENSE_WARNINGS.contains(dt, leastRestrictiveLicense)) {
eventHandler.handle(
Event.warn(location, "Build target '" + target
+ "' has a potential licensing issue "
+ "with a '" + this + "' license from target '" + licensedTarget + "'"));
}
}
return true;
}
/**
* @return an immutable set of {@link LicenseType}s contained in this {@code
* License}
*/
public Set<LicenseType> getLicenseTypes() {
return licenseTypes;
}
/**
* @return an immutable set of {@link Label}s that describe exceptions to the
* {@code License}
*/
public Set<Label> getExceptions() {
return exceptions;
}
@SuppressWarnings("ReferenceEquality")
public boolean isSpecified() {
return this != License.NO_LICENSE;
}
/**
* A simple toString implementation which generates a canonical form of the
* license. (The order of license types is guaranteed to be canonical by
* EnumSet, and the order of exceptions is guaranteed to be lexicographic
* order by TreeSet.)
*/
@Override
public String toString() {
if (exceptions.isEmpty()) {
return licenseTypes.toString().toLowerCase();
} else {
return licenseTypes.toString().toLowerCase() + " with exceptions " + exceptions;
}
}
/**
* A simple equals implementation leveraging the support built into Set that
* delegates to its contents.
*/
@Override
public boolean equals(Object o) {
return o == this || (o instanceof License
&& ((License) o).licenseTypes.equals(this.licenseTypes)
&& ((License) o).exceptions.equals(this.exceptions));
}
/**
* A simple hashCode implementation leveraging the support built into Set that
* delegates to its contents.
*/
@Override
public int hashCode() {
return licenseTypes.hashCode() * 43 + exceptions.hashCode();
}
@Override
public void repr(SkylarkPrinter printer) {
printer.append("<license object>");
}
}