Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

Gradle/Kotlinで開発する私的ベストプラクティス2022

こちらのエントリーが素敵だなと思ったので、最近書いてるKotlinプロジェクトのベストプラクティスをまとめてみます。一部はJavaプロジェクトにおいても利用できるはずです。

zenn.dev

基本方針

  • 参加障壁を下げる。OSSプロジェクトでもプロプライエタリ・ソフトウェアプロジェクトでも、新しい開発者が参加するコストを下げることには大きな意義がある。
  • 環境差異を吸収する。javaにPATHが通ってさえいればOSに関係なくビルドが通るようにする。
  • プロジェクト固有ルールを作らない。Conventional CommitsやKeep a changelogなど、ひろく世に使われているルールを採用する。

Gradleを設定する

Spotlessを使う

コードのフォーマットはformatterに任せて人間は細かいことを考えない、というのが不特定多数が参加するソフトウェアプロジェクトのあるべき姿だと考えています。ここを妙にこだわるとエディタ縛りだとかタブ幅だとかのいわゆる”地雷”の多い話題を避けて通れませんし、プロジェクト固有のルールができて敷居が高くなり保守コストが高くなるという課題もあります。

逆に言えば、フォーマットをツールに任せれば、人間はエディタやOSの選択の自由を享受できます。

Gradleの場合、最もシームレスに使えるツールはSpotlessでしょう。MarkdownとKotlin、Kotlin Buildscriptすべてのフォーマットをこのツールで管理できます。

github.com

pre-merge buildでは spotlessCheck タスクを使ってフォーマットを確認します。このタスクは通常 check タスクから依存されていますので、深いことを考えずに ./gradlew build すれば充分です。

Git hookを導入する

commit時にSpotlessを自動実行するには、git hookを使います。git hookの設定にはghooksを使っていたのですが、脆弱性対応含む更新が止まっており*1使いにくい状態です。後述するsemantic-releaseを使うのであれば、NodeJSにPATHが通っている前提でhuskyを使っても良いでしょう。

JVM toolchainを使う

現時点でJavaには8,11,17という3つのLTSリリースがあります。誰でも簡単にビルドできるプロジェクトを作るためには、どのバージョンにJAVA_HOME環境変数が通っていても問題なくビルドできるべきです。このためのアプローチには「Java 8で動くようにする」と「JVM toolchainを使う」の2択があります。

既にJava 8をサポートしていないGoogle Errorproneやサポート終了を予告しているApache Camelのようなツールもありますので、JVM toolchainを使って「JAVA_HOMEがJava 8を指していても常にJava 11あるいは17を使ってビルドする」方が現時点でしょう。最新のKotlinプラグインならtoolchainの利用は容易です:

kotlin {
    jvmToolchain {
        (this as JavaToolchainSpec).languageVersion.set(JavaLanguageVersion.of(17))
    }
}

.gitattributes を書く

GitHubを使っている場合、人間に差分をレビューさせたくないファイルがあれば linguist-generated=true を指定しておきます。

./gradlew init で生成されるファイルを参考に、Windows向けの改行コード設定 *.bat text eol=crlf を加えても良いでしょう。

*.bat text eol=crlf
gradlew linguist-generated=true
gradlew.bat linguist-generated=true

settings.gradle.kts を書く

公式ドキュメントで言及されているとおり、設定ファイルをプロジェクトルートに置くことが強く推奨されています。 ./gradlew initでプロジェクトを作っていれば自動的に作成されているはずです。

最低限rootProject.nameを設定しておきます。これでどのようなフォルダ名が使われていても同じプロジェクト名になります。 またビルドスキャンをする予定があるなら、このタイミングで設定をしておくと良いでしょう。

plugins { id("com.gradle.enterprise") version "3.8.1" }

rootProject.name = "foo-bar"

gradleEnterprise {
  buildScan {
    termsOfServiceUrl = "https://gradle.com/terms-of-service"
    termsOfServiceAgree = "yes"
  }
}

gradle.properties を書く

公式ドキュメントをもとに設定しておきます。Java併用時に google-java-format を使う場合、少なくとも Java 17では--add-exportsの指定が必要です。

org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-XX:+HeapDumpOnOutOfMemoryError -Xmx1G --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
org.gradle.parallel=true

ドキュメンテーションコメントにはDokkaを使う

私はあまり語れるほど使っていないのですが、KotlinプロジェクトではKDocをドキュメンテーションコメントに使い、KDocからの文書生成にDokkaを使います。設定も単純なので特に迷うことなく使えるはずです。

github.com

build タスクに必要なタスクすべてを実行させる

Gradleプロジェクトでは基本的に、 ./gradlew build によってプロジェクトのビルドと検証に必要なタスクがすべて実行されるべきです。assemble タスクによって成果物が作られ、 check タスクによってすべてのテストや検証が実行される状態を保つことで、これを実現できます。これらのタスクの役割は ライフサイクルタスクとしてドキュメントに説明されています。

ただSpotlessを使っている場合、手元では spotlessApply も実行したいがCI環境ではspotlessApply を実行させたくない、というケースがあるかもしれません。

これにはDSLによりCI環境の場合に分岐する、CI環境では -x オプションにより特定タスクをスキップする、手元で ./gradlew spotlessApply を明示的に実行させる、などの手法があります。プロジェクト固有ルールを作らないという方針と、標準では build タスクが spotlessApply タスクに依存していない現状を踏まえると、手元で ./gradlew spotlessApply を明示的に実行するのが良いと考えています。

とはいえ人間に「push前に./gradlew spotlessApplyを実行してね」というプロジェクト固有ルールを作りたくはないので、前述のGit hookに実行させる方針を採ることになるでしょう。

GitHub Actionsを設定する

ごく一般的なワークフロー設定は以下のようになります。 なおWindowsでビルドする場合は、--no-daemonオプションでGradle daemonを無効化する必要があります

jobs:
  build:
    strategy:
      matrix:
        os: ['windows-latest', 'ubuntu-latest']
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v2
      - uses: gradle/wrapper-validation-action@v1
      - run: ./gradlew build --no-daemon
      - uses: actions/upload-artifact@v2
        if: always()
        with:
          name: reports (${{ matrix.os }})
          path: build/reports

Dependabotを設定する

現時点では buildSrc プロジェクトの依存は更新対象に入っていません。以下のように明示的に指定する必要があります。

version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "daily"
    commit-message:
      prefix: "build"
  - package-ecosystem: "gradle"
    directory: "/"
    schedule:
      interval: "daily"
    commit-message:
      prefix: "fix"
  - package-ecosystem: "gradle"
    directory: "/buildSrc"
    schedule:
      interval: "daily"
    commit-message:
      prefix: "build"

逆に言えば、ビルドに用いるツールの依存を buildSrc プロジェクトに集めることで、ビルドに用いるツールとプロジェクトそのものの依存とを分けて扱うことができます。上記設定ではプロジェクトそのものの依存にのみ fix 接頭辞を用いることで、semantic-releaseによるリリースを発火させています。

semantic-release を使う

以下の記事で以前紹介したので詳細は省きます。なお今なら拙作GradleプラグインがあるのでGradleでもsemantic-releaseのフル機能を用いて開発できます。

blog.kengo-toda.jp

PATHにNode最新のLTSが通っている状態にする必要がありますが、これはnvmやasdfを使うことになるでしょう。最新のactions/setup-node@v2はnode-version-fileでファイルからNodeのバージョンを読み込めます:

- uses: actions/setup-node@v2
  with:
    node-version-file: '.nvmrc'
    cache: 'npm'
- run: |
     npm ci
     npx semantic-release

以上です。なおJava用ではありますが、ビルドの細かい話やGitHub Actionsの使い方については以下の本にも書いてますのでご参考まで。

zenn.dev

*1:PRは作ったんですが見てもらえてなさそう

--add-exportsをMaven/Gradleで使う

--add-exports なんてオプションは使わないに越したことはないのですが、依存先ライブラリの都合でどうしても必要という私のような人のためのメモ。

ポイントは javac だけでなく javadoc ないし java (テスト実行)コマンドに対するオプション提供も必要という点です。Gradle用のサンプルプロジェクトMaven用のサンプルプロジェクトもあります。

コンパイラ用の設定

注意点として、--add-exports オプションは --release オプションと同時に使えません

Maven3

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.9.0</version>
        <configuration>
          <source>11</source>
          <target>11</target>
          <encoding>${project.build.sourceEncoding}</encoding>
          <forceJavacCompilerUse>true</forceJavacCompilerUse>
          <showWarnings>true</showWarnings>
          <compilerArgs>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
            <arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
          </compilerArgs>

Gradle

// build.gradle.kts
val exportsArgs = listOf(
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
    "--add-exports",
    "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
)

tasks.withType<JavaCompile>() {
    sourceCompatibility = "11"
    targetCompatibility = "11"
    options.compilerArgs.addAll(exportsArgs)
}

javadoc用の設定

いろいろとオプションを指定する手法が提供されていますが、以下のように<additionalOptions>を使う方法での動作を確認しました。内部的にはOptionファイルを経由してjavadocコマンドのオプションを設定しています。

Maven

  <build>
    <plugins>
      <plugin>
      <artifactId>maven-javadoc-plugin</artifactId>
      <version>3.3.1</version>
      <configuration>
        <additionalOptions>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
          <arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
        </additionalOptions>
      </configuration>
    </plugin>

Gradle

一気にややこしくなります。 options.addStringOption()では動作しないので、Maven同様にOptionファイルを経由しての指定が望ましいです。

// build.gradle.kts
val addExportsFile = file("$buildDir/tmp/javadoc/add-exports.txt")
val createJavadocOptionFile by tasks.registering {
    outputs.file(addExportsFile)
    doLast {
        addExportsFile.printWriter().use { writer ->
            exportsArgs.chunked(2).forEach {
                writer.println("${it[0]}=${it[1]}")
            }
        }
    }
}
tasks {
    withType<Javadoc> {
        dependsOn(createJavadocOptionFile)
        options.optionFiles(addExportsFile)
    }
}

テスト実行

Maven

surefireプラグインJVMをforkするなら、forkするJVM側にオプションを設定します。そうではない場合、以下のような .mvn/jvm.config ファイルでMavenを実行するJVM自体に設定を行います。

--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED

Gradle

jvmArgs プロパティを経由して設定します。このプロパティは標準でnullなので、ビルドスクリプトの他の箇所で設定していないなら .addAll() ではなく = を使って設定します。

// build.gradle.kts
tasks {
    test {
        useJUnit()
        jvmArgs = exportsArgs
    }

JavaScript Actionsをnode16で動かすようにする

この記事はCI/CD Advent Calendar 2021に参加しています。


先日GitHub ActionsがNodeJS v12のみならずv16でも動くようになりました。

github.com

今まではNodeJS v12しかサポートされていませんでしたが、このv12は来年の4月でサポートが切れます。速やかにv16に移行したほうが良さそうですね。必要な作業は actions.ymlruns.using を書き換えるだけではありますが、他に必要になるであろう作業もいくつか紹介します。

package.jsonengines.node を更新

package.jsonで実行に利用するNodeJSのバージョンを指定していた場合、それを更新する必要があります。16.13.1 が現在の最新バージョンなので、望ましい設定は以下のようになるでしょう:

  "engines": {
    "node": "^16.13.1"
  }

tsconfig.json を更新

NodeJSを12から16に上げると、ES2019ではなくES2021を使えるようになります。TypeScript公式の推奨設定を参考に、compilerOptionsを更新します:

{
  "compilerOptions": {
    "lib": ["ES2021"],
    "module": "commonjs",
    "target": "ES2021"
  }
}

TypeScriptを更新

ES2021をサポートしていないTypeScriptを使っている場合、TypeScript自体の更新が必要になります。 npm add -D typescript@^4.5.4 などとして更新しましょう。

なおTypeScript 4.4で、catchされたオブジェクトの型をanyからunknownに変更されています。トップレベルでエラーをキャッチして core.setFailed() を呼んでいる場合は修正が必要です:

-     core.setFailed(error.message)
+     if (error instanceof Error) {
+       core.setFailed(error)
+     } else {
+       core.setFailed(JSON.stringify(error))
+     }

ESLintを更新

ESLintの現時点での最新は8.5.0です。私が更新した多くのリポジトリではいくつかのプラグインも導入していましたが、そのような場合は npm add -D eslint@^8.5.0 @typescript-eslint/parser@^5.8.0 eslint-plugin-github@^4.3.5 eslint-plugin-jest@^25.3.0 などとして同時にアップデートすることが良いと思います。

.eslintrc.json の更新はESLintのリリースノートに載っているマイグレーションガイド(例えば3.0.0用マイグレーション)に従っても良いですが、特に設定にこだわりがないならば actions/typescript-actionのデフォルトブランチ から最新の推奨設定を引っ張ってくる方が時間は短縮できます。

GitHub Actions WorkflowでNodeJS v16を使う

GitHub hosted runnersでworkflowを実行し、かつactions/setup-node を使っていない場合、今月10日ごろから既にv16を使ったビルドが走っています。特に対応不要です。

actions/setup-node を使っている場合は、node-versionを変更する必要があります。ただ個人的には .nvmrc.node-version に使うバージョンを書いておいて、GitHub Actionsではその値を読むのがメンテ箇所が減るためおすすめです。setup-nodev2.5.0からnode-version-file設定がサポートされています

  - uses: actions/setup-node@v2
    with:
      node-version-file: '.nvmrc'

まとめ

以上です。GitHub JavaScript ActionはいつNodeJSのバージョンを上げるんだろう……と不安になっていた方々、やっと来ましたのでバンバン上げていきましょう。私が実際に作成した変更をいくつか並べておきますので、ご参考まで。

2021年のOSS活動状況まとめ

昨年のに引き続きOSS活動状況をまとめます。2021年12月20日時点の情報です。

概要:昨年比30%増

GitHubのプロファイルページによると今年のpublic contributionsは1,865で、昨年が1,440だったので約30%増です。commit 63%のpull requests 12%なので、引き続き手を動かしてコードを書けたと思います。

f:id:eller:20211220102614p:plain
my GitHub profile (2021/Dec/20)

主なリリースはspotbugs-gradle-plugin 4.6.1~5.0.3SpotBugs 4.2.1~4.5.2gradle-semantic-release-plugin 1.4.14~1.6.0actions-setup-docker-compose 1.0.0~1.0.4でした。

SpotBugs周辺の開発

一番大きい貢献はsonar-findbugsのメンテナを引き渡したことだと思います。このプロダクトを開発する過程においてSonarQubeと距離を置きたいと思うようになったため、先日のv4.0.5リリースから新しいメンテナ2名にPRのレビューやマージ、リリースプロセスの扱いなど全般を引き渡しました。活発なプロダクトの引き渡しはこれが初めてだったのですが、いまのところうまく回っているようで安心しています。

またSpotBugsコアにおいてSARIFレポートのGitHub Code Scanning API対応(v4.4.1)と複数レポートの出力(v4.5)とを完了したため、人間向けのHTMLレポートと機械向けのXMLやSARIFレポートとを同時生成するという普遍的なニーズに応えられるようにできました。これユーザには地味に便利な更新なんじゃないかと思います。

なお偽陽性偽陰性は3つ修正しました。これらも歴史が長くユーザ基盤のあるプロダクトとしては望まれるものでしょう。

発展的内容としてはGraalVMのnative-imageでパフォーマンスが向上するか試しています。残念ながら優位な差は出ませんでしたが、picocliで作ったCLIツールのネイティブイメージを作るための必要な知見が溜まったのは良かったです。

GitHub Actions周りの開発

actions/setup-java v2.3.0dependency cacheを実装しました。JavaプロジェクトをGitHub Actionsでビルドしている人なら、この機能による高速化とWorkflow定義の短縮化の恩恵を受けているのではないでしょうか。名前こそ出ませんでしたがGitHub公式ブログで紹介されたのが嬉しかったです。

また5月にはwrapper-validation-actionの安定性を向上させています。このActionは通信断でわりとすぐ落ちていたので、この変更の恩恵を受けている開発チームはけっこうあるんじゃないでしょうか。

GitHub Actionsについてはブログ記事も多く投稿しました。Dependabot周りで変更が多かったため、Dependabotが作るPRでいかに(SonarQubeのために)Secretsを使うか、を探求していた気がします。

なおreadthedocs-action公式のPRレビュー機能が充分に実装されたため、そろそろ不要かなという気がしています。

プログラミング言語

2021年はGroovyとJavaをKotlinで置き換えた年になりました。Javaはもちろん良い言語で、自分の強みの大きな部分を構成する言語です。と同時に変化の速い言語環境に身を置くことはアンテナを高く保つために有用ですし、Gradleビルドスクリプトに静的型付けを持ち込めるという利点は大きいと判断しています。並列実行性能を上げる技術基盤としての価値も期待していますが、そちらはまだ検証段階に至れていません。

似た理由でPowerful Command-Line Applications in GoをベースにGolangのキャッチアップもはじめましたが、こちらは2022年の挑戦になりそうです。

また自分がとても尊敬しているstatic program analysisの講義をしているAarhus Universityが新しくFlix言語を開発中、ということを知ってキャッチアップしています。PureとImpureを明示的に扱うというアプローチが面白いです。なおPRはひとつ送りましたが、残念ながらinvalidでした。最新のリリースではなく、デフォルトブランチでの挙動を確認すべきでした。
Flixはまだツールチェインに乏しいのでflix-gradle-pluginを実装してみましたが、言語標準のPackagerに手を入れないと依存管理周りは良いものにならなさそうなため、最近はScalaを読んでいます。

その他

Vert.xでReactiveなドキュメントビルダーのPoCも書いたのですが、こちらは技術的に無理は無さそうだけど言うほどパフォーマンスに寄与しなさそうという結論を出しました。外部サービスにビルドを委ねる大型プロジェクトでは意義があるかも?ですが、新ツールを書き直す程ではないかなと。
ReactorやVert.xのようなReactiveにプログラムを動作させるための取り組みには、引き続き関心を払っていこうと思います。

Sponsorを募集しています

昨年2月からGitHub Sponsorsを始めています。特にSpotBugsないしそのGradle プラグインをご利用の皆さま、ご支援のほどよろしくご検討ください。 SpotBugsのような古く自分ではあまり使わないプロダクトの保守を続けるにあたって、ユーザーや開発者からのわかりやすく明示的な支持があるとありがたいです。

github.com

継続的に支援するほどでは……という場合、ぜひこちらのGitHub Discussionsにて「誰が、どこで、どのように使っているか」を共有していただけるだけでも助かります。よろしくお願いします。

github.com

Gradleプラグインのメジャーアップデートにおいて、古いGradleへのサポートをいかにして切るか

spotbugs-gradle-plugin v5をリリースしました。beta1のリリースから約3ヶ月間かかっています。

github.com

Gradleプラグイン開発はややマイナーな取り組みだと思うので、Gradleプラグイン開発のメジャーアップデートがどういうものだったのかをちょっと紹介します。

なぜ後方互換を壊す必要があったのか

spotbugsプラグイン固有の理由はもちろんあります。デフォルトの設定がうまくなかった、リソースリークの解消にはデフォルトの動作モードを変える必要があった、などです。これに加えてGradleプラグインならではの理由があります。

古いGradleへのサポートを切るため

Gradle 7をサポートするには、Gradle 7で削除されたGradle APIへの依存を切る必要があります。この場合、移行先Gradle APIが存在するGradleのバージョンが最低限サポートできるバージョンになります。例えば今回削除された compile configurationの代替となるimplementation, api configurationsはGradle 3.4で導入されているので、プラグインがこれらのconfigurationに依存する場合は3.4が最低限サポートできるバージョンになります。

新しいGradleで導入された機能を使うため

Gradleは新しい機能を継続的に取り入れており、中にはJavaプロジェクト開発の体験を大きく変えるものも存在します。依存管理やキャッシュの改善、差分コンパイルの実現によるビルド時間の短縮が良い例ですね。Gradle社としてはこうした改善に命運を賭けているようで、Developer Productivity Engineering (DPE)という造語を作って強く押し出しています。

gradle.com

そしてこうした機能を前提にプラグイン開発を行う場合、自然と古いGradleに対するサポートを切る必要が出てきます。 例えば今回入れたJava toolchain対応はGradle 6.7で導入され、7.0以降で安定しています。これがv5とv6のサポートを切る主な動機となっています。

コードを簡潔に保つため

とはいえ動的にGradleVersionを調べるなどしてプラグインの動作を切り替えることで、古いGradleをサポートしつつ新しい機能を使うことも可能です。例えば今回のリリースでもGradle 7.1で取り入れられた JavaPluginExtension を使いつつGradle 7.0もサポートしています。

github.com

しかしこの方法はコードが複雑化する上、概念レベルでの変更への対応が難しいため、そこまでコストを掛けて互換を保つ判断はあまりしないのではと思います。 むしろこうした変更をパッチアップデートで行うspotlessのようなプラグインも存在します。

ユーザのスムーズな移行のため、Gradleのメジャーバージョンを複数サポートするバージョンを提供することは重要ですが、同様に新しいGradleメジャーバージョンに集中して管理しやすいリポジトリを保つこともまた必要です。Gradleの場合コミュニティの進歩が速く、ユーザもそれを前提に頻繁なアップデートをある程度覚悟していますので、プラグイン側が過度に古いGradleのサポートに固執する必要はないとも思われます。

まとめ

Gradleプラグインのメジャーアップデートにおいて、古いGradleへのサポートはコードを簡潔に保つためにも早めに切ってしまうべきです。マイナーアップデートやパッチアップデートで古いGradleへのサポートを切るプラグインもあり、それがコミュニティに受け入れられているくらいですので、古いGradleをサポートすることよりも新しいGradleをサポートするための素早い継続的デプロイを実現することにコストを割いたほうが良いでしょう。

なおGradleプロジェクトにおける素早い継続的デプロイには、semantic-releaseの活用がおすすめです。私が実装しメンテナンスしているgradle-semantic-releaseについては以下の記事で紹介しています。

blog.kengo-toda.jp

WIP: Gradleの機能でどこまでビルド性能が改善するのか

Gradleの --parallelJUnit並列実行、Configuration Cacheなどがどの程度ビルド性能を改善するのか、いくつかのOSSで実測してみた。利用した機能の概要は以下オフィシャルサイトを参照のこと。

docs.gradle.org

SonarQubeの事例

バージョン 9.0.1.46107 リリース後の master ブランチで実践。進捗などは以下のIssueで管理している:

github.com

もともとGradle 6.8.2を使っていたが、1,092ものbuild deprecationsが報告されておりGradle 7に直接アップグレードすることができない状態。Gradle 7ではいくつかのTaskのaccessorがLazy ConfigurationのためのAPIで置き換えられており、移行が必要。また依存管理に非推奨となっているcompiledefaultを使っており、implementationruntimeClasspathJava Library Pluginが提供するapiなどへの移行が必要。

最大の難関は com.github.hierynomus.license プラグインcom.github.johnrengelman.shadow プラグインが古く、数多くの警告が出ているというもの。タスクの入出力が不明瞭なためキャッシュも効いていない。またプラグインの最新版がGradle 6をサポートしていないため、これらのアップグレードはGradleの7へのアップグレードと同時に実行する必要があった。

f:id:eller:20211020102235p:plain
手を加える前のBuild scan結果。プラグイン由来の警告が多い。

改善結果

21分半かかっていたフルビルドが16分弱(73.3%)で終わるようになった。

f:id:eller:20211204093213p:plain
中央値で比較して73.3%に短縮

Parallel buildはビルド時間を1分程度短縮する効果があったが、GitHub ActionsのHosted Runnerは2コアしかないためデフォルトでは2並列にしかならない。サブプロジェクトが多いなら --max-workers などの設定でワーカー数を増やす方が良い。

f:id:eller:20211020104306p:plain
4並列でビルドしたときのタイムライン

テストの並列実行は残念ながらビルド性能の改善に繋がらなかった。Parallel buildでCPUを使い切っており性能が出なかったためと思われる。実際サブプロジェクトひとつのテストを実施してみると、テストの並列実行を有効化したほうが性能が出る。サーバが必要なBuild Cacheは試していないが、これを有効化することでフルビルドの必要性が下がるため、テスト並列実行の重要性も上がると期待される。

SpotBugs

実験中。Antから無理やり移したタスクがEclipse plugin周辺で残っているので、まず buildSrc にビルドロジックを移動させるところからやる必要がある。 Eclipse関連の依存をMaven Centralからダウンロードするようにしたいが、以下の課題が未解決。

www.eclipse.org

JJUG CCC 2021 FallでLT参加してきました

JJUG CCC 2021 FallにてJSR305に代わる静的解析用標準アノテーションの策定を目指す活動について紹介しました。スライドは以下からご覧いただけます:

speakerdeck.com

2021年11月24日更新:動画はJJUG公式から公開いただいています:

www.youtube.com

以下、単に感想です。

静的解析用アノテーションが多数存在する件

この17年間で生まれた静的解析アノテーション、数はかなりあるんですが歴史等まとまっているものではないので、Maven Centralにデプロイされた日をベースに整理していました。スライド草案に記載した歴史は以下のように非常に長いものになりました:

f:id:eller:20211121203045p:plain
関連アノテーションの歴史

地味に困ったのがJSR305に関する意思決定がJCPのサイトで公開されていないことです。

JCPからリンクされているGoogle Groupを見ても「なぜ活動が止まったか」はわかりません。ここでの活動自体が非活発になったので。近年のJEPがチケットやメーリングリストで管理され透明性が高いのは、こうした反省を踏まえたものなのかもとか思いました。

わかりやすい動画を作成したい

私の動画はmacOSのスクリーンキャプチャ機能で録画しただけのものです。解像度を変えるためにQuickResを購入しましたが、ほかはマイクのようなハードウェア含め工夫をしていません。

が、今回みなさんの動画を見ていると、画面隅に顔を映している方がいらして、しかもその方が気持ちわかりやすかったりするんですよね。デジタル表現の工夫や技法は重要になっていますし、きちんと学びたいなと思いました。まぁ動画編集の時間が作れるかは別の問題ですけど……(今回も体調不良で急遽幼稚園を休んだ子供の面倒を見ながらの録画だった)

JJUG CCCでの過去発表へのリンク