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

com.autonomousapps.internal.utils.gradleStrings.kt Maven / Gradle / Ivy

There is a newer version: 2.6.1
Show newest version
// Copyright (c) 2024. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.internal.utils

import com.autonomousapps.internal.GradleVersions
import com.autonomousapps.model.*
import org.gradle.api.GradleException
import org.gradle.api.artifacts.*
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.ModuleDependency
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.artifacts.component.ComponentIdentifier
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.component.ModuleComponentSelector
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.artifacts.result.ResolvedArtifactResult
import org.gradle.api.artifacts.result.ResolvedComponentResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.artifacts.result.ResolvedVariantResult
import org.gradle.api.capabilities.Capability
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.ConfigurableFileTree
import org.gradle.api.internal.artifacts.DefaultProjectComponentIdentifier
import org.gradle.api.provider.Provider
import org.gradle.internal.component.local.model.OpaqueComponentArtifactIdentifier
import org.gradle.internal.component.local.model.OpaqueComponentIdentifier
import java.io.File
import java.io.Serializable

/** Converts this [ResolvedDependencyResult] to group-artifact-version (GAV) coordinates in a tuple of (GA, V?). */
internal fun ResolvedDependencyResult.toCoordinates(): Coordinates =
  compositeRequest() ?: selected.id.wrapInIncludedBuildCoordinates(resolvedVariant)

/** If this is a composite substitution, returns it as such. We care about the request as well as the result. */
private fun ResolvedDependencyResult.compositeRequest(): IncludedBuildCoordinates? {
  val gradleVariantIdentification = resolvedVariant.toGradleVariantIdentification()
  if (!selected.selectionReason.isCompositeSubstitution) return null
  val requestedModule = requested as? ModuleComponentSelector ?: return null

  val requested = ModuleCoordinates(
    identifier = requestedModule.moduleIdentifier.toString(),
    resolvedVersion = requestedModule.version,
    gradleVariantIdentification = gradleVariantIdentification
  )
  val resolved = ProjectCoordinates(
    identifier = (selected.id as ProjectComponentIdentifier).identityPath(),
    gradleVariantIdentification = gradleVariantIdentification,
    buildPath = (selected.id as ProjectComponentIdentifier).build.let {
      if (GradleVersions.isAtLeastGradle82) it.buildPath else @Suppress("DEPRECATION") it.name
    }
  )

  return IncludedBuildCoordinates.of(requested, resolved)
}

private fun ProjectComponentIdentifier.identityPath(): String {
  return (this as? DefaultProjectComponentIdentifier)?.identityPath?.toString()
    ?: error("${toCoordinates(GradleVariantIdentification.EMPTY)} is not a DefaultProjectComponentIdentifier")
}

internal fun ResolvedArtifactResult.toCoordinates(): Coordinates {
  return id.componentIdentifier.wrapInIncludedBuildCoordinates(variant)
}

private fun ComponentIdentifier.wrapInIncludedBuildCoordinates(variant: ResolvedVariantResult?): Coordinates {
  val variantIdentification = variant.toGradleVariantIdentification()
  val resolved = toCoordinates(variantIdentification)

  // No resolved variant, so there are no capabilities to extract the components coordinates from
  if (variant == null) return resolved

  // Doesn't resolve to a project, so can't be an included build. Return as-is.
  if (resolved !is ProjectCoordinates) return resolved

  // Module may have been resolved from an included build. Construct IncludedBuildCoordinates if possible.
  // This is a very naive heuristic. Doesn't work for Gradle < 7.2, where capabilities is empty.
  val projectName = (variant.owner as ProjectComponentIdentifier).projectName
  val requested = variant.capabilities.find { it.name.startsWith(projectName) }?.let { c ->
    c.version?.let { v ->
      ModuleCoordinates(
        identifier = "${c.group}:$projectName",
        resolvedVersion = v,
        gradleVariantIdentification = variantIdentification
      )
    }
  } ?: return resolved

  return IncludedBuildCoordinates.of(
    requested = requested,
    resolvedProject = resolved
  )
}

/** Returns the [coordinates][Coordinates] of the root of [this][Configuration]. */
internal fun Configuration.rootCoordinates(): Coordinates = incoming.resolutionResult.root.rootCoordinates()

