blob: d3e412b84e285449018d9693ab3202002e5f2f2b [file] [log] [blame]
Bob Badour9ee7d032021-10-25 16:51:48 -07001// Copyright 2021 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package compliance
16
17import (
18 "regexp"
19 "strings"
20)
21
22var (
Bob Badour67d8ae32022-01-10 18:32:54 -080023 // RecognizedAnnotations identifies the set of annotations that have
24 // meaning for compliance policy.
25 RecognizedAnnotations = map[string]string{
26 // used in readgraph.go to avoid creating 1000's of copies of the below 3 strings.
27 "static": "static",
28 "dynamic": "dynamic",
29 "toolchain": "toolchain",
30 }
31
Bob Badour9ee7d032021-10-25 16:51:48 -070032 // ImpliesUnencumbered lists the condition names representing an author attempt to disclaim copyright.
33 ImpliesUnencumbered = ConditionNames{"unencumbered"}
34
35 // ImpliesPermissive lists the condition names representing copyrighted but "licensed without policy requirements".
36 ImpliesPermissive = ConditionNames{"permissive"}
37
38 // ImpliesNotice lists the condition names implying a notice or attribution policy.
39 ImpliesNotice = ConditionNames{"unencumbered", "permissive", "notice", "reciprocal", "restricted", "proprietary", "by_exception_only"}
40
41 // ImpliesReciprocal lists the condition names implying a local source-sharing policy.
42 ImpliesReciprocal = ConditionNames{"reciprocal"}
43
44 // Restricted lists the condition names implying an infectious source-sharing policy.
45 ImpliesRestricted = ConditionNames{"restricted"}
46
47 // ImpliesProprietary lists the condition names implying a confidentiality policy.
48 ImpliesProprietary = ConditionNames{"proprietary"}
49
50 // ImpliesByExceptionOnly lists the condition names implying a policy for "license review and approval before use".
51 ImpliesByExceptionOnly = ConditionNames{"proprietary", "by_exception_only"}
52
53 // ImpliesPrivate lists the condition names implying a source-code privacy policy.
54 ImpliesPrivate = ConditionNames{"proprietary"}
55
56 // ImpliesShared lists the condition names implying a source-code sharing policy.
57 ImpliesShared = ConditionNames{"reciprocal", "restricted"}
58)
59
60var (
61 anyLgpl = regexp.MustCompile(`^SPDX-license-identifier-LGPL.*`)
62 versionedGpl = regexp.MustCompile(`^SPDX-license-identifier-GPL-\p{N}.*`)
63 genericGpl = regexp.MustCompile(`^SPDX-license-identifier-GPL$`)
64 ccBySa = regexp.MustCompile(`^SPDX-license-identifier-CC-BY.*-SA.*`)
65)
66
67// Resolution happens in two passes:
68//
69// 1. A bottom-up traversal propagates license conditions up to targets from
70// dendencies as needed.
71//
72// 2. For each condition of interest, a top-down traversal adjusts the attached
73// conditions pushing restricted down from targets into linked dependencies.
74//
75// The behavior of the 2 passes gets controlled by the 2 functions below.
76//
77// The first function controls what happens during the bottom-up traversal. In
78// general conditions flow up through static links but not other dependencies;
79// except, restricted sometimes flows up through dynamic links.
80//
81// In general, too, the originating target gets acted on to resolve the
82// condition (e.g. providing notice), but again restricted is special in that
83// it requires acting on (i.e. sharing source of) both the originating module
84// and the target using the module.
85//
86// The latter function controls what happens during the top-down traversal. In
87// general, only restricted conditions flow down at all, and only through
88// static links.
89//
90// Not all restricted licenses are create equal. Some have special rules or
91// exceptions. e.g. LGPL or "with classpath excption".
92
93// depActionsApplicableToTarget returns the actions which propagate up an
94// edge from dependency to target.
95//
96// This function sets the policy for the bottom-up traversal and how conditions
97// flow up the graph from dependencies to targets.
98//
99// If a pure aggregation is built into a derivative work that is not a pure
100// aggregation, per policy it ceases to be a pure aggregation in the context of
101// that derivative work. The `treatAsAggregate` parameter will be false for
102// non-aggregates and for aggregates in non-aggregate contexts.
103func depActionsApplicableToTarget(e TargetEdge, depActions actionSet, treatAsAggregate bool) actionSet {
104 result := make(actionSet)
105 if edgeIsDerivation(e) {
106 result.addSet(depActions)
107 for _, cs := range depActions.byName(ImpliesRestricted) {
108 result.add(e.Target(), cs)
109 }
110 return result
111 }
112 if !edgeIsDynamicLink(e) {
113 return result
114 }
115
116 restricted := depActions.byName(ImpliesRestricted)
117 for actsOn, cs := range restricted {
118 for _, lc := range cs.AsList() {
119 hasGpl := false
120 hasLgpl := false
121 hasClasspath := false
122 hasGeneric := false
123 hasOther := false
124 for _, kind := range lc.origin.LicenseKinds() {
125 if strings.HasSuffix(kind, "-with-classpath-exception") {
126 hasClasspath = true
127 } else if anyLgpl.MatchString(kind) {
128 hasLgpl = true
129 } else if versionedGpl.MatchString(kind) {
130 hasGpl = true
131 } else if genericGpl.MatchString(kind) {
132 hasGeneric = true
133 } else if kind == "legacy_restricted" || ccBySa.MatchString(kind) {
134 hasOther = true
135 }
136 }
137 if hasOther || hasGpl {
138 result.addCondition(actsOn, lc)
139 result.addCondition(e.Target(), lc)
140 continue
141 }
142 if hasClasspath && !edgeNodesAreIndependentModules(e) {
143 result.addCondition(actsOn, lc)
144 result.addCondition(e.Target(), lc)
145 continue
146 }
147 if hasLgpl || hasClasspath {
148 continue
149 }
150 if !hasGeneric {
151 continue
152 }
153 result.addCondition(actsOn, lc)
154 result.addCondition(e.Target(), lc)
155 }
156 }
157 return result
158}
159
160// targetConditionsApplicableToDep returns the conditions which propagate down
161// an edge from target to dependency.
162//
163// This function sets the policy for the top-down traversal and how conditions
164// flow down the graph from targets to dependencies.
165//
166// If a pure aggregation is built into a derivative work that is not a pure
167// aggregation, per policy it ceases to be a pure aggregation in the context of
168// that derivative work. The `treatAsAggregate` parameter will be false for
169// non-aggregates and for aggregates in non-aggregate contexts.
170func targetConditionsApplicableToDep(e TargetEdge, targetConditions *LicenseConditionSet, treatAsAggregate bool) *LicenseConditionSet {
171 result := targetConditions.Copy()
172
173 // reverse direction -- none of these apply to things depended-on, only to targets depending-on.
174 result.RemoveAllByName(ConditionNames{"unencumbered", "permissive", "notice", "reciprocal", "proprietary", "by_exception_only"})
175
176 if !edgeIsDerivation(e) && !edgeIsDynamicLink(e) {
177 // target is not a derivative work of dependency and is not linked to dependency
178 result.RemoveAllByName(ImpliesRestricted)
179 return result
180 }
181 if treatAsAggregate {
182 // If the author of a pure aggregate licenses it restricted, apply restricted to immediate dependencies.
183 // Otherwise, restricted does not propagate back down to dependencies.
184 restricted := result.ByName(ImpliesRestricted).AsList()
185 for _, lc := range restricted {
186 if lc.origin.name != e.e.target {
187 result.Remove(lc)
188 }
189 }
190 return result
191 }
192 if edgeIsDerivation(e) {
193 return result
194 }
195 restricted := result.ByName(ImpliesRestricted).AsList()
196 for _, lc := range restricted {
197 hasGpl := false
198 hasLgpl := false
199 hasClasspath := false
200 hasGeneric := false
201 hasOther := false
202 for _, kind := range lc.origin.LicenseKinds() {
203 if strings.HasSuffix(kind, "-with-classpath-exception") {
204 hasClasspath = true
205 } else if anyLgpl.MatchString(kind) {
206 hasLgpl = true
207 } else if versionedGpl.MatchString(kind) {
208 hasGpl = true
209 } else if genericGpl.MatchString(kind) {
210 hasGeneric = true
211 } else if kind == "legacy_restricted" || ccBySa.MatchString(kind) {
212 hasOther = true
213 }
214 }
215 if hasOther || hasGpl {
216 continue
217 }
218 if hasClasspath && !edgeNodesAreIndependentModules(e) {
219 continue
220 }
221 if hasGeneric && !hasLgpl && !hasClasspath {
222 continue
223 }
224 result.Remove(lc)
225 }
226 return result
227}
228
229// edgeIsDynamicLink returns true for edges representing shared libraries
230// linked dynamically at runtime.
231func edgeIsDynamicLink(e TargetEdge) bool {
232 return e.e.annotations.HasAnnotation("dynamic")
233}
234
235// edgeIsDerivation returns true for edges where the target is a derivative
236// work of dependency.
237func edgeIsDerivation(e TargetEdge) bool {
238 isDynamic := e.e.annotations.HasAnnotation("dynamic")
239 isToolchain := e.e.annotations.HasAnnotation("toolchain")
240 return !isDynamic && !isToolchain
241}
242
243// edgeNodesAreIndependentModules returns true for edges where the target and
244// dependency are independent modules.
245func edgeNodesAreIndependentModules(e TargetEdge) bool {
246 return e.Target().PackageName() != e.Dependency().PackageName()
247}