-
Notifications
You must be signed in to change notification settings - Fork 315
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
Add an initial BitBake analyzer implementation #8642
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# About | ||
|
||
This is a package manager plugin for the [OSS Review Toolkit][ORT] to analyze [Yocto] projects managed by [BitBake]. | ||
It supersedes the combination of the [meta-doubleopen] and [do-convert] projects by relying on upstream [SBOM] generation in [SPDX] format, and converting the generated files to an ORT analyzer result file via ORT's [SPDX document file analyzer]. | ||
|
||
[ORT]: https://github.com/oss-review-toolkit/ort | ||
[BitBake]: https://docs.yoctoproject.org/bitbake.html | ||
[Yocto]: https://www.yoctoproject.org/ | ||
[meta-doubleopen]: https://github.com/doubleopen-project/meta-doubleopen | ||
[do-convert]: https://github.com/doubleopen-project/do-convert | ||
[SBOM]: https://docs.yoctoproject.org/dev/dev-manual/sbom.html | ||
[SPDX]: https://spdx.dev/ | ||
[SPDX document file analyzer]: https://oss-review-toolkit.org/ort/docs/tools/analyzer | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. commit: Why did you choose to use the analyzer for mapping the SPDX file? (I don't know that analyzer inside out, but I recall that it was implemented with some specific use case in mind which is beyond the SDPX spec) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That was the easiest way to make use of
Yes, indeed we might run into issues here when fully implementing document resolution. Ideally, any such issues could be fixed in |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>) | ||
* | ||
* 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 | ||
* | ||
* https://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. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* License-Filename: LICENSE | ||
*/ | ||
|
||
plugins { | ||
// Apply precompiled plugins. | ||
id("ort-library-conventions") | ||
} | ||
|
||
dependencies { | ||
api(projects.analyzer) | ||
api(projects.model) | ||
|
||
implementation(projects.utils.commonUtils) | ||
implementation(projects.plugins.packageManagers.spdxPackageManager) | ||
|
||
funTestImplementation(projects.downloader) | ||
funTestImplementation(projects.plugins.versionControlSystems.gitVersionControlSystem) | ||
funTestImplementation(testFixtures(projects.analyzer)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/* | ||
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>) | ||
* | ||
* 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 | ||
* | ||
* https://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. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* License-Filename: LICENSE | ||
*/ | ||
|
||
package org.ossreviewtoolkit.plugins.packagemanagers.bitbake | ||
|
||
import io.kotest.core.spec.style.WordSpec | ||
import io.kotest.engine.spec.tempdir | ||
import io.kotest.matchers.collections.beEmpty | ||
import io.kotest.matchers.collections.shouldHaveSize | ||
import io.kotest.matchers.result.shouldBeSuccess | ||
import io.kotest.matchers.should | ||
import io.kotest.matchers.shouldBe | ||
import io.kotest.matchers.string.shouldMatch | ||
|
||
import org.ossreviewtoolkit.analyzer.Analyzer | ||
import org.ossreviewtoolkit.analyzer.create | ||
import org.ossreviewtoolkit.model.Identifier | ||
import org.ossreviewtoolkit.model.VcsInfo | ||
import org.ossreviewtoolkit.model.VcsType | ||
import org.ossreviewtoolkit.model.config.AnalyzerConfiguration | ||
import org.ossreviewtoolkit.plugins.versioncontrolsystems.git.Git | ||
import org.ossreviewtoolkit.utils.test.ExpensiveTag | ||
import org.ossreviewtoolkit.utils.test.shouldNotBeNull | ||
|
||
class BitBakeFunTest : WordSpec({ | ||
"BitBake" should { | ||
"get the version correctly" { | ||
val bitBake = create("BitBake") as BitBake | ||
|
||
val version = bitBake.getBitBakeVersion(tempdir()) | ||
|
||
version shouldMatch "\\d+\\.\\d+\\.\\d+" | ||
} | ||
} | ||
|
||
"Analyzing recipes from Poky" should { | ||
val projectDir = tempdir() | ||
val pokyVcsInfo = VcsInfo(VcsType.GIT, "https://git.yoctoproject.org/poky", "kirkstone-4.0.17") | ||
|
||
Git().run { | ||
val workingTree = initWorkingTree(projectDir, pokyVcsInfo) | ||
updateWorkingTree(workingTree, pokyVcsInfo.revision) | ||
} shouldBeSuccess pokyVcsInfo.revision | ||
|
||
"create an SPDX file for the 'quilt-native' package" { | ||
val recipeFileName = "quilt-native_0.67.bb" | ||
val result = Analyzer(AnalyzerConfiguration()).run { | ||
val fileInfo = findManagedFiles(projectDir) | ||
val singleFileInfo = fileInfo.copy( | ||
managedFiles = fileInfo.managedFiles.map { (packageManager, definitionsFiles) -> | ||
packageManager to definitionsFiles.filter { it.name == recipeFileName } | ||
}.toMap() | ||
) | ||
analyze(singleFileInfo) | ||
} | ||
|
||
result.analyzer?.result shouldNotBeNull { | ||
projects shouldHaveSize 1 | ||
|
||
with(projects.single()) { | ||
id shouldBe Identifier("BitBake:OpenEmbedded ():quilt-native:0.67") | ||
declaredLicenses shouldBe setOf("GPL-2.0-only") | ||
homepageUrl shouldBe "http://savannah.nongnu.org/projects/quilt/" | ||
scopes should beEmpty() | ||
} | ||
} | ||
} | ||
|
||
"create a SPDX files for the 'xmlto' package".config(tags = setOf(ExpensiveTag)) { | ||
val recipeFileName = "xmlto_0.0.28.bb" | ||
val result = Analyzer(AnalyzerConfiguration()).run { | ||
val fileInfo = findManagedFiles(projectDir) | ||
val singleFileInfo = fileInfo.copy( | ||
managedFiles = fileInfo.managedFiles.map { (packageManager, definitionsFiles) -> | ||
packageManager to definitionsFiles.filter { it.name == recipeFileName } | ||
}.toMap() | ||
) | ||
analyze(singleFileInfo) | ||
} | ||
|
||
result.analyzer?.result shouldNotBeNull { | ||
projects shouldHaveSize 90 | ||
} | ||
} | ||
} | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
/* | ||
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>) | ||
* | ||
* 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 | ||
* | ||
* https://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. | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* License-Filename: LICENSE | ||
*/ | ||
|
||
package org.ossreviewtoolkit.plugins.packagemanagers.bitbake | ||
|
||
import java.io.File | ||
|
||
import kotlin.time.measureTime | ||
|
||
import org.apache.logging.log4j.kotlin.logger | ||
|
||
import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory | ||
import org.ossreviewtoolkit.analyzer.PackageManager | ||
import org.ossreviewtoolkit.analyzer.PackageManagerResult | ||
import org.ossreviewtoolkit.model.ProjectAnalyzerResult | ||
import org.ossreviewtoolkit.model.config.AnalyzerConfiguration | ||
import org.ossreviewtoolkit.model.config.RepositoryConfiguration | ||
import org.ossreviewtoolkit.plugins.packagemanagers.spdx.SpdxDocumentFile | ||
import org.ossreviewtoolkit.utils.common.ProcessCapture | ||
import org.ossreviewtoolkit.utils.common.getCommonParentFile | ||
import org.ossreviewtoolkit.utils.common.safeDeleteRecursively | ||
import org.ossreviewtoolkit.utils.common.withoutPrefix | ||
import org.ossreviewtoolkit.utils.ort.createOrtTempDir | ||
import org.ossreviewtoolkit.utils.ort.createOrtTempFile | ||
|
||
/** | ||
* A package manager that uses OpenEmbedded's "bitbake" tool to create SPDX SBOMs [1][2] e.g. for Yocto distributions, | ||
* and post-processes these into ORT analyzer results. | ||
* | ||
* [1]: https://docs.yoctoproject.org/dev/dev-manual/sbom.html | ||
* [2]: https://dev.to/angrymane/create-spdx-with-yocto-2od9 | ||
*/ | ||
class BitBake( | ||
name: String, | ||
analysisRoot: File, | ||
analyzerConfig: AnalyzerConfiguration, | ||
repoConfig: RepositoryConfiguration | ||
) : PackageManager(name, analysisRoot, analyzerConfig, repoConfig) { | ||
class Factory : AbstractPackageManagerFactory<BitBake>("BitBake") { | ||
override val globsForDefinitionFiles = listOf("*.bb") | ||
|
||
override fun create( | ||
analysisRoot: File, | ||
analyzerConfig: AnalyzerConfiguration, | ||
repoConfig: RepositoryConfiguration | ||
) = BitBake(type, analysisRoot, analyzerConfig, repoConfig) | ||
} | ||
|
||
private val scriptFile by lazy { extractResourceToTempFile(BITBAKE_SCRIPT_NAME).apply { setExecutable(true) } } | ||
private val spdxConfFile by lazy { extractResourceToTempFile(SPDX_CONF_NAME) } | ||
|
||
private val spdxManager by lazy { SpdxDocumentFile(name, analysisRoot, analyzerConfig, repoConfig) } | ||
|
||
override fun resolveDependencies(definitionFiles: List<File>, labels: Map<String, String>): PackageManagerResult { | ||
val commonDefinitionDir = getCommonParentFile(definitionFiles) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you help me understand where the assumption comes from, that all definition files always share a single init script? Or rather, could it be that there are multiple init scripts for independent definition files? |
||
val workingDir = requireNotNull(commonDefinitionDir.searchUpwardsForFile(INIT_SCRIPT_NAME)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the search be limited to remain within the analyzer root? |
||
"No '$INIT_SCRIPT_NAME' script file found for directory '$commonDefinitionDir'." | ||
} | ||
|
||
logger.info { "Determined the working directory to be '$workingDir'." } | ||
|
||
val localVersion = getBitBakeVersion(workingDir) | ||
val globalVersion = createOrtTempDir().let { dir -> | ||
getBitBakeVersion(dir).also { dir.safeDeleteRecursively(force = true) } | ||
} | ||
|
||
if (localVersion != globalVersion) { | ||
logger.warn { "Local $managerName version $localVersion differs from global version $globalVersion." } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this a warning? e.g. what problem could this cause? |
||
} | ||
|
||
val deployDirs = mutableSetOf<File>() | ||
|
||
definitionFiles.forEach { definitionFile -> | ||
val target = definitionFile.nameWithoutExtension.substringBeforeLast('_') | ||
|
||
val deployDir = getDeployDir(workingDir, target) | ||
deployDirs += deployDir | ||
|
||
val spdxFile = deployDir.findSpdxFiles().find { it.name == "recipe-$target.spdx.json" } | ||
if (spdxFile != null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In order to ensure the SPDX file contains the right content, I believe the |
||
logger.info { "Not creating SPDX files for target '$target' as it already exists at '$spdxFile'." } | ||
} else { | ||
logger.info { "Creating SPDX files for target '$target'..." } | ||
|
||
// This implicitly triggers the build and can take a very long time. | ||
val duration = measureTime { createSpdx(workingDir, target) } | ||
|
||
logger.info { "Creating SPDX files for target '$target' took $duration." } | ||
} | ||
} | ||
|
||
if (!scriptFile.delete()) logger.warn { "Unable to delete the temporary '$scriptFile' file." } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider putting the '$scriptFile' to the very end of the sentence. |
||
if (!spdxConfFile.delete()) logger.warn { "Unable to delete the temporary '$spdxConfFile' file." } | ||
|
||
val commonDeployDir = deployDirs.singleOrNull() ?: getCommonParentFile(deployDirs) | ||
val spdxFiles = commonDeployDir.findSpdxFiles().toList() | ||
|
||
logger.info { "Found ${spdxFiles.size} SPDX file(s) in '$commonDeployDir'." } | ||
|
||
return spdxManager.resolveDependencies(spdxFiles, labels) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without mapping the result from the manager, I suspect that |
||
} | ||
|
||
override fun resolveDependencies(definitionFile: File, labels: Map<String, String>): List<ProjectAnalyzerResult> = | ||
throw NotImplementedError("This function is not supported for $managerName.") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a code comment explaining why. |
||
|
||
private fun getDeployDir(workingDir: File, target: String): File { | ||
val bitbakeEnv = runBitBake(workingDir, "-e", target) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be Alternative:
|
||
return bitbakeEnv.stdout.lineSequence().mapNotNull { it.withoutPrefix("DEPLOY_DIR=") }.first() | ||
.let { File(it.removeSurrounding("\"")) } | ||
} | ||
|
||
private fun createSpdx(workingDir: File, target: String) = | ||
runBitBake(workingDir, "-r", spdxConfFile.absolutePath, "-c", "create_spdx", target) | ||
|
||
private fun File.findSpdxFiles() = resolve("spdx").walk().filter { it.isFile && it.name.endsWith(".spdx.json") } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be moved to file level. |
||
|
||
private fun runBitBake(workingDir: File, vararg args: String): ProcessCapture = | ||
ProcessCapture(scriptFile.absolutePath, workingDir.absolutePath, *args, workingDir = workingDir) | ||
.requireSuccess() | ||
|
||
internal fun getBitBakeVersion(workingDir: File): String = | ||
runBitBake(workingDir, "--version").stdout.lineSequence().first { | ||
it.startsWith("BitBake Build Tool") | ||
}.substringAfterLast(' ') | ||
|
||
private fun extractResourceToTempFile(resourceName: String): File { | ||
val prefix = resourceName.substringBefore('.') | ||
val suffix = resourceName.substringAfter(prefix) | ||
val scriptFile = createOrtTempFile(prefix, suffix) | ||
val script = checkNotNull(javaClass.getResource("/$resourceName")).readText() | ||
|
||
return scriptFile.apply { writeText(script) } | ||
} | ||
} | ||
|
||
private const val INIT_SCRIPT_NAME = "oe-init-build-env" | ||
private const val BITBAKE_SCRIPT_NAME = "bitbake.sh" | ||
private const val SPDX_CONF_NAME = "spdx.conf" | ||
|
||
private fun File.searchUpwardsForFile(searchFileName: String): File? { | ||
if (!isDirectory) return null | ||
|
||
var currentDir: File? = absoluteFile | ||
|
||
while (currentDir != null && !currentDir.resolve(searchFileName).isFile) { | ||
currentDir = currentDir.parentFile | ||
} | ||
|
||
return currentDir | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
org.ossreviewtoolkit.plugins.packagemanagers.bitbake.BitBake$Factory |
Check warning
Code scanning / Scorecard
Pinned-Dependencies Medium