Mavenリポジトリの指定は記述順序を考えないと解決時のパフォーマンスがじわじわ下がっていく

Androidアプリ開発などをしていれば、大概のプロジェクトはGradleを用いてライブラリの依存関係を解決しようとすることになる。そして開発・運用期間の長いアプリになればなるほど、後追いで導入されたサードパーティ製ライブラリとその取得元のMavenリポジトリ定義が肥大化していくケースが散見される。

普段なんとなく設定しているMavenリポジトリの定義だが、実はビルド時のパフォーマンス低下の原因となるケースがあるため、注意が必要だ。


たとえば、以下のようなスクリプトがあるとする。追加してあるライブラリ郡にはさほど意味がなく、単に大量のライブラリが必要な状態を再現するための記述である:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
    ...
}

repositories {
    google()
    jcenter()
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    implementation 'androidx.annotation:annotation:1.1.0'
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.arch.core:core-common:2.0.1'
    implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'
    implementation 'androidx.browser:browser:1.0.0'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'androidx.collection:collection:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.contentpager:contentpager:1.0.0'
    implementation 'androidx.coordinatorlayout:coordinatorlayout:1.0.0'
    implementation 'androidx.cursoradapter:cursoradapter:1.0.0'
    implementation 'androidx.customview:customview:1.0.0'
    implementation 'androidx.documentfile:documentfile:1.0.1'
    implementation 'androidx.drawerlayout:drawerlayout:1.0.0'
    implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0'
    implementation 'androidx.emoji:emoji:1.0.0'
    implementation 'androidx.exifinterface:exifinterface:1.0.0'
}

これをビルドしてみる。

export TZ="Asia/Tokyo"
$(rm ./local.properties || true) && ./gradlew clean && date +%T && ./gradlew build --debug && date +%T
# rm: cannot remove './local.properties': No such file or directory
# Downloading https://services.gradle.org/distributions/gradle-5.1.1-all.zip
# ...
# 13:48:45
# ... 
# 13:50:50

125秒。


ここに、参照するライブラリは変えずに "リポジトリだけ" 追加する。今回は仮に実在しないリポジトリを追加してみた:

...
repositories {
    maven { url 'https://example.com/m2-repo-1' }
    google()
    jcenter()
}
...

この状態で、さきほど同様に実行する:

export TZ="Asia/Tokyo"
$(rm ./local.properties || true) && ./gradlew clean && date +%T && ./gradlew build --debug && date +%T
# rm: cannot remove './local.properties': No such file or directory
# Downloading https://services.gradle.org/distributions/gradle-5.1.1-all.zip
# ...
# 14:00:41
# ... 
# 14:03:49

188秒。この程度なら誤差かもしれない。


では、大量に存在するとどうだろう。今回も、仮に実在しないリポジトリを追加する:

...
repositories {
    maven { url 'https://example.com/m2-repo-1' }
    maven { url 'https://example.com/m2-repo-2' }
    maven { url 'https://example.com/m2-repo-3' }
    maven { url 'https://example.com/m2-repo-4' }
    maven { url 'https://example.com/m2-repo-5' }
    google()
    jcenter()
}
...
export TZ="Asia/Tokyo"
$(rm ./local.properties || true) && ./gradlew clean && date +%T && ./gradlew build --debug && date +%T
# rm: cannot remove './local.properties': No such file or directory
# Downloading https://services.gradle.org/distributions/gradle-5.1.1-all.zip
# ...
# 14:07:55
# ...
# 14:13:33

338秒。元の125秒比較すると実に2.7倍近い時間が掛かるようになってしまった。


試しに順序を変えてみる。実在しないリポジトリたちは後ろへ移動する:

...
repositories {
    google()
    jcenter()
    maven { url 'https://example.com/m2-repo-1' }
    maven { url 'https://example.com/m2-repo-2' }
    maven { url 'https://example.com/m2-repo-3' }
    maven { url 'https://example.com/m2-repo-4' }
    maven { url 'https://example.com/m2-repo-5' }
}
...
export TZ="Asia/Tokyo"
$(rm ./local.properties || true) && ./gradlew clean && date +%T && ./gradlew build --debug && date +%T
# rm: cannot remove './local.properties': No such file or directory
# Downloading https://services.gradle.org/distributions/gradle-5.1.1-all.zip
# ...
# 14:17:31
# ...
# 14:19:34

123秒。リポジトリ自体は定義されているにも関わらず、元の定義に匹敵する速度まで回復した。


なぜこんな現象が起こるのかは、ログを確認すればわかる。 338秒という最も長い時間の必要だったビルドにおけるログを確認してみると、下記のような出力が散見される:

...
05:39:19.831 [DEBUG] [org.apache.http.impl.execchain.MainClientExec] Executing request GET /m2-repo-1/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom HTTP/1.1
...
05:39:19.955 [INFO] [org.gradle.internal.resource.transport.http.HttpClientHelper] Resource missing. [HTTP GET: https://example.com/m2-repo-1/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom]
...
05:39:20.465 [DEBUG] [org.apache.http.impl.execchain.MainClientExec] Executing request GET /m2-repo-2/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom HTTP/1.1
...
05:39:20.586 [INFO] [org.gradle.internal.resource.transport.http.HttpClientHelper] Resource missing. [HTTP GET: https://example.com/m2-repo-2/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom]
...

m2-repo-1, m2-repo-2... と順にリポジトリを手繰っている。repositories {}で定義するリポジトリは、純粋に上から下へ優先順位を持っており、それを順々に参照するのである。
これを自身の参照したいライブラリ毎に行うため、当然ながらじわじわと解決の速度を低下させる要因になりうるのである。


広告系のSDKなどの特定の事業に依存したライブラリの多くは、Maven CentralやJCenterのようなパブリックリポジトリではなく 何かしらのプライベートリポジトリを利用している。特に複数のライブラリを併用する場合は、この参照先リポジトリがどんどん肥大化していくことが多い。

依存解決のためのリクエストは 成功するまで順々に投げられていくため、もし上位に(たとえば1つのライブラリしか置かれていないような)ほとんど使われないリポジトリが指定されていれば、すべての依存関係解決のために毎回無駄なリクエストを投げることになってしまう。


よって、このリポジトリ定義の順序は、下記の優先順位で決定するとよい:

  1. 特別な理由で最優先にしなければならないリポジトリ / 信頼性の高いリポジトリ
  2. 自身のアプリが持つ依存関係の中で最も利用数が多いリポジトリ

この記事の趣旨で言うと、本来であれば2だけを気にすればよい。


が、Android開発などの場合には特別な事情があるため併記しておく。

JCenter(JFrog Bintray)ではGoogle Maven Repositoryのミラーリング(Proxyではない)を独自に行っているが、一時Google Maven Repository内に配置してあるChecksumが不正な内容になってしまった時がある。
JCenterは仕組み上Checksumが不正な場合には404ではなく409を返すようになっている。その結果、google()よりもjcenter()の優先順位を高く設定していた場合には依存関係の解決に失敗してしまう、という問題が発生したことが過去にある

ライブラリの参照元は原則として発行元の推奨するものを利用すべきであるため、Android開発においては(たとえほとんどのライブラリが他のリポジトリ由来であっても)Google Maven Repositoryを最優先にすべきである。

FCM用のFirebaseプロジェクトを別プロジェクトへ移行する

AndroidアプリでPush通知を行う場合、Firebase Cloud Messaging (旧Google Cloud Messaging) を経由しGoogle Play Servicesがよしなに処理してくれるよう投げることになる。
FCMを使うということは、すなわちFirebaseのプロジェクトを firebase.google.com 上で作成するということになる。

アプリ開発の現場でよくあるケースに、当初はAndroid用のPush通知目的でのみ作成したFirebaseプロジェクトを、iOS含めAnalyticsやRemote Configを本格的に利用しようとした時にエイヤッと統合されたものへ移行する、というものがある。

この記事は、既にFCMを利用しているアプリのプロジェクトを移行できるのか検証した時のログである。

前提1

アプリ自体のリニューアル(package nameの変更)は行わないという前提のため、APK署名時に使う証明書は同じものとする。

前提2

今回移行するアプリは、以下のように設定して試した:

プロジェクト名 mynotifymigrationapp-orgproj
プロジェクトID mynotifymigrationapp-orgproj-1
Androidパッケージ名 jp.s64.android.example.mynotifymigrationapp
アプリのニックネーム (省略可) MyNotifyMigrationApp (oldproj)
デバッグ用の署名証明書 SHA-1(省略可) 89:E6:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:34:E5
credential登録方法 google-services.jsonをアプリ内に配置し、google-servicesプラグインで認証
SHA証明書フィンガープリント(※リリース用) EA:F3:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:71:C3
通知の利用 数回なげて、受信できることを確認した

1. 旧プロジェクトから証明書のハッシュを削除する

FirebaseがInvites, Dynamic Links, Authenticationなどの機能を利用するアプリが不正なものではないことを確認できるのは、Googleのサーバにpackage nameと証明書フィンガープリントのペアをグローバルに登録し、それと一致することを条件とするため。
よって、FCMではマストではないSHA1フィンガープリントの登録ではあるが、他のプロジェクト(他の人物、というとわかりやすいか?)が同一の組み合わせを登録しようとするとエラーにで弾かれるようになっている。

すなわち一番はじめにすることは、旧プロジェクトに紐付いた証明書に関する設定を削除すること。

旧プロジェクトのProject Overview -> Settings -> プロジェクトの設定 -> 全般 -> マイアプリ -> Android アプリ -> 当該アプリ(MyNotifyMigrationApp (oldproj)) 内に SHA 証明書フィンガープリント という項目があるため、この内容を削除する。

今回の場合は、

  • 89:E6:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:34:E5
  • EA:F3:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:71:C3

の2件。

2. アプリの認証情報を削除する

この時点で既に旧アプリでの認証情報は不要になるため、削除してしまう。
アプリ内に配置したgoogle-services.jsonを削除して、おしまい。

3. 移行先プロジェクトを作成する

既に移行先プロジェクトが存在する場合はスキップ。
今回は以下の内容にした:

プロジェクト名 mynotifymigrationapp-newproj
プロジェクトID mynotifymigrationapp-newproj-1
Androidパッケージ名 jp.s64.android.example.mynotifymigrationapp ※元と同じ
アプリのニックネーム (省略可) MyNotifyMigrationApp (newproj)
デバッグ用の署名証明書 SHA-1(省略可) 89:E6:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:34:E5 ※元と同じ

4. 新たな認証情報をアプリに入れる

作成の途中でgoogle-services.jsonをダウンロードすることができるので、これをアプリに配置しておく。ステップ2で削除した分。

5. リリース用のハッシュをプロジェクトに追加する

ステップ1で削除した分。移行先プロジェクトの Project Overview -> Settings -> プロジェクトの設定 -> 全般 -> マイアプリ -> Android アプリ -> 当該アプリ(MyNotifyMigrationApp (newproj)) に、本番の証明書から取ったSHA1ハッシュを追加する。
今回の場合は EA:F3:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:71:C3 となる。

6. 必要なら、FCMのサーバーキーを変更する

Amazon SNSやReproなどを使っている場合、FCMへAPI経由でアクセスするためのサーバーキーを新しいものへ変更する必要がある。
Project Overview -> Settings -> プロジェクトの設定 -> クラウドメッセージング -> プロジェクト認証情報 から取得できるので、これを用いて変更作業をする。

Proxyで傍受可能なオレオレ証明書を通せるAndroidアプリの作り方

人間誰しも1度はProxyを通して通信内容をデバッグしたい時がある。
要はオレオレ証明書を発行して間に挟めば傍受も改ざんもやりたい放題なのだが、Android 7以降からは一筋縄では実現できない。

In Android Nougat, we’ve changed how Android handles trusted certificate authorities (CAs) to provide safer defaults for secure app traffic. Most apps and users should not be affected by these changes or need to take any action. The changes include:

  • Safe and easy APIs to trust custom CAs.
  • Apps that target API Level 24 and above no longer trust user or admin-added CAs for secure connections, by default.
  • All devices running Android Nougat offer the same standardized set of system CAs—no device-specific customizations.

https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html

上記のとおり、AOSPで設定されたCA以外は信頼されず、ユーザが独自にインストールしたCAすらもデフォルトで無効になる。
しかしこれらをオプトインで利用することも当然可能になっており、またアプリ内にderファイルを同梱させ利用させることも可能だ。

これらは設定ファイルを記述して自分のアプリに組み込めば実現できる。

今回目指すこと

今回は、

  • Proxyサーバが、アプリが要求した https://example.com の内容を傍受できるようにする
  • 上記の内容を改竄し、アプリがへ届けられるようにする

の2つの実現を目指す。
ただし、傍受や改竄自体は便利なアプリが複数存在する(たとえば Burp Suite など)ので、実際に行うのは独自CAの許可のみだ。

以降で実装手順を解説する。実際のコードはGitHubに掲載した。

github.com

1. Response bodyを表示する簡単なアプリを作る

まずは挙動が確認できないことには始まらないので、通信した結果を表示するだけの画面を作る。
ざっくり以下のようになるはずだ:

class MainActivity : AppCompatActivity() {

    companion object {

        private const val ENDPOINT = "https://example.com"

    }

    private val client = OkHttpClient()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.button)
            .setOnClickListener {
                val req = Request.Builder()
                    .url(ENDPOINT)
                    .build()

                GlobalScope.launch(Dispatchers.IO) {
                    val result = client.newCall(req).execute().let {
                        it.body()?.string() ?: it.code().toString()
                    }

                    GlobalScope.launch(Dispatchers.Main) {
                        findViewById<TextView>(R.id.text)
                            .text = result
                    }
                }
            }
    }
}

このような表示を期待する。

f:id:S64:20190424173808p:plain:w250

2. 構成ファイルを作成・配置する

今回の設定はアプリ内でのみ有効になる。試しに

  • 全ての通信において、プリインストールのCAとres/raw/myca.derを信頼する

という設定にし、src/main/res/xml/nsc.xml に配置した:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config>
        <trust-anchors>
            <certificates src="system" />
            <certificates src="@raw/myca" />
        </trust-anchors>
    </base-config>
</network-security-config>

3. CAを追加する

前項で設定したとおり、src/main/res/raw/myca.derとして追加する。

4. Manifestに設定する

nsc.xmlというファイル名自体には意味がなく、これを AndroidManifest.xml から参照する必要がある。

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    ...

    <application
            ...
            android:networkSecurityConfig="@xml/nsc">
        ...
    </application>

</manifest>

<application/> 要素に android:networkSecurityConfig で設定する。

以上で、独自証明書を用いた通信の傍受や改竄が可能になったはずだ。

`Cannot find a version of '*' that satisfies the version constraints` というエラーがでた時

Android開発で、Gradleから以下のようなエラーが出た:

> Task :app:lint FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:lint'.
> Could not resolve all artifacts for configuration ':${submodule}:debugAndroidTestRuntimeClasspath'.
   > Could not resolve com.android.support:support-annotations:28.0.0.
     Required by:
         project :${submodule}
      > Cannot find a version of 'com.android.support:support-annotations' that satisfies the version constraints: 
           Dependency path '${project}:${submodule}:unspecified' --> 'com.android.support:support-annotations:28.0.0'
           Constraint path '${project}:${submodule}:unspecified' --> 'com.android.support:support-annotations' strictly '26.1.0' because of the following reason: debugRuntimeClasspath uses version 26.1.0

