All Downloads are FREE. Search and download functionalities are using the official Maven repository.

toolkit.plugins.packagemanagers.gradle-inspector.43.0.1.source-code.GradleInspector.kt Maven / Gradle / Ivy

Go to download

Part of the OSS Review Toolkit (ORT), a suite to automate software compliance checks.

There is a newer version: 44.0.0
Show newest version
/*
 * Copyright (C) 2023 The ORT Project Authors (see )
 *
 * 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.gradleinspector

import OrtDependencyTreeModel

import java.io.ByteArrayOutputStream
import java.io.File
import java.util.Properties
import java.util.concurrent.TimeUnit

import org.apache.logging.log4j.kotlin.logger

import org.gradle.tooling.GradleConnector
import org.gradle.tooling.events.ProgressListener
import org.gradle.tooling.internal.consumer.DefaultGradleConnector
import org.gradle.tooling.model.build.BuildEnvironment

import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory
import org.ossreviewtoolkit.analyzer.PackageManager
import org.ossreviewtoolkit.analyzer.PackageManagerResult
import org.ossreviewtoolkit.downloader.VersionControlSystem
import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.Issue
import org.ossreviewtoolkit.model.Project
import org.ossreviewtoolkit.model.ProjectAnalyzerResult
import org.ossreviewtoolkit.model.Severity
import org.ossreviewtoolkit.model.VcsInfo
import org.ossreviewtoolkit.model.config.AnalyzerConfiguration
import org.ossreviewtoolkit.model.config.PackageManagerConfiguration
import org.ossreviewtoolkit.model.config.RepositoryConfiguration
import org.ossreviewtoolkit.model.createAndLogIssue
import org.ossreviewtoolkit.model.utils.DependencyGraphBuilder
import org.ossreviewtoolkit.utils.common.Os
import org.ossreviewtoolkit.utils.common.collectMessages
import org.ossreviewtoolkit.utils.common.safeMkdirs
import org.ossreviewtoolkit.utils.common.splitOnWhitespace
import org.ossreviewtoolkit.utils.common.unquote
import org.ossreviewtoolkit.utils.ort.JavaBootstrapper
import org.ossreviewtoolkit.utils.ort.ortToolsDirectory

import org.semver4j.Semver

/**
 * The names of Gradle (Groovy, Kotlin script) build files for a Gradle project.
 */
private val GRADLE_BUILD_FILES = listOf("build.gradle", "build.gradle.kts")

/**
 * The names of Gradle (Groovy, Kotlin script) settings files for a Gradle build.
 */
private val GRADLE_SETTINGS_FILES = listOf("settings.gradle", "settings.gradle.kts")

/**
 * The Gradle user home directory.
 */
private val GRADLE_USER_HOME = Os.env["GRADLE_USER_HOME"]?.let { File(it) } ?: Os.userHomeDirectory.resolve(".gradle")

/**
 * The name of the option to specify the Gradle version.
 */
const val OPTION_GRADLE_VERSION = "gradleVersion"

/**
 * The name of the option to specify the Java version to use.
 */
const val OPTION_JAVA_VERSION = "javaVersion"

/**
 * The name of the option to specify the Java home to use.
 */
const val OPTION_JAVA_HOME = "javaHome"

/**
 * The [Gradle](https://gradle.org/) package manager for Java. Also see the
 * [compatibility matrix](https://docs.gradle.org/current/userguide/compatibility.html).
 *
 * This package manager supports the following [options][PackageManagerConfiguration.options]:
 * - *gradleVersion*: The version of Gradle to use when analyzing projects. Defaults to the version defined in the
 *   Gradle wrapper properties.
 * - *javaVersion*: The version of Java to use when analyzing projects. By default, the same Java version as for ORT
 *   itself it used. Overrides `javaHome` if both are specified.
 * - *javaHome*: The directory of the Java home to use when analyzing projects. By default, the same Java home as for
 *   ORT itself is used.
 */
