|
| 1 | +/* |
| 2 | + * Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>) |
| 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 | + * https://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 | + * SPDX-License-Identifier: Apache-2.0 |
| 17 | + * License-Filename: LICENSE |
| 18 | + */ |
| 19 | + |
| 20 | +package org.ossreviewtoolkit.plugins.packagemanagers.bitbake |
| 21 | + |
| 22 | +import java.io.File |
| 23 | + |
| 24 | +import kotlin.time.measureTime |
| 25 | + |
| 26 | +import org.apache.logging.log4j.kotlin.logger |
| 27 | + |
| 28 | +import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory |
| 29 | +import org.ossreviewtoolkit.analyzer.PackageManager |
| 30 | +import org.ossreviewtoolkit.analyzer.PackageManagerResult |
| 31 | +import org.ossreviewtoolkit.model.ProjectAnalyzerResult |
| 32 | +import org.ossreviewtoolkit.model.config.AnalyzerConfiguration |
| 33 | +import org.ossreviewtoolkit.model.config.RepositoryConfiguration |
| 34 | +import org.ossreviewtoolkit.plugins.packagemanagers.spdx.SpdxDocumentFile |
| 35 | +import org.ossreviewtoolkit.utils.common.ProcessCapture |
| 36 | +import org.ossreviewtoolkit.utils.common.getCommonParentFile |
| 37 | +import org.ossreviewtoolkit.utils.common.safeDeleteRecursively |
| 38 | +import org.ossreviewtoolkit.utils.common.withoutPrefix |
| 39 | +import org.ossreviewtoolkit.utils.ort.createOrtTempDir |
| 40 | +import org.ossreviewtoolkit.utils.ort.createOrtTempFile |
| 41 | + |
| 42 | +/** |
| 43 | + * A package manager that uses OpenEmbedded's "bitbake" tool to create SPDX SBOMs [1][2] e.g. for Yocto distributions, |
| 44 | + * and post-processes these into ORT analyzer results. |
| 45 | + * |
| 46 | + * [1]: https://docs.yoctoproject.org/dev/dev-manual/sbom.html |
| 47 | + * [2]: https://dev.to/angrymane/create-spdx-with-yocto-2od9 |
| 48 | + */ |
| 49 | +class BitBake( |
| 50 | + name: String, |
| 51 | + analysisRoot: File, |
| 52 | + analyzerConfig: AnalyzerConfiguration, |
| 53 | + repoConfig: RepositoryConfiguration |
| 54 | +) : PackageManager(name, analysisRoot, analyzerConfig, repoConfig) { |
| 55 | + class Factory : AbstractPackageManagerFactory<BitBake>("BitBake") { |
| 56 | + override val globsForDefinitionFiles = listOf("*.bb") |
| 57 | + |
| 58 | + override fun create( |
| 59 | + analysisRoot: File, |
| 60 | + analyzerConfig: AnalyzerConfiguration, |
| 61 | + repoConfig: RepositoryConfiguration |
| 62 | + ) = BitBake(type, analysisRoot, analyzerConfig, repoConfig) |
| 63 | + } |
| 64 | + |
| 65 | + private val scriptFile by lazy { extractResourceToTempFile(BITBAKE_SCRIPT_NAME).apply { setExecutable(true) } } |
| 66 | + private val spdxConfFile by lazy { extractResourceToTempFile(SPDX_CONF_NAME) } |
| 67 | + |
| 68 | + private val spdxManager by lazy { SpdxDocumentFile(name, analysisRoot, analyzerConfig, repoConfig) } |
| 69 | + |
| 70 | + override fun resolveDependencies(definitionFiles: List<File>, labels: Map<String, String>): PackageManagerResult { |
| 71 | + val commonDefinitionDir = getCommonParentFile(definitionFiles) |
| 72 | + val workingDir = requireNotNull(commonDefinitionDir.searchUpwardsForFile(INIT_SCRIPT_NAME)) { |
| 73 | + "No '$INIT_SCRIPT_NAME' script file found for directory '$commonDefinitionDir'." |
| 74 | + } |
| 75 | + |
| 76 | + logger.info { "Determined the working directory to be '$workingDir'." } |
| 77 | + |
| 78 | + val localVersion = getBitBakeVersion(workingDir) |
| 79 | + val globalVersion = createOrtTempDir().let { dir -> |
| 80 | + getBitBakeVersion(dir).also { dir.safeDeleteRecursively(force = true) } |
| 81 | + } |
| 82 | + |
| 83 | + if (localVersion != globalVersion) { |
| 84 | + logger.warn { "Local $managerName version $localVersion differs from global version $globalVersion." } |
| 85 | + } |
| 86 | + |
| 87 | + val deployDirs = mutableSetOf<File>() |
| 88 | + |
| 89 | + definitionFiles.forEach { definitionFile -> |
| 90 | + val target = definitionFile.nameWithoutExtension.substringBeforeLast('_') |
| 91 | + |
| 92 | + val deployDir = getDeployDir(workingDir, target) |
| 93 | + deployDirs += deployDir |
| 94 | + |
| 95 | + val spdxFile = deployDir.findSpdxFiles().find { it.name == "recipe-$target.spdx.json" } |
| 96 | + if (spdxFile != null) { |
| 97 | + logger.info { "Not creating SPDX files for target '$target' as it already exists at '$spdxFile'." } |
| 98 | + } else { |
| 99 | + logger.info { "Creating SPDX files for target '$target'..." } |
| 100 | + |
| 101 | + // This implicitly triggers the build and can take a very long time. |
| 102 | + val duration = measureTime { createSpdx(workingDir, target) } |
| 103 | + |
| 104 | + logger.info { "Creating SPDX files for target '$target' took $duration." } |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + if (!scriptFile.delete()) logger.warn { "Unable to delete the temporary '$scriptFile' file." } |
| 109 | + if (!spdxConfFile.delete()) logger.warn { "Unable to delete the temporary '$spdxConfFile' file." } |
| 110 | + |
| 111 | + val commonDeployDir = deployDirs.singleOrNull() ?: getCommonParentFile(deployDirs) |
| 112 | + val spdxFiles = commonDeployDir.findSpdxFiles().toList() |
| 113 | + |
| 114 | + logger.info { "Found ${spdxFiles.size} SPDX file(s) in '$commonDeployDir'." } |
| 115 | + |
| 116 | + return spdxManager.resolveDependencies(spdxFiles, labels) |
| 117 | + } |
| 118 | + |
| 119 | + override fun resolveDependencies(definitionFile: File, labels: Map<String, String>): List<ProjectAnalyzerResult> = |
| 120 | + throw NotImplementedError("This function is not supported for $managerName.") |
| 121 | + |
| 122 | + private fun getDeployDir(workingDir: File, target: String): File { |
| 123 | + val bitbakeEnv = runBitBake(workingDir, "-e", target) |
| 124 | + return bitbakeEnv.stdout.lineSequence().mapNotNull { it.withoutPrefix("DEPLOY_DIR=") }.first() |
| 125 | + .let { File(it.removeSurrounding("\"")) } |
| 126 | + } |
| 127 | + |
| 128 | + private fun createSpdx(workingDir: File, target: String) = |
| 129 | + runBitBake(workingDir, "-r", spdxConfFile.absolutePath, "-c", "create_spdx", target) |
| 130 | + |
| 131 | + private fun File.findSpdxFiles() = resolve("spdx").walk().filter { it.isFile && it.name.endsWith(".spdx.json") } |
| 132 | + |
| 133 | + private fun runBitBake(workingDir: File, vararg args: String): ProcessCapture = |
| 134 | + ProcessCapture(scriptFile.absolutePath, workingDir.absolutePath, *args, workingDir = workingDir) |
| 135 | + .requireSuccess() |
| 136 | + |
| 137 | + internal fun getBitBakeVersion(workingDir: File): String = |
| 138 | + runBitBake(workingDir, "--version").stdout.lineSequence().first { |
| 139 | + it.startsWith("BitBake Build Tool") |
| 140 | + }.substringAfterLast(' ') |
| 141 | + |
| 142 | + private fun extractResourceToTempFile(resourceName: String): File { |
| 143 | + val prefix = resourceName.substringBefore('.') |
| 144 | + val suffix = resourceName.substringAfter(prefix) |
| 145 | + val scriptFile = createOrtTempFile(prefix, suffix) |
| 146 | + val script = checkNotNull(javaClass.getResource("/$resourceName")).readText() |
| 147 | + |
| 148 | + return scriptFile.apply { writeText(script) } |
| 149 | + } |
| 150 | +} |
| 151 | + |
| 152 | +private const val INIT_SCRIPT_NAME = "oe-init-build-env" |
| 153 | +private const val BITBAKE_SCRIPT_NAME = "bitbake.sh" |
| 154 | +private const val SPDX_CONF_NAME = "spdx.conf" |
| 155 | + |
| 156 | +private fun File.searchUpwardsForFile(searchFileName: String): File? { |
| 157 | + if (!isDirectory) return null |
| 158 | + |
| 159 | + var currentDir: File? = absoluteFile |
| 160 | + |
| 161 | + while (currentDir != null && !currentDir.resolve(searchFileName).isFile) { |
| 162 | + currentDir = currentDir.parentFile |
| 163 | + } |
| 164 | + |
| 165 | + return currentDir |
| 166 | +} |
0 commit comments