the - Configuración del proyecto repetitivo en Gradle con Gradle Kotlin DSL
kotlin gradle plugin (4)
He estado haciendo algo como esto en mi compilación
buildscript {
project.apply {
from("${rootProject.projectDir}/sharedValues.gradle.kts")
}
val configureRepository: (Any) -> Unit by extra
configureRepository.invoke(repositories)
}
En mi archivo sharedValues.gradle.kts
tengo un código como este:
/**
* This method configures the repository handler to add all of the maven repos that your company relies upon.
* When trying to pull this method out of the [ExtraPropertiesExtension] use the following code:
*
* For Kotlin:
* ```kotlin
* val configureRepository : (Any) -> Unit by extra
* configureRepository.invoke(repositories)
* ```
* Any other casting will cause a compiler error.
*
* For Groovy:
* ```groovy
* def configureRepository = project.configureRepository
* configureRepository.invoke(repositories)
* ```
*
* @param repoHandler The RepositoryHandler to be configured with the company repositories.
*/
fun repositoryConfigurer(repoHandler : RepositoryHandler) {
repoHandler.apply {
// Do stuff here
}
}
var configureRepository : (RepositoryHandler) -> Unit by extra
configureRepository = this::repositoryConfigurer
Sigo un patrón similar para configurar la estrategia de resolución para complementos.
Lo bueno de este patrón es que cualquier cosa que configure en sharedValues.gradle.kts
también puede usarse desde su proyecto buildSrc
, lo que significa que puede reutilizar las declaraciones del repositorio.
Actualizado:
Puede aplicar otro script desde una URL, por ejemplo haciendo esto:
apply {
// This was actually a plugin that I used at one point.
from("http://dl.bintray.com/shemnon/javafx-gradle/8.1.1/javafx.plugin")
}
Simplemente aloje el script que desea que compartan todas sus compilaciones en algún servidor http (recomendaría el uso de HTTPS para que su compilación no pueda ser atacada por un hombre en el ataque central).
El inconveniente de esto es que no creo que los scripts aplicados desde las URL no se almacenen en caché, por lo que se volverán a descargar cada vez que ejecutes tu compilación. Esto puede haber sido arreglado por ahora, no estoy seguro.
Actualmente estoy tratando de mejorar la forma en que nuestros proyectos comparten su configuración. Tenemos muchos proyectos gradle de múltiples módulos para todas nuestras bibliotecas y microservicios (es decir, muchos repositorios de git).
Mis principales objetivos son:
- Para no tener la configuración de mi repositorio Nexus duplicada en cada proyecto (también, puedo asumir con seguridad que la URL no cambiará)
- Para hacer que mis complementos Gradle personalizados (publicados en Nexus) estén disponibles para cada proyecto con un mínimo de repetición / duplicación (deberían estar disponibles para cada proyecto, y lo único que le importa al proyecto es la versión que está usando)
- Sin magia, debería ser obvio para los desarrolladores cómo está configurado todo
Mi solución actual es una distribución personalizada de Gradle con un script de inicio que:
- agrega
mavenLocal()
y nuestro repositorio Nexus a los repositorios del proyecto (muy similar al ejemplo de la documentación del script de inicio de Gradle, excepto que agrega repos y los valida) - configura una extensión que permite que nuestros complementos de gradle se agreguen a la ruta de clase de buildscript (usando esta solución ). También agrega nuestro repositorio Nexus como un repositorio buildscript ya que es donde se alojan los complementos. Tenemos bastantes complementos (creados a partir de los excelentes complementos de nebulosa de Netflix) para varias repeticiones: configuración estándar del proyecto (configuración de kotlin, configuración de prueba, etc.), publicación, documentación, etc. y significa que nuestros proyectos
build.gradle
son bastante Solo para dependencias.
Aquí está el script de inicio (saneado):
/**
* Gradle extension applied to all projects to allow automatic configuration of Corporate plugins.
*/
class CorporatePlugins {
public static final String NEXUS_URL = "https://example.com/repository/maven-public"
public static final String CORPORATE_PLUGINS = "com.example:corporate-gradle-plugins"
def buildscript
CorporatePlugins(buildscript) {
this.buildscript = buildscript
}
void version(String corporatePluginsVersion) {
buildscript.repositories {
maven {
url NEXUS_URL
}
}
buildscript.dependencies {
classpath "$CORPORATE_PLUGINS:$corporatePluginsVersion"
}
}
}
allprojects {
extensions.create(''corporatePlugins'', CorporatePlugins, buildscript)
}
apply plugin: CorporateInitPlugin
class CorporateInitPlugin implements Plugin<Gradle> {
void apply(Gradle gradle) {
gradle.allprojects { project ->
project.repositories {
all { ArtifactRepository repo ->
if (!(repo instanceof MavenArtifactRepository)) {
project.logger.warn "Non-maven repository ${repo.name} detected in project ${project.name}. What are you doing???"
} else if(repo.url.toString() == CorporatePlugins.NEXUS_URL || repo.name == "MavenLocal") {
// Nexus and local maven are good!
} else if (repo.name.startsWith("MavenLocal") && repo.url.toString().startsWith("file:")){
// Duplicate local maven - remove it!
project.logger.warn("Duplicate mavenLocal() repo detected in project ${project.name} - the corporate gradle distribution has already configured it, so you should remove this!")
remove repo
} else {
project.logger.warn "External repository ${repo.url} detected in project ${project.name}. You should only be using Nexus!"
}
}
mavenLocal()
// define Nexus repo for downloads
maven {
name "CorporateNexus"
url CorporatePlugins.NEXUS_URL
}
}
}
}
}
Luego configuro cada nuevo proyecto agregando lo siguiente al archivo root build.gradle:
buildscript {
// makes our plugins (and any others in Nexus) available to all build scripts in the project
allprojects {
corporatePlugins.version "1.2.3"
}
}
allprojects {
// apply plugins relevant to all projects (other plugins are applied where required)
apply plugin: ''corporate.project''
group = ''com.example''
// allows quickly updating the wrapper for our custom distribution
task wrapper(type: Wrapper) {
distributionUrl = ''https://com.example/repository/maven-public/com/example/corporate-gradle/3.5/corporate-gradle-3.5.zip''
}
}
Si bien este enfoque funciona, permite compilaciones reproducibles (a diferencia de nuestra configuración anterior que aplicaba un script de compilación desde una URL, que en ese momento no se podía almacenar en caché), y permite trabajar sin conexión, lo hace un poco mágico y me preguntaba si Podría hacer las cosas mejor.
Todo esto se activó al leer un comentario en Github por el desarrollador de Gradle Stefan Oehme que indica que una compilación debería funcionar sin depender de un guión de inicio, es decir, los guiones de inicio deberían ser decorativos y hacer cosas como el ejemplo documentado: evitar reposiciones no autorizadas, etc.
Mi idea fue escribir algunas funciones de extensión que me permitieran agregar nuestro repo y complementos Nexus a una compilación de la forma en que parecían estar integrados en gradle (similar a las funciones de extensión gradleScriptKotlin()
y kotlin-dsl()
proporcionadas por El Gradle Kotlin DSL.
Así que creé mis funciones de extensión en un proyecto gradle kotlin:
package com.example
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.artifacts.dsl.RepositoryHandler
import org.gradle.api.artifacts.repositories.MavenArtifactRepository
fun RepositoryHandler.corporateNexus(): MavenArtifactRepository {
return maven {
with(it) {
name = "Nexus"
setUrl("https://example.com/repository/maven-public")
}
}
}
fun DependencyHandler.corporatePlugins(version: String) : Any {
return "com.example:corporate-gradle-plugins:$version"
}
Con el plan para usarlos en el build.gradle.kts
mi proyecto de la siguiente manera:
import com.example.corporateNexus
import com.example.corporatePlugins
buildscript {
repositories {
corporateNexus()
}
dependencies {
classpath(corporatePlugins(version = "1.2.3"))
}
}
Sin embargo, Gradle no pudo ver mis funciones cuando se usó en el bloque de buildscript
(no se pudo compilar el script). Sin embargo, usarlos en los repositorios / dependencias normales del proyecto funcionó bien (son visibles y funcionan como se espera).
Si esto funcionaba, esperaba agrupar el jar en mi distribución personalizada, lo que significa que mi script de inicio podría hacer una validación simple en lugar de ocultar el complemento mágico y la configuración de repo. Las funciones de extensión no tendrían que cambiar, por lo que no sería necesario liberar una nueva distribución de Gradle cuando cambian los complementos.
Lo que intenté:
- agregar mi jarra a la ruta de clase de buildscript del proyecto de prueba (es decir,
buildscript.dependencies
) - no funciona (tal vez esto no funcione por diseño, ya que no parece correcto agregar una dependencia abuildscript
que se hace referencia en el mismo bloque) ) -
buildSrc
las funciones enbuildSrc
(que funciona para deps / repos de proyectos normales pero no parabuildscript
, pero no es una solución real, ya que solo mueve el boilerplate) - Dejando caer el frasco en la carpeta
lib
de la distribución.
Así que mi pregunta realmente se reduce a:
- ¿Lo que estoy tratando de lograr es posible (es posible hacer que las clases / funciones personalizadas sean visibles para el bloque de
buildScript
)? - ¿Existe un mejor enfoque para configurar un repositorio Nexus corporativo y hacer que los complementos personalizados (publicados para Nexus) estén disponibles en muchos proyectos separados (es decir, bases de código totalmente diferentes) con una configuración mínima de repetición?
Le prometí a @eskatos que volvería y le daría una respuesta a su respuesta, ¡así que aquí está!
Mi solución final consiste en:
- Gradle 4.7 contenedor por proyecto (señalado en un espejo de http://services.gradle.org/distributions setup en Nexus como un repositorio de proxy en bruto, es decir, es Gradle de vainilla pero descargado a través de Nexus)
- Complementos personalizados de Gradle publicados en nuestro repositorio Nexus junto con marcadores de complementos (generados por el complemento de desarrollo de complementos de Java Gradle )
- Reflejando el Portal de Gradle Plugin en nuestro repositorio Nexus (es decir, un repositorio proxy que apunta a https://plugins.gradle.org/m2 )
- Un archivo
settings.gradle.kts
por proyecto que configura nuestro espejo mao y el espejo del portal de complementos de gradle (ambos en Nexus) como repositorios de administración de complementos.
El archivo settings.gradle.kts
contiene lo siguiente:
pluginManagement {
repositories {
// local maven to facilitate easy testing of our plugins
mavenLocal()
// our plugins and their markers are now available via Nexus
maven {
name = "CorporateNexus"
url = uri("https://nexus.example.com/repository/maven-public")
}
// all external gradle plugins are now mirrored via Nexus
maven {
name = "Gradle Plugin Portal"
url = uri("https://nexus.example.com/repository/gradle-plugin-portal")
}
}
}
Esto significa que todos los complementos y sus dependencias ahora se procesan a través de Nexus, y Gradle encontrará nuestros complementos por id, ya que los marcadores de complementos también se publican en Nexus. Tener mavenLocal
allí también facilita la fácil prueba de los cambios de nuestro plugin a nivel local.
El archivo root build.gradle.kts
cada proyecto aplica los complementos de la siguiente manera:
plugins {
// plugin markers for our custom plugins allow us to apply our
// plugins by id as if they were hosted in gradle plugin portal
val corporatePluginsVersion = "1.2.3"
id("corporate-project") version corporatePluginsVersion
// ''apply false` means this plugin can be applied in a subproject
// without having to specify the version again
id("corporate-publishing") version corporatePluginsVersion apply false
// and so on...
}
Y configura la envoltura de gradle para usar nuestra distribución reflejada, que cuando se combina con lo anterior significa que todo (gradle, plugins, dependencias) todos vienen a través de Nexus):
tasks {
"wrapper"(Wrapper::class) {
distributionUrl = "https://nexus.example.com/repository/gradle-distributions/gradle-4.7-bin.zip"
}
}
Esperaba evitar la repetición en los archivos de configuración utilizando la sugerencia de @ eskatos de aplicar un script desde una URL remota en settings.gradle.kts
. es decir
apply { from("https://nexus.example.com/repository/maven-public/com/example/gradle/corporate-settings/1.2.3/corporate-settings-1.2.3.kts" }
Incluso logré generar un script con plantilla (publicado junto con nuestros complementos) que:
- Configuró los repositorios del plugin (como en el script de configuración anterior)
- usó una estrategia de resolución para aplicar la versión de los complementos asociados con el script si la identificación del complemento solicitada era uno de nuestros complementos y la versión no fue suministrada (por lo tanto, puede aplicarlos por identificación)
Sin embargo, a pesar de que eliminó la repetición, significaba que nuestras compilaciones dependían de tener una conexión con nuestro repositorio Nexus, ya que parece que a pesar de que los scripts aplicados desde una URL están almacenados en la memoria caché, Gradle realiza una solicitud HEAD para verificar los cambios. También hizo que fuera molesto probar los cambios de los complementos localmente, ya que tuve que apuntarlos manualmente al script en mi directorio local de Maven. Con mi configuración actual, simplemente puedo publicar los complementos en maven local y actualizar la versión en mi proyecto.
Estoy bastante contento con la configuración actual. Creo que ahora es mucho más obvio para los desarrolladores cómo se aplican los complementos. Y ha hecho que sea mucho más fácil actualizar Gradle y nuestros complementos de forma independiente ahora que no hay dependencia entre los dos (y no se requiere una distribución personalizada de Gradle).
Si desea beneficiarse de todas las bondades de Gradle Kotlin DSL, debe esforzarse por aplicar todos los complementos utilizando el bloque de plugins {}
. Ver https://github.com/gradle/kotlin-dsl/blob/master/doc/getting-started/Configuring-Plugins.md
Puede administrar repositorios de complementos y estrategias de resolución (por ejemplo, su versión) en sus archivos de configuración. Comenzando con Gradle 4.4, este archivo se puede escribir utilizando el DSL de Kotlin, también conocido como settings.gradle.kts
. Consulte https://docs.gradle.org/4.4-rc-1/release-notes.html .
Teniendo esto en cuenta, podrías tener un complemento de script de Settings
centralizado que configura las cosas y lo aplica en los archivos de settings.gradle.kts
:
// corporate-settings.gradle.kts
pluginManagement {
repositories {
maven {
name = "Corporate Nexus"
url = uri("https://example.com/repository/maven-public")
}
gradlePluginPortal()
}
}
y:
// settings.gradle.kts
apply(from = "https://url.to/corporate-settings.gradle.kts")
Luego, en los scripts de compilación de su proyecto, simplemente puede solicitar complementos desde su repositorio corporativo:
// build.gradle.kts
plugins {
id("my-corporate-plugin") version "1.2.3"
}
Si desea que los scripts de compilación de su proyecto en una compilación multiproyecto no repitan la versión del complemento, puede hacerlo con Gradle 4.3 declarando versiones en su proyecto raíz. Tenga en cuenta que también puede configurar las versiones en settings.gradle.kts
usando pluginManagement.resolutionStrategy
si todas las compilaciones usan la misma versión de complementos es lo que necesita.
También tenga en cuenta que para que todo esto funcione, sus complementos deben publicarse con su artefacto marcador de complementos . Esto se hace fácilmente usando el java-gradle-plugin
plugin.
Una solución que me ofreció Stefan Oehme cuando tuve un problema similar fue vender mi propia distribución personalizada de Gradle. Según él, esto es algo común en las grandes empresas.
Simplemente cree una bifurcación personalizada del repositorio de gradle, agregue la salsa especial de su empresa a cada proyecto utilizando esta versión personalizada de gradle.