使用 Gradle Kotlin DSL 在 Gradle 中进行样板项目配置
Posted
技术标签:
【中文标题】使用 Gradle Kotlin DSL 在 Gradle 中进行样板项目配置【英文标题】:Boilerplate project configuration in Gradle with Gradle Kotlin DSL 【发布时间】:2017-09-25 06:00:24 【问题描述】:我目前正在尝试改进我们的项目共享其配置的方式。我们所有的库和微服务(即许多 git repos)都有许多不同的多模块 gradle 项目。
我的主要目标是:
不要在每个项目中重复我的 Nexus 存储库配置(另外,我可以放心地假设 URL 不会更改) 让我的自定义 Gradle 插件(发布到 Nexus)以最少的样板/重复用于每个项目(它们应该可用于每个项目,并且项目唯一关心的是它使用的版本) 没有魔法 - 开发人员应该很清楚一切是如何配置的我当前的解决方案是一个自定义的 gradle 发行版,它带有一个 init 脚本:
将mavenLocal()
和我们的 Nexus 存储库添加到项目存储库(非常类似于 Gradle init script documentation example,除了它添加存储库并验证它们)
配置一个扩展,允许将我们的 gradle 插件添加到 buildscript 类路径中(使用this workaround)。它还将我们的 Nexus 存储库添加为 buildscript 存储库,因为这是托管插件的地方。我们有很多插件(基于 Netflix 优秀的 nebula plugins 构建)用于各种样板:标准项目设置(kotlin 设置、测试设置等)、发布、发布、文档等,这意味着我们的项目 build.gradle
文件很漂亮仅用于依赖项。
这是初始化脚本(已清理):
/**
* 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
然后我通过将以下内容添加到根 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'
虽然这种方法有效,但允许可重现的构建(与我们之前的设置不同,它从 URL 应用构建脚本 - 当时不可缓存),并且允许离线工作,它确实让它有点神奇,我是想知道我是否可以做得更好。
这一切都是通过阅读 Gradle 开发人员 Stefan Oehme 的 a comment on Github 触发的,他指出构建应该在不依赖 init 脚本的情况下工作,即 init 脚本应该只是装饰性的,并且可以执行文档示例之类的操作 - 防止未经授权的 repos 等.
我的想法是编写一些扩展函数,让我可以将我们的 Nexus 存储库和插件添加到构建中,就像它们内置在 gradle 中一样(类似于提供的扩展函数 gradleScriptKotlin()
和 kotlin-dsl()
由 Gradle Kotlin DSL 提供。
所以我在一个 kotlin gradle 项目中创建了我的扩展函数:
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"
计划在我的项目build.gradle.kts
中使用它们如下:
import com.example.corporateNexus
import com.example.corporatePlugins
buildscript
repositories
corporateNexus()
dependencies
classpath(corporatePlugins(version = "1.2.3"))
但是,在 buildscript
块中使用时,Gradle 无法看到我的函数(无法编译脚本)。不过,在正常的项目存储库/依赖项中使用它们效果很好(它们是可见的并且按预期工作)。
如果这可行,我希望将 jar 捆绑到我的自定义发行版中,这意味着我的 init 脚本可以进行简单的验证,而不是隐藏神奇的插件和 repo 配置。扩展功能无需更改,因此无需在插件更改时发布新的 Gradle 发行版。
我尝试了什么:
将我的 jar 添加到测试项目的 buildscript 类路径(即buildscript.dependencies
) - 不起作用(也许这在设计上不起作用,因为向 buildscript
添加依赖项似乎不正确到同一块)
将函数放在buildSrc
中(适用于普通项目deps/repos 但不适用于buildscript
,但不是真正的解决方案,因为它只是移动样板)
将 jar 放到分发版的 lib
文件夹中
所以我的问题归结为:
我想要实现的目标是否可行(是否可以使自定义类/函数对buildScript
块可见)?
是否有更好的方法来配置企业 Nexus 存储库并使自定义插件(发布到 Nexus)在许多单独的项目(即完全不同的代码库)中可用,且样板配置最少?
【问题讨论】:
我想总结一下你想要做什么,你想给buildscript
块添加扩展?您使用的 Gradle 版本是否有下限?
@mkobit 不,我正在升级到 4.1。我想我真的在尝试改进我们的构建配置 nexus 的方式,并使我们的插件可用于项目。当前的解决方案(如本问题开头所述)有效,但插件配置特别像黑客!
感谢您的澄清。可能有几种方法可能/可能不起作用/改进您已经完成的工作。一种是写一个init script plugin,您可以在settings.gradle
中申请。在当前的4.2-rc
中,还支持 脚本插件被缓存,并且仅在必要时下载,而不是在每次构建时下载。 这可能会改善您遇到的一些问题。另一个想法可能是提供一个自定义插件门户,例如 (github.com/linkedin/custom-gradle-plugin-portal)。
我在 KotlinConf 与 Rodrigo(又名 Bamboo)就此事进行了交谈。他的建议是将引导插件发布到 gradle 插件门户。我们还讨论了远程脚本插件在缓存后可能不再那么糟糕了。如果有人想出一个非常干净和令人信服的方法,那就太棒了——我还是有点犹豫不决!
嗨@mkobit 我刚刚添加了一个答案,其中包含我最终使用的解决方案。我确实尝试从远程脚本应用插件管理配置,但即使它被缓存,它仍然尝试对脚本执行 HEAD 请求,当我没有连接到我们的 Nexus 存储库(脚本托管的地方)时失败)。所以我最终在每个settings.gradle.kts
文件中都有一些样板,但这还不错!
【参考方案1】:
如果您想从 Gradle Kotlin DSL 的所有优点中受益,您应该努力使用 plugins
块来应用所有插件。见https://github.com/gradle/kotlin-dsl/blob/master/doc/getting-started/Configuring-Plugins.md
您可以在设置文件中管理插件存储库和解析策略(例如它们的版本)。从 Gradle 4.4 开始,这个文件可以使用 Kotlin DSL 编写,也就是 settings.gradle.kts
。见https://docs.gradle.org/4.4-rc-1/release-notes.html。
考虑到这一点,您可以拥有一个集中的 Settings
脚本插件,用于设置并将其应用到您的构建 settings.gradle.kts
文件中:
// corporate-settings.gradle.kts
pluginManagement
repositories
maven
name = "Corporate Nexus"
url = uri("https://example.com/repository/maven-public")
gradlePluginPortal()
和:
// settings.gradle.kts
apply(from = "https://url.to/corporate-settings.gradle.kts")
然后在您的项目构建脚本中,您可以简单地从您的公司存储库中请求插件:
// build.gradle.kts
plugins
id("my-corporate-plugin") version "1.2.3"
如果您希望多项目构建中的项目构建脚本不重复插件版本,您可以使用Gradle 4.3 通过在根项目中声明版本来实现。请注意,如果您需要所有构建都使用相同的插件版本,您也可以使用 pluginManagement.resolutionStrategy
在 settings.gradle.kts
中设置版本。
另请注意,要使所有这些工作,您的插件必须使用其plugin marker artifact 发布。这可以通过使用java-gradle-plugin
插件轻松完成。
【讨论】:
我还没有时间尝试这个,但是赏金已经到了,我觉得这是最符合我问题精神的答案。享受你的 500 分 :) :) 一旦你有机会实现这个功能,请回来提供一些反馈 嗨,保罗,我终于有时间回来发布我的结果了 - 请参阅下面的答案。基本上我接受了你的建议,减去了从 URL 应用(因为它使测试插件更新变得更加困难,并且引入了对与 Nexus 的连接的依赖,因为那是我托管脚本的地方)。【参考方案2】:我向 @eskatos 保证我会回来并就他的回答提供反馈 - 所以就在这里!
我的最终解决方案包括:
每个项目的 Gradle 4.7 包装器(指向 Nexus 中 http://services.gradle.org/distributions 设置的镜像作为原始代理存储库,即它是普通的 Gradle,但通过 Nexus 下载) 自定义 Gradle 插件与插件标记一起发布到我们的 Nexus 存储库(由 Java Gradle Plugin Development Plugin 生成) 在我们的 Nexus 存储库中镜像 Gradle 插件门户(即指向 https://plugins.gradle.org/m2 的代理存储库) 每个项目都有一个settings.gradle.kts
文件,用于将我们的 maven 存储库和 gradle 插件门户镜像(都在 Nexus 中)配置为插件管理存储库。
settings.gradle.kts
文件包含以下内容:
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")
这意味着所有插件及其依赖项现在都通过 Nexus 代理,并且随着插件标记也发布到 Nexus,Gradle 将通过 id 找到我们的插件。在其中添加mavenLocal
也有助于在本地轻松测试我们的插件更改。
每个项目的根build.gradle.kts
文件然后应用插件如下:
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...
并配置 gradle 包装器以使用我们的镜像发行版,当与上述内容结合时,这意味着一切(gradle、插件、依赖项)都来自 Nexus):
tasks
"wrapper"(Wrapper::class)
distributionUrl = "https://nexus.example.com/repository/gradle-distributions/gradle-4.7-bin.zip"
我希望使用@eskatos 的建议来避免设置文件中的样板,即从settings.gradle.kts
中的远程URL 应用脚本。即
apply from("https://nexus.example.com/repository/maven-public/com/example/gradle/corporate-settings/1.2.3/corporate-settings-1.2.3.kts"
我什至设法生成了一个模板化脚本(与我们的插件一起发布):
配置插件仓库(如上面的设置脚本) 如果请求的插件 ID 是我们的插件之一并且未提供版本(因此您可以通过 ID 应用它们),则使用解析策略应用与脚本关联的插件版本然而,即使它删除了样板,这意味着我们的构建依赖于与我们的 Nexus 存储库的连接,因为似乎即使从 URL 应用的脚本被缓存,Gradle 还是会发出 HEAD 请求来检查变化。在本地测试插件更改也很烦人,因为我必须手动将其指向本地 maven 目录中的脚本。使用我当前的配置,我可以简单地将插件发布到本地 maven 并更新我的项目中的版本。
我对当前的设置非常满意 - 我认为现在对开发人员来说插件的应用方式更加明显。由于两者之间没有依赖关系(并且不需要自定义 gradle 分发),因此独立升级 Gradle 和我们的插件变得更加容易。
【讨论】:
【参考方案3】:我一直在我的构建中做这样的事情
buildscript
project.apply
from("$rootProject.projectDir/sharedValues.gradle.kts")
val configureRepository: (Any) -> Unit by extra
configureRepository.invoke(repositories)
在我的sharedValues.gradle.kts
文件中,我有这样的代码:
/**
* 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
我遵循类似的模式来配置插件的解析策略。
这种模式的好处是您在sharedValues.gradle.kts
中配置的任何内容也可以从您的buildSrc
项目中使用,这意味着您可以重用存储库声明。
更新:
您可以从 URL 应用另一个脚本,例如这样做:
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")
只需在某个 http 服务器上托管您希望所有构建共享的脚本(强烈建议使用 HTTPS,这样您的构建就不会被中间人攻击)。
这样做的缺点是我认为从 url 应用的脚本不会被缓存,因此每次运行构建时都会重新下载它们。 这可能现在已经解决了,我不确定。
【讨论】:
嗯,这与我目前使用的 workaround 类似——除了我的样板文件在 initscript (在我的自定义发行版中)而不是在每个代码库的buildSrc
中定义一次。
嗨乔纳森,我已经更新了我的问题。希望它让我们更清楚地看到我们都有一个非常相似的方法——除了你通过extra
(可以远程托管并缓存在新版本的 Gradle 中)从你的 sharedValues
脚本配置东西,我从一个通过扩展初始化脚本插件。我希望找到更好的解决方案 - 我对我们的任何解决方案都不满意(抱歉!)。【参考方案4】:
当我遇到类似问题时,Stefan Oehme 向我提供的解决方案是提供我自己的自定义 Gradle 发行版。据他说,这在大公司是很常见的。
只需创建 gradle 存储库的自定义分支,使用此自定义版本的 gradle 将您公司的特殊调味料添加到每个项目中。
【讨论】:
对我来说,这是最后的手段。它肯定会奏效,正如我们在 Slack 上讨论的那样,可能是像 Netflix 这样的大公司可能如何解决这个问题,但是维护开销对我来说是反对这种方法的一个重要问题(必须不断维护一个如此活跃的分支回购)。使用我目前的方法,我只需在每次 Gradle 更新并将我的新自定义 dist 发布到 Nexus 时提升版本 - 我不必合并,也不必从源代码构建。【参考方案5】:在每个项目中复制通用配置时,我遇到了类似的问题。通过使用 init 脚本中定义的通用设置的自定义 gradle 分发解决了这个问题。
创建了一个 gradle 插件来准备此类自定义分发 - custom-gradle-dist。它非常适合我的项目,例如一个库项目的 build.gradle 看起来像这样(这是一个完整的文件):
dependencies
compile 'org.springframework.kafka:spring-kafka'
【讨论】:
以上是关于使用 Gradle Kotlin DSL 在 Gradle 中进行样板项目配置的主要内容,如果未能解决你的问题,请参考以下文章
使用 Gradle Kotlin DSL 发布 Kotlin MPP 元数据