これはなにかというと、マルチモジュールなプロジェクトで依存関係を組んでいる時、

  • 上位モジュールが要求するライブラリがstrictにバージョンを指定している
  • その下位のモジュールは当該ライブラリを直接利用はしていない
  • が、このモジュールの依存する他のライブラリが要求するため、間接的に依存している
    • そしてこの間接的に使われる当該ライブラリバージョンは上位のモノと一致していない
  • 結果、ライブラリバージョンがconflictしているために決定できない

という状況で発生する。


すなわちやることはシンプルで、下位モジュールにも同様に設定すればいい。
今回の場合は、上位のモジュールで28.0.0が打たれているcom.android.support:support-annotations が、下位のモジュールではバージョンがunspecifiedになっているわけなので、その依存を追加すればいい:

...
dependencies {
    implementation 'com.android.support:support-annotations:28.0.0'
}
...

Android Lintで`Unknown issue id "all"`が出る場合

Android Lintを掛けようとして、こんなエラーが出た:

Error: Unknown issue id "all", found in /${projectdir}/lint.xml [LintError]

設定ファイルの当該箇所はこれ:

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    ...
    <issue id="all"> <!-- 🈁 -->
        <ignore path="..."/>
        ...
    </issue>
    ...
</lint>

使っていたAndroid Gradle Pluginはこれ:

...
buildscript {
    ...
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
        ...
    }
}
...

これはなにかというと、単にAndroid Gradle Pluginのバグである。 ここで報告されていた
この問題はAndroid Gradle Plugin 3.3.0で既に修正されている。

...
buildscript {
    ...
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.0'
        ...
    }
}
...

ただし、このAndroid Gradle Plugin 3.3.0は要求するGradleバージョンの引き上げも含まれるため注意。

> Failed to apply plugin [id 'com.android.application']
   > Minimum supported Gradle version is 4.10.1. Current version is [currentversion]. If using the gradle wrapper, try editing the distributionUrl in /${projectdir}/gradle/wrapper/gradle-wrapper.properties to gradle-4.10.1-all.zip

具体的には、gradle-4.10.1以上にしなければいけない。

./gradlew wrapper --gradle-version=4.10.1
# 
# Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0.
# See https://docs.gradle.org/4.8.1/userguide/command_line_interface.html#sec:command_line_warnings
# 
# BUILD SUCCESSFUL in 5s
# 1 actionable task: 1 executed
git status
# On branch ${yourbranch}
# Your branch is ahead of 'origin/${yourbranch}' by ${num} commit.
#   (use "git push" to publish your local commits)
# 
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
# 
#  modified:   gradle/wrapper/gradle-wrapper.properties
# 
# no changes added to commit (use "git add" and/or "git commit -a")
./gradlew wrapper
# 
# Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0.
# Use '--warning-mode all' to show the individual deprecation warnings.
# See https://docs.gradle.org/4.10.1/userguide/command_line_interface.html#sec:command_line_warnings
# 
# BUILD SUCCESSFUL in 5s
# 1 actionable task: 1 executed
git status
# On branch ${yourbranch}
# Your branch is ahead of 'origin/${yourbranch}' by ${num} commit.
#   (use "git push" to publish your local commits)
# 
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
# 
#  modified:   gradle/wrapper/gradle-wrapper.jar
#  modified:   gradle/wrapper/gradle-wrapper.properties
# 
# no changes added to commit (use "git add" and/or "git commit -a")

Gitリポジトリの中身を全部一気に消す

Gitリポジトリの中のファイルを一括で消すコマンドといえば、よく言われるのはこれ:

git rm -rf .

しかしこれはちょっと問題があって、

  • GitでUntrackedなファイルは削除対象じゃない
  • 削除対象ファイルが0コの場合はエラーになる

というちょっと扱いづらいもの。


ならば当然こういう選択肢が出る:

rm -rf ./*

ただこれだと今度は.gitディレクトリまで削除されてしまう。


そこでオススメするonelinearはコチラ:

git clean -fdx && test $(git ls-files | wc -l) -eq 0 || git rm -rf .

ちょっと解説。

git clean -fdx

git clean -fdxのオプションは、

  • -f: force
  • -d: ディレクトリを対象に
  • -x: ignore対象も削除

これでまずUntrackedなファイル含めクリーンに。

&& test $(git ls-files | wc -l) -eq 0

次に test $(git ls-files | wc -l) -eq 0 では、

  • gitの管理するファイル一覧の行数をカウントし、
  • ファイル数が0ならOK、
  • そうでない場合はエラーにする

という内容。

|| git rm -rf .

最後に注目すべきは、||でつなげてあるということ。
これにより、手前のtestが失敗している(= ファイルが残っている)なら実行するし、成功している(= ファイルがもうない)なら実行されない。

よって、エラーを吐かせずにリポジトリの中身がクリーンにできちゃう。

sonarqube-gradle-pluginで `Component key * not found` となった時

org.sonarsource.scanner.gradle:sonarqube-gradle-plugin を使っていて、設定次第ではまれにこのようなエラーが出る場合がある:

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':sonarqube'.
> Unable to load component class org.sonar.scanner.report.MetadataPublisher

* Try:
Run with --debug option to get more log output. Run with --scan to get full insights.

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':sonarqube'.
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:103)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:73)
    at org.gradle.api.internal.tasks.execution.OutputDirectoryCreatingTaskExecuter.execute(OutputDirectoryCreatingTaskExecuter.java:51)
    at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:59)
    at org.gradle.api.internal.tasks.execution.ResolveTaskOutputCachingStateExecuter.execute(ResolveTaskOutputCachingStateExecuter.java:54)
    at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:59)
    at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:101)
    at org.gradle.api.internal.tasks.execution.FinalizeInputFilePropertiesTaskExecuter.execute(FinalizeInputFilePropertiesTaskExecuter.java:44)
    at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:91)
    at org.gradle.api.internal.tasks.execution.ResolveTaskArtifactStateTaskExecuter.execute(ResolveTaskArtifactStateTaskExecuter.java:62)
    at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:59)
    at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:54)
    at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
    at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:34)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker$1.run(DefaultTaskGraphExecuter.java:256)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:249)
    at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:238)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.processTask(DefaultTaskPlanExecutor.java:123)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.access$200(DefaultTaskPlanExecutor.java:79)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:104)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker$1.execute(DefaultTaskPlanExecutor.java:98)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.execute(DefaultTaskExecutionPlan.java:663)
    at org.gradle.execution.taskgraph.DefaultTaskExecutionPlan.executeWithTask(DefaultTaskExecutionPlan.java:597)
    at org.gradle.execution.taskgraph.DefaultTaskPlanExecutor$TaskExecutorWorker.run(DefaultTaskPlanExecutor.java:98)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
    at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
Caused by: java.lang.IllegalStateException: Unable to load component class org.sonar.scanner.report.MetadataPublisher
    at org.sonar.core.platform.ComponentContainer$ExtendedDefaultPicoContainer.getComponent(ComponentContainer.java:65)
    at org.picocontainer.DefaultPicoContainer.getComponent(DefaultPicoContainer.java:621)
    at org.picocontainer.parameters.CollectionComponentParameter.getArrayInstance(CollectionComponentParameter.java:334)
    at org.picocontainer.parameters.CollectionComponentParameter.access$100(CollectionComponentParameter.java:49)
    at org.picocontainer.parameters.CollectionComponentParameter$1.resolveInstance(CollectionComponentParameter.java:139)
    at org.picocontainer.parameters.ComponentParameter$1.resolveInstance(ComponentParameter.java:141)
    at org.picocontainer.injectors.SingleMemberInjector.getParameter(SingleMemberInjector.java:78)
    at org.picocontainer.injectors.ConstructorInjector$CtorAndAdapters.getParameterArguments(ConstructorInjector.java:309)
    at org.picocontainer.injectors.ConstructorInjector$1.run(ConstructorInjector.java:335)
    at org.picocontainer.injectors.AbstractInjector$ThreadLocalCyclicDependencyGuard.observe(AbstractInjector.java:270)
    at org.picocontainer.injectors.ConstructorInjector.getComponentInstance(ConstructorInjector.java:364)
    at org.picocontainer.injectors.AbstractInjectionFactory$LifecycleAdapter.getComponentInstance(AbstractInjectionFactory.java:56)
    at org.picocontainer.behaviors.AbstractBehavior.getComponentInstance(AbstractBehavior.java:64)
    at org.picocontainer.behaviors.Stored.getComponentInstance(Stored.java:91)
    at org.picocontainer.DefaultPicoContainer.instantiateComponentAsIsStartable(DefaultPicoContainer.java:1034)
    at org.picocontainer.DefaultPicoContainer.addAdapterIfStartable(DefaultPicoContainer.java:1026)
    at org.picocontainer.DefaultPicoContainer.startAdapters(DefaultPicoContainer.java:1003)
    at org.picocontainer.DefaultPicoContainer.start(DefaultPicoContainer.java:767)
    at org.sonar.core.platform.ComponentContainer.startComponents(ComponentContainer.java:135)
    at org.sonar.core.platform.ComponentContainer.execute(ComponentContainer.java:122)
    at org.sonar.scanner.task.ScanTask.execute(ScanTask.java:48)
    at org.sonar.scanner.task.TaskContainer.doAfterStart(TaskContainer.java:82)
    at org.sonar.core.platform.ComponentContainer.startComponents(ComponentContainer.java:136)
    at org.sonar.core.platform.ComponentContainer.execute(ComponentContainer.java:122)
    at org.sonar.scanner.bootstrap.GlobalContainer.executeTask(GlobalContainer.java:131)
    at org.sonar.batch.bootstrapper.Batch.doExecuteTask(Batch.java:116)
    at org.sonar.batch.bootstrapper.Batch.executeTask(Batch.java:111)
    at org.sonarsource.scanner.api.internal.batch.BatchIsolatedLauncher.execute(BatchIsolatedLauncher.java:63)
    at org.sonarsource.scanner.api.internal.IsolatedLauncherProxy.invoke(IsolatedLauncherProxy.java:60)
    at com.sun.proxy.$Proxy96.execute(Unknown Source)
    at org.sonarsource.scanner.api.EmbeddedScanner.doExecute(EmbeddedScanner.java:233)
    at org.sonarsource.scanner.api.EmbeddedScanner.runAnalysis(EmbeddedScanner.java:151)
    at org.sonarqube.gradle.SonarQubeTask.run(SonarQubeTask.java:99)
    at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:73)
    at org.gradle.api.internal.project.taskfactory.StandardTaskAction.doExecute(StandardTaskAction.java:46)
    at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:39)
    at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:26)
    at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:788)
    at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:755)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$1.run(ExecuteActionsTaskExecuter.java:124)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:336)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:328)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:199)
    at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:110)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:113)
    at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:95)
    ... 30 more
Caused by: java.lang.IllegalStateException: Unable to load component class org.sonar.scanner.rule.ModuleQProfiles
    at org.sonar.core.platform.ComponentContainer$ExtendedDefaultPicoContainer.getComponent(ComponentContainer.java:65)
    at org.picocontainer.DefaultPicoContainer.getComponent(DefaultPicoContainer.java:632)
    at org.picocontainer.parameters.BasicComponentParameter$1.resolveInstance(BasicComponentParameter.java:118)
    at org.picocontainer.parameters.ComponentParameter$1.resolveInstance(ComponentParameter.java:136)
    at org.picocontainer.injectors.SingleMemberInjector.getParameter(SingleMemberInjector.java:78)
    at org.picocontainer.injectors.ConstructorInjector$CtorAndAdapters.getParameterArguments(ConstructorInjector.java:309)
    at org.picocontainer.injectors.ConstructorInjector$1.run(ConstructorInjector.java:335)
    at org.picocontainer.injectors.AbstractInjector$ThreadLocalCyclicDependencyGuard.observe(AbstractInjector.java:270)
    at org.picocontainer.injectors.ConstructorInjector.getComponentInstance(ConstructorInjector.java:364)
    at org.picocontainer.injectors.AbstractInjectionFactory$LifecycleAdapter.getComponentInstance(AbstractInjectionFactory.java:56)
    at org.picocontainer.behaviors.AbstractBehavior.getComponentInstance(AbstractBehavior.java:64)
    at org.picocontainer.behaviors.Stored.getComponentInstance(Stored.java:91)
    at org.picocontainer.DefaultPicoContainer.getInstance(DefaultPicoContainer.java:699)
    at org.picocontainer.DefaultPicoContainer.getComponent(DefaultPicoContainer.java:647)
    at org.sonar.core.platform.ComponentContainer$ExtendedDefaultPicoContainer.getComponent(ComponentContainer.java:63)
    ... 75 more
Caused by: Failed to load the quality profiles of project 'project': Component key 'project' not found

最後のここに注目:

Caused by: Failed to load the quality profiles of project 'project': Component key 'project' not found

この Component key '*' not found の部分がプロジェクトのルートディレクトリ名だった場合、恐らく問題は簡単。

ドキュメントによると、このプラグインはprojectKey設定の際、デフォルトではルートのプロジェクト名を用いる。
そしてGradleはルートのプロジェクトにプロジェクト名が設定されていない場合、そのプロジェクトのルートディレクトリ名を用いる。

開発現場ではルートディレクトリ名はGitリポジトリ名とリンクしていることが多いため、開発者ほぼ全員が偶然期待通りの設定がデフォルト値として用いられる。
しかし例えばCircleCIなどのSaaSでは、プロジェクトがcloneされる先が ~/project となるため、この前提が崩れるのである。

解決方法

このように環境によって不安定な状態で用いるのは望ましくないが、手っ取り早く治すなら プロジェクトルートにある settings.gradle へ以下の1行を書き足せば良い。

rootProject.name = '期待するディレクトリ名'

余談

私はこれで2営業日とかしました