diff --git a/analyzer/src/main/kotlin/PackageManager.kt b/analyzer/src/main/kotlin/PackageManager.kt index 4aa8f44184203..4a6999fe905c1 100644 --- a/analyzer/src/main/kotlin/PackageManager.kt +++ b/analyzer/src/main/kotlin/PackageManager.kt @@ -104,7 +104,7 @@ abstract class PackageManager( */ fun findManagedFiles( directory: File, - packageManagers: Collection = ALL.values, + packageManagers: Collection = ENABLED_BY_DEFAULT, excludes: Excludes = Excludes.EMPTY ): ManagedProjectFiles { require(directory.isDirectory) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f1821ae8bd31a..ca8d997c7805a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -131,6 +131,8 @@ log4jApiToSlf4j = { module = "org.apache.logging.log4j:log4j-to-slf4j", version. logbackClassic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } mavenCompat = { module = "org.apache.maven:maven-compat", version.ref = "maven" } mavenCore = { module = "org.apache.maven:maven-core", version.ref = "maven" } +mavenModel = { module = "org.apache.maven:maven-model", version.ref = "maven" } +mavenModelBuilder = { module = "org.apache.maven:maven-model-builder", version.ref = "maven" } mavenResolverApi = { module = "org.apache.maven.resolver:maven-resolver-api", version.ref = "mavenResolver" } mavenResolverConnectorBasic = { module = "org.apache.maven.resolver:maven-resolver-connector-basic", version.ref = "mavenResolver" } mavenResolverTransportFile = { module = "org.apache.maven.resolver:maven-resolver-transport-file", version.ref = "mavenResolver" } diff --git a/plugins/package-managers/build.gradle.kts b/plugins/package-managers/build.gradle.kts index be2e194a9115d..7baf17250a679 100644 --- a/plugins/package-managers/build.gradle.kts +++ b/plugins/package-managers/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { api(project(":plugins:package-managers:cocoapods-package-manager")) api(project(":plugins:package-managers:composer-package-manager")) api(project(":plugins:package-managers:conan-package-manager")) + api(project(":plugins:package-managers:gradle-inspector")) api(project(":plugins:package-managers:gradle-model")) api(project(":plugins:package-managers:gradle-package-manager")) api(project(":plugins:package-managers:pub-package-manager")) diff --git a/plugins/package-managers/gradle-inspector/README.md b/plugins/package-managers/gradle-inspector/README.md new file mode 100644 index 0000000000000..2e0367eb7bac6 --- /dev/null +++ b/plugins/package-managers/gradle-inspector/README.md @@ -0,0 +1,57 @@ +# GradleInspector + +The [GradleInspector] is an alternative analyzer for projects that use the Gradle package manager. It is supposed to +address [several][] [shortcomings][] of the "legacy" [Gradle] analyzer, but in order to not interfere with it, the +[GradleInspector] is disabled by default. + +## Usage + +As the [GradleInspector] is disabled by default, it needs to be enabled explicitly (along with any other package +managers that should be enabled): + + ort analyze -P ort.analyzer.enabledPackageManagers=GradleInspector[,...] + +It is recommended to *not* also enable the "legacy" [Gradle] analyzer at the same time, as both analyzers would find the +same definition files. + +## Implementation + +In contrast to the "legacy" [Gradle] analyzer which is fully implemented as an [initialization script], the +[GradleInspector] only uses a minimal [init.gradle] to apply a [Gradle plugin], which in turn does nothing else than +registering the [OrtModelBuilder] for the ORT-specific [data model for Gradle projects]. The [GradleInspector] then +injects both the [init.gradle] and a fat-JAR for the [Gradle plugin] into the project to analyze. + +## Limitations + +The retrieval of the checksum values for remote artifacts is currently done via plain OkHttp calls, which means it will +not work out of the box for private repositories. To worka round this, crednetials need to be configured in `.netrc` +additionally to in Gradle. This is similar to how the "legacy" [Gradle] analyzer required to additionally configure +credentials in Maven. + +Also, the `isModified` check which compares with artifacts of the same name in Maven Central is not implemented yet. + +## Building + +Due to some optimizations IntelliJ IDEA performs when building Gradle projects, it might be that bundling the fat-JAR +for the [Gradle plugin] as a resource into the [GradleInspector] does not always work reliably. In that case ensure that +IntelliJ IDEA has "Gradle" configured for the "Build and run using" and "Run tests using" settings, and / or try to +run the `:plugins:package-managers:gradle-plugin:fatJar` and `:plugins:package-managers:gradle-inspector:jar` tasks +manually once. + +## Debugging + +Due to this setup of the [GradleInspector], the [OrtModelBuilder] can actually be debugged. To do so, create a run +configuration in IntelliJ IDEA that runs `ort analyze` and sets the *VM options* to `-Dorg.gradle.debug=true`. Also, +create another *Remote JVm Debug* run configuration with default settings. Now, when debugging the first run +configuration, it will block execution of the Gradle plugin until the remote debugger is attached by debugging the +second run configuration, and any breakpoints in the [OrtModelBuilder] will be hit. + +[GradleInspector]: ./src/main/kotlin/GradleInspector.kt +[several]: https://github.com/oss-review-toolkit/ort/issues/4694 +[shortcomings]: https://github.com/oss-review-toolkit/ort/issues/5782 +[Gradle]: ../gradle/src/main/kotlin/Gradle.kt +[initialization script]: https://docs.gradle.org/current/userguide/init_scripts.html +[init.gradle]: ./src/main/resources/init.gradle.template +[Gradle plugin]: ../gradle-plugin/src/main/kotlin/OrtModelPlugin.kt +[OrtModelBuilder]: ../gradle-plugin/src/main/kotlin/OrtModelBuilder.kt +[data model for Gradle projects]: ../gradle-model/src/main/kotlin/GradleModel.kt diff --git a/plugins/package-managers/gradle-inspector/build.gradle.kts b/plugins/package-managers/gradle-inspector/build.gradle.kts new file mode 100644 index 0000000000000..1aa4efe1e0ace --- /dev/null +++ b/plugins/package-managers/gradle-inspector/build.gradle.kts @@ -0,0 +1,79 @@ +/* + * 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 + */ + +import org.jetbrains.gradle.ext.settings +import org.jetbrains.gradle.ext.taskTriggers + +@Suppress("DSL_SCOPE_VIOLATION") // See https://youtrack.jetbrains.com/issue/KTIJ-19369. +plugins { + // Apply core plugins. + `java-library` + + // Apply third-party plugins. + alias(libs.plugins.ideaExt) +} + +repositories { + exclusiveContent { + forRepository { + maven("https://repo.gradle.org/gradle/libs-releases/") + } + + filter { + includeGroup("org.gradle") + } + } +} + +dependencies { + api(project(":analyzer")) + api(project(":model")) + + implementation(project(":downloader")) + implementation(project(":plugins:package-managers:gradle-model")) + implementation(project(":utils:common-utils")) + implementation(project(":utils:ort-utils")) + implementation(project(":utils:spdx-utils")) + + // Use the latest version that is not affected by https://github.com/gradle/gradle/issues/23208. + implementation("org.gradle:gradle-tooling-api:7.4.2") + + implementation(libs.log4jApi) + implementation(libs.log4jApiKotlin) + + funTestImplementation(testFixtures(project(":analyzer"))) +} + +val processResources = tasks.named("processResources").configure { + val gradlePluginProject = project(":plugins:package-managers:gradle-plugin") + val gradlePluginJarTask = gradlePluginProject.tasks.named("fatJar") + val gradlePluginJarFile = gradlePluginJarTask.get().outputs.files.singleFile + + // As the Copy-task simply skips non-existing files, add explicit dependencies on the Jar-tasks. + dependsOn(gradlePluginJarTask) + + // Bundle the plugin JAR as a resource so the inspector can copy it at runtime to the init script's classpath. + from(gradlePluginJarFile) + + // Ensure a constant file name without a version suffix. + rename(gradlePluginJarFile.name, "gradle-plugin.jar") +} + +// Work around https://youtrack.jetbrains.com/issue/IDEA-173367. +rootProject.idea.project.settings.taskTriggers.beforeBuild(processResources) diff --git a/plugins/package-managers/gradle-inspector/src/main/kotlin/GradleInspector.kt b/plugins/package-managers/gradle-inspector/src/main/kotlin/GradleInspector.kt new file mode 100644 index 0000000000000..f3d9423d848db --- /dev/null +++ b/plugins/package-managers/gradle-inspector/src/main/kotlin/GradleInspector.kt @@ -0,0 +1,428 @@ +/* + * 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 OrtDependency +import OrtDependencyTreeModel + +import java.io.ByteArrayOutputStream +import java.io.File +import java.util.SortedSet +import java.util.concurrent.TimeUnit + +import org.apache.logging.log4j.kotlin.Logging + +import org.gradle.tooling.GradleConnector +import org.gradle.tooling.events.ProgressListener +import org.gradle.tooling.internal.consumer.DefaultGradleConnector + +import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory +import org.ossreviewtoolkit.analyzer.PackageManager +import org.ossreviewtoolkit.downloader.VcsHost +import org.ossreviewtoolkit.downloader.VersionControlSystem +import org.ossreviewtoolkit.model.Hash +import org.ossreviewtoolkit.model.Identifier +import org.ossreviewtoolkit.model.Issue +import org.ossreviewtoolkit.model.Package +import org.ossreviewtoolkit.model.PackageLinkage +import org.ossreviewtoolkit.model.PackageReference +import org.ossreviewtoolkit.model.Project +import org.ossreviewtoolkit.model.ProjectAnalyzerResult +import org.ossreviewtoolkit.model.RemoteArtifact +import org.ossreviewtoolkit.model.Scope +import org.ossreviewtoolkit.model.Severity +import org.ossreviewtoolkit.model.VcsInfo +import org.ossreviewtoolkit.model.VcsType +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.parseRepoManifestPath +import org.ossreviewtoolkit.utils.common.splitOnWhitespace +import org.ossreviewtoolkit.utils.common.withoutPrefix +import org.ossreviewtoolkit.utils.ort.DeclaredLicenseProcessor +import org.ossreviewtoolkit.utils.ort.OkHttpClientHelper +import org.ossreviewtoolkit.utils.ort.createOrtTempFile +import org.ossreviewtoolkit.utils.spdx.SpdxOperator + +/** + * 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 name of the option to specify the Gradle version. + */ +const val OPTION_GRADLE_VERSION = "gradleVersion" + +/** + * The [Gradle](https://gradle.org/) package manager for Java. + * + * 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. + */ +class GradleInspector( + name: String, + analysisRoot: File, + analyzerConfig: AnalyzerConfiguration, + repoConfig: RepositoryConfiguration +) : PackageManager(name, analysisRoot, analyzerConfig, repoConfig) { + internal companion object : Logging + + class Factory : AbstractPackageManagerFactory("GradleInspector", isEnabledByDefault = false) { + // 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 fun extractInitScript(): File { + fun extractResource(name: String, target: File): File { + target.outputStream().use { outputStream -> + val resource = checkNotNull(javaClass.getResource(name)) { + "Resource '$name' not found." + } + + logger.debug { "Extracting resource '${resource.path.substringAfterLast('/')}' to '$target'..." } + + resource.openStream().use { inputStream -> + inputStream.copyTo(outputStream) + } + } + + return target + } + + val pluginJar = extractResource("/gradle-plugin.jar", createOrtTempFile(prefix = "plugin", suffix = ".jar")) + + val initScriptText = javaClass.getResource("/init.gradle.template").readText() + .replace("", pluginJar.invariantSeparatorsPath) + + val initScript = createOrtTempFile("init", ".gradle") + + logger.debug { "Extracting Gradle init script to '$initScript'..." } + + return initScript.apply { writeText(initScriptText) } + } + + private fun GradleConnector.getOrtDependencyTreeModel(projectDir: File): OrtDependencyTreeModel = + forProjectDirectory(projectDir).connect().use { connection -> + val initScriptFile = extractInitScript() + + val stdout = ByteArrayOutputStream() + val stderr = ByteArrayOutputStream() + + // 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) + .addProgressListener(ProgressListener { logger.debug { it.displayName } }) + .setStandardOutput(stdout) + .setStandardError(stderr) + .withArguments("--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.warn { + "Analyzing the project in '$projectDir' produced the following error output:\n" + + stderr.toString().prependIndent("\t") + } + } + + if (!initScriptFile.delete()) { + logger.warn { "Init script file '$initScriptFile' could not be deleted." } + } + + 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 dependencyTreeModel = gradleConnector.getOrtDependencyTreeModel(projectDir) + + val issues = mutableListOf() + + 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 = managerName, + namespace = dependencyTreeModel.group, + name = dependencyTreeModel.name, + version = dependencyTreeModel.version + ) + + val packageDependencies = mutableSetOf() + + val scopes = dependencyTreeModel.configurations.filterNot { + excludes.isScopeExcluded(it.name) + }.mapTo(sortedSetOf()) { + Scope(name = it.name, dependencies = it.dependencies.toPackageRefs(packageDependencies)) + } + + val project = Project( + id = projectId, + definitionFilePath = VersionControlSystem.getPathInfo(definitionFile).path, + authors = emptySet(), + declaredLicenses = emptySet(), + vcs = VcsInfo.EMPTY, + vcsProcessed = processProjectVcs(definitionFile.parentFile), + homepageUrl = "", + scopeDependencies = scopes + ) + + val packages = packageDependencies.associateBy { + // Deduplicate OrtDependency serialization proxy objects by Identifier. + Identifier("Maven", it.groupId, it.artifactId, it.version) + }.mapNotNullTo(mutableSetOf()) { (id, dep) -> + val model = dep.mavenModel ?: run { + issues += createAndLogIssue( + source = "Gradle", + message = "No Maven model available for '${id.toCoordinates()}'." + ) + + return@mapNotNullTo Package.EMPTY.copy(id = id) + } + + val vcs = dep.toVcsInfo() + val vcsFallbackUrls = listOfNotNull(model.vcs?.browsableUrl, model.homepageUrl).toTypedArray() + val vcsProcessed = processPackageVcs(vcs, *vcsFallbackUrls) + + Package( + id = id, + authors = model.authors, + declaredLicenses = model.licenses, + declaredLicensesProcessed = DeclaredLicenseProcessor.process( + model.licenses, + // See http://maven.apache.org/ref/3.6.3/maven-model/maven.html#project saying: "If multiple + // licenses are listed, it is assumed that the user can select any of them, not that they must + // accept all." + operator = SpdxOperator.OR + ), + description = model.description.orEmpty(), + homepageUrl = model.homepageUrl.orEmpty(), + binaryArtifact = createRemoteArtifact(dep.pomFile, dep.classifier, dep.extension), + sourceArtifact = createRemoteArtifact(dep.pomFile, "sources", "jar"), + vcs = vcs, + vcsProcessed = vcsProcessed + ) + } + + val result = ProjectAnalyzerResult(project, packages, issues) + return listOf(result) + } +} + +/** + * Recursively convert a collection of [OrtDependency] objects to a set of [PackageReference] objects for use in [Scope] + * while flattening all dependencies into the [packageDependencies] collection. + */ +private fun Collection.toPackageRefs( + packageDependencies: MutableCollection +): SortedSet = + mapTo(sortedSetOf()) { dep -> + val (id, linkage) = if (dep.localPath != null) { + val id = Identifier("Gradle", dep.groupId, dep.artifactId, dep.version) + id to PackageLinkage.PROJECT_DYNAMIC + } else { + packageDependencies += dep + + val id = Identifier("Maven", dep.groupId, dep.artifactId, dep.version) + id to PackageLinkage.DYNAMIC + } + + PackageReference(id, linkage, dep.dependencies.toPackageRefs(packageDependencies)) + } + +/** + * Create a [RemoteArtifact] based on the given [pomUrl], [classifier], [extension] and hash [algorithm]. The hash value + * is retrieved remotely. + */ +private fun createRemoteArtifact( + pomUrl: String?, classifier: String = "", extension: String = "jar", algorithm: String = "sha1" +): RemoteArtifact { + val artifactBaseUrl = pomUrl?.removeSuffix(".pom") ?: return RemoteArtifact.EMPTY + + val artifactUrl = buildString { + append(artifactBaseUrl) + if (classifier.isNotEmpty()) append("-$classifier") + if (extension.isNotEmpty()) append(".$extension") + } + + // TODO: How to handle authentication for private repositories here, or rely on Gradle for the download? + val checksum = OkHttpClientHelper.downloadText("$artifactUrl.$algorithm") + .getOrElse { return RemoteArtifact.EMPTY } + + return RemoteArtifact(artifactUrl, parseChecksum(checksum, algorithm)) +} + +/** + * Split the provided [checksum] by whitespace and return a [Hash] for the first element that matches the provided + * algorithm. If no element matches, return [Hash.NONE]. This works around the issue that Maven checksum files sometimes + * contain arbitrary strings before or after the actual checksum. + */ +private fun parseChecksum(checksum: String, algorithm: String) = + checksum.splitOnWhitespace().firstNotNullOfOrNull { + runCatching { Hash.create(it, algorithm) }.getOrNull() + } ?: Hash.NONE + +// See http://maven.apache.org/pom.html#SCM. +private val SCM_REGEX = Regex("scm:(?[^:@]+):(?.+)") +private val USER_HOST_REGEX = Regex("scm:(?[^:@]+)@(?[^:]+)[:/](?.+)") + +private fun OrtDependency.toVcsInfo() = + mavenModel?.vcs?.run { + SCM_REGEX.matchEntire(connection)?.let { match -> + val type = match.groups["type"]!!.value + val url = match.groups["url"]!!.value + + handleValidScmInfo(type, url, tag) + } ?: handleInvalidScmInfo(connection, tag) + } ?: VcsInfo.EMPTY + +private fun OrtDependency.handleValidScmInfo(type: String, url: String, tag: String) = + when { + // CVS URLs usually start with ":pserver:" or ":ext:", but as ":" is also the delimiter used by the Maven SCM + // plugin, no double ":" is used in the connection string, and we need to fix it up here. + type == "cvs" && !url.startsWith(":") -> { + VcsInfo(type = VcsType.CVS, url = ":$url", revision = tag) + } + + // Maven does not officially support git-repo as an SCM, see http://maven.apache.org/scm/scms-overview.html, so + // come up with the convention to use the "manifest" query parameter for the path to the manifest inside the + // repository. An earlier version of this workaround expected the query string to be only the path to the + // manifest, for backward compatibility convert such URLs to the new syntax. + type == "git-repo" -> { + val manifestPath = url.parseRepoManifestPath() + ?: url.substringAfter('?').takeIf { it.isNotBlank() && it.endsWith(".xml") } + val urlWithManifest = url.takeIf { manifestPath == null } + ?: "${url.substringBefore('?')}?manifest=$manifestPath" + + VcsInfo( + type = VcsType.GIT_REPO, + url = urlWithManifest, + revision = tag + ) + } + + type == "svn" -> { + val revision = tag.takeIf { it.isEmpty() } ?: "tags/$tag" + VcsInfo(type = VcsType.SUBVERSION, url = url, revision = revision) + } + + url.startsWith("//") -> { + // Work around the common mistake to omit the Maven SCM provider. + val fixedUrl = "$type:$url" + + // Try to detect the Maven SCM provider from the URL only, e.g. by looking at the host or special URL paths. + VcsHost.parseUrl(fixedUrl).copy(revision = tag).also { + GradleInspector.logger.info { + "Fixed up invalid SCM connection without a provider in '$groupId:$artifactId:$version' to $it." + } + } + } + + else -> { + val trimmedUrl = if (!url.startsWith("git://")) url.removePrefix("git:") else url + + VcsHost.fromUrl(trimmedUrl)?.let { host -> + host.toVcsInfo(trimmedUrl)?.let { vcsInfo -> + // Fixup paths that are specified as part of the URL and contain the project name as a prefix. + val projectPrefix = "${host.getProject(trimmedUrl)}-" + vcsInfo.path.withoutPrefix(projectPrefix)?.let { path -> + vcsInfo.copy(path = path) + } + } + } ?: VcsInfo(type = VcsType.forName(type), url = trimmedUrl, revision = tag) + } + } + +private fun OrtDependency.handleInvalidScmInfo(connection: String, tag: String) = + USER_HOST_REGEX.matchEntire(connection)?.let { match -> + // Some projects omit the provider and use the SCP-like Git URL syntax, for example + // "scm:git@github.com:facebook/facebook-android-sdk.git". + val user = match.groups["user"]!!.value + val host = match.groups["host"]!!.value + val path = match.groups["path"]!!.value + + if (user == "git" || host.startsWith("git")) { + VcsInfo(type = VcsType.GIT, url = "https://$host/$path", revision = tag) + } else { + VcsInfo.EMPTY + } + } ?: run { + val dep = "$groupId:$artifactId:$version" + + if (connection.startsWith("git://") || connection.endsWith(".git")) { + // It is a common mistake to omit the "scm:[provider]:" prefix. Add fall-backs for nevertheless clear + // cases. + GradleInspector.logger.info { + "Maven SCM connection '$connection' in '$dep' lacks the required 'scm' prefix." + } + + VcsInfo(type = VcsType.GIT, url = connection, revision = tag) + } else { + if (connection.isNotEmpty()) { + GradleInspector.logger.info { + "Ignoring Maven SCM connection '$connection' in '$dep' due to an unexpected format." + } + } + + VcsInfo.EMPTY + } + } diff --git a/plugins/package-managers/gradle-inspector/src/main/resources/META-INF/services/org.ossreviewtoolkit.analyzer.PackageManagerFactory b/plugins/package-managers/gradle-inspector/src/main/resources/META-INF/services/org.ossreviewtoolkit.analyzer.PackageManagerFactory new file mode 100644 index 0000000000000..61ff08d7a6fbd --- /dev/null +++ b/plugins/package-managers/gradle-inspector/src/main/resources/META-INF/services/org.ossreviewtoolkit.analyzer.PackageManagerFactory @@ -0,0 +1 @@ +org.ossreviewtoolkit.plugins.packagemanagers.gradleinspector.GradleInspector$Factory diff --git a/plugins/package-managers/gradle-inspector/src/main/resources/init.gradle.template b/plugins/package-managers/gradle-inspector/src/main/resources/init.gradle.template new file mode 100644 index 0000000000000..a979f7a5854f6 --- /dev/null +++ b/plugins/package-managers/gradle-inspector/src/main/resources/init.gradle.template @@ -0,0 +1,30 @@ +/* + * 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 + */ + +import org.ossreviewtoolkit.plugins.packagemanagers.gradleplugin.OrtModelPlugin + +initscript { + dependencies { + classpath files("") + } +} + +allprojects { + apply plugin: OrtModelPlugin +} diff --git a/plugins/package-managers/gradle-model/src/main/kotlin/GradleModel.kt b/plugins/package-managers/gradle-model/src/main/kotlin/GradleModel.kt index eb1c0e6687f71..0cd71b033af87 100644 --- a/plugins/package-managers/gradle-model/src/main/kotlin/GradleModel.kt +++ b/plugins/package-managers/gradle-model/src/main/kotlin/GradleModel.kt @@ -47,5 +47,20 @@ interface OrtDependency { val error: String? val warning: String? val pomFile: String? + val mavenModel: OrtMavenModel? val localPath: String? } + +interface OrtMavenModel { + val licenses: Set + val authors: Set + val description: String? + val homepageUrl: String? + val vcs: OrtVcsModel? +} + +interface OrtVcsModel { + val connection: String + val tag: String + val browsableUrl: String +} diff --git a/plugins/package-managers/gradle-plugin/build.gradle.kts b/plugins/package-managers/gradle-plugin/build.gradle.kts new file mode 100644 index 0000000000000..865dab4712536 --- /dev/null +++ b/plugins/package-managers/gradle-plugin/build.gradle.kts @@ -0,0 +1,60 @@ +/* + * 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 + */ + +plugins { + `java-gradle-plugin` +} + +gradlePlugin { + plugins.register("OrtModelPlugin") { + id = "org.ossreviewtoolkit.plugins.packagemanagers.gradle.plugin" + implementationClass = "org.ossreviewtoolkit.plugins.packagemanagers.gradleplugin.OrtModelPlugin" + } +} + +dependencies { + api(project(":plugins:package-managers:gradle-model")) + + api(libs.mavenModel) + api(libs.mavenModelBuilder) +} + +tasks.register("fatJar", Jar::class) { + description = "Creates a fat JAR that includes all required runtime dependencies." + group = "Build" + + archiveBaseName.set("${project.name}-fat") + + manifest.from(tasks.jar.get().manifest) + + val classpath = configurations.runtimeClasspath.get().filter { + // Only bundle JARs that are not specific to the Gradle version. + it.isFile && it.extension == "jar" && !("gradle" in it.path && gradle.gradleVersion in it.path) + }.map { + zipTree(it) + } + + from(classpath) { + exclude("META-INF/*.DSA") + exclude("META-INF/*.RSA") + exclude("META-INF/*.SF") + } + + with(tasks.jar.get()) +} diff --git a/plugins/package-managers/gradle-plugin/src/main/kotlin/FileModelBuilder.kt b/plugins/package-managers/gradle-plugin/src/main/kotlin/FileModelBuilder.kt new file mode 100644 index 0000000000000..02bd0a2a78227 --- /dev/null +++ b/plugins/package-managers/gradle-plugin/src/main/kotlin/FileModelBuilder.kt @@ -0,0 +1,105 @@ +/* + * 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.gradleplugin + +import java.io.File +import java.util.concurrent.ConcurrentHashMap + +import org.apache.maven.model.Dependency +import org.apache.maven.model.Parent +import org.apache.maven.model.Repository +import org.apache.maven.model.building.DefaultModelBuilderFactory +import org.apache.maven.model.building.DefaultModelBuildingRequest +import org.apache.maven.model.building.FileModelSource +import org.apache.maven.model.building.ModelBuildingException +import org.apache.maven.model.building.ModelBuildingRequest +import org.apache.maven.model.building.ModelBuildingResult +import org.apache.maven.model.building.ModelCache +import org.apache.maven.model.building.ModelSource2 +import org.apache.maven.model.resolution.ModelResolver + +typealias ModelSourceResolver = (groupId: String, artifactId: String, version: String) -> ModelSource2 + +/** + * A Maven model builder for POM files. + */ +class FileModelBuilder(resolve: ModelSourceResolver) { + private val simpleModelCache = SimpleModelCache() + private val simpleModelResolver = SimpleModelResolver(resolve) + + /** + * Build the Maven model for the given [pomFile] and return it. Throws [ModelBuildingException] on error. + */ + fun buildModel(pomFile: File): ModelBuildingResult { + val request = DefaultModelBuildingRequest().apply { + isProcessPlugins = false + isTwoPhaseBuilding = true // Required for dependency management handling. + modelCache = simpleModelCache + modelResolver = simpleModelResolver + modelSource = FileModelSource(pomFile) + validationLevel = ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL + } + + return DefaultModelBuilderFactory().newInstance().build(request) + } +} + +/** + * A simple in-memory cache for built Maven models. + */ +private class SimpleModelCache : ModelCache { + private data class CacheKey( + val groupId: String, + val artifactId: String, + val version: String, + val tag: String + ) + + private val cache = ConcurrentHashMap() + + override fun put(groupId: String, artifactId: String, version: String, tag: String, data: Any) { + val key = CacheKey(groupId, artifactId, version, tag) + cache[key] = data + } + + override fun get(groupId: String, artifactId: String, version: String, tag: String): Any? { + val key = CacheKey(groupId, artifactId, version, tag) + return cache[key] + } +} + +/** + * A simple Maven model resolver that does not support repositories. + */ +private class SimpleModelResolver(private val resolve: ModelSourceResolver) : ModelResolver { + override fun resolveModel(groupId: String, artifactId: String, version: String): ModelSource2 = + resolve(groupId, artifactId, version) + + override fun resolveModel(parent: Parent): ModelSource2 = + resolve(parent.groupId, parent.artifactId, parent.version) + + override fun resolveModel(dependency: Dependency): ModelSource2 = + resolve(dependency.groupId, dependency.artifactId, dependency.version) + + override fun addRepository(repository: Repository) = Unit + override fun addRepository(repository: Repository, replace: Boolean) = Unit + + override fun newCopy(): ModelResolver = this +} diff --git a/plugins/package-managers/gradle-plugin/src/main/kotlin/MavenModelExtensions.kt b/plugins/package-managers/gradle-plugin/src/main/kotlin/MavenModelExtensions.kt new file mode 100644 index 0000000000000..3060233659fe6 --- /dev/null +++ b/plugins/package-managers/gradle-plugin/src/main/kotlin/MavenModelExtensions.kt @@ -0,0 +1,78 @@ +/* + * 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.gradleplugin + +import OrtVcsModel + +import org.apache.maven.model.Model +import org.apache.maven.model.Scm +import org.apache.maven.model.building.ModelBuildingResult + +fun Model.collectAuthors(): Set = + buildSet { + organization?.let { + if (!it.name.isNullOrEmpty()) add(it.name) + } + + val developers = developers.mapNotNull { it.organization.orEmpty().ifEmpty { it.name } } + addAll(developers) + } + +fun Model.collectLicenses(): Set = + licenses.mapNotNullTo(mutableSetOf()) { license -> + listOfNotNull(license.name, license.url, license.comments).firstOrNull { it.isNotBlank() } + } + +fun ModelBuildingResult.getVcsModel(): OrtVcsModel? { + val scm = getOriginalScm() ?: return null + + return OrtVcsModelImpl( + connection = scm.connection.orEmpty(), + tag = scm.tag?.takeIf { it != "HEAD" }.orEmpty(), + browsableUrl = scm.url.orEmpty() + ) +} + +fun ModelBuildingResult.getOriginalScm(): Scm? { + val scm = effectiveModel.scm + var parent = effectiveModel.parent + + while (parent != null) { + val parentModel = getRawModel("${parent.groupId}:${parent.artifactId}:${parent.version}") + + parentModel.scm?.let { parentScm -> + parentScm.connection?.let { parentConnection -> + if (parentConnection.isNotBlank() && scm.connection.startsWith(parentConnection)) { + scm.connection = parentScm.connection + } + } + + parentScm.url?.let { parentUrl -> + if (parentUrl.isNotBlank() && scm.url.startsWith(parentUrl)) { + scm.url = parentScm.url + } + } + } + + parent = parentModel.parent + } + + return scm +} diff --git a/plugins/package-managers/gradle-plugin/src/main/kotlin/OrtModelBuilder.kt b/plugins/package-managers/gradle-plugin/src/main/kotlin/OrtModelBuilder.kt new file mode 100644 index 0000000000000..e044043b7518a --- /dev/null +++ b/plugins/package-managers/gradle-plugin/src/main/kotlin/OrtModelBuilder.kt @@ -0,0 +1,308 @@ +/* + * 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.gradleplugin + +import OrtDependency +import OrtDependencyTreeModel + +import org.apache.maven.model.building.FileModelSource +import org.apache.maven.model.building.ModelBuildingResult + +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.ModuleVersionIdentifier +import org.gradle.api.artifacts.component.ComponentIdentifier +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.artifacts.component.ProjectComponentSelector +import org.gradle.api.artifacts.repositories.UrlArtifactRepository +import org.gradle.api.artifacts.result.DependencyResult +import org.gradle.api.artifacts.result.ResolvedArtifactResult +import org.gradle.api.artifacts.result.ResolvedDependencyResult +import org.gradle.api.artifacts.result.UnresolvedDependencyResult +import org.gradle.api.attributes.Attribute +import org.gradle.api.internal.artifacts.DefaultModuleIdentifier +import org.gradle.api.internal.artifacts.result.ResolvedComponentResultInternal +import org.gradle.api.logging.Logging +import org.gradle.internal.component.external.model.DefaultModuleComponentIdentifier +import org.gradle.internal.deprecation.DeprecatableConfiguration +import org.gradle.internal.resolve.ModuleVersionResolveException +import org.gradle.maven.MavenModule +import org.gradle.maven.MavenPomArtifact +import org.gradle.tooling.provider.model.ToolingModelBuilder +import org.gradle.util.GradleVersion + +class OrtModelBuilder : ToolingModelBuilder { + private lateinit var repositories: Map + + private val visitedDependencies = mutableSetOf() + private val visitedProjects = mutableSetOf() + + // Note: Using "LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE" does not work for the lookup as it is not String-typed. + private val libraryElementsAttribute = Attribute.of("org.gradle.libraryelements", String::class.java) + + private val logger = Logging.getLogger(OrtModelBuilder::class.java) + private val errors = mutableListOf() + private val warnings = mutableListOf() + + override fun canBuild(modelName: String): Boolean = + modelName == OrtDependencyTreeModel::class.java.name + + override fun buildAll(modelName: String, project: Project): OrtDependencyTreeModel { + repositories = project.repositories.associate { it.name to (it as? UrlArtifactRepository)?.url?.toString() } + + val resolvableConfigurations = project.configurations.filter { it.isResolvable() }.run { + if (project.isAndroidProject()) { + // Filter out dependencies metadata configuration as created by the Android Gradle plugin 4.0.0 and + // higher, see [1]. Also see [2] for valuable information in this context. + // + // [1]: https://developer.android.com/studio/past-releases/past-agp-releases/agp-4-0-0-release-notes#dependency-metadata + // [2]: https://gist.github.com/h0tk3y/41c73d1f822378f52f1e6cce8dcf56aa#orgjetbrainskotlinplatformtype + filterNot { it.name.endsWith("DependenciesMetadata") } + } else { + this + } + } + + val ortConfigurations = resolvableConfigurations.mapNotNull { config -> + // Explicitly resolve all POM files and their parents, as the latter otherwise may get resolved in Gradle's + // own binary "descriptor.bin" format only. + val poms = project.resolvePoms(config) + + // Get the root of the resolved dependency graph. This is also what Gradle's own "dependencies" task uses to + // recursively obtain information about resolved dependencies. Resolving dependencies triggers the download + // of metadata (like Maven POMs) only, not of binary artifacts, also see [1]. + // + // [1]: https://docs.gradle.org/current/userguide/dependency_management.html#obtaining_module_metadata + val root = config.incoming.resolutionResult.root + + // Omit configurations without dependencies. + root.dependencies.takeUnless { it.isEmpty() }?.let { dep -> + // Reset visited dependencies and projects per configuration. + visitedDependencies.clear() + visitedProjects.clear() + + OrtConfigurationImpl(name = config.name, dependencies = dep.toOrtDependencies(poms)) + } + } + + return OrtDependencyTreeModelImpl( + group = project.group.toString(), + name = project.name, + version = project.version.toString(), + configurations = ortConfigurations, + repositories = repositories.values.filterNotNull(), + errors = errors, + warnings = warnings + ) + } + + private fun Configuration.isResolvable(): Boolean { + val canBeResolved = GradleVersion.current() < GradleVersion.version("3.3") || isCanBeResolved + + val isDeprecatedConfiguration = GradleVersion.current() >= GradleVersion.version("6.0") + && this is DeprecatableConfiguration && resolutionAlternatives != null + + return canBeResolved && !isDeprecatedConfiguration + } + + /** + * Return whether the Android plugin for Gradle has been applied to the project. + */ + private fun Project.isAndroidProject() = + plugins.hasPlugin("com.android.application") || plugins.hasPlugin("com.android.library") + + /** + * Resolve the POM files for all dependences in the given [Gradle configuration][config] incl. their parent POMs. + */ + private fun Project.resolvePoms(config: Configuration): Map { + val allComponentIds = config.incoming.resolutionResult.allDependencies + .filterIsInstance() + .map { it.selected.id } + .distinct() + + // Get the POM files for all resolved dependencies. + val pomFiles = resolvePoms(allComponentIds) + + val fileModelBuilder = FileModelBuilder { groupId, artifactId, version -> + val moduleId = DefaultModuleIdentifier.newId(groupId, artifactId) + val componentId = DefaultModuleComponentIdentifier.newId(moduleId, version) + + val pomFile = resolvePoms(listOf(componentId)).single().file + + FileModelSource(pomFile) + } + + return pomFiles.associate { + // Trigger resolution of parent POMs by building the POM model. + it.id.componentIdentifier.toString() to fileModelBuilder.buildModel(it.file) + } + } + + /** + * Resolve the POM files for the given [componentIds] and return them. + */ + private fun Project.resolvePoms(componentIds: List): List { + val resolutionResult = dependencies.createArtifactResolutionQuery() + .forComponents(componentIds) + .withArtifacts(MavenModule::class.java, MavenPomArtifact::class.java) + .execute() + + return resolutionResult.resolvedComponents.flatMap { + it.getArtifacts(MavenPomArtifact::class.java) + }.filterIsInstance() + } + + private fun Collection.toOrtDependencies( + poms: Map + ): List = + if (GradleVersion.current() < GradleVersion.version("5.1")) { + this + } else { + filterNot { it.isConstraint } + }.mapNotNull { dep -> + when (dep) { + is ResolvedDependencyResult -> { + val selectedComponent = dep.selected + val attributes = dep.resolvedVariant.attributes + + when (val id = selectedComponent.id) { + is ModuleComponentIdentifier -> { + // Cut the graph on cyclic dependencies. + if (id in visitedDependencies) return@mapNotNull null + visitedDependencies += id + + val extension = attributes.getAttribute(libraryElementsAttribute) ?: "jar" + + val repositoryName = (selectedComponent as? ResolvedComponentResultInternal)?.repositoryName + val pomFile = repositories[repositoryName]?.let { repositoryUrl -> + // Note: Only Maven-style layout is supported for now. + buildString { + append(repositoryUrl.removeSuffix("/")) + append('/') + append(id.group.replace('.', '/')) + append('/') + append(id.module) + append('/') + append(id.version) + append('/') + append(id.module) + append('-') + append(id.version) + append(".pom") + } + } + + val modelBuildingResult = poms.getValue(id.toString()) + + OrtDependencyImpl( + groupId = id.group, + artifactId = id.module, + version = id.version, + classifier = "", + extension = extension, + dependencies = selectedComponent.dependencies.toOrtDependencies(poms), + error = null, + warning = null, + pomFile = pomFile, + mavenModel = OrtMavenModelImpl( + licenses = modelBuildingResult.effectiveModel.collectLicenses(), + authors = modelBuildingResult.effectiveModel.collectAuthors(), + description = modelBuildingResult.effectiveModel.description.orEmpty(), + homepageUrl = modelBuildingResult.effectiveModel.url.orEmpty(), + vcs = modelBuildingResult.getVcsModel() + ), + localPath = null + ) + } + + is ProjectComponentIdentifier -> { + val moduleId = selectedComponent.moduleVersion ?: return@mapNotNull null + + // Cut the graph on cyclic dependencies. + if (moduleId in visitedProjects) return@mapNotNull null + visitedProjects += moduleId + + OrtDependencyImpl( + groupId = moduleId.group, + artifactId = moduleId.name, + version = moduleId.version, + classifier = "", + extension = "", + dependencies = selectedComponent.dependencies.toOrtDependencies(poms), + error = null, + warning = null, + pomFile = null, + mavenModel = null, + localPath = id.projectPath + ) + } + + else -> { + val message = "Unhandled component identifier type $id." + + logger.error(message) + errors += message + + null + } + } + } + + is UnresolvedDependencyResult -> { + if (dep.attempted is ProjectComponentSelector) { + // Ignore unresolved project dependencies. For example for complex Android projects, Gradle's + // own "dependencies" task runs into "AmbiguousConfigurationSelectionException", but the project + // still builds fine, probably due to some Android plugin magic. Omitting a project dependency + // is uncritical in terms of resovling dependencies, as the for the project itself dependencies + // will still get resolved. + return@mapNotNull null + } + + val message = buildString { + append(dep.failure.message?.removeSuffix(".")) + append(" from ") + append(dep.from) + append(".") + + val causes = (dep.failure as? ModuleVersionResolveException)?.causes?.takeIf { it.isNotEmpty() } + if (causes != null) { + appendLine(" Causes are:") + append(causes.joinToString("\n") { it.toString() }) + } + } + + logger.warn(message) + warnings += message + + null + } + + else -> { + val message = "Unhandled dependency result type '$dep' in '${dep.from}'." + + logger.error(message) + errors += message + + null + } + } + } +} diff --git a/plugins/package-managers/gradle-plugin/src/main/kotlin/OrtModelImpl.kt b/plugins/package-managers/gradle-plugin/src/main/kotlin/OrtModelImpl.kt new file mode 100644 index 0000000000000..a0ae0e4b9e019 --- /dev/null +++ b/plugins/package-managers/gradle-plugin/src/main/kotlin/OrtModelImpl.kt @@ -0,0 +1,76 @@ +/* + * 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.gradleplugin + +import OrtConfiguration +import OrtDependency +import OrtDependencyTreeModel +import OrtMavenModel +import OrtVcsModel + +import java.io.Serializable + +@Suppress("SerialVersionUIDInSerializableClass") +class OrtDependencyTreeModelImpl( + override val group: String, + override val name: String, + override val version: String, + override val configurations: List, + override val repositories: List, + override val errors: List, + override val warnings: List +) : OrtDependencyTreeModel, Serializable + +@Suppress("SerialVersionUIDInSerializableClass") +class OrtConfigurationImpl( + override val name: String, + override val dependencies: List +) : OrtConfiguration, Serializable + +@Suppress("LongParameterList", "SerialVersionUIDInSerializableClass") +class OrtDependencyImpl( + override val groupId: String, + override val artifactId: String, + override val version: String, + override val classifier: String, + override val extension: String, + override val dependencies: List, + override val error: String?, + override val warning: String?, + override val pomFile: String?, + override val mavenModel: OrtMavenModel?, + override val localPath: String? +) : OrtDependency, Serializable + +@Suppress("SerialVersionUIDInSerializableClass") +class OrtMavenModelImpl( + override val licenses: Set, + override val authors: Set, + override val description: String?, + override val homepageUrl: String?, + override val vcs: OrtVcsModel? +) : OrtMavenModel, Serializable + +@Suppress("SerialVersionUIDInSerializableClass") +class OrtVcsModelImpl( + override val connection: String, + override val tag: String, + override val browsableUrl: String +) : OrtVcsModel, Serializable diff --git a/plugins/package-managers/gradle-plugin/src/main/kotlin/OrtModelPlugin.kt b/plugins/package-managers/gradle-plugin/src/main/kotlin/OrtModelPlugin.kt new file mode 100644 index 0000000000000..9c6db7a4234fa --- /dev/null +++ b/plugins/package-managers/gradle-plugin/src/main/kotlin/OrtModelPlugin.kt @@ -0,0 +1,33 @@ +/* + * 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.gradleplugin + +import javax.inject.Inject + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry + +class OrtModelPlugin @Inject constructor( + private val registry: ToolingModelBuilderRegistry +) : Plugin { + override fun apply(project: Project) = + registry.register(OrtModelBuilder()) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 1810a9f82e6cf..6d125548c91a4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -53,7 +53,9 @@ include(":plugins:package-managers:cocoapods") include(":plugins:package-managers:composer") include(":plugins:package-managers:conan") include(":plugins:package-managers:gradle") +include(":plugins:package-managers:gradle-inspector") include(":plugins:package-managers:gradle-model") +include(":plugins:package-managers:gradle-plugin") include(":plugins:package-managers:pub") include(":plugins:package-managers:python") include(":plugins:package-managers:spdx")