はじめに
私が保守を担当しているプロダクト(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のオプションでうまくできるといいなと思っていたのですが、私が調べた限りではちょうどよいものはありませんでした。