Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LicenseChoices: Add repository license choices #3801

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions docs/config-file-ort-yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ resolutions:
### When to Use License Choices

For multi-licensed dependencies a specific license can be selected.
The license choice can be applied to a package.
The license choice can be applied to a package or globally to an SPDX expression in the project.
A choice is only valid for licenses combined with the SPDX operator `OR`.
The choices are applied in the evaluator, and the reporter to the effective license of a package, which is calculated
by the chosen [LicenseView](../model/src/main/kotlin/licenses/LicenseView.kt).
Expand All @@ -276,7 +276,7 @@ by the chosen [LicenseView](../model/src/main/kotlin/licenses/LicenseView.kt).
To select a license from a multi-licensed dependency, specified by its `packageId`, an SPDX expression for a `choice`
must be provided.
The `choice` is either applied to the whole effective SPDX expression of the package or to an optional `given` SPDX
expression that can represent only a sub-expression of the whole effective SPDX expression.
expression that can represent only a sub-expression of the whole effective SPDX expression.

e.g.
```yaml
Expand All @@ -300,6 +300,25 @@ license_choices:
# The result would be: C AND E
```

### License Choice for the Project

To globally select a license from an SPDX expression, that offers a choice, an SPDX expression for a `given` and a
`choice` must be provided.
The `choice` is applied to the whole `given` SPDX expression.
With a repository license choice, the license choice is applied to each package that offers this license as a choice.
Not allowing `given` to be null helps only applying the choice to a wanted `given` as opposed to all licenses with that
choice, which could lead to unwanted choices.
The license choices for a project can be overwritten by applying a
[license choice to a package](#license-choice-by-package).

e.g.
```yaml
license_choices:
repository_license_choice:
- given: "A OR B"
choice: "B"
```

---
**NOTE**

Expand Down
3 changes: 2 additions & 1 deletion evaluator/src/main/kotlin/PackageRule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ open class PackageRule(
*/
fun licenseRule(name: String, licenseView: LicenseView, block: LicenseRule.() -> Unit) {
resolvedLicenseInfo.filter(licenseView, filterSources = true)
.applyChoices(ruleSet.ortResult.getLicenseChoices(pkg.id)).forEach { resolvedLicense ->
.applyChoices(ruleSet.ortResult.getLicenseChoices(pkg.id))
.applyChoices(ruleSet.ortResult.getRepositoryLicenseChoices()).forEach { resolvedLicense ->
mnonnenmacher marked this conversation as resolved.
Show resolved Hide resolved
resolvedLicense.sources.forEach { licenseSource ->
licenseRules += LicenseRule(name, resolvedLicense, licenseSource).apply(block)
}
Expand Down
35 changes: 28 additions & 7 deletions evaluator/src/test/kotlin/RuleSetTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2017-2020 HERE Europe B.V.
* Copyright (C) 2021 Bosch.IO GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,6 +25,7 @@ import io.kotest.matchers.collections.haveSize
import io.kotest.matchers.should

import org.ossreviewtoolkit.model.licenses.LicenseView
import org.ossreviewtoolkit.spdx.SpdxExpression
import org.ossreviewtoolkit.spdx.toSpdx

class RuleSetTest : WordSpec() {
Expand Down Expand Up @@ -122,25 +124,44 @@ class RuleSetTest : WordSpec() {
ruleSet.violations should haveSize(4)
}

"add no license errors if license is removed by license choice" {
"add no license errors if license is removed by package license choice in the correct order" {
val ruleSet = ruleSet(ortResult) {
dependencyRule("test") {
licenseRule("test", LicenseView.CONCLUDED_OR_DECLARED_AND_DETECTED) {
licenseRule("test", LicenseView.ONLY_CONCLUDED) {
require {
object : RuleMatcher {
override val description = "containsLicense(license)"
+containsLicense("LicenseRef-b".toSpdx())
}

error(errorMessage, howToFix)
}
}
}

override fun matches() = license == "LicenseRef-b".toSpdx()
}
ruleSet.violations should haveSize(1)
}

"add no license errors if license is removed by repository license choice" {
val ruleSet = ruleSet(ortResult) {
dependencyRule("test") {
licenseRule("test", LicenseView.ONLY_CONCLUDED) {
require {
+containsLicense("LicenseRef-c".toSpdx())
}

error(errorMessage, howToFix)
}
}
}

ruleSet.violations should haveSize(5)
ruleSet.violations should haveSize(0)
}
}
}

private fun PackageRule.LicenseRule.containsLicense(expression: SpdxExpression) =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The extraction of this function could have happened in a preparing commit, as it's unrelated to the actual change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to leave these changes together. As with the extraction came a fix for the actual test we weren't aware of before.

object : RuleMatcher {
override val description = "containsLicense(license)"

override fun matches() = license == expression
}
}
8 changes: 7 additions & 1 deletion evaluator/src/test/kotlin/TestData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import org.ossreviewtoolkit.spdx.toSpdx
import org.ossreviewtoolkit.utils.DeclaredLicenseProcessor
import org.ossreviewtoolkit.utils.Environment

val concludedLicense = "LicenseRef-a OR LicenseRef-b".toSpdx()
val concludedLicense = "LicenseRef-a OR LicenseRef-b OR LicenseRef-c or LicenseRef-d".toSpdx()
val declaredLicenses = sortedSetOf("Apache-2.0", "MIT")
val declaredLicensesProcessed = DeclaredLicenseProcessor.process(declaredLicenses)

Expand Down Expand Up @@ -167,6 +167,12 @@ val ortResult = OrtResult(
)
),
licenseChoices = LicenseChoices(
repositoryLicenseChoices = listOf(
// This license choice will not be applied to "only-concluded-license" since the package license
// choice takes precedence.
LicenseChoice("LicenseRef-a OR LicenseRef-b".toSpdx(), "LicenseRef-b".toSpdx()),
LicenseChoice("LicenseRef-c OR LicenseRef-d".toSpdx(), "LicenseRef-d".toSpdx())
),
packageLicenseChoices = listOf(
PackageLicenseChoice(
packageId = Identifier("Maven:org.ossreviewtoolkit:package-with-only-concluded-license:1.0"),
Expand Down
7 changes: 7 additions & 0 deletions model/src/main/kotlin/OrtResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,13 @@ data class OrtResult(
fun getLicenseChoices(id: Identifier): List<LicenseChoice> =
repository.config.licenseChoices.packageLicenseChoices.find { it.packageId == id }?.licenseChoices.orEmpty()

/**
* Return all [LicenseChoice]s applicable for the scope of the whole [repository].
*/
@JsonIgnore
fun getRepositoryLicenseChoices(): List<LicenseChoice> =
repository.config.licenseChoices.repositoryLicenseChoices

/**
* Return the list of [AdvisorResult]s for the given [id].
*/
Expand Down
20 changes: 19 additions & 1 deletion model/src/main/kotlin/config/LicenseChoices.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,34 @@ import com.fasterxml.jackson.annotation.JsonInclude

import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.spdx.model.LicenseChoice
import org.ossreviewtoolkit.utils.ORT_REPO_CONFIG_FILENAME

/**
* The license choices configured for a repository.
*/
data class LicenseChoices(
/**
* [LicenseChoice]s that are applied to all packages in the repository.
* Since the [LicenseChoice] is applied to each package that offers this license as a choice, [LicenseChoice.given]
* can not be null. This helps only applying the choice to a wanted [LicenseChoice.given] as opposed to all
* licenses with that choice, which could lead to unwanted applied choices.
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
val repositoryLicenseChoices: List<LicenseChoice> = emptyList(),

@JsonInclude(JsonInclude.Include.NON_EMPTY)
val packageLicenseChoices: List<PackageLicenseChoice> = emptyList()
) {
@JsonIgnore
fun isEmpty() = packageLicenseChoices.isEmpty()
fun isEmpty() = packageLicenseChoices.isEmpty() && repositoryLicenseChoices.isEmpty()

init {
val choicesWithoutGiven = repositoryLicenseChoices.filter { it.given == null }
require(choicesWithoutGiven.isEmpty()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do repositoryLicenseChoices require a given but packageLicenseChoices do not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using a package license choice we have a specific effective license of the package a choice (or several) can be applied to, which is most likely to be known before applying the choice. When we have a repository license choice, the license choice would be applied to each package that offers this license as a choice. Making sure that there is a given helps only applying the choice to a wanted given as opposed to all licenses with that choice, which could lead to unwanted choices.
For consistency it would be possible to make given mandatory for packageLicenseChoices, but this was previously discussed before and decided otherwise.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation, can you add that to the code docs, e.g. for the LicenseChoices class or the properties? And maybe also to the documentation from the last commit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the explanation to the RepositoryLicenseChoices, but am unsure if this is how and where it belongs.
I also added it to the documentation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I think this is good enough for now. I just wanted to avoid that when you read this code without knowing the context you stumble over the fact that only one of the properties has this requirement.

"LicenseChoices ${choicesWithoutGiven.joinToString()} defined in $ORT_REPO_CONFIG_FILENAME are missing " +
"the 'given' expression."
}
}
}

/**
Expand Down
7 changes: 4 additions & 3 deletions model/src/main/kotlin/licenses/ResolvedLicenseInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,17 @@ data class ResolvedLicenseInfo(
/**
* Return the effective [SpdxExpression] of this [ResolvedLicenseInfo] based on their [licenses] filtered by the
* [licenseView] and the applied [licenseChoices]. Effective, in this context, refers to an [SpdxExpression] that
* can be used as a final license of this [ResolvedLicenseInfo].
* can be used as a final license of this [ResolvedLicenseInfo]. [licenseChoices] will be applied in the order they
neubs-bsi marked this conversation as resolved.
Show resolved Hide resolved
* are given to the function.
*/
fun effectiveLicense(licenseView: LicenseView, licenseChoices: List<LicenseChoice> = emptyList()): SpdxExpression? {
fun effectiveLicense(licenseView: LicenseView, vararg licenseChoices: List<LicenseChoice>): SpdxExpression? {
val resolvedLicenseInfo = filter(licenseView, filterSources = true)

return resolvedLicenseInfo.licenses.flatMap { it.originalExpressions.values }
.flatten()
.toSet()
.reduceOrNull(SpdxExpression::and)
?.applyChoices(licenseChoices)
?.applyChoices(licenseChoices.asList().flatten())
?.validChoices()
?.reduceOrNull(SpdxExpression::or)
}
Expand Down
31 changes: 31 additions & 0 deletions model/src/test/kotlin/config/RepositoryConfigurationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@

package org.ossreviewtoolkit.model.config

import com.fasterxml.jackson.databind.exc.ValueInstantiationException
import com.fasterxml.jackson.module.kotlin.readValue

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.WordSpec
import io.kotest.matchers.collections.haveSize
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldNotContain

import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.yamlMapper
Expand All @@ -45,6 +49,23 @@ class RepositoryConfigurationTest : WordSpec({
config.excludes.paths[0].matches("android/project1/build.gradle") shouldBe true
}

"throw ValueInstantiationException if no given is supplied for repository_license_choices" {
val configuration = """
license_choices:
repository_license_choices:
- given: Apache-2.0 or GPL-2.0-only
choice: GPL-2.0-only
- choice: MIT
""".trimIndent()

val exception = shouldThrow<ValueInstantiationException> {
yamlMapper.readValue<RepositoryConfiguration>(configuration)
}

exception.message shouldContain "problem: LicenseChoices LicenseChoice(given=null, choice=MIT)"
exception.message shouldNotContain "GPL-2.0-only"
}

"be deserializable" {
val configuration = """
excludes:
Expand All @@ -70,6 +91,9 @@ class RepositoryConfigurationTest : WordSpec({
reason: "INEFFECTIVE_VULNERABILITY"
comment: "vulnerability comment"
license_choices:
repository_license_choices:
- given: Apache-2.0 or GPL-2.0-only
choice: GPL-2.0-only
package_license_choices:
- package_id: "Maven:com.example:lib:0.0.1"
license_choices:
Expand Down Expand Up @@ -120,6 +144,13 @@ class RepositoryConfigurationTest : WordSpec({
comment shouldBe "vulnerability comment"
}

val repositoryLicenseChoices = repositoryConfiguration.licenseChoices.repositoryLicenseChoices
repositoryLicenseChoices should haveSize(1)
with(repositoryLicenseChoices.first()) {
given shouldBe "Apache-2.0 or GPL-2.0-only".toSpdx()
choice shouldBe "GPL-2.0-only".toSpdx()
}

val packageLicenseChoices = repositoryConfiguration.licenseChoices.packageLicenseChoices
packageLicenseChoices should haveSize(1)
with(packageLicenseChoices.first()) {
Expand Down
18 changes: 18 additions & 0 deletions model/src/test/kotlin/licenses/ResolvedLicenseInfoTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,24 @@ class ResolvedLicenseInfoTest : WordSpec() {

effectiveLicense shouldBe mit.toSpdx()
}

"apply package and repository license choice for LicenseView.ONLY_CONCLUDED in the correct order" {
val repositoryChoices = listOf(
LicenseChoice("$apache OR $mit".toSpdx(), mit.toSpdx()),
LicenseChoice("$bsd OR $gpl".toSpdx(), bsd.toSpdx())
)
val packageChoices = listOf(
LicenseChoice("$apache OR $mit".toSpdx(), apache.toSpdx())
)

val effectiveLicense = createResolvedLicenseInfo().effectiveLicense(
LicenseView.ALL,
packageChoices,
repositoryChoices
)

effectiveLicense shouldBe "$apache and ($mit or $gpl) and $bsd".toSpdx()
}
}

"applyChoices(licenseChoices)" should {
Expand Down
Loading