devops

Gradle总结

2020-02-11  本文已影响0人  chandarlee

本篇参考Gradle官方文档,主要是从Android开发者的视角,介绍在使用Gradle进行构建的过程中涉及到的一些基础概念!不对之处,敬请指正。下面直接切入正题!

脚本语言
脚本类型

gradle在构建时,会对脚本文件进行编译,生成对应的脚本对象。如将gradle脚本编译成一个实现Script接口的对象;将build.gradle.kts编译成KotlinBuildScript类型对象,将settings.gradle.kotlin编译成KotlinSettingsScript类型对象。因此,在脚本文件中,可以访问对应脚本对象中声明的属性与方法。

基本概念

关于settings.gradle文件,有以下几点需要注意:

  1. 单项目构建该文件可选,但多项目构建这个文件是必须的,因为需要在这个文件中声明哪些子项目需要参与构建,也包括子项目路径、名称等
  2. Gradle允许在任意子项目中进行多项目的构建,那Gradle如何决定此次构建是多项目还是单项目构建呢?如果该构建目录中存在settings.gradle文件,那么就依据该文件来进行构建;如果不存在该文件,那么会向上一级目录查询文件是否存在(注意:只会向父目录查询,而不是一直向上级目录递归查询),如果存在就依据该文件进行构建,否则此次构建就是一个单项目的构建。因此,如果需要在多项目的一个工程目录结构中进行单项目的构建,我们可以在目标子项目的根目录下创建一个settings.gradle文件
构建流程
// include two projects, 'foo' and 'foo:bar'
// directories are inferred by replacing ':' with '/'
include 'foo:bar'  //(1)

// include one project whose project dir does not match the logical project path
include 'baz'  //(2)
project(':baz').name = ‘myBaz’
project(':baz').projectDir = file('foo/baz')

// include many projects whose project dirs do not match the logical project paths
file('subprojects').eachDir { dir ->
  include dir.name       //(3)
  project(":${dir.name}").projectDir = dir
}

//add a project with path :flatProject, name flatProject and project directory $rootDir/../flatProject
includeFlat('flatProject') //(4)

(1)include方法参数为projectpath,非文件路径,因此不能包含传统的目录分隔符号'/',取而代之使用需要使用冒号:分隔(在project path中冒号代表的是root project);projectpath的最后一个节点作为project名称,且默认情况下gradle会将projectpath转为相对于rootProject的路径。如foo:barprojectpath,会对应两个project,分别为foofoo:bar,且工程根路径分别为$rootProjectDir/foo$rootProjectDir/foo/bar
(2) 添加了一个baz工程,名称自定义为myBaz,默认此工程的根目录为$rootProjectDir/baz,但此目录不存在,gradle允许我们修改此路径,这里我们修改为$rootProjectDir/foo/baz
(3) 遍历subproject目录下的所有子目录,添加为一个project,并设置projectdir;
(4) includeFlat方法也能添加一个project,但查找工程projectDir的策略与include不一致,它会查找相对于rootProject父目录下的文件路径。因此,这里flatProject的工程路径为$rootProject/../flatProject

软件模块:Module

这里的Module并不是指Android工程中的Module,而是指向一个依赖,是一个可以随着时间不断更新的软件模块,如Google Guava库。每一个Module都有一个名称module name,随着时间的推移,模块会不断改善并重新发布,而模块的每一次发布都会有一个版本号,使用module version来描述。Module一般会被组织到Reposity中,通过module name以及module version可以精确定位到该模块。

产物:Artifact

产物特指由一次build生成的一个文件或一个目录,如一个Jar、Zip、AAR等。产物的生成就是为了给其他用户或Project使用的。

伴随着Module的每一次发布,都会有相应的产物,称之为artifact,一个模块可以产出多个产物,如.jar包、.aar包等,而每一个artifact都可以有自己独立的Transitive Dependencies。这些产物都会有对应的描述信息,保存在module的metadata文件中(如maven repository中该文件为pom.xml)。在进行Module依赖解析时,Gradle会根据需要从repository中选择合适的产物下载使用,且默认情况下还会自动解析Transitive Dependencies。
此外,Configuration也可以有相应的产物,这样在声明依赖时,就可以指定具体使用哪个configuration artifact了。如:implementation(project(path = ":api", configuration = "spi"))

val myJar by tasks.registering(Jar::class)
val someFile = file("$buildDir/somefile.txt")
artifacts {
  add("taskArtifact", myJar) //taskArtifact指向myJar
  add("fileArtifact", someFile)
}

何以为家,Module的容器:Repository

通常情况下,依赖是以modules的方式存在和引用的,在声明依赖时,我们需要告知Gradle上哪去获取这些依赖。依赖保存的位置或路径称之为repository。 类型主要有:

