滅入るんるん

何か書きます

Gradleプロジェクトで使用しているライブラリーのライセンス通知を自動生成するツールを作りました

タイトルの通りです。作っちゃいました

github.com

Librarianという安直な名前です、深く考えずにネーミングしました

モチベーション

この手のライブラリー・ツールはいろんな人が作っていて何個かありますが自分の用途には使えなかったり情報の推測精度がいまいちだったりとしたので作ってみるか~という感じで作成し始めました。Librarianで成し遂げたい・成し遂げたことは以下の感じです

MarkdownやJsonによる出力

まず、Librarianを自作しなければいけなかった理由としてMarkdownによる出力に対応してるライブラリーが見当たらなかったことにあります*1。アプリケーション開発者はMarkdownで出力しても嬉しいことなんてあまりないかもしれませんが、ライブラリー開発者にとってはGitHubでリポジトリーを公開するときに使用したライブラリーを通知する必要があります*2。ライブラリーの1つや2つぐらいなら手作業で通知文書を書いてもいいかもしれませんが、複数ライブラリーを作っていたりライブラリー自体が巨大であったりすると使用したライブラリーのライセンスを記述することが非常に面倒になるので作ろうと思ったわけです

次に、この手のライブラリーはViewer側はHTMLを表示する形にすることが多いですが、巨大なアプリケーションになるとただHTMLを表示するだけだと使ってるライブラリーを確認するのに一苦労します。可読性大事

この点はJsonで出力してネイティブのViewerで表示すればいいというアプローチをとりました。そっちのほうがパフォーマンスいいしね

正確にライブラリーを集計する

使用しているライブラリーのライセンス通知は2通りの考え方があると思います。1つ目は自らが直接依存してるライブラリーのみをライセンス通知。2つ目は依存しているライブラリーがさらに依存しているライブラリーまですべてのライブラリーをライセンス通知

1つ目のほうは自分が記載した依存ライブラリーは何かということを把握できてるので問題ありませんが、2つ目のほうでライブラリーの誤検知をした場合は通知すべきライブラリーなのかどうかが非常にわかりにくいです*3*4

ユーザビリティーのためにもできる限り誤検知しないようにする必要があります(Librarianで行ってる試みは後述)

できる限り自動でライセンス集計するように

使用してるライブラリーが多岐にわたると通知に必要な情報を推測できなかった場合にはユーザー(つまりこのLibrarianの利用者)がライブラリーの情報を補完する必要があります。ただし、ライブラリーの情報を補完するのは究極的には人類でたった1人だけでいい作業なのでなるべく省略したいものです

省略するためのアプローチとしてLibrarianではプリセットとしていくつかのライブラリーの情報を保持しています。プリセットにあるライブラリーならユーザーが情報を補完せずともライセンス通知ができるようにしてあります

同一ライブラリーなら1つの通知にまとめる

世の中のライブラリーにはソースリポジトリーは同じだけど配布物としてはバラバラな場合があります。こういう場合は機能毎に配布してたりという感じなのですが、機械的にライブラリーを集計する都合によって配布物単位でのライセン通知になりがちです

それはあまりにも見にくく数も膨大になるのでLibrarianでは同一ライブラリーなら1つの通知にまとめるGroup機能を搭載させました。これによって何十とあるandroidxとかの通知が1つにまとめることができます

使い方

インストール

まず最初にLibrarianを使うにはGradle 5.5以上である必要があります(Android開発の場合はAndroid Gradle Plugin 3.5以上である必要もあります)

Gradleのバージョンの確認とアップグレードの仕方はプロジェクト配下のgradle/wrapper/gradle-wrapper.propertiesを見てdistributionUrlの末尾のバージョンを確認したり上げたりすればいいです
例: distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip

LibrarianはGitHub PackagesとBintrayに上げていますが基本的にはBintrayのほうを参照すればよくて

buildscript {
    repositories {
        maven { url "https://dl.bintray.com/meilcli/librarian" }
    }
}
buildscript {
    dependencies {
        classpath "net.meilcli.librarian:plugin-core:VERSION" // replace VERSION
        classpath "net.meilcli.librarian:plugin-preset:VERSION" // replace VERSION
    }
}

という感じにbuild.gradleに記載すればいいです(Viewerも参照する場合はprojectのほうのrepositoriesとdependenciesにも記載します)

設定

まずはライセンス通知を生成したいモジュールのbuild.gradleでプラグインを適用します

apply plugin: 'librarian'
apply plugin: 'librarian-preset'

適用してgradle syncしたらlibrarianXxxXxxなタスクが追加されるのでそれらのタスクを使ってLibrarianのconfigをしていきます

librarianShowConfigurationsタスクを実行したらそのモジュールやそのモジュールが依存してるモジュールのconfigurationがひとまとめにされて出力されます。dependenciesブロックで依存の追加をするときにimplementationとかcompileとかapiとかを記述しますがそのときのimplementationとかがconfigurationと呼ばれます。なので集計対象のconfigurationを決めるためにまずは一覧で出力するというわけです
※dependenciesブロックで記述した名称とconfigurationが一致するとは限りません、たとえばimplementationimplementationDependenciesMetadataというconfiguration名です

librarian {
    failOnGeneratePageWhenFoundPlaceholder = false
    pages {
        "sample-from-maven" {
            title = "Using Libraries"
            description = "sample-from-maven is using this libraries."
            configurations {
                contain {
                    value = [
                        "implementationDependenciesMetadata",
                        "releaseImplementationDependenciesMetadata"
                    ]
                }
            }
        }
    }
}

集計対象のconfigurationが決まったら上記のようにbuild.gradleにlibrarianブロックを追加してください。pagesブロックでライセンス通知の個別設定をします。例ではsample-from-mavenがライセンス通知名でtitle, descriptionとどのようなライセンス通知であるかを説明するための情報を書き込んでいます

configurationsブロックでcontainの中に記載しているのが集計対象のconfigurationです。シングルモジュールプロジェクトではcontainを使用すればいいです。exactという記載方法もありますが、こちらはマルチモジュールプロジェクトでモジュールの依存先に別のモジュールがある場合に使用します

またfailOnGeneratePageWhenFoundPlaceholderによって集計結果の中で情報の推測ができなかったものがあってもエラーにしないようにします。これは最初だとどうしても情報の推測ができないライブラリーが多かったりするためです(情報の推測ができないとトライ&エラーで情報の補完をしていくことになります)

ここまでの記載ができたらgradle syncしてlibrarianGeneratePresetPipelineを実行します。コンソールの出力にPlaceholderが見つかったよという警告が出なかったら1発合格です。警告が出たら後述するgroupsブロックで情報の補完をしていくことになります。作業が完了したらfailOnGeneratePageWhenFoundPlaceholderの記述を削除して本番運用できる形にしてください

librarian {
    groups {
        "Kotlin" { // group name, must be unique
            artifacts = [
                    "org.jetbrains.kotlin:kotlin-gradle-plugin",
                    "org.jetbrains.kotlin:kotlin-serialization",
                    "org.jetbrains.kotlin:kotlin-stdlib-jdk7"
            ] // Array of String, default is empty list
            author = null // String?, default value is null
            url = null // String?, default value is null
            description = null // String?, default value is null
            licenseName = null // String?, default value is null
            licenseUrl = null // String?, default value is null
        }
    }
}

さて、groupsの記載ですが上記のような感じで記述していきます。見た目そのままなので説明は割愛します

Group機能は最初に説明したように同一ライブラリーの通知をひとまとめにするための機能ですが、情報の補完や上書きの用途にも使用することができます

サンプル

AndroidのViewerの使い方とかを説明したほうがいいかもしれませんが、このようなニッチなライブラリーのことを読む読者の方々ならサンプルを見ていただければ使い方がわかると思うのでリンクを貼っておきます

解説

使用ライブラリーを誤検知させない

Gradleではプラグインを作るためにapiが用意されていたり、依存しているライブラリーとかを取得できる仕組みが用意されています。このとき依存しているライブラリーはconfigurationでまとめていて、configurationと依存物(ライブラリー or モジュール)という形になっています

シングルモジュールプロジェクトの場合は誤検知のしようがないですが、マルチモジュールプロジェクトの場合はツールの作り方によっては誤検知してしまうことがあります

たとえばモジュールparent, childが存在するときに

// parent
implementation project(":child")

// child
api "example:example:1.0"

という形になっていたとします。この場合はimplementationもapiもどちらもバイナリーに含まれるので集計対象とします

しかし以下のときはどうでしょうか

// parent
test project(":child")

// child
api "example:example:1.0"

この場合にchild側のconfigurationしか見ていなかったら本来バイナリーに含まれないライブラリーも通知対象として集計してしまいます

Librarianではこういう事態を避けるためにdependency graphを作成して集計対象のライブラリーかどうかを判定しています(そのためにconfigurationsブロックにexactというものが存在してる)
ただ、dependency graphはマルチモジュールプロジェクトでconfigurationが多かったりモジュール数が多かったりすると指数関数的に計算量が増えていくのでfailOnTooManyResolvingConfigurationLimitの設定値を限界値としています*5

情報の推測

ライブラリーの情報を推測する場合の材料としてpomファイルがあります。これ以外にもGitHubとかBintrayとかからも推測することができることがありますが、あまり現実的な計算量ではありません*6

そこで、pomファイルを完全に理解することが情報の推測精度を上げることに繋がります(完全に理解したとは言っていない)

maven.apache.org

authorの推測

authorの推測ですが、pomファイル的にはdevelopersタグとorganizationタグが使えます

<organization>
    <name>MeilCli<name/>
</organization>
<developers>
    <developer>
        <name>MeilCli</name>
        <organization>MeilCli</organization>
    </developer>
</developers>

必要ないタグは省いています。これらのタグはオプション扱いなので必ずしもpomファイルに書かれているとは限りませんが、書かれていたらauthorとして使える情報になります

似たタグとしてcontributorsタグがありますが、コントリビューターをauthorとして記載するのはちょっと微妙だなと感じたのでLibrarianでは参照していません

他にもurlやdescriptionやnameやlicensesタグなどからも推測してますがauthorほど複雑ではないので割愛

parentの参照

pomファイルの情報を継承できるという感じの機能としてparentタグがあります

<parent>
    <groupId>example</groupId>
    <artifactId>example</artifactId>
    <version>1.0</version>
</parent>

観測する限りではJavaの開発に慣れてそうなチームが配布してるライブラリーでたまに使われています。parentタグでlicenseやurlなどの情報を記載して派生先のpomファイルでは最小限の情報しか記載していないことがあるため、parentタグに対応しているだけでも情報の推測精度が上がります

最後に

ライブラリー作成者やマルチモジュールプロジェクトで誤検知とかでつらい思いをしていた開発者はLibrarianをぜひ試して欲しいところです

今後の機能拡張としてはconfig周りをjsonでできるようにしたいと思っていたりプリセット部分を自動で生成できるようにしたりと考えていたりします

また、Librarianを使ってくれた人の中で情報の推測ができてないライブラリーを見つけた方はIssueで報告してくれると対応できるかもしれません(プリセット追加のPRとかくれると嬉しかったり)

*1:探せばあったのかもしれないけど軽く見渡した感じは見つからなかったです

*2:ライブラリー開発者、使用したライブラリーを通知しわすれまくる問題はある

*3:たとえばアプリケーションとして配布する場合はバイナリーに含まれてるライブラリーを通知する必要がありますが、(ライセンス次第ですが)コンパイル時とかのみで使うライブラリーは通知する必要がなかったりします

*4:dependenciesタスクを使ってバイナリーに含まれてるかを確認するという方法がとれます

*5:この計算量の推測値計算は多少曖昧な数値になるので必要に応じて設定値を緩和してもいいと思います

*6:Bintrayからの推測機能をつけようとして実際にBintrayから推測できるライブラリーが少なすぎたのでアルファ版としてほったらかしにしてる顔