Skip to content

Commit b4d40e4

Browse files
committed
WIP: LicenseCompatibilityInterpreter, LicenseCompatibilityInterpreterTest, VerifyLicenseCompatibilityTask
1 parent beaa3fe commit b4d40e4

File tree

3 files changed

+253
-12
lines changed

3 files changed

+253
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2019 Vladimir Sitnikov <[email protected]>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package com.github.vlsi.gradle.license
19+
20+
import com.github.vlsi.gradle.license.api.ConjunctionLicenseExpression
21+
import com.github.vlsi.gradle.license.api.DisjunctionLicenseExpression
22+
import com.github.vlsi.gradle.license.api.LicenseEquivalence
23+
import com.github.vlsi.gradle.license.api.LicenseExpression
24+
import com.github.vlsi.gradle.license.api.disjunctions
25+
26+
internal sealed class LicenseCompatibility {
27+
abstract val reason: String
28+
29+
internal data class Allow(override val reason: String) : LicenseCompatibility() {
30+
override fun toString() = "Allow(reason=$reason)"
31+
}
32+
internal data class Reject(override val reason: String) : LicenseCompatibility() {
33+
override fun toString() = "Reject(reason=$reason)"
34+
}
35+
internal data class Unknown(override val reason: String) : LicenseCompatibility() {
36+
override fun toString() = "Unknown(reason=$reason)"
37+
}
38+
}
39+
40+
internal class LicenseCompatibilityInterpreter(
41+
private val licenseEquivalence: LicenseEquivalence,
42+
private val acceptableLicenseExpressions: Set<LicenseExpression>
43+
) {
44+
private val acceptedLicenses = acceptableLicenseExpressions
45+
.asSequence()
46+
.flatMap { licenseExpression ->
47+
licenseEquivalence.expand(licenseExpression)
48+
.disjunctions().asSequence().map { it to licenseExpression }
49+
}
50+
.toMap()
51+
52+
override fun toString() =
53+
"LicenseCompatibilityInterpreter(acceptableLicenses=$acceptableLicenseExpressions)"
54+
55+
fun eval(licenseExpression: LicenseExpression?): LicenseCompatibility {
56+
if (licenseExpression == null) {
57+
return LicenseCompatibility.Reject("License is null")
58+
}
59+
val e = licenseEquivalence.expand(licenseExpression)
60+
if (e in acceptedLicenses) {
61+
return LicenseCompatibility.Allow("License is in the allowed list: $e")
62+
}
63+
64+
return when (e) {
65+
is DisjunctionLicenseExpression ->
66+
// A or X => A
67+
e.unordered
68+
.takeIf { it.isNotEmpty() }
69+
?.map { eval(it) }
70+
?.reduce { a, b ->
71+
when {
72+
// allow OR any -> allow
73+
a is LicenseCompatibility.Allow -> a
74+
b is LicenseCompatibility.Allow -> b
75+
// unknown OR next -> next
76+
a is LicenseCompatibility.Unknown -> b
77+
b is LicenseCompatibility.Unknown -> a
78+
else -> a
79+
}
80+
}
81+
is ConjunctionLicenseExpression ->
82+
e.unordered
83+
.takeIf { it.isNotEmpty() }
84+
?.map { eval(it) }
85+
?.reduce { a, b ->
86+
when {
87+
// reject AND any -> reject
88+
a is LicenseCompatibility.Reject -> a
89+
b is LicenseCompatibility.Reject -> b
90+
// unknown AND any -> unknown
91+
a is LicenseCompatibility.Unknown -> a
92+
b is LicenseCompatibility.Unknown -> b
93+
else -> a
94+
}
95+
}
96+
else -> LicenseCompatibility.Unknown("No rules found for $e")
97+
} ?: LicenseCompatibility.Unknown("No licenses in $licenseExpression")
98+
}
99+
}

plugins/license-gather-plugin/src/main/kotlin/com/github/vlsi/gradle/license/VerifyLicenseCompatibilityTask.kt

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,32 @@
1818
package com.github.vlsi.gradle.license
1919

