はじめに
私が保守を担当しているプロダクト(our-product)では、依存関係の管理にgradleを利用しています。特徴として、3rdPartyのライブラリだけではなく、社内の別プロダクト(other-product)とも依存関係があります。
別プロダクトとの依存関係を定義しつつ、自分たちが利用しているライブラリも正しく管理したいのですが、その際にハマったことがあるのでまとめます。
ハマったこと
別プロダクトの成果物(other-product-1.0.0.jar)が参照しているライブラリを、build.gradleのdependenciesに定義しなくてもコンパイルが成功してしまったことです。
例えば、commons-lang:commons-lang:2.6とother-product-1.0.0.jarを参照している場合、dependenciesには下記のように記載する必要があります。
dependencies {
implementation "commons-lang:commons-lang:2.6"
implementation "jp.co.qiita:other-product:1.0.0"
}
しかし、other-product-1.0.0.jarがcommons-lang:commons-lang:2.6を参照していた場合、下記の記述でもコンパイルが成功してしまいます。
dependencies {
implementation "jp.co.qiita:other-product:1.0.0"
}
ただ成果物を作りたいだけならこれでもいいのですが、自分たちのプロダクトで利用しているライブラリは、そうとわかるように管理したいのでこれだと困ります。
なぜこの状態でコンパイルが成功してしまうのか
おそらくcompileClassPathにcommons-lang:commons-lang:2.6が含まれているためだと思います。dependencyを出力すると下記のようになりました。
\--- jp.co.qiita:other-product:1.0.0
\--- commons-lang:commons-lang:2.6
jp.co.qiita:other-product:1.0.0の推移的な依存関係で追加されているため、ためしにcommons-lang:commons-langをexcludeしてみます。
dependencies {
implementation ("jp.co.qiita:other-product:1.0.0") {
exclude group: "commons-lang", module: "commons-lang"
}
}
予想通りコンパイルは失敗しました。最初にコンパイルが成功してしまったのは推移的な依存関係でcommons-lang:commons-langが追加されたからというのは間違いなさそうです。
この状態で発生するリスク
コンパイルできるならこのままでもいいのではないかと思ってしまいそうですが、そうではありません。下記のようなことが起こってしまいます。
- 開発者が意識せずに
jp.co.qiita:other-productが推移的に依存しているライブラリを利用してしまう - コンパイル時にエラーとならないため、開発者はそれに気づくことができない
-
our-productのbuild.gradleで管理できていないライブラリが利用されている状態となる - とあるライブラリで重大な脆弱性等が発生した場合に、
our-productでそのライブラリを利用しているのかをbuild.gradleから正確に判断できない
our-productの依存関係を出力すると、our-productが直接利用しているものとother-productが推移的に利用しているものが出力されます。そのライブラリがどちらにあたるのか、依存関係を出力したらすぐに判断できるようになっていなければなりません。
考えられる解決策
build.gradleで直接管理できていないライブラリを意図せず利用してしまう問題の解決策として考えられるものを記載します。
transitive= falseにしてしまう
推移的な依存関係を解決しないように、transitive = falseとして管理するという方法が考えられます。
dependencies {
implementation "commons-lang:commons-lang:2.6"
implementation ("jp.co.qiita:other-product:1.0.0") {
transitive = false
}
}
こうすれば、jp.co.qiita:other-productの直接的な依存関係しか解決されないため、この問題は解決しそうです。しかし、この方法にも問題があります。
our-productとother-productは同じサーバー上で動くことがあります。よって双方が利用している3rdPartyのライブラリは同じバージョンになっていなければなりません。この問題を解決するために、定期的にour-productのdependenciesを出力し、バージョンにずれがないかをツールでチェックしています。transitive = falseで依存関係を定義してしまうと、この仕組みが意味をなさなくなってしまいます。
transitiveを外から渡せるようにする
下記のように記述し、ビルドするときは./gradlew -Ptransitive=false jarのように外側から情報を渡せるようにします。
ext {
TRANSITIVE = findProperty('transitive') ?: "true"
}
dependencies {
implementation "commons-lang:commons-lang:2.6"
implementation ("jp.co.qiita:other-product:1.0.0") {
if ("${TRANSITIVE}" == "false") {
transitive = false
}
}
}
利用ライブラリのバージョンチェックを実施するときはtransitive=trueとし、PRのプレマージのチェックではtransitive=falseでコンパイルが成功するかを確認します。
こうすれば、やりたいことは実現できそうですが、dependenciesの中に条件分岐が入ってなんか嫌だなという感じはします。
最後に
おそらくour-productは建付けがやや特殊なんだろうなと思います。gradleのオプションでうまくできるといいなと思っていたのですが、私が調べた限りではちょうどよいものはありませんでした。