/** Returns the [coordinates][Coordinates] of the root of [this][ResolvedComponentResult]. */
internal fun ResolvedComponentResult.rootCoordinates(): Coordinates {
  return id
    // For the root, the 'GradleVariantIdentification' is always empty as there is only one root (which we match later)
    .toCoordinates(GradleVariantIdentification(setOf("ROOT"), emptyMap()))
}

/** Converts this [ComponentIdentifier] to group-artifact-version (GAV) coordinates in a tuple of (GA, V?). */
private fun ComponentIdentifier.toCoordinates(gradleVariantIdentification: GradleVariantIdentification): Coordinates {
  val identifier = toIdentifier()
  return when (this) {
    is ProjectComponentIdentifier -> {
      val buildPath = if (GradleVersions.isAtLeastGradle82) build.buildPath else @Suppress("DEPRECATION") build.name
      ProjectCoordinates(identifier, gradleVariantIdentification, buildPath)
    }

    is ModuleComponentIdentifier -> {
      resolvedVersion()?.let { resolvedVersion ->
        ModuleCoordinates(identifier, resolvedVersion, gradleVariantIdentification)
      } ?: FlatCoordinates(identifier)
    }

    else -> FlatCoordinates(identifier)
  }
}

/**
 * Convert this [ComponentIdentifier] to a group-artifact identifier, such as "org.jetbrains.kotlin:kotlin-stdlib" in
 * the case of an external module, or a project identifier, such as ":foo:bar", in the case of an internal module.
 */
private fun ComponentIdentifier.toIdentifier(): String = when (this) {
  is ProjectComponentIdentifier -> projectPath
  is ModuleComponentIdentifier -> {
    // flat JAR/AAR files have no group. I don't trust that, if absent, it will be blank rather
    // than null.
    if (moduleIdentifier.group.isNullOrBlank()) moduleIdentifier.name
    else moduleIdentifier.toString()
  }
  // e.g. "Gradle API"
  is OpaqueComponentIdentifier -> displayName
  // for a file dependency
  is OpaqueComponentArtifactIdentifier -> displayName
  else -> throw GradleException("Cannot identify ComponentIdentifier subtype. Was ${javaClass.simpleName}, named $this")
}.intern()

/**
 * Gets the resolved version from this [ComponentIdentifier]. Note that this may be different from the version
 * requested.
 */
private fun ComponentIdentifier.resolvedVersion(): String? = when (this) {
  is ProjectComponentIdentifier -> null
  is ModuleComponentIdentifier -> {
    // flat JAR/AAR files have no version, but rather than null, it's empty.
    version.ifBlank { null }
  }
  // e.g. "Gradle API"
  is OpaqueComponentIdentifier -> null
  // for a file dependency
  is OpaqueComponentArtifactIdentifier -> null
  else -> throw GradleException("Cannot identify ComponentIdentifier subtype. Was ${javaClass.simpleName}, named $this")
}?.intern()

/**
 * This has to be public because it's used as part of a task input, but should otherwise be considered an internal
 * implementation detail.
 */
class ModuleInfo(
  val identifier: String,
  val version: String? = null,
) : Serializable, Comparable {

  override fun compareTo(other: ModuleInfo): Int {
    return compareBy(ModuleInfo::identifier)
      .thenComparing(compareBy(nullsFirst()) { it.version })
      .compare(this, other)
  }
}

/**
 * Given [Configuration.getDependencies], return this dependency set as a set of identifiers, per
 * [ComponentIdentifier.toIdentifier].
 */
internal fun DependencySet.toIdentifiers(): Set> = mapNotNullToSet {
  it.toIdentifier()
}

internal fun Dependency.toCoordinates(): Coordinates? {
  val identifier = toIdentifier() ?: return null
  return when (this) {
    is ProjectDependency -> ProjectCoordinates(identifier.first.identifier, identifier.second)
    is ModuleDependency -> {
      resolvedVersion()?.let { resolvedVersion ->
        ModuleCoordinates(identifier.first.identifier, resolvedVersion, identifier.second)
      } ?: FlatCoordinates(identifier.first.identifier)
    }

    else -> FlatCoordinates(identifier.first.identifier)
  }
}

/**
 * Given a [Dependency] retrieved from a [Configuration], return it as a
 * pair of 'identifier' and 'GradleVariantIdentification'
 */