2020
import com.github.vlsi.gradle.license.api.License
21+
import com.github.vlsi.gradle.license.api.LicenseEquivalence
2122
import com.github.vlsi.gradle.license.api.LicenseExpression
22-
import com.github.vlsi.gradle.license.api.SimpleLicense
2323
import com.github.vlsi.gradle.license.api.asExpression
24-
import com.github.vlsi.gradle.license.api.disjunctions
2524
import org.gradle.api.DefaultTask
26-
import org.gradle.api.artifacts.Configuration
25+
import org.gradle.api.GradleException
26+
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
27+
import org.gradle.api.logging.LogLevel
2728
import org.gradle.api.model.ObjectFactory
2829
import org.gradle.api.tasks.Input
2930
import org.gradle.api.tasks.InputFiles
3031
import org.gradle.api.tasks.OutputFile
3132
import org.gradle.api.tasks.TaskAction
32-
import org.gradle.kotlin.dsl.mapProperty
33-
import org.gradle.kotlin.dsl.resolver.DefaultResolverEventLogger.outputFile
33+
import org.gradle.kotlin.dsl.property
3434
import org.gradle.kotlin.dsl.setProperty
35+
import java.util.*
3536
import javax.inject.Inject
3637

37-
class VerifyLicenseCompatibilityTask @Inject constructor(
38+
open class VerifyLicenseCompatibilityTask @Inject constructor(
3839
objectFactory: ObjectFactory
3940
) : DefaultTask() {
4041
@InputFiles
4142
val metadata = objectFactory.fileCollection()
4243

44+
@Input
45+
val failOnIncompatibleLicense = objectFactory.property<Boolean>().convention(true)
46+
4347
@Input
4448
val acceptableLicenses = objectFactory.setProperty<LicenseExpression>()
4549

@@ -57,18 +61,76 @@ class VerifyLicenseCompatibilityTask @Inject constructor(
5761
acceptableLicenses.add(licenseExpression)
5862
}
5963

64+
private fun Appendable.appendComponents(
65+
components: List<Pair<ModuleComponentIdentifier, LicenseExpression?>>
66+
) =
67+
components
68+
.groupByTo(TreeMap(nullsFirst(LicenseExpression.NATURAL_ORDER)),
69+
{ it.second }, { it.first })
70+
.forEach { (license, components) ->
71+
append('\n')
72+
append(license?.toString() ?: "Unknown license").append('\n')
73+
components.forEach {
74+
append("* ${it.displayName}\n")
75+
}
76+
}
77+
6078
@TaskAction
6179
fun run() {
6280
val dependencies = MetadataStore.load(metadata).dependencies
63-
val output = outputFile.get().asFile.apply {
81+
val licenseCompatibilityInterpreter = LicenseCompatibilityInterpreter(
82+
// TODO: make it configurable
83+
LicenseEquivalence(),
84+
acceptableLicenses.get()
85+
)
86+
87+
val ok = StringBuilder()
88+
val ko = StringBuilder()
89+
90+
dependencies
91+
.asSequence()
92+
.map { (component, licenseInfo) -> component to licenseInfo.license}
93+
.groupByTo(TreeMap()) { (component, license) ->
94+
licenseCompatibilityInterpreter.eval(license).also {
95+
logger.log(
96+
when (it) {
97+
is LicenseCompatibility.Allow -> LogLevel.DEBUG
98+
is LicenseCompatibility.Unknown -> LogLevel.INFO
99+
is LicenseCompatibility.Reject -> LogLevel.LIFECYCLE
100+
},
101+
"License compatibility for {}: {} -> {}", component, license, it
102+
)
103+
}
104+
}
105+
.forEach { (licenseCompatibility, components) ->
106+
val header = when (licenseCompatibility) {
107+
is LicenseCompatibility.Reject -> "Reject: ${licenseCompatibility.reason}"
108+
is LicenseCompatibility.Unknown -> "Unknown: ${licenseCompatibility.reason}"
109+
is LicenseCompatibility.Allow -> "Allow: ${licenseCompatibility.reason}"
110+
}
111+
val sb = if (licenseCompatibility is LicenseCompatibility.Allow) ok else ko
112+
sb.append(header).append('\n')
113+
sb.append("=".repeat(header.length)).append('\n')
114+
sb.appendComponents(components)
115+
}
116+
117+
outputFile.get().asFile.apply {
64118
parentFile.mkdirs()
119+
bufferedWriter().use {
120+
if (ko.isNotEmpty()) {
121+
it.append(ko)
122+
it.append('\n')
123+
}
124+
it.append(ok)
125+
}
65126
}
66127

67-
val okLicenses = acceptableLicenses.get()
68-
.flatMapTo(mutableSetOf()) { it.disjunctions() }
69-
70-
for ((dependency, license) in dependencies) {
71-
if (license.)
128+
if (ko.isNotEmpty()) {
129+
if (failOnIncompatibleLicense.get()) {
130+
throw GradleException(ko.toString())
131+
} else {
132+
logger.warn(ko.toString())
133+
}
72134
}
73135
}
74136
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2019 Vladimir Sitnikov <[email protected]>
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package com.github.vlsi.gradle.license
19+
20+
import com.github.vlsi.gradle.license.api.LicenseEquivalence
21+
import com.github.vlsi.gradle.license.api.LicenseExpression
22+
import com.github.vlsi.gradle.license.api.SpdxLicense
23+
import com.github.vlsi.gradle.license.api.and
24+
import com.github.vlsi.gradle.license.api.asExpression
25+
import com.github.vlsi.gradle.license.api.or
26+
import com.github.vlsi.gradle.license.api.orLater
27+
import org.junit.jupiter.api.Assertions.assertEquals
28+
import org.junit.jupiter.params.ParameterizedTest
29+
import org.junit.jupiter.params.provider.Arguments.arguments
30+
import org.junit.jupiter.params.provider.MethodSource
31+
32+
class LicenseCompatibilityInterpreterTest {
33+
companion object {
34+
private val interpreter = LicenseCompatibilityInterpreter(
35+
LicenseEquivalence(),
36+
setOf(
37+
// public domain
38+
SpdxLicense.CC0_1_0.asExpression(),
39+
SpdxLicense.MIT.asExpression(),
40+
SpdxLicense.Apache_2_0.asExpression()
41+
)
42+
)
43+
44+
@JvmStatic
45+
fun data() = listOf(
46+
arguments(
47+
SpdxLicense.MIT.asExpression(),
48+
LicenseCompatibility.Allow("License is in the allowed list: MIT")
49+
),
50+
arguments(
51+
SpdxLicense.MIT or SpdxLicense.CC0_1_0,
52+
LicenseCompatibility.Allow("License is in the allowed list: MIT")
53+
),
54+
arguments(
55+
SpdxLicense.GFDL_1_1_only or SpdxLicense.MIT,
56+
LicenseCompatibility.Allow("License is in the allowed list: MIT")
57+
),
58+
arguments(
59+
SpdxLicense.GFDL_1_1_only and SpdxLicense.MIT,
60+
LicenseCompatibility.Unknown("No rules found for GFDL-1.1-only")
61+
),
62+
arguments(
63+
SpdxLicense.Apache_1_0.asExpression(),
64+
LicenseCompatibility.Unknown("No rules found for Apache-1.0")
65+
),
66+
arguments(
67+
SpdxLicense.Apache_1_0.orLater(),
68+
LicenseCompatibility.Allow("License is in the allowed list: Apache-2.0")
69+
)
70+
)
71+
}
72+
73+
@ParameterizedTest
74+
@MethodSource("data")
75+
internal fun test(input: LicenseExpression?, expected: LicenseCompatibility) {
76+
assertEquals(expected, interpreter.eval(input)) {
77+
"input: $input evaluated with $interpreter"
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)