class GradleInspector(
    name: String,
    analysisRoot: File,
    analyzerConfig: AnalyzerConfiguration,
    repoConfig: RepositoryConfiguration
) : PackageManager(name, "Gradle", analysisRoot, analyzerConfig, repoConfig) {
    class Factory : AbstractPackageManagerFactory("GradleInspector") {
        // Gradle prefers Groovy ".gradle" files over Kotlin ".gradle.kts" files, but "build" files have to come before
        // "settings" files as we should consider "settings" files only if the same directory does not also contain a
        // "build" file.
        override val globsForDefinitionFiles = GRADLE_BUILD_FILES + GRADLE_SETTINGS_FILES

        override fun create(
            analysisRoot: File,
            analyzerConfig: AnalyzerConfiguration,
            repoConfig: RepositoryConfiguration
        ) = GradleInspector(type, analysisRoot, analyzerConfig, repoConfig)
    }

    private val graphBuilder = DependencyGraphBuilder(GradleDependencyHandler(managerName))
    private val initScriptFile by lazy { extractInitScript() }

    private fun extractInitScript(): File {
        fun extractResource(name: String, target: File) =
            target.apply {
                val resource = checkNotNull(GradleInspector::class.java.getResource(name)) {
                    "Resource '$name' not found."
                }

                logger.debug { "Extracting resource '${resource.path.substringAfterLast('/')}' to '$target'..." }

                resource.openStream().use { inputStream ->
                    outputStream().use { outputStream ->
                        inputStream.copyTo(outputStream)
                    }
                }
            }

        val toolsDir = ortToolsDirectory.resolve(managerName).apply { safeMkdirs() }
        val pluginJar = extractResource("/gradle-plugin.jar", toolsDir.resolve("gradle-plugin.jar"))

        val initScriptText = javaClass.getResource("/template.init.gradle").readText()
            .replace("", pluginJar.invariantSeparatorsPath)

        val initScript = toolsDir.resolve("init.gradle")

        logger.debug { "Extracting Gradle init script to '$initScript'..." }

        return initScript.apply { writeText(initScriptText) }
    }

    private fun GradleConnector.getOrtDependencyTreeModel(
        projectDir: File,
        issues: MutableList
    ): OrtDependencyTreeModel =
        forProjectDirectory(projectDir).connect().use { connection ->
            val stdout = ByteArrayOutputStream()
            val stderr = ByteArrayOutputStream()

            val gradleProperties = readGradleProperties(GRADLE_USER_HOME) + readGradleProperties(projectDir)
            val jvmArgs = gradleProperties["org.gradle.jvmargs"].orEmpty()
                .replace("MaxPermSize", "MaxMetaspaceSize") // Replace a deprecated JVM argument.
                .splitOnWhitespace()
                .map { it.unquote() }

            val environment = connection.model(BuildEnvironment::class.java).get()
            val buildGradleVersion = Semver.coerce(environment.gradle.gradleVersion)

            logger.info { "The project at '$projectDir' uses Gradle version $buildGradleVersion." }

            // In order to debug the plugin, pass the "-Dorg.gradle.debug=true" option to the JVM running ORT. This will
            // then block execution of the plugin until a remote debug session is attached to port 5005 (by default),
            // also see https://docs.gradle.org/current/userguide/troubleshooting.html#sec:troubleshooting_build_logic.
            val model = connection.model(OrtDependencyTreeModel::class.java)
                .apply {
                    // Work around https://github.com/gradle/gradle/issues/28464.
                    if (logger.delegate.isDebugEnabled && buildGradleVersion?.isEqualTo("8.5.0") != true) {
                        addProgressListener(ProgressListener { logger.debug(it.displayName) })
                    }

                    val javaHome = options[OPTION_JAVA_VERSION]
                        ?.takeUnless { JavaBootstrapper.isRunningOnJdk(it) }
                        ?.let {
                            JavaBootstrapper.installJdk("TEMURIN", it)
                                .onFailure { e -> issues += createAndLogIssue(managerName, e.collectMessages()) }
                                .getOrNull()
                        } ?: options[OPTION_JAVA_HOME]?.let { File(it) }

                    javaHome?.also {
                        logger.info { "Setting Java home for project analysis to '$it'." }
                        setJavaHome(it)
                    }
                }
                .setJvmArguments(jvmArgs)
                .setStandardOutput(stdout)
                .setStandardError(stderr)
                .withArguments("-Duser.home=${Os.userHomeDirectory}", "--init-script", initScriptFile.path)
                .get()

            if (stdout.size() > 0) {
                logger.debug {
                    "Analyzing the project in '$projectDir' produced the following standard output:\n" +
                        stdout.toString().prependIndent("\t")
                }
            }

            if (stderr.size() > 0) {
                logger.debug {
                    "Analyzing the project in '$projectDir' produced the following error output:\n" +
                        stderr.toString().prependIndent("\t")
                }
            }

            model
        }

    override fun resolveDependencies(definitionFile: File, labels: Map): List {
        val projectDir = definitionFile.parentFile

        val gradleConnector = GradleConnector.newConnector()

        val gradleVersion = options[OPTION_GRADLE_VERSION]
        if (gradleVersion != null) {
            gradleConnector.useGradleVersion(gradleVersion)
        }

        if (gradleConnector is DefaultGradleConnector) {
            // Note that the Gradle Tooling API always uses the Gradle daemon, see
            // https://docs.gradle.org/current/userguide/third_party_integration.html#sec:embedding_daemon.
            gradleConnector.daemonMaxIdleTime(1, TimeUnit.SECONDS)
        }

        val issues = mutableListOf()
        val dependencyTreeModel = gradleConnector.getOrtDependencyTreeModel(projectDir, issues)

        dependencyTreeModel.errors.distinct().mapTo(issues) {
            createAndLogIssue(source = managerName, message = it, severity = Severity.ERROR)
        }

        dependencyTreeModel.warnings.distinct().mapTo(issues) {
            createAndLogIssue(source = managerName, message = it, severity = Severity.WARNING)
        }

        val projectId = Identifier(
            type = "Gradle",
            namespace = dependencyTreeModel.group,
            name = dependencyTreeModel.name,
            version = dependencyTreeModel.version
        )

        dependencyTreeModel.configurations.filterNot {
            excludes.isScopeExcluded(it.name)
        }.forEach { configuration ->
            graphBuilder.addDependencies(projectId, configuration.name, configuration.dependencies)
        }

        val project = Project(
            id = projectId,
            definitionFilePath = VersionControlSystem.getPathInfo(definitionFile).path,
            authors = emptySet(),
            declaredLicenses = emptySet(),
            vcs = VcsInfo.EMPTY,
            vcsProcessed = processProjectVcs(definitionFile.parentFile),
            homepageUrl = "",
            scopeNames = graphBuilder.scopesFor(projectId)
        )

        val result = ProjectAnalyzerResult(project, emptySet(), issues)
        return listOf(result)
    }

    override fun createPackageManagerResult(projectResults: Map>) =
        PackageManagerResult(projectResults, graphBuilder.build(), graphBuilder.packages())
}

/**
 * Read the `gradle.properties` file in [projectDir] or in any of its parent directories, and return the contained
 * properties as a map.
 */
private fun readGradleProperties(projectDir: File): Map {
    val gradleProperties = mutableListOf>()

    var currentDir: File? = projectDir
    do {
        val propertiesFile = currentDir?.resolve("gradle.properties")

        if (propertiesFile?.isFile == true) {
            propertiesFile.inputStream().use {
                val properties = Properties().apply { load(it) }

                properties.mapNotNullTo(gradleProperties) { (key, value) ->
                    ((key as String) to (value as String)).takeUnless { key.startsWith("systemProp.") }
                }
            }

            break
        }

        currentDir = currentDir?.parentFile
    } while (currentDir != null)

    return gradleProperties.toMap()
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy