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

foundry.gradle.Platforms.kt Maven / Gradle / Ivy

/*
 * Copyright (C) 2022 Slack Technologies, LLC
 *
 * 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.
 */
package foundry.gradle

import com.squareup.moshi.JsonClass
import foundry.common.json.JsonTools
import foundry.gradle.dependencies.DependencyCollection
import foundry.gradle.dependencies.DependencyDef
import foundry.gradle.dependencies.flattenedPlatformCoordinates
import foundry.gradle.dependencies.identifierMap
import foundry.gradle.properties.sneakyNull
import java.io.File
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.internal.provider.MissingValueException
import org.gradle.api.logging.Logger
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory

public object Platforms {

  private val IGNORED_GROUPS =
    setOf(
      // Lint is handled by the regular shadow build since they're tied to AGP
      "com.android.tools.lint",
      "com.android.tools",

      // Databinding is tied to viewbinding, which in turn is tied to AGP like lint
      "androidx.databinding",

      // Paging v3 is a complete Kotlin rewrite and not API compatible
      "androidx.paging",

      // Timber's 5.x snapshot contains breaking changes
      "com.jakewharton.timber",

      // Junit will actually see JUnit 5, which is basically a different library.
      "junit",

      // We are on a very old constraintlayout
      "androidx.constraintlayout",

      // Kotlin updates are handled by the regular shadow build.
      "org.jetbrains.kotlin",
      "org.jetbrains.kotlinx",
    )
  private val IGNORED_IDENTIFIERS =
    setOf(
      // Some google libraries' snapshots seems to actually be old.
      "com.google.auto:auto-common",
      "com.google.testing.compile:compile-testing",
    )

  private fun tomlLibIdentifier(identifierMap: Map, identifier: String) =
    tomlLibForPath(identifierMap.getValue(identifier))

  private fun tomlLibForPath(path: String) =
    path.removePrefix("SlackDependencies.").split(".").joinToString("-") { it.decapitalizeUS() }

  /** Rewrites build files in the new toml format instead. */
  public fun rewriteBuildFiles(
    rootProjectDir: File,
    logger: Logger,
    dependencyCollection: DependencyCollection,
  ) {
    val identifierMap = dependencyCollection.identifierMap()
    val pathMap = identifierMap.entries.associateBy({ it.value }) { it.key }
    rootProjectDir
      .walkTopDown()
      .filter { it.name == "build.gradle.kts" }
      // Don't modify slack-platform, its usage is intentional
      .filterNot { it.parentFile.name == "slack-platform" }
      .forEach { buildFile ->
        var modified = false
        val newLines = mutableListOf()
        var skipNext = false
        for (line in buildFile.readLines()) {
          if (line.isBlank() && skipNext) {
            skipNext = false
            continue
          }
          skipNext = false
          if ("(SlackDependencies." in line) {
            val newLine =
              line.replace(Regex("\\((SlackDependencies.[a-zA-Z_0-9.-]+)\\)")) { match ->
                val path = match.groupValues[1]
                modified = true
                val identifier = pathMap[path] ?: error("Couldn't find $path in $identifierMap")
                val tomlIdentifier = tomlLibIdentifier(identifierMap, identifier)
                val newIdentifier = tomlIdentifier.replace("-", ".")
                "(libs.$newIdentifier)"
              }
            newLines.add(newLine)
          } else if (line.trim() == "import slack.gradle.dependencies.SlackDependencies") {
            // skip this line and the next one if empty
            skipNext = true
            modified = true
          } else {
            newLines.add(line)
          }
        }
        if (modified) {
          // Pad with a trailing newline
          logger.lifecycle("Patching $buildFile")
          buildFile.writeText(newLines.joinToString("\n") + "\n")
        }
      }
  }

  /** Applies constraints from a given [catalog]. */
  public fun applyFromCatalog(
    project: Project,
    catalog: VersionCatalog = project.getVersionsCatalog(),
  ) {
    check(project.pluginManager.hasPlugin("org.gradle.java-platform")) {
      "Must be a java-platform project!"
    }
    // TODO
    //  - Support overriding values from a VERSIONS_JSON env property
    //    - snapshots should use strict versions

    project.dependencies.apply {
      constraints {
        for (alias in catalog.libraryAliases) {
          add("api", catalog.findLibrary(alias).get())
        }
      }
    }
  }

  /**
   * Applies dependencies in `java-platform` [project] by bridging their definitions from a given
   * [dependency collection][dependencies] to versions defined in gradle properties (usually via
   * `gradle.properties`.
   *
   * It currently has support for overriding values from a `VERSIONS_JSON` env property, but this
   * will be removed eventually.
   */
  public fun applyToProject(project: Project, dependencies: DependencyCollection) {
    check(project.pluginManager.hasPlugin("org.gradle.java-platform")) {
      "Must be a java-platform project!"
    }

    val logger = project.logger
    val foundryProperties = project.foundryProperties
    val snapshotsEnabled = foundryProperties.enableSnapshots

    // Overrides provider, used when testing newer dependency versions on shadow builds
    // TODO We should just make the shadow build replace the versions in gradle.properties and
    //  remove all this extra logic
    val overridesProvider =
      project.providers.provider {
        val path = foundryProperties.versionsJson ?: return@provider sneakyNull()
        println("Parsing versions json at $path")
        JsonTools.fromJson(path)
      }

    val providers = project.providers

    val flattened = dependencies.flattenedPlatformCoordinates()
    project.dependencies.apply {
      constraints {
        for (def in flattened) {
          if (def.isBomManaged) continue
          val version = getOrOverride(providers, def, overridesProvider, logger)
          if (snapshotsEnabled && version.endsWith("-SNAPSHOT")) {
            add("api", def.coordinates) { version { strictly(version) } }
          } else {
            add("api", def.coordinates) { version { require(version) } }
          }
        }
      }
    }
  }

  private fun getOrOverride(
    providers: ProviderFactory,
    dependencyDef: DependencyDef,
    overridesProvider: Provider,
    logger: Logger,
  ): String {
    val isOverridable =
      dependencyDef.group !in IGNORED_GROUPS &&
        dependencyDef.identifier !in IGNORED_IDENTIFIERS &&
        overridesProvider.isPresent

    val expectedProperty = dependencyDef.gradleProperty
    val defaultProvider = providers.gradleProperty(expectedProperty)

    val versionProvider =
      if (isOverridable) {
        providers
          .provider {
            overridesProvider.get().identifierMap[dependencyDef.identifier]?.available?.newTarget()
          }
          .zip(defaultProvider) { overridden, default ->
            if (overridden != null) {
              println("[SlackPlatform] override: ${dependencyDef.identifier}:$overridden")
              overridden
            } else {
              logger.debug("[SlackPlatform] constraining ${dependencyDef.identifier} to $default")
              default
            }
          }
      } else {
        defaultProvider
      }

    return try {
      versionProvider.get()
    } catch (e: MissingValueException) {
      val message =
        "No version found for '${dependencyDef.identifier}' " +
          "(key: '$expectedProperty'). Please add " +
          "'${expectedProperty.replace(":", "\\:")}' in gradle.properties"
      throw GradleException(message)
    }
  }
}

@JsonClass(generateAdapter = true)
internal data class VersionsOutput(val outdated: Outdated) {
  val identifierMap = outdated.dependencies.associateBy { "${it.group}:${it.name}" }
}

@JsonClass(generateAdapter = true) internal data class Outdated(val dependencies: Set)

@JsonClass(generateAdapter = true)
internal data class Artifact(
  val group: String,
  val available: Available,
  val version: String,
  val name: String,
)

@JsonClass(generateAdapter = true)
internal data class Available(val release: String?, val integration: String?) {
  fun newTarget(): String {
    return release ?: integration ?: error("No available target found")
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy