mike-neckのブログ

Java or Groovy or Swift or Golang

あなたの知らない Gradle の kotlin-dsl プラグイン

やんくさんに頼まれたので、誰かが書くだろうと思って待ってたけど誰も書かない Gradle の kotlin-dsl プラグインについて書きます。


まず、先日(2020/12/11)開催された JJUG ナイトセミナーにて、時間がないのでほぼ飛ばしてしまった、こちらの話題について解説します。

f:id:mike_neck:20201212182915p:plain

build.gradle(build.gradle) の中で if 文(Kotlin だと式ですね)とか for 文(こっちは式だったっけ?)を書くと、宣言的なコード(例えば依存ライブラリーの宣言など)と命令的なコードが混在してしまいます。そのため、ビルドスクリプトを読むために頭の切り替えが必要になるのでメンテナンス性が下がるというのが理由です。したがって、命令的なコードを書いている部分はすべてプラグインにして、 buildSrc プロジェクトに移動するのがよいと公式のドキュメントに書いてあります。

docs.gradle.org

しかし、これは素直におすすめできないところがあって、プラグインを書くためには少しだけプラグインの仕組みの学習コストがかかります(まあ、大したコストではありませんが…) 。そこで今持っている build.gradle(build.gradle.kts) の 知識でプラグインを無理なく書ける仕組みを使うのがよいでしょう。

プリコンパイルド・スクリプトプラグイン(Precompiled script plugins) というのがその仕組です。

docs.gradle.org

具体的には以前このブログで紹介した groovy-gradle-plugin が Groovy の場合のプラグインです。ここでは詳しく解説しないので、以下のエントリーを読んでください。

mike-neck.hatenadiary.com

これの Kotlin 版が kotlin-dsl プラグインです。(この言い方は語弊があって、先に kotlin-dsl があって、その後に Groovy 版ができたらしい)

なお、ググラビリティが悪いので、このプラグインの名前はどうにかしてほしい


kotlin-dsl の書き方

buildSrc/build.gradle.kts ファイルに以下のように書きます。 repositories {} ブロックを忘れると kotlin-dsl をビルドするための Kotlin をダウンロードできなくなってエラーが発生します

plugins {
  `kotlin-dsl`
}

repositories {
  jcenter()
}

buildSrc/src/main/kotlin の下に任意の名前で .gradle.kts ファイルを作成します。ここではサンプルのため、 example.gradle.kts というファイル名にします。なお、内容的には先の Java toolchains のエントリーの build.gradle と同じ様な内容にしています

plugins {
  `java`
}

repositories {
  mavenCentral()
}

dependencies {
  compileOnly("org.jetbrains:annotations:19.0.0")
  implementation("org.slf4j:slf4j-api:1.7.30")
  testImplementation("org.junit.jupiter:junit-jupiter:5.7.0")
  testImplementation("org.assertj:assertj-core:3.18.1")
}

internal val ss = sourceSets
internal val testSourceSets = ss.getByName("test")
@Suppress("UnstableApiUsage")
internal val javaToolChains = extensions.getByType<JavaToolchainService>()
@Suppress("UnstableApiUsage")
internal val javaPluginExt = extensions.getByType<JavaPluginExtension>()

javaPluginExt.toolchain {
  @Suppress("UnstableApiUsage")
  languageVersion.convention(JavaLanguageVersion.of(8))
  @Suppress("UnstableApiUsage")
  vendor.convention(JvmVendorSpec.AZUL)
  @Suppress("UnstableApiUsage")
  implementation.convention(JvmImplementation.VENDOR_SPECIFIC)
}

open class ProjectTestConfig {
  @Suppress("UnstableApiUsage")
  // 指定されたバージョンの Java でテストを実行するタスクを登録します
  fun testOnJavaVersion(vararg javaVersions: Int): Map<Int, TaskProvider<Test>> =
    javaVersions.map { javaVer ->
      javaVer to tasks.register<Test>("testJava${javaVer}") {
        group = "verification"
        description = "runs test on Java $javaVer"
        classpath = testSourceSets.runtimeClasspath
        testClassesDirs = testSourceSets.output.classesDirs
        useJUnitPlatform()
        javaLauncher.convention(javaToolChains.launcherFor {
          languageVersion.convention(JavaLanguageVersion.of(javaVer))
        })
      }
    }.onEach { pair ->
      tasks.named("check").configure { dependsOn(pair.second) }
    }.fold(mapOf()) { acc, pair ->
      acc + pair
    }
}

extensions.add<ProjectTestConfig>("testConfig", ProjectTestConfig())

なお、このスクリプトですが、書いている途中でコンパイルエラーが発生すると補完が効かなくなるので、気合で乗り越えていく必要があります。 これが僕が Kotlin DSL をあまり好まない理由の一つです。

f:id:mike_neck:20201212194334p:plain

f:id:mike_neck:20201212194356p:plain
tasks くらい取れるだろ

f:id:mike_neck:20201212194446p:plain
`launcherFor` だったっけな? `languageVersion` だったっけな?補完してくれなきゃわからん

最後に、ルートディレクトリーつまりメインプロジェクトの build.gradle.kts です。ここで、先程の .gradle.kts ファイルの拡張子を抜いた部分(ここまでの例だと example.gradle.ktsexample) をプラグインとして指定します。

plugins {
    `example`
}

// テストは Java 11 と 15 でも実行する
testConfig.testOnJavaVersion(11, 15)

かなりシンプル(というより、なにもない)になりました。

では、 Java 8 以外で実行すると落ちるようなテストを書いて実行してみます

class Version {

    @Test
    void show() {
        String javaHome = System.getProperty("java.home");
        assertThat(javaHome).contains("jre");
    }
}

f:id:mike_neck:20201212195301p:plain
check タスクから起動。途中のテストで落ちるので continue オプション付き

f:id:mike_neck:20201212195430p:plain
Java 11 と 15 でめでたくテストが落ちたようです

というわけで、 kotlin-dsl を使うとビルドスクリプトがシンプルになったと思います。また、 groovy での記事でも書いたように複数のサブプロジェクトに対してプロジェクト横断的に同一な記述が必要になるケース(例えば、同じ Kotlin のバージョンと Linter と フォーマッターを使いたい)で、 kotlin-dsl で作ったプラグインをサブプロジェクトで利用できます。


kotlin-dsl のおさらい

  1. プロジェクト(プロダクト)用の build.gradle.kts から命令的なコードを取り除いて、 buildSrc プロジェクトに命令的なコードを記述する
  2. buildSrcプラグインを書く場合に kotlin-dsl を使うと .gradle.kts ファイルを作成する
    1. で作成した .gradle.kts ファイルのピリオドよりも前の部分をメインのスクリプトプラグインに適用する

以上のような形でビルドスクリプトをシンプルにできます。

ffmpeg で色のバランスを変更する

colorbalance オプションを指定して変更する

このオプションの指定方法は次のパラメーターがある

  • rs/gs/bs
    • 一番小さい値を指定する(-1.01.0)
  • rm/gm/bm
    • 真ん中の値を指定する(-1.01.0)
  • rh/gh/bh
    • 最も高い値を指定する(-1.01.0)

このオプションにより、完全に黒な画像に色を付けられる

実行例

ffmpeg -i black-icon.png -vf colorbalance=rs=.5:gs=.3:bs=.1 yellow-icon.png

JetBrains の Compose for Desktop で build.gradle.kts でなく build.gradle を使う

どうも、 build.gradle.kts の良さがわからない老害おじさんです

標記の件、以下の通りにやれば動きます。多分

settings.gradle

pluginManagement {
    repositories {
        gradlePluginPortal()
        maven {
            url "https://maven.pkg.jetbrains.space/public/p/compose/dev"
        }
    }
}

rootProject.name = 'プロジェクトの名前'

build.gradle

plugins {
    id 'org.jetbrains.kotlin.jvm' version '1.4.10'
    id("org.jetbrains.compose") version "0.1.0-m1-build62"
}

version = '0.1'

repositories {
    jcenter()
    maven {
        url "https://maven.pkg.jetbrains.space/public/p/compose/dev"
    }
}

import org.jetbrains.compose.ComposePlugin.DesktopDependencies

dependencies {
    implementation DesktopDependencies.INSTANCE.currentOs
}

compose.desktop.application.mainClass = 'com.example.AppKt'

JJUG CCC 2020 Fall で jq の発表をしてきた

表題のとおり発表しました。

多分、そのうち YouTube で動画が公開されると思います。

発表資料ですが、資料の作り方の関係上、公開できるものはありません。


動画について

今回の 発表資料は動画で提出するという新しい試みでした。

したがって、いつものような発表直前まで資料を作るというグータラなやり方はできませんでした。

一方、資料を作った上で動画を撮影することも求められました。

自分はしゃべるのが苦手というより喋りたくない人間なので、長時間(数十分)を間違いなしにしゃべるのは無理だと動画を撮影する前に想定しましたが、実際に資料の作成開始前にサンプルとして 10 分ほど動画を撮影してみたら、間違えまくり、かみまくりだったので、通しで動画を撮影することは無理であると考えすぐに諦めました。

そこで、今回の資料は 2 〜 3 分くらいの動画を、 十数個作って、それを編集でつなげるという作戦で資料を作ることにしました。 資料もその動画撮影の方針に合わせて小さな断片的な資料を十数個作る形となりました。 また、動画という提出形式もあって動かない資料よりも、 実際にターミナルで操作している様子を動画で見せたほうがよいだろうということもあって、 断片的な資料の中身もほとんどが動画となりました。

以上の理由から、断片的な資料をまとめたものがない(面倒くさい)ため、今回発表資料を slideshare 等で公開できませんでした。


資料作成中に考えたこと

コロナ禍で集会を避けるようになり、オンラインで勉強会が開催されるようになりました。 これを家の作業を片付けてから参加できるので気軽に参加できるようになったという歓迎の声が聞こえるようになりました。 一方で自分の方は家に集中力を妨げるもの(ゲーム、ゲーム、ゲーム) が多いことから、勉強会の動画を見ててもわりと集中力が続かず、 なんか勉強会に対する熱が冷めたなと思うようになりました。

このようなテクノロジーによって社会が大きな変革を迎えるときの人間の心理や考え方の変化を記述しているエッセイがあります。

ヴァルター・ベンヤミンの『複製技術時代の芸術作品』という小さなエッセイです。

このエッセイの要点は銅像、絵画や風景さらには人物に漂っていたオーラ(今ここ性)というものが写真や映画にはなくなるため人間の視覚が変わるというものです。 このエッセイでは、オーラというものが宗教に結びついていると指摘しています。

『複製技術時代の芸術作品』を詠みながら、自分を勉強会に向かわせてたのは、なるほど宗教的熱狂でしたかと反省していました。


動画撮影について

上記の通り 2 〜 3 分の断片を iMovie で編集しましたが、この断片はものによっては撮影を十数回撮り直すなどしていました。 そして、そのうち動画撮影に飽きてきて、しゃべるのが面倒になってきたので、 AWS の Polly で喋らせたものを貼り付けたりするようになりました。

Polly は ssml の xml を送りつけると、音声をストリームで返してくれるという面白いサービスで、 どうやったらいい感じに喋ってもらえるかを考えながら API を叩いていました。

で、なぜか、音声合成面白いと思うようになったので、

VOCALOIDを買って、初音ミクさんが FizzBuzz する動画を作って遊んでいる現在です