31

我目前正在尝试改进我们的项目共享配置的方式。我们所有的库和微服务(即许多 git repos)都有许多不同的多模块 gradle 项目。

我的主要目标是:

  • 不要在每个项目中重复我的 Nexus 存储库配置(另外,我可以放心地假设 URL 不会更改)
  • 为了使我的自定义 Gradle 插件(发布到 Nexus)可用于每个项目,且样板/重复最少(它们应该可用于每个项目,并且项目唯一关心的是它使用的版本)
  • 没有魔法 - 开发人员应该很清楚一切是如何配置的

我当前的解决方案是一个自定义的 gradle 发行版,它带有一个 init 脚本:

  • mavenLocal()我们的 Nexus 存储库添加到项目存储库(非常类似于 Gradle初始化脚本文档示例,除了它添加存储库并验证它们)
  • 配置一个扩展,允许将我们的 gradle 插件添加到 buildscript 类路径中(使用此解决方法)。它还将我们的 Nexus 存储库添加为 buildscript 存储库,因为这是托管插件的地方。我们有很多插件(基于 Netflix 优秀的nebula 插件构建)用于各种样板:标准项目设置(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在 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"))
    }
}

但是,Gradle 在buildscript块中使用时无法看到我的函数(无法编译脚本)。不过,在正常的项目存储库/依赖项中使用它们效果很好(它们是可见的并且按预期工作)。

如果这可行,我希望将 jar 捆绑到我的自定义发行版中,这意味着我的 init 脚本可以只进行简单的验证,而不是隐藏神奇的插件和 repo 配置。扩展功能不需要更改,因此在插件更改时不需要发布新的 Gradle 发行版。

我尝试了什么:

  • 将我的 jar 添加到测试项目的 buildscript 类路径(即buildscript.dependencies) - 不起作用(也许这在设计上不起作用,因为buildscript在同一个块中添加依赖项似乎不正确)
  • 将功能放入buildSrc(这适用于正常的项目部门/回购但不是buildscript,但不是真正的解决方案,因为它只是移动样板)
  • 将 jar 放到lib发行版的文件夹中

所以我的问题真的归结为:

  • 我想要实现的目标是否可行(是否可以使自定义类/函数对buildScript块可见)?
  • 是否有更好的方法来配置企业 Nexus 存储库并使自定义插件(发布到 Nexus)在许多单独的项目(即完全不同的代码库)中可用,且样板配置最少?
4

5 回答 5

13

如果您想从 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通过在根项目中声明版本来实现。请注意,如果您需要所有构建使用相同的插件版本,您也可以设置settings.gradle.kts使用pluginManagement.resolutionStrategy的版本。

另请注意,要使所有这些工作,您的插件必须与其插件标记 artifact一起发布。这很容易通过使用java-gradle-plugin插件来完成。

于 2017-11-22T08:27:11.463 回答
5

我向@eskatos 承诺,我会回来并就他的回答提供反馈——就在这里!

我的最终解决方案包括:

  • 每个项目的 Gradle 4.7 包装器(指向 Nexus 中http://services.gradle.org/distributions设置的镜像作为原始代理存储库,即它是香草 Gradle,但通过 Nexus 下载)
  • 与插件标记一起发布到我们的 Nexus 存储库的自定义 Gradle 插件(由Java Gradle 插件开发插件生成)
  • 在我们的 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. IE

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 和我们的插件变得更加容易。

于 2018-06-03T12:22:13.927 回答
2

我一直在我的构建中做这样的事情

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 应用的脚本不会被缓存,因此每次运行构建时都会重新下载它们。这可能现在已经解决了,我不确定。

于 2017-05-10T15:44:57.350 回答
1

当我遇到类似问题时,Stefan Oehme 提供给我的解决方案是提供我自己的 Gradle 自定义发行版。据他说,这在大公司是很常见的事情。

只需创建 gradle repo 的自定义分支,使用此自定义版本的 gradle 将您公司的特殊调味料添加到每个项目中。

于 2017-11-19T18:27:19.297 回答
0

当在每个项目中复制通用配置时,我遇到了类似的问题。通过使用 init 脚本中定义的通用设置的自定义 gradle 分发解决了这个问题。

创建了一个用于准备此类自定义发行版的 gradle 插件 - custom-gradle-dist。它非常适合我的项目,例如库项目的build.gradle看起来像这样(这是一个完整的文件):

dependencies {
    compile 'org.springframework.kafka:spring-kafka'
}
于 2018-10-19T23:37:43.750 回答