在构建脚本中,我们需要使用repositories{}DSL声明这些repository的位置,它可以指向本地或远程的仓库。如下所示:

buildscript {
  repositories {
    google() -- (1)
    jcenter() -- (2)
    flatDir("name" to "libs", "dirs" to "libs") -- (3)
  }
  dependencies {
    //dependency declaration
  }
}

脚本中我们声明了依赖查找的3个目标仓库,(1)是google的仓库,(2)是jcenter仓库、(3)则是通过flatDir方法传入本地文件系统的路径来指定本地仓库,这里创建了一个本地reposity,命名为libs,路径为$rootProject/libs。(Android工程中经常使用flatDir来包含本地的aar依赖包)

在构建过程中,Gradle会在声明的reposity仓库中查找定位我们声明的依赖,从远程下载或从本地目录、仓库获取这些依赖的产物用于构建,并保存在本地缓存中。这个过程称为依赖解析。Gradle根据声明的依赖,按Repository声明的顺序,从上到下依次查找。只要找到了一项就返回,不会继续往下查找!因此,需要注意Repository声明的顺序。

物以类聚,依赖的组织形式: Configuration

Gradle允许针对不同的构建场景声明不同的依赖集合;如在编译打包发布时,我们不希望把测试用例的代码,以及跑测试用例需要依赖到的其他三方库与代码一起编译打包。因此,我们需要把这两种使用场景进行划分,不同的场景各自声明自己感兴趣的依赖,这样在不同场景下构建时,可以只解析和使用自己声明的依赖,避免了代码的混乱,也有利于代码的组织管理以及维护。在Gradle中,Configuration是一组有名称的依赖的集合,代表了该依赖组的一个使用场景(或作用域),如implementation configuration是编译project所需依赖的集合,testImplementation configuration是编译测试用例所需依赖的集合。Gradle要求每一个依赖都必须显示的指定其所属的Configuration。
Gradle框架本身以及我们日常引用的各类插件都会预先根据不同的使用场景定义出不同的configuration供使用,如Android插件中包含的implementationapicompileOnlyruntimeOnly等,当然我们也可以进行自定义。

val bar by configurations.registering //委托属性创建
configurations {
    create("baz") //直接创建
    bar.get().extendsFrom(getByName("implementation")) //继承

    providedCompile
    compile.extendsFrom providedCompile
}
dependencies {
  implementation "group:module:version"
  testImplementation "group:module1:version"
  bar "group:module2:version"
}

我们在日常开发中使用较多的mtl-publishmaven发布插件,其实也自定义了一个名为providedCompile的Configuration,并将compile从其继承,如上代码片段所示。
由于存在以上的继承关系,因此在构建过程中providedCompile与compile的作用其实是一样的,都能引用到依赖库aar包中的代码和资源文件
但在生成pom描述文件时(该文件中收集了该模块的所有三方依赖),会将所有providedCompile的依赖修改为provided依赖!
(注:如果一开始就使用provided,那么在开发阶段将无法引用到三方依赖库aar包中的资源,因此在引用依赖库资源时编译会失败)

你依赖我,我依赖它:Transitive Dependency

Module一般会提供额外的元数据,来详细描述该module的详细信息(如.module、.pom、ivy.xml文件)。如该module在repository中的坐标信息(group、name、version),作者author信息等。Module之间也可以相互依赖,因此在这些元数据中,包含一类特殊的数据,用于声明该module依赖的其他module,如JUnit 5 platform module需要依赖platform commons module。这些依赖称之为传递依赖(transitive dependencies),默认情况下,Gradle会自动解析和使用该module声明的其他module依赖,当然我们也可以针对性的配置这些依赖的解析规则。

依赖类型

dependencies {
  runtimeOnly(group: 'org.springframework', name: 'spring-  core', version:'2.5') {
      because("demenstrate the reason we pick this version")
      isTransitive = true //是否解析传递依赖,默认为true
      isChanging = true //是否为可变版本,过期后会重新获取
      isForce = false //当有依赖冲突时,是否强制使用该版本依赖
      exclude(group="name", name="name") //解析该依赖的transitive dependency时,不解析被exclude的部分
  }
}
dependencies {
  //files构建一个FileCollection对象,包含了相对于当前脚本目录的 libs/a.jar和libs/b.jar两个依赖文件
  runtimeOnly(files("libs/a.jar", "libs/b.jar"))
  //通过libs路径构建一个目录树对象FileTree,该类继承于FileCollection,通过include匹配该目录下的所有.jar文件作为依赖
  runtimeOnly(fileTree("libs") { include("*.jar") })
  implementation files("$buildDir/classes") {
  //通过builtBy指定文件依赖的产物是由compile这个task生成的
  //因此,会优先执行该task生成文件依赖
      builtBy 'compile'
  }
}

依赖声明

dependencies {
  implementation 'group:name:5.0.2.RELEASE'
}
dependencies {
  implementation 'group:name'
}
dependencies {
  constraints {
    implementation 'group:name:5.0.2.RELEASE'
  }
}
dependencies {
  implementation 'group:name:5.+'
}
dependencies {
  implementation("org.slf4j:slf4j-api") {
    version {
      strictly("[1.7, 1.8[")
      prefer("1.7.25")
    }
  }
}
dependencies {
  //min指定变种为已混淆;@js指定扩展名
  js 'jquery:jquery:3.2.1:min@js'
}

依赖解析

Gradle允许我们自定义依赖解析的规则,可以帮助我们解决依赖冲突。说明如下:

dependencies {
    //version scheme
    implementation("group:name:default")
}

configurations.all { //this: Configuration
    resolutionStrategy.eachDependency { //this: DependencyResolveDetails
        //1. change dependency version
        if (requested.group == "group") {
            useVersion("1.2")
            because("why")
        }
        //2.deal with version scheme
        if (requested.group == "group" && requested.version == "default") {
            useVersion("1.1")
            because("why")
        }
        //3.change group/name
        if (requested.group == "group") {
            useTarget("group2:name2:1.1")
            because("why")
        }
    }
}
configurations.all { //this: Configuration
    resolutionStrategy.dependencySubstitution { //this: DependencySubstitutions
        //1. substitute module dependency with project dependency
        substitute(module("org.utils:api")).apply {
            with(project(":projectA"))
            because("tell me why")
        }
        //2. substitute project dependency with module dependency
        substitute(project(":projectC")).apply {
            with(module("org.utils:api:1.3"))
            because("tell me why")
        }
    }
}
configurations {
    implementation {
        withDependencies {//this:DependencySet
            val dep = find { it.name == "to-modify" } as ExternalModuleDependency
            dep.version {
                strictly("1.2")
            }
            dep.isChanging = false
            dep.isTransitive = true
            dep.exclude("group", "module")
        }
    }
}

依赖检查与构建检查

通过不同方式,可以输出Project的依赖列表、依赖关系以及依赖冲突如何解决,以及最终所解析至的依赖版本。

./gradlew dependencies --configuration implementation
configurations {
    create("scm")
}
dependencies {
    "scm"("junit:junit:4.12")
}
./gradlew dependencyInsight --configuration scm --dependency junit:junit 

如何解决依赖冲突

configurations.all {
  resolutionStrategy {
    failOnVersionConflict()
    preferProjectModules()
  }
}

考虑如下场景:A 模块依赖 B1 和 C, D 模块依赖了 B2,其中 B1 和 B1 是同一个Module B的两个不同版本;同时,工程中我们同时依赖了 A 和 D。这种情况下,会存在针对Module B的依赖冲突!默认情况下Gradle会尝试帮我们解决依赖冲突,解决的方式是使用最新的版本;这里的最新不是判断版本号大小,应该是根据发布时间来决定!现在我们自己手动进行依赖冲突的解决:

(1) 方案一:针对 A 或 D 配置 transitive。这里针对A配置,不解析A模块的传递依赖,因此依赖中不再包含 B1 和 C,这里需要手动添加依赖 C
dependencies {
    implementation A {
      transitive = false
    }
    implementation C 
    implementation D {
      //transitive = false
    }
}
(2) 方案二:针对 A 或 D 配置 exclude规则
dependencies {
    implementation A {
      exclude  B1
    }
    implementation D {
      //exclude  B2
    }
}
configurations.all {
    resolutionStrategy {
       force B1
       // force B2 
    }
}
启用插件
//groovy dsl
apply from: 'other.gradle'
apply from: 'http://www.example.com/other.gradle'

//kotlin dsl
apply(from="other.gradle.kts")
apply(from="http://www.example.com/other.gradle.kts")
//rootProject: build.gradle.kts
buildscript {
    repositories {
      google()  // (1)
    }
    dependencies {
      classpath ‘com.android.tools.build:gradle:3.4.0’ //(2)
    }
}

//appProject: app/build.gradle.kts
plugins {
  id("com.android.application")  //(3)
}
//apply(plugin="com.android.application")

在主工程目录下的build.gradle.kts文件中,一般都会包含如上的片段代码:使用buildscript{}块来声明脚本运行时的依赖。repositories{}块声明工程中依赖查找的仓库,这里声明了google()仓库,脚本中使用的依赖都将首先尝试从该reposity中进行解析获取;在(2)中又声明了classpath依赖,指向了com.android.tools.build:gradle:3.4.0。意思就是将这个依赖包(从google仓库解析并下载jar包)添加到脚本文件编译执行的classpath中去。这样一来,在脚本文件中我们就可以import以及使用这个依赖包(jar包)中包含的相关类和方法、属性了。比如在(3)的位置,我们启用了com.android.application这个插件,这个插件的代码其实就是包含在com.android.tools.build:gradle:3.4.0这个依赖声明中的。

project and task path
apply方法解析

apply方法的作用主要有:

如下面的示例:

task configure {
  doLast {
  def pos = new java.text.FieldPosition(10)
  // Apply the script
  apply from: 'other.gradle', to: pos
  println pos.beginIndex
  println pos.endIndex
  }
}
// Set properties.
beginIndex = 1
endIndex = 5

apply from: 'other.gradle', to: pos语句中,apply允许我们传递一个map对象。在这个map中,我们使用from这个key声明被应用的原始脚本文件为other.gradle,使用to这个key声明委托对象为pos。这样一来在other.gradle中的方法或属性都将以pos这个类型为FieldPosition的对象作为委托对象,脚本中访问的未声明方法和属性会在delegate对象中寻找,如beginIndex和endIndex其实是访问了委托对象的属性。该Feature在KotlinDSL中尚未得到支持。

SourceSet

Gradle Java Support引入了SourceSet的概念来构建基于源代码的Project,因为我们一般都是以类型来对源代码以及资源文件进行分类和组织,如应用代码、单元测试代码、集成测试代码,它们通常被分开并放在不同的目录下。每一个逻辑分组可以有自己的一组源代码文件依赖、classpath。SourceSet通常涉及以下几个和编译相关的方面:

Gradle会自动为定义的每一个SourceSet生成一些编译Task以及Dependency Configuration。如compile<SourceSet>Java 、process<SourceSet>Resources、<SourceSet>Implementation等,除了main这个SourceSet外。main是一个特殊的SourceSet,用于工程的主生产代码(used for the project’s production code)。

The main source set

Most language plugins, Java included, automatically create a source set called main,
which is used for the project’s production code. This source set is special in that its
name is not included in the names of the configurations and tasks, hence why you
have just a compileJava task and compileOnly and implementation configurations
rather than compileMainJava, mainCompileOnly and mainImplementationrespectively。

除了main这个SourceSet外,一般插件还会生成其他的SourceSet,如Android的插件还提供了testandroidTest,分别用于跑测试用例和android的测试用例。我们可以配置sourceSet配置额外的代码、资源路径、exclude规则等

sourceSets {
  main {
    java {
      srcDir 'thirdParty/src/main/java'
    }
  }
}

SourceSet Properties

configuration与artifacts
subprojects {
  apply(plugin = "java")
  group = "org.gradle.sample"
  version = "1.0"
}
project(":api") {
  configurations {
    create("spi") -----------(1)
  }
  dependencies {
    "implementation"(project(":shared"))
  }
  tasks.register<Jar>("spiJar") {
    archiveBaseName.set("api-spi")
    from(project.the<SourceSetContainer>()["main"].output)
    include("org/gradle/sample/api/**")
  }
  artifacts {
    add("spi", tasks["spiJar"]) -------(2)
  }
}
project(":services:personService") {
  dependencies {
    "implementation"(project(":shared"))
    "implementation"(project(path = ":api", configuration = "spi")) ---------(3)
    "testImplementation"("junit:junit:4.12")
    "testImplementation"(project(":api"))
  }
}

关于FlatDir

flatDir可以指定一个本地的目录作为一个备选的仓库,Gradle在搜索该仓库,查找对应的Module;在下面的示例中,我们指定aars目录作为一个本地仓库,其中包含名为'moduleA.aar'文件以及'moduleB.aar'文件。在(1)的依赖声明中,我们指定依赖的name为moduleA、扩展名为'aar', Gradle最终会解析到aars目录下的'moduleA.aar'文件。在(2)的依赖声明中,我们使用常规的'group:name:version@ext'声明,最终Gradle也会解析到aars目录下的'moduleB.aar'文件。因为,使用flatDir指定本地仓库时,Gradle会忽略group或者version部分,只要aars目录中存在'moduleB.aar'或者'moduleB-1.0.0.aar'这样的文件,Gradle都会成功解析到该依赖。

  repositories {
    flatDir {
            dirs 'aars'
        }
  }
  dependencies {
    implementation(name: 'moduleA', ext: 'aar') // (1)
    implementation('groupname.fake:moduleB:1.0.0') //(2)
  }
上一篇下一篇

猜你喜欢

热点阅读