internal fun Dependency.toIdentifier(): Pair? = when (this) {
  is ProjectDependency -> {
    val identifier = dependencyProject.path
    Pair(ModuleInfo(identifier.intern()), targetGradleVariantIdentification())
  }

  is ModuleDependency -> {
    // flat JAR/AAR files have no group.
    val identifier = if (group != null) "${group}:${name}" else name
    Pair(ModuleInfo(identifier.intern(), version), targetGradleVariantIdentification())
  }

  is FileCollectionDependency -> {
    // Note that this only gets the first file in the collection, ignoring the rest.
    when (files) {
      is ConfigurableFileCollection -> {
        (files as ConfigurableFileCollection).from.firstOrNull()
          ?.let { first ->
            // https://github.com/gradle/gradle/pull/26317
            val firstFile = if (first is Array<*>) {
              first.firstOrNull()
            } else {
              first
            }

            // Handle weirdness that seems to come from KGP? Unclear. See comments from
            // https://github.com/autonomousapps/dependency-analysis-gradle-plugin/issues/997#issuecomment-1826627186
            when (firstFile) {
              is Function0<*> -> null // "() -> Any?"
              is Provider<*> -> null  // "property 'destinationDirectory'"
              is File -> firstFile.invariantSeparatorsPath.substringAfterLast('/')
              else -> firstFile?.toString()?.let { it.substring(it.lastIndexOfAny(charArrayOf('/', '\\')) + 1) }
            }
          }?.let {
            Pair(ModuleInfo(it.intern()), GradleVariantIdentification.EMPTY)
          }
      }

      is ConfigurableFileTree -> files.firstOrNull()?.name?.let {
        Pair(ModuleInfo((it.intern())), GradleVariantIdentification.EMPTY)
      }

      else -> null
    }
  }
  // Don't have enough information, so ignore it. Please note that a `FileCollectionDependency` is
  // also a `SelfResolvingDependency`, but not all `SelfResolvingDependency`s are
  // `FileCollectionDependency`s.
  is SelfResolvingDependency -> null
  else -> throw GradleException("Unknown Dependency subtype: \n$this\n${javaClass.name}")
}

internal fun Dependency.resolvedVersion(): String? = when (this) {
  is ProjectDependency -> null
  is ModuleDependency -> {
    // flat JAR/AAR files have no version, but rather than null, it's empty.
    version?.ifBlank { null }
  }

  is FileCollectionDependency -> null
  is SelfResolvingDependency -> null
  else -> throw GradleException("Unknown Dependency subtype: \n$this\n${javaClass.name}")
}?.intern()

/**
 * Returns the 'capabilities' and 'attributes' that are used by Gradle to determine which variant in the other
 * module this dependency points at. Next to the 'main' variant, this can be for example, testFixtures() or
 * another Feature Variant. This function reads the required capabilities and attributes DIRECTLY declared on
 * a dependency (testFixtures(...) and platform(...) are shortcut notations that add a capability/attribute).
 *
 * See Gradle user manual:
 * - Capabilities: https://docs.gradle.org/current/userguide/component_capabilities.html
 * - Feature Variants: https://docs.gradle.org/current/userguide/feature_variants.html
 *   Feature Variants use Capabilities to distinguish different variants (source sets) of a project.
 * - Test Fixtures: https://docs.gradle.org/current/userguide/java_testing.html#sec:java_test_fixtures
 *   'testFixtures' is a Feature Variant added and configured by the 'java-test-fixtures' plugin.
 */
internal fun Dependency.targetGradleVariantIdentification() = when (this) {
  is ModuleDependency -> GradleVariantIdentification(
    requestedCapabilities.map { it.toGA() }.toSet(),
    attributes.keySet().associate { it.name to attributes.getAttribute(it).toString() }
  )

  else -> GradleVariantIdentification.EMPTY
}

/**
 * Return the 'capabilities' of a selected variant. Will be used to determine if a declaration
 * will (most likely) result in resolving to a certain variant of a component.
 * Attributes of the resolved variant are not taken into account. Attributes are used for exclusive
 * selection. Which means there can only be one variant in the graph and not multiple variants with different
 * attributes (and the same capability). Hence, we do not need to distinguish variants based on attributes (only by
 * capabilities).
 */
internal fun ResolvedVariantResult?.toGradleVariantIdentification(): GradleVariantIdentification {
  if (this == null) return GradleVariantIdentification.EMPTY

  return GradleVariantIdentification(
    capabilities.map { it.toGA() }.toSet(), emptyMap()
  )
}

private fun Capability.toGA() = "$group:$name".intern()




© 2015 - 2025 Weber Informatics LLC | Privacy Policy