gradle で共通の設定をまとめる

バージョン

  • Gradle 6.8

この記事の目的

gradle で共通の設定をまとめたい場合にとれる方法について列挙。 実際どれがいいかについては今の利用状況によるので何とも言えないのでそこまでは書かない。

内容

gradle の buildscript で記述を共通化したい

自分の gradle プロジェクトで multi-project を採用しており複数の project が存在するとき、 buildscript で何度もおなじようなことを書くことがよくあり、このあたりを共通化したい場合がある。

例えば java project で spring boot で開発してるので、 lombok gradle plugin と spring boot gradle plugin は毎回設定し、 annotationProcessor configuration に spring-boot-annotation-processor を適用し、 implememtation configuration に spring-boot-starter を追加している、などである。

この時取れる方法は以下の2つ。

  • allprojects や subprojects に書いて project tree に対してまとめて書く
  • plugin で書く

本家ドキュメントの記載を見ると下記の通り。

Sharing Build Logic between Subprojects

前者は cross project configuration と呼ばれており、 gradle のドキュメントでは良くない書き方とされている。 前述のリンク先には直接記述はないが kotlin DSL を使う場合でも問題があり、 type-safe accessor が動かなくなる。*1

後者は convention plugin と呼ばれており、gradle のドキュメントでは推奨された書き方となっている。

以下この convention plugin について考える。

Convention Plugin を書く

この convention plugin を開発するためにはどうすればいいかを考える。 gradle のドキュメントでは色々な場所に目的に合わせて色々書いてあってわかりにくいが、ここでの話ではそのプロジェクトにおける buildscript の共通化を目的としているので基本的には buildSrc 以下に置く 。*2 buildSrc 以下に置く場合でも開発方法としては以下の三種類がある。

  • 通常の java/kotlin プロジェクトとして開発する
  • script plugin として script を 直接 apply する形で利用されるもので開発する
  • Precompiled Script plugin を利用して buildscript と同じような記述で開発する

通常の java/kotlin プロジェクトとして開発する

ドキュメント: https://docs.gradle.org/6.8/userguide/custom_plugins.html

昔からある方法で、たいていのプラグインはこの形で実装されている。 ただ簡単に共通化したいといった場合、すでにある buildscript から Plugin クラスを継承したりしないといけないためオーバーテクノロジー感がある。

script plugin として script を 直接 apply する形で利用されるもので開発する

https://docs.gradle.org/6.8/userguide/plugins.html#sec:script_plugins

こちらも昔からある方法で、普通に書いたbuildscript を抽出して共通化する簡単な方法の一つ。

制約

  • 6.8 現在では script plugin で追加された configuration などは typesafe accessor として利用できない。*3
  • 6.8 現在では plugins DSL を使って plugin を適用することができない。*4

Precompiled Script plugin を利用して buildscript と同じような記述で開発する

https://docs.gradle.org/6.8/userguide/custom_plugins.html#sec:precompiled_plugins

結果としてできるライブラリは1番目とほぼ同じだが記述方法とが buildscript と同じ書き方をそのまま利用できる。 最近の gradle init でできる初期構成などはこの方法も含まれている。 *5 このプラグインの書き方ができたときは kotlin DSL しか対応してなかったが最近になって groovy DSL も対応したようなので groovy の場合でもそのままコピペして共通化できるようになった(らしい。確認したことはない)。

その他巨大なプロジェクトの場合の構成例についてのユーザガイドがある。

Structuring Large Projects

その他情報

歴史

release note
Gradle 4.10 Release Notes 4.10 から kotlin DSL を使えるようになる。Get ready for Kotlin DSL 1.0
Gradle 5.0 Release Notes 5.0 で stable Precompiled Script Plugin がサポート Release 1.0.4 · gradle/kotlin-dsl-samples · GitHub
Gradle 6.4 Release Notes 6.4 から groovy を使った precompiled script を利用できるようになった kts に書き換えなくても共通化できるようになった
Gradle 6.7 Release Notes 6.7 から composite build が buildSrc から見えるようになった

リンク

*1: Gradle Kotlin DSL Primer

*2: Organizing Gradle Projects

*3: Gradle Kotlin DSL Primer

*4: Using Gradle Plugins

*5:gradle 6.8 で gradle init すると下記のサンプルのような project が出力され、このリンクも表示される。https://docs.gradle.org/6.8/samples/sample_building_kotlin_applications_multi_project.html

2020 年まとめ

技術編

  • Kotlin コードをだいぶ書いた
  • Spring Boot にそれなりに詳しくなった
  • Gradle にそれなりに詳しくなった
  • apache kafka を触り始めた
  • Rust を少し触った

f:id:nise_nabe:20201231163919p:plain f:id:nise_nabe:20211231124401p:plain

ゲーム編

  • PS4
    • Spider-Man Miles Morales
    • サクナヒメ
  • Switch
  • PC
    • Apex Legends
    • Oxygen Not Included
    • Satisfactry
    • X4 Foundation
    • Shapez.io
    • ARK
    • Graveyard Keeper
    • Outer Wilds
    • Little Witch Nobeta
    • while True: Learn()
    • Transport Fever2
    • Space Haven
    • Cities: Skyline
    • Moon Hunters
    • Project Winter
    • Don't Starve Together
    • Risk of Rain 2

マンガ編

  • 1161 冊買った

家編

  • ウォーキングマシンを買った
  • 象印スチーム加湿器を買った(2台目)
  • プリンターを買った
  • Logicool Ergo E575 を買った
  • ふるさと納税を始めた
  • 税金を調べ始めた

コーヒー編

全部生豆 10kg 級を数回買った

  • ブエノスアイレス農園
  • プロヴィデンシア農園
  • トミオフクダブルボン
  • カフェインレス コロンビア
  • ブルーマウンテン No. 1
  • クリスタルマウンテン
  • エスメラルダ農園 ゲイシ

Java(Jar) で大量クラスを取り扱う時の zip64 について

あるプロジェクトのビルドをしようとしたところ下記のようなエラーがでてきた。 このプロジェクトは gradle のビルドパフォーマンスを調べるために 10 万クラスを適当に作ったプロジェクトである。

Execution failed for task ':jar'.
> archive contains more than 65535 entries.

jar タスクを実行しようとしたとき jar に含められるエントリー数が 65535 を超えているとビルドできないらしい。

軽く検索してみると以下のようなページが出てくる。

java - What is the maximum number of files per jar? - Stack Overflow

この制限は jar アーカイブの話であり、java7 以上であれば zip64 format を使うと一つの jar に 65535 以上のファイルをつかえるらしい。

以下気になったので調べたことをまとめる。

ZIP64 とは

wikipedia を見ると zip64 は zip であり、その拡張領域に zip64 であること、または zip64 の情報が含まれているように見える。 また最近の環境であれば大体サポート対象のため普通に動くようにみえる。

zip の仕様 の version としては 4.5 以上。

ZIP (ファイルフォーマット) - Wikipedia

https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT

多分 fat jar とは関係ないが zipinfo コマンドなどでファイルの中身を見ると fat と書かれている、File Allocation Table の略が出てくる。

File Allocation Table - Wikipedia

Java における zip64

Java SE 7 以上ならばサポートされている様子。

BTS は以下のものの様子。

Bug ID: JDK-4681995 Add support for large (> 4GB) zip/jar files

javadoc をみると zip64 サポートはオプションであり、実装によりそう。

例として adopt openjdk を見るとサポートされている様子。今はもうだいたい Java SE 8 以上だと思うんで 1.6 この辺りは深堀しない。

https://github.com/AdoptOpenJDK/openjdk-jdk/commit/21aa30606a26191e0418ab9fdfd04d9ecfe155a0

Fat Jar における zip64

with Gradle

gradle で build するときには zip64 であるかどうかのオプションを指定することで zip64 形式で jar を生成できる様子。このときの実装は apache ant の ZipOutputStream。 gradle plugin など gradle 側で利用する側の jar が zip64 形式のとき動くかどうかはドキュメントやコードを見ただけではよくわからなかった。 gradle 1.12 以上ならば サポートされている様子。

Jar - Gradle DSL Version 6.7.1

Support zip/jar with more than 64k files & total size >2G by MisterTea · Pull Request #245 · gradle/gradle · GitHub

ZipOutputStream (Apache Ant API)

適当に Web 検索すると gradle buildscript に zip64 オプションを突然入れている記事が見えるので世の中ではよくある要件っぽくみえる。

with Spring Boot

Spring Boot では fully executable jar として jar ファイルを同梱して利用している様子。 この jar ファイルは Spring Boot 側の機能で実現されているようであるため Spring Boot の Loader 自前で頑張って zip64 format を解釈してるように見える。

このサポートは Spring Boot 2.2 から。ただしサポートされているのは同梱するライブラリのほうであり、 Spring Boot Gradle Plugin などで作られる実行可能な jar ファイルについてはサポートされていない。

Spring Boot 2.2 Release Notes · spring-projects/spring-boot Wiki · GitHub

Zip64 files are now supported inside "Fat Jars".

Support zip64 jars by cvienot · Pull Request #16091 · spring-projects/spring-boot · GitHub

Deploying Spring Boot Applications

A zip64-format jar file cannot be made fully executable. Attempting to do so will result in a jar file that is reported as corrupt when executed directly or with java -jar. A standard-format jar file that contains one or more zip64-format nested jars can be fully executable.

このあたりの事情を鑑みるに、Spring Boot Application を実装するプロジェクト上では 65535 以上のファイルを含んだ実行可能ファイルを作ることができない。 そのため一部のコードなどを別のプロジェクトとしてビルドし、それを依存するような構成などにするようなワークアラウンドが必要そう。

with other fat/uber jar

gradle shadow plugin や maven shade plugin などで生成される jar は一回 unzip してその中のクラスファイル等を追加して zip しなおす。 このときの jar はおそらく gradle の jar task に依存するため zip64 option を設定することで zip64 形式の jar を吐き出すことができる様子。

Gradle で Single-Project から Multi-Project への変換

適当に single project で始めた後に事業拡張等で複数のアプリケーションが必要になったり共通化したりしたくなった場合にどのような作業を行うといいかというの検討します。

バージョン情報

  • Gradle 6.5

Gradle で Single-Project から Multi-Project への変換

Gradle での Multi Project とは?

gradle がそのプロジェクトが single か multi かを判別するのは ビルドライフサイクルでの3つあるフェーズのうちの initialize phase のときです1

そしてこれは settings.gradle(.kts) ファイルを見つけ出し、 include で自身以外のプロジェクトが追加されているかどうかで決まります。 Multi Project に含まれるプロジェクトは Root Project や Sub Project と呼ばれます2

Single Project というのはつまり Multi Project における root project をそのまま利用しているという状況なので、これを include で指定した場所にそのまま移動させて sub project として追加できると良さそうです。

Single-Project の開発

例として以下のようなプロジェクトを作成します。build script を Kotlin DSL で記述してありますがここで行う作業では Groovy DSL でも大きくは変わりません。

❯ gradle init --type basic --dsl kotlin --project-name single-to-multi-project

> Task :init
Get more help with your project: https://guides.gradle.org/creating-new-gradle-builds

BUILD SUCCESSFUL in 517ms
2 actionable tasks: 2 executed

以下のコマンドでプロジェクトが root project のみであることが確認できます。

❯ ./gradlew projects

> Task :projects

------------------------------------------------------------
Root project
------------------------------------------------------------

Root project 'single-to-multi-project'
No sub-projects

To see a list of the tasks of a project, run gradlew <project-path>:tasks
For example, try running gradlew :tasks

BUILD SUCCESSFUL in 458ms
1 actionable task: 1 executed

ある程度開発したとして build.gradle.kts を以下のようなものになったとします。

plugins {
  java
  id("org.springframework.boot")
  id("io.spring.dependency-management")
}

repositories {
    mavenCentral()
}

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web")
}

settings.gradle.kts

pluginManagement {
    repositories {
        gradlePluginPortal()
    }

    plugins {
        id("org.springframework.boot") version "2.3.0.RELEASE"
        id("io.spring.dependency-management") version "1.0.9.RELEASE"
    }
}

rootProject.name = "single-to-multi-project"

Single-Project から Multi-Project へ

以下のようにして sub project を作ってみます。 このとき、実体となる build script はこの時点では必要ありません。

❯ echo 'include("single-to-multi-project")' >> settings.gradle.kts 
❯ ./gradlew projects                                    

> Task :projects

------------------------------------------------------------
Root project
------------------------------------------------------------

Root project 'single-to-multi-project'
\--- Project ':single-to-multi-project'

To see a list of the tasks of a project, run gradlew <project-path>:tasks
For example, try running gradlew :single-to-multi-project:tasks

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

ちなみにこの時点での構成は下記であるとします。

❯ tree
.
├── build.gradle.kts
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
    └── main
        └── java
            └── com
                └── example
                    └── ExampleApplication.java

このとき、gradle build を実行すると build/libs 以下に single-to-multi-project.jar という jar が生成されます。

❯ ./gradlew build

BUILD SUCCESSFUL in 769ms
2 actionable tasks: 2 executed

❯ tree build
 build
 ├── classes
 │   └── java
 │       └── main
 │           └── com
 │               └── example
 │                   └── ExampleApplication.class
 ├── generated
 │   └── sources
 │       ├── annotationProcessor
 │       │   └── java
 │       │       └── main
 │       └── headers
 │           └── java
 │               └── main
 ├── libs
 │   └── single-to-multi-project.jar
 └── tmp
     ├── bootJar
     │   └── MANIFEST.MF
     └── compileJava
         └── source-classes-mapping.txt

さてここで本題となる、sub project に中身も移動して multi project を完成させましょう。

❯ mkdir single-to-multi-project
❯ mv build.gradle.kts single-to-multi-project
❯ tree
.
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── single-to-multi-project
    ├── build.gradle.kts
    └── src
        └── main
            └── java
                └── com
                    └── example
                        └── ExampleApplication.java

8 directories, 7 files

以下のコマンドでビルドしてみます。

いずれの場合であっても subproject 側の build ディレクトリに jar ファイルが生成されていると思います。

❯ ./gradlew build
or
❯ ./gradlew :single-to-multi-project:build

❯ tree single-to-multi-project/build 
single-to-multi-project/build
├── classes
│   └── java
│       └── main
│           └── com
│               └── example
│                   └── ExampleApplication.class
├── generated
│   └── sources
│       ├── annotationProcessor
│       │   └── java
│       │       └── main
│       └── headers
│           └── java
│               └── main
├── libs
│   └── single-to-multi-project.jar
└── tmp
    ├── bootJar
    │   └── MANIFEST.MF
    └── compileJava
        └── source-classes-mapping.txt

ここまでで一旦 multi project に変換するまでを行いました。multi project の場合の共通処理の書き方やビルドパフォーマンスを上げる方法などいくつか話題はありますが次回以降の話題とします。

Single-Project から Multi-Project へ(別解)

ある project と他の project を関連させながら開発を行う方法として gradle の Composite Build3 という仕組みがあります。 これは厳密には Multi Project ではないため、Gradleドキュメント上で Multi Project と記載されているものと混同しないように注意してください。 この方法のメリットとしては、元のプロジェクトに全く手を加えることなく他のプロジェクトを作成し、依存として利用することができます。

さて、「Single-Project の開発」からの続きであるとして、その次のステップとして root project に相当する project を作成します。これは元のプロジェクトとは別のプロジェクトとして作成します。 注意点としては、このプロジェクトは 現在の gradle バージョンにおいては composite build の制限により同名のプロジェクト名を利用できません

❯ gradle init --type basic --dsl kotlin --project-name multi-project 

> Task :init
Get more help with your project: https://guides.gradle.org/creating-new-gradle-builds

BUILD SUCCESSFUL in 492ms
2 actionable tasks: 2 executed

他のプロジェクトと連携したい project を multi-project に追加します。

❯ echo 'includeBuild("single-to-multi-project")' >> settings.gradle.kts
❯ mv path/to/single-to-multi-project .

build.gradle.kts に以下のような記述を追加します(現在の gradle の制限により(まだ)直接コマンドラインからは起動できないため4 )。

tasks.register("build") {
    dependsOn(gradle.includedBuild("single-to-multi-project").task(":build"))
}
❯ ./gradlew build                                           

BUILD SUCCESSFUL in 1s

対象プロジェクトの jar ができていることが確認できます。

❯ tree single-to-multi-project/build/
single-to-multi-project/build/
├── classes
│   └── java
│       └── main
│           └── com
│               └── example
│                   └── ExampleApplication.class
├── generated
│   └── sources
│       ├── annotationProcessor
│       │   └── java
│       │       └── main
│       └── headers
│           └── java
│               └── main
├── libs
│   └── single-to-multi-project.jar
└── tmp
    ├── bootJar
    │   └── MANIFEST.MF
    └── compileJava
        └── source-classes-mapping.txt

ここまでで一旦 multi project に変換、 というより他のプロジェクトからビルドするまでを行いました。

このまま他のプロジェクトを追加する際の注意点などはいろいろありますが、構成や制限などがいろいろありややこしいため別途 composite build として話題とするか ドキュメント5 を参照してください。

以上です。

gradle wrapper の jar バイナリファイルが正しいものかどうかを確認する

gradle wrapper の jar を更新した時、 jar ファイルが更新されたりされなかったりして正しいかどうかがよくわからない場合がある。

手元で確認する場合は以下のように実行すると良い。(gradle 6.1.1 の場合)

$ cd gradle/wrapper
$ curl --location --output gradle-wrapper.jar.sha256 https://services.gradle.org/distributions/gradle-6.1.1-wrapper.jar.sha256
$ echo "  gradle-wrapper.jar" >> gradle-wrapper.jar.sha256
$ shasum --check gradle-wrapper.jar.sha256

本来のモチベーションとしては OSS 上で Gradle バージョンアップおじさんにもらう Pull Request で妙な binary を入れられてもわかるようにするというような目的の様子。

https://docs.gradle.org/6.1.1/userguide/gradle_wrapper.html#wrapper_checksum_verification

Verifying Gradle Wrappers with GitHub Actions

Gradle で optional のようなものを実現するには

バージョン情報

  • Gradle 6.0.1
  • propdeps plugin 0.0.9
  • maven 3.6.2

一言で言うと

featureVariants を使う。

詳しく言うと

Spring Boot などで auto configuration を実装しようとする場合など、対象のサポートをするが必須ではないライブラリを使うようにしたい場合がある*1

maven の場合はoptionalタグを true にすることで、開発しているライブラリ側では利用することができるがそのライブラリを利用する側では無視される*2

gradle の場合は方法としては 2 つ

それぞれ違いがあるが適当にかくと以下のような感じ。

方法 pom.xml 出力 plugin artifact 出力タスク 備考
propdeps plugin を使う maven uploadArchives gradle 6.0 以降では maven plugin を用いた publish は非推奨
featureVariants を使う maven-publish publish maven plugin では optional を出力できない

(最近はまだ gradle module metadata はあまり使われている気配はないので割愛)

propdeps plugin について

propdeps pluguin は spring team が作っている gradle plugin。 optional configuration を追加し、 maven での optional のような動きをするようにする。

挙動としては compile configuration の classpathoptional configuration を追加する。挙動としては compileOnly とほぼ同じ気はする。 ライブラリとして publish する場合には propdeps-maven plugin を利用し、 <scope>optional</scope> という(maven の仕様にはない) scope を一旦追加したあと gradle core apiuploadArchives task を用いる場合に <scope>compile</scope><optional>true</optional> に差し替えて pom.xml を出力する。ちなみに maven plugin にしか対応してないので maven-publish plugin で publish task を実行した場合 は <scope>optional</scope> で pom.xml が出力されてしまい思ったように動かなくなるかもしれない。

使うべきかどうかについては、おそらく新規では採用しない方がよさそう。というのも propdeps plugin はしばらく更新もされておらず(2017 年最終)、publish に使用する maven plugin が gradle 6.0 以上で deprecated, gradle 7.0 以上で 削除予定なのであまり利用しない方がよさそう*3。中で使ってるgradle のバージョンも 3.3 でだいぶ古い*4

featureVariants について

Gradle 5.3 から導入されている機能*5。詳しい説明は Modeling feature variants and optional dependencies を参照。

ドキュメントにも以下のように記載がある。

a (better) substitute for Maven optional dependencies

自分の理解としては 1つの componentvariantsimplementationruntime 等)を跨いで何らかの利用するかどうかを選択できる機能を実現する場合などに利用する*6

local component*7 や gradle module metadata としてそのライブラリを依存に含めて利用する場合は capability として名前をつけていくつかの依存をまとめて 依存に含めることができたりする。そうでない maven や ivy(コチラはよく知らない)などの external component はそれらに対応する機能に変換されて出力され、 maven-publish plugin の場合は その依存モジュールに <optional>true</optional> が付与される*8

ちなみに Modeling feature variants and optional dependencies には 以下のように記載があるが pom.xml には classifier タグとして出力される気配がないのでよくわからない。

using POM metadata (Maven), feature variants are published as optional dependencies and artifacts of feature variants are published with different classifiers

gradle 単体で運用してるならば capability の指定(capability の命名規則はデフォルトでは group:artifactId-(feature name の kebab case))により意味のある名前でまとめて 依存追加できるし maven repo への publish もサポートしてるので こちらの方がよさそうに見える。

2019 年まとめ

技術編

  • コードを少し書いた
  • ひたすらコードレビューをしていた
  • 運用 VPNOpenVPN から WireGuard に変更した
  • Spring Boot に比較的詳しくなった
  • Gradle に比較的詳しくなった
  • VPSlinode に集め始めた
  • Unity を少し書いた
  • 電子工作をすこし学んだ

ゲーム編

2019 年に購入または 2019 年にプレイした形跡があるもの

マンガ編

  • 1392 冊買った

家編

  • ブレーカーの電流を上げる契約をした
  • マイナンバーカードを作った
  • SoftbankAir を解約した
  • MacBook Pro を買った
  • Surface Laptop 3 を買った
  • G Suite を利用し始めた

コーヒー編

旅行編

  • 国内(出張込み)
    • 東京
      • ディズニーシー
    • 福岡
    • 大阪
    • 静岡
    • 名古屋
    • 神戸
    • 有馬温泉
    • 日光
  • 国外
    • 台湾