我目前正在努力改进我们的项目共享其配置的方式.我们有很多不同的多模块Gradle项目,适用于我们所有的图书馆和微服务(即许多git repos).

我的主要目标是:

  • To not have my Nexus repository config duplicated in every project (also, I can safely assume that the URL won't change)
  • 使我的自定义Gradle插件(发布到Nexus)对每个项目都可用,最大限度地减少样板/重复(它们应该对每个项目都可用,而项目唯一关心的就是它正在使用的版本)
  • 没有魔法——开发人员应该清楚地知道所有东西是如何配置的

我目前的解决方案是一个定制的gradle发行版,带有一个init脚本:

  • adds mavenLocal() and our Nexus repository to the project repos (very similar to the Gradle init script documentation example, except it adds repos as well as validating them)
  • 配置一个扩展,允许我们的gradle插件添加到buildscript类路径(使用this workaround).它还将我们的Nexus repo添加为buildscript repo,因为这是插件托管的地方.我们有很多插件(基于Netflix的优秀nebula plugins构建)用于各种样板:标准项目设置(kotlin设置、测试设置等)、发布、发布、文档等,这意味着我们的project 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
                }
            }
        }

    }

}

然后,我通过向根构建添加以下内容来配置每个新项目.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'
    }
}

While this approach works, allows reproducible builds (unlike our previous setup which applied a build script from a URL - which at the time wasn't cacheable), and allows working offline, it does make it a little magical and I was wondering if I could do things better.

This was all triggered by reading a comment on Github by Gradle dev Stefan Oehme stating that a build should work without relying on an init script, i.e. init scripts should just be decorative and do things like the documented example - preventing unauthorised repos, etc.

我的 idea 是编写一些扩展函数,允许我将Nexus repo和插件添加到一个构建中,就像它们被内置到gradle中一样(类似于gradle Kotlin DSL提供的扩展函数gradleScriptKotlin()kotlin-dsl()).

So I created my extension functions in a kotlin gradle project:

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块中使用时无法看到我的函数(无法编译脚本).不过,在正常的项目回购/依赖中使用它们效果很好(它们是可见的,并且按照预期工作).

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

我try 的是:

  • adding my jar to the test project's buildscript classpath (i.e. buildscript.dependencies) - doesn't work (maybe this doesn't work by design as it doesn't seem right to be adding a dependency to buildscript that's referred to in the same block)
  • putting the functions in buildSrc (which works for normal project deps/repos but not buildscript, but is not a real solution as it just moves the boilerplate)
  • 将jar放入分发版的lib文件夹中

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

  • 我试图实现的目标可能吗(是否可以使自定义类/函数对buildScript000个挡路可见)?
  • Is there a better approach to configuring a corporate Nexus repo and making custom plugins (published to Nexus) available across lots of separate projects (i.e. totally different codebases) with minimal boilerplate configuration?

推荐答案

我向@eskatospromise ,我会回来对他的答案给出反馈——就是这样!

我的最终解决方案包括:

  • Gradle 4.7 wrapper per project (pointed at a mirror of http://services.gradle.org/distributions setup in Nexus as a raw proxy repository, i.e. it's vanilla Gradle but downloaded via Nexus)
  • 定制Gradle插件发布到我们的Nexus repo以及插件标记(由Java Gradle Plugin Development Plugin生成)
  • 在我们的Nexus repo中镜像Gradle插件门户(即指向https://plugins.gradle.org/m2的代理repo)
  • 每个项目settings.gradle.kts个文件,将我们的maven repo和gradle插件门户镜像(都在Nexus中)配置为插件管理存储库.

The settings.gradle.kts file contains the following:

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代理,Gradle将根据id找到我们的插件,因为插件标记也会发布到Nexus.同时拥有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"
    }
}

I was hoping to avoid the boilerplate in the settings files using @eskatos's suggestion of applying a script from a remote URL in settings.gradle.kts. i.e.

apply { from("https://nexus.example.com/repository/maven-public/com/example/gradle/corporate-settings/1.2.3/corporate-settings-1.2.3.kts" }

I even managed to generate a templated script (published alongside our plugins) that:

  • 已配置插件报告(与上面的设置脚本相同)
  • 如果请求的插件id是我们的插件之一,并且没有提供版本,则使用解析策略应用与脚本关联的插件版本(因此您可以通过id应用它们)

然而,即使它删除了样板文件,这意味着我们的构建依赖于与Nexus repo的连接,因为似乎即使从URL应用的脚本被缓存,Gradle仍然会执行HEAD请求以判断更改.在本地测试插件更改也很烦人,因为我必须手动将其指向本地maven目录中的脚本.使用当前配置,我可以简单地将插件发布到maven local,并在项目中更新版本.

我对目前的设置相当满意--我认为现在对开发人员来说,插件是如何应用的要明显得多.现在Gradle和我们的插件之间没有依赖关系(也不需要定制的Gradle发行版),这使得独立升级Gradle和我们的插件变得容易得多.

Kotlin相关问答推荐

在没有外部 map 的情况下转换列表项

Lambda和普通Kotlin函数有什么区别?

如何在Jetpack Compose中从领域查询中读取数据?

为什么onEach不是挂起函数,而Collect是?

关键字';在When Kotlin When-语句中

&x是T&q;和&q;(x为?T)!=空(&Q;)?

在kotlin中匹配多个变量

Kotlin 列表扩展功能

高效匹配两个 Flux

kotlin 单例异常是好是坏?

Moshi:解析单个对象或对象列表(kotlin)

Map.mapTo 到另一个map

如何在 android jetpack compose 中相互重叠列表项?

如何从定义它们的类外部调用扩展方法?

Kotlin 是如何编译的?

Kotlin中具有多个参数的绑定适配器

在 Kotlin 中返回函数有不那么丑陋的方法吗?

带有注释为@RegisterExtension的字段的 JUnit 5 测试在 Kotlin 中不起作用

如何在 kotlin 中创建重复对象数组?

Kotlin 中的限制函数