scalacを使用したScalaコードのコンパイルは遅いですが、sbtは多くの場合、それを高速化します。その仕組みを理解することで、コンパイルをさらに高速化する方法も理解できます。多くの依存関係を持つソースファイルを変更する場合、すべての依存関係をコンパイルする代わりに(たとえば2分かかる場合)、それらのソースファイルのみをリコンパイルするだけで済む場合があります(たとえば5秒かかる場合)。多くの場合、どちらの場合になるかを制御し、いくつかのコーディングプラクティスで開発を高速化できます。
Scalaコンパイルのパフォーマンス向上はsbtの主要な目標であり、そのため、sbtがもたらす高速化は、sbtを使用する主な動機の一つです。sbtのソースと開発努力のかなりの部分は、コンパイルの高速化のための戦略に費やされています。
コンパイル時間を短縮するために、sbtは2つの戦略を使用します。
ソースコードを適切に構成することで、変更の影響を受けるコードの量を最小限に抑えることができます。sbtは、どの依存関係をリコンパイルする必要があるかを正確に判断することはできません。目標は保守的な近似値を計算することであり、ファイルのリコンパイルが必要なときはいつでも、余分なファイルをリコンパイルする可能性があっても、リコンパイルが行われます。
sbtは、ソースファイルの粒度でソース依存関係を追跡します。各ソースファイルについて、sbtはそのファイルに直接依存するファイルをトラッキングします。ファイル内のクラス、オブジェクト、またはトレイトの**インターフェース**が変更された場合、そのソースに依存するすべてのファイルがリコンパイルされる必要があります。現時点では、sbtは次のアルゴリズムを使用して、特定のソースファイルに依存するソースファイルを計算しています。
名前ハッシュ最適化は、sbt 0.13.6以降、デフォルトで有効になっています。
sbtで使用されるヒューリスティックは、次のユーザーに見える結果を意味し、クラスへの変更が他のクラスに影響するかを決定します。
メソッドに関する上記の議論はすべて、フィールドとメンバー全般にも適用されます。同様に、クラスへの参照はオブジェクトとトレイトにも拡張されます。
このセクションでは、インクリメンタルコンパイラの詳細な実装について説明します。インクリメンタルコンパイラが解決しようとしている問題の概要から始まり、現在のインプリメンテーションに至った設計上の選択について説明します。
インクリメンタルコンパイルの目標は、ソースファイルまたはクラスパスへの変更を検出し、最終結果を完全なバッチコンパイルの結果と同じにするような方法で、リコンパイルするファイルの小さなセットを決定することです。変更に対応する際に、インクリメンタルコンパイラは互いに矛盾する目標を持っています。
最初の目標は再コンパイルを高速化することであり、これは増分コンパイラの存在意義そのものです。2つ目の目標は正しさであり、再コンパイルされるファイルの集合のサイズの下限を設定します。その集合を決定することが、増分コンパイラが解決しようとする中心的な問題です。概要でこの問題を少し掘り下げ、増分コンパイラの実装を困難にする要因を理解しましょう。
非常に単純な例を考えてみましょう。
// A.scala
package a
class A {
def foo(): Int = 12
}
// B.scala
package b
class B {
def bar(x: a.A): Int = x.foo()
}
これらの両方のファイルが既にコンパイル済みであり、ユーザーが`A.scala`を変更して次のようになったと仮定します。
// A.scala
package a
class A {
def foo(): Int = 23 // changed constant
}
増分コンパイルの最初のステップは、変更されたソースファイルをコンパイルすることです。これは、増分コンパイラがコンパイルしなければならない最小限のファイル集合です。定数の変更は型チェックエラーを導入しないため、`A.scala`の変更版は正常にコンパイルされます。増分コンパイルの次のステップは、`A.scala`に適用された変更が他のファイルに影響を与えるかどうかを決定することです。上記の例では、メソッド`foo`が返す定数のみが変更されており、他のファイルのコンパイル結果には影響しません。
`A.scala`への別の変更を考えてみましょう。
// A.scala
package a
class A {
def foo(): String = "abc" // changed constant and return type
}
前述と同様に、増分コンパイルの最初のステップは変更されたファイルをコンパイルすることです。この場合、`A.scala`をコンパイルし、コンパイルは正常に終了します。2番目のステップは再び、`A.scala`の変更が他のファイルに影響を与えるかどうかを決定することです。`foo`公開メソッドの戻り値の型が変更されていることがわかるため、これは他のファイルのコンパイル結果に影響を与える可能性があります。実際、`B.scala`には`foo`メソッドへの呼び出しが含まれているため、2番目のステップでコンパイルする必要があります。`B.bar`メソッドでの型ミスマッチのため、`B.scala`のコンパイルは失敗し、そのエラーがユーザーに報告されます。これが、この場合の増分コンパイルが終了する時点です。
上記の例で判断を下すために必要だった2つの主要な情報要素を特定しましょう。増分コンパイラアルゴリズムは、
これらの2つの情報要素はどちらもScalaコンパイラから抽出されます。
増分コンパイラは、さまざまな方法でScalaコンパイラと連携します。
API抽出フェーズは、Trees、Types、Symbolsから情報を抽出し、api.specificationファイルに記述されている増分コンパイラの内部データ構造にマッピングします。これらのデータ構造により、Scalaコンパイラのバージョンに依存しない方法でAPIを表現できます。また、そのような表現は永続的であるため、ディスク上にシリアル化され、コンパイラの複数実行間、またはsbtの複数実行間で再利用されます。
API抽出フェーズは、2つの主要なコンポーネントで構成されます。
TypesとSymbolsのマッピングを担当するロジックは、API.scalaに実装されています。Scalaリフレクションの導入により、TypesとSymbolsの複数のバリアントがあります。増分コンパイラは、`scala.reflect.internal`パッケージで定義されているバリアントを使用します。
また、明白ではない可能性のある設計上の選択が1つあります。クラスまたはトレイトに対応する型をマッピングする場合、そのクラス/トレイトの宣言ではなく、継承されたすべてのメンバーがコピーされます。このようにするのは、クラスに関連するすべての情報が1箇所に格納されるため、API表現の分析が大幅に簡素化され、親型の表現を検索する必要がないためです。この簡素化にはコストがかかります。同じ情報が何度もコピーされるため、パフォーマンスが低下します。たとえば、すべてのクラスには`java.lang.Object`のメンバーとそのシグネチャに関する完全な情報が重複して含まれます。
増分コンパイラ(現在の実装では)は、APIに関する非常に詳細な情報が必要ありません。増分コンパイラは、最後にインデックス付けされてからAPIが変更されたかどうかを知るだけで十分です。その目的にはハッシュ合計で十分であり、多くのメモリを節約できます。したがって、API表現は、単一のコンパイル単位が処理された直後にハッシュ化され、ハッシュ合計のみが永続的に保存されます。
以前のバージョンでは、増分コンパイラはハッシュ化されませんでした。その結果、メモリ消費量が非常に多く、シリアル化/デシリアル化のパフォーマンスが低下しました。
ハッシュ化ロジックは、HashAPI.scalaファイルに実装されています。
増分コンパイラは、指定されたコンパイル単位が依存する(参照する)すべてのSymbolsを抽出し、次にそれらを対応するソース/クラスファイルにマッピングしようとします。Symbolをソースファイルにマッピングするのは、ソースファイルから派生したSymbolsに設定されている`sourceFile`属性を使用することで実行されます。Symbolを(バイナリ)クラスファイルにマッピングする方がトリッキーです。Scalaコンパイラは、バイナリファイルから派生したSymbolsの起源を追跡しないためです。したがって、修飾されたクラス名を対応するクラスパスエントリにマッピングする単純なヒューリスティックが使用されます。このロジックは、完全なクラスパスにアクセスできる依存関係フェーズに実装されています。
指定されたコンパイル単位が依存するSymbolsの集合は、ツリーウォークを実行することで取得されます。ツリーウォークは、依存関係を導入する可能性のあるすべてのツリーノード(別のSymbolを参照する)を調べ、それらに割り当てられたすべてのSymbolsを収集します。Symbolsは、型チェックフェーズ中にScalaコンパイラによってツリーノードに割り当てられます。
増分コンパイラは、依存関係の収集に`CompilationUnit.depends`に依存していました。ただし、名前ハッシュ化には、より正確な依存関係情報が必要です。詳細は#1002を参照してください。.
生成されたクラスファイルのコレクションは、バックエンドがJVMクラスファイルとして出力するすべてのICodeクラスを含む`CompilationUnit.icode`プロパティの内容を検査することで抽出されます。
次の例を考えてみましょう。
// A.scala
class A {
def inc(x: Int): Int = x+1
}
// B.scala
class B {
def foo(a: A, x: Int): Int = a.inc(x)
}
これらの両方のファイルがコンパイル済みであり、ユーザーが`A.scala`を変更して次のようになったと仮定します。
// A.scala
class A {
def inc(x: Int): Int = x+1
def dec(x: Int): Int = x-1
}
ユーザーが保存して増分コンパイラにプロジェクトを再コンパイルするように要求すると、次の操作を実行します。
要約すると、Scalaコンパイラを2回呼び出します。1回目は`A.scala`を再コンパイルするため、2回目は`A`に新しいメソッド`dec`があるため`B.scala`を再コンパイルするためです。
ただし、この単純なシナリオでは、`B.scala`の再コンパイルは不要であることが簡単にわかります。なぜなら、`A`クラスへの`dec`メソッドの追加は、`B`クラスがそれを使用しておらず、何らかの方法で影響を受けていないため、`B`クラスとは無関係だからです。
2つのファイルの場合、あまりにも多くの再コンパイルを行うという事実はそれほど悪く聞こえません。しかし、実際には、依存関係グラフはかなり密であるため、プロジェクト全体にほとんど影響を与えない変更に対して、プロジェクト全体を再コンパイルすることになる可能性があります。これは、ルートが変更された場合にPlayプロジェクトで発生するまさにそれです。ルートと逆ルートの性質は、すべてのテンプレートとすべてのコントローラーがこれらの2つのクラス(`Routes`と`ReversedRoutes`)で定義されているいくつかのメソッドに依存していることですが、特定のルート定義の変更は、通常、すべてのテンプレートとコントローラーの小さなサブセットのみに影響を与えます。
名前ハッシュ化の背後にある考え方は、その観察を利用して、少数のファイルに影響を与える可能性のある変更について、無効化アルゴリズムをよりスマートにすることです。
指定されたソースファイル`X.scala`のAPIへの変更は、`Y.scala`が`X.scala`に依存していても、`Y.scala`のコンパイル結果に影響を与えない場合、無関係と呼ばれます。
この定義から、変更は、指定された依存関係に関してのみ無関係と宣言できることが簡単にわかります。逆に、あるファイルのAPIの指定された変更が他のファイルのコンパイル結果に影響を与えない場合、2つのソースファイル間の依存関係は、そのファイルの指定されたAPIの変更に関して無関係と宣言できます。以降は、無関係な依存関係の検出に焦点を当てます。
無関係な依存関係を検出する非常にナイーブな方法は、`Y.scala`で使用されているすべてのメソッドを追跡し、`X.scala`のメソッドが追加、削除、または変更された場合、それが`Y.scala`で使用されているかどうかを確認することです。使用されていない場合は、この特定のケースにおいて`Y.scala`の`X.scala`への依存関係を無関係とみなします。
この戦略を検討した場合にすぐに発生する問題のプレビューとして、次の2つのシナリオを考えてみましょう。
別のソースファイルで使用されていないメソッドが、そのコンパイル結果にどのように影響するかを見てみましょう。次の構造を考えます。
// A.scala
abstract class A
// B.scala
class B extends A
クラス`A`に抽象メソッドを追加してみましょう。
// A.scala
abstract class A {
def foo(x: Int): Int
}
`A.scala`を再コンパイルした後、`A.foo`は`B`クラスで使用されていないため、`B.scala`を再コンパイルする必要はないと考えることができます。しかし、これは正しくありません。`B`は新しく導入された抽象メソッドを実装しておらず、エラーが報告されるはずです。
したがって、使用されているメソッドを見るという単純な戦略だけでは、特定の依存関係が関連しているかどうかを判断するには不十分です。
ここでは、(まだどこにも使用されていない)新しく導入されたメソッドが、他のファイルのコンパイル結果に影響する別のケースを見てみましょう。今回は継承は関与しませんが、代わりにエンリッチメントパターン(暗黙的な変換)を使用します。
次の構造があると仮定しましょう。
// A.scala
class A
// B.scala
class B {
class AOps(a: A) {
def foo(x: Int): Int = x+1
}
implicit def richA(a: A): AOps = new AOps(a)
def bar(a: A): Int = a.foo(12) // this is expanded to richA(a).foo so we are calling AOPs.foo method
}
ここで、`A`に`foo`メソッドを追加します。
// A.scala
class A {
def foo(x: Int): Int = x-1
}
`A.scala`を再コンパイルし、`A`クラスに新しいメソッドが定義されていることを検出すると、これが`B.scala`の`A.scala`への依存関係に関連しているかどうかを検討する必要があります。`B.scala`では`A.foo`は使用していません(`B.scala`がコンパイルされた時点では存在しませんでした)。しかし、`AOps.foo`を使用しており、`AOps.foo`が`A.foo`と何らかの関係があることはすぐに分かりません。`A`で`foo`が見つからなかったために挿入された暗黙的な変換`richA`の結果として`AOps.foo`への呼び出しという事実を検出する必要があります。
この種の分析は、すぐにScalaの型チェッカーの実装の複雑さに到達し、一般的なケースで実装することは実現可能ではありません。
上記のすべては、APIの構造と使用されているメソッドに関する完全な情報を実際に保持しており、それを利用できると仮定していました。API表現のハッシュ化で説明されているように、API全体の表現ではなく、そのハッシュ値のみを保存しています。また、依存関係はクラス/メソッドレベルではなく、ソースファイルレベルで追跡されます。
より多くの情報を追跡するために現在の設計をやり直すことも考えられますが、それは非常に大規模な作業になります。また、増分コンパイラはAPI全体の構造を保持していましたが、結果として実現不可能なメモリ要件のためにハッシュ化に切り替えました。
前の章で見たように、ソースファイルで使用されているものに関するより多くの情報を追跡するという直接的なアプローチは、非常に迅速に複雑になります。既存の実装よりも大きな改善をもたらす、よりシンプルで精度が低いアプローチを考案したいと考えています。
そのアイデアは、使用されているすべてのメンバーを追跡し、特定のメンバーへの変更が他のファイルのコンパイル結果にいつ影響するかについて非常に正確に推論することではありません。代わりに使用されている*単純な名前*のみを追跡し、指定された単純な名前を持つすべてのメンバーのハッシュ値も追跡します。単純な名前とは、項または型の修飾されていない名前を意味します。
エンリッチメントパターンの問題にこの簡素化された戦略がどのように対処するかをまず見てみましょう。名前ハッシュアルゴリズムをシミュレートすることで行います。元のコードから始めましょう。
// A.scala
class A
// B.scala
class B {
class AOps(a: A) {
def foo(x: Int): Int = x+1
}
implicit def richA(a: A): AOps = new AOps(a)
def bar(a: A): Int = a.foo(12) // this is expanded to richA(a).foo so we are calling AOPs.foo method
}
これらの2つのファイルのコンパイル中に、次の情報を抽出します。
usedNames("A.scala"): A
usedNames("B.scala"): B, AOps, a, A, foo, x, Int, richA, AOps, bar
nameHashes("A.scala"): A -> ...
nameHashes("B.scala"): B -> ..., AOps -> ..., foo -> ..., richA -> ..., bar -> ...
`usedNames`関係は、指定されたソースファイルに記載されているすべての名前を追跡します。`nameHashes`関係は、同じ単純な名前を持つバケットにまとめられたメンバーのグループのハッシュ値を提供します。上記の情報に加えて、`B.scala`の`A.scala`への依存関係も追跡します。
ここで、`A`クラスに`foo`メソッドを追加すると
// A.scala
class A {
def foo(x: Int): Int = x-1
}
再コンパイルすると、次の(更新された)情報が得られます。
usedNames("A.scala"): A, foo
nameHashes("A.scala"): A -> ..., foo -> ...
増分コンパイラは変更前後の名前ハッシュを比較し、`foo`のハッシュ値が変更された(追加された)ことを検出します。したがって、`A.scala`に依存するすべてのソースファイル(この場合は`B.scala`のみ)を調べ、`foo`が使用されている名前として表示されるかどうかを確認します。表示されるため、`B.scala`を意図したとおりに再コンパイルします。
これで、`xyz`のような別のメソッドを`A`に追加した場合、`B.scala`は再コンパイルされません。`B.scala`ではどこにも`xyz`という名前は記載されていないためです。したがって、名前の衝突が比較的少ない場合は、ソースファイル間の多くの依存関係が無関係としてマークされる恩恵を受けるはずです。
この単純な名前ベースのヒューリスティックが「エンリッチメントパターン」テストに耐えられるのは非常に素晴らしいことです。しかし、名前ハッシュ化は継承の別のテストに合格できません。この問題に対処するには、継承によって導入される依存関係とメンバー参照によって導入される依存関係を詳しく調べる必要があります。
名前ハッシュアルゴリズムの背後にあるコアな仮定は、ユーザーがクラスのメンバー(例:メソッド)を追加/変更/削除した場合、他のクラスがその特定のメンバーを使用していない限り、他のクラスのコンパイル結果は影響を受けないということです。さまざまなオーバーライドチェックを伴う継承は、状況全体をはるかに複雑にします。ミキシン合成と組み合わせると、トレイトを継承するクラスに新しいフィールドが導入されるため、継承には特別な処理が必要になることがすぐにわかります。
現在のアイデアは、継承が関与している場合は古いスキームに戻ることです。したがって、メンバー参照によって導入された依存関係と継承によって導入された依存関係を個別に追跡します。継承によって導入されたすべての依存関係は、名前ハッシュ化分析の対象にならないため、無関係としてマークされることはありません。
継承によって導入される依存関係の背後にある直感は非常にシンプルです。それは、あるクラス/トレイトが別のクラス/トレイトを継承することによって導入される依存関係です。その他の依存関係は、別のクラスからメンバー(メソッド、型エイリアス、内部クラス、valなど)を参照(選択)することによって導入されるため、メンバー参照による依存関係と呼ばれます。クラスを継承するには、そのクラスを参照する必要があるため、継承によって導入される依存関係はメンバー参照依存関係の厳密なサブセットです。
違いを示す例を以下に示します。
// A.scala
class A {
def foo(x: Int): Int = x+1
}
// B.scala
class B(val a: A)
// C.scala
trait C
// D.scala
trait D[T]
// X.scala
class X extends A with C with D[B] {
// dependencies by inheritance: A, C, D
// dependencies by member reference: A, C, D, B
}
// Y.scala
class Y {
def test(b: B): Int = b.a.foo(12)
// dependencies by member reference: B, Int, A
}
2つの点に注意してください。
`X`は継承によって`B`に依存しません。`B`は`D`への型パラメーターとして渡されるためです。私たちは
`X`の親として表示される型のみを考慮します。
`Y`は、ソースファイルに`A`の明示的な言及がない場合でも、`A`に依存します。私たちは
`A`で定義されたメソッド`foo`を選択し、それは依存関係を導入するのに十分です。
要約すると、継承とその問題に対処したい方法は、継承によって導入されたすべての依存関係を個別に追跡し、依存関係の無効化の方法をはるかに厳しくすることです。基本的に、継承による依存関係がある場合、親型のあらゆる(些細な)変更に反応します。
これまで簡単に触れてきたことの1つは、名前ハッシュが実際どのように計算されるかです。
前述のように、すべての定義は単純な名前でグループ化され、1つのバケットとしてハッシュ化されます。定義(例:クラス)に他の定義が含まれている場合、それらのネストされた定義はハッシュ値には寄与しません。ネストされた定義は、それらの名前によって選択されたバケットのハッシュ値に寄与します。
クラスへのどのような変更がクライアントの再コンパイルを必要とするかを理解するのは、驚くほど難しいです。Javaで有効なルールははるかに単純です(微妙な点も含まれていますが)。それらをScalaに適用しようとすると、不満が生じるでしょう。いくつかの驚くべき点を例示するために、いくつかの驚くべき点をリストアップします。このリストは完全を意図したものではありません。
誤った増分再コンパイルが発生した場合、または抽出されたインターフェースへの変更によって増分再コンパイルが発生する理由を理解したい場合は、sbt 0.13に適切なツールがあります。
ソースコードを変更して再コンパイルするときにインターフェース表現とその変更をデバッグするには、2つの手順を実行する必要があります。
警告
apiDebug
オプションを有効にすると、増分コンパイラのメモリ消費量が大幅に増加し、パフォーマンスが低下します。根本的な原因は、インターフェースの差異に関する意味のあるデバッグ情報を生成するために、増分コンパイラがデフォルトでハッシュ値のみを保持するのではなく、インターフェースの完全な表現を保持する必要があるためです。増分コンパイラの問題をデバッグする場合のみ、このオプションを有効にしてください。
以下は、プロジェクトでインターフェースデバッグを有効にする方法を示す完全なトランスクリプトです。まず、diffutils
jarをダウンロードしてsbtに渡します。
curl -O https://java-diff-utils.googlecode.com/files/diffutils-1.2.1.jar
sbt -Dsbt.extraClasspath=diffutils-1.2.1.jar
[info] Loading project definition from /Users/grek/tmp/sbt-013/project
[info] Set current project to sbt-013 (in build file:/Users/grek/tmp/sbt-013/)
> set incOptions := incOptions.value.withApiDebug(true)
[info] Defining *:incOptions
[info] The new value will be used by compile:incCompileSetup, test:incCompileSetup
[info] Reapplying settings...
[info] Set current project to sbt-013 (in build file:/Users/grek/tmp/sbt-013/)
Test.scala
に次のソースコードがあるとします。
class A {
def b: Int = 123
}
これをコンパイルし、Test.scala
ファイルを次のように変更します。
class A {
def b: String = "abc"
}
そして、もう一度compile
を実行します。ここでlast compile
を実行すると、デバッグログに次の行が表示されます。
> last compile
[...]
[debug] Detected a change in a public API:
[debug] --- /Users/grek/tmp/sbt-013/Test.scala
[debug] +++ /Users/grek/tmp/sbt-013/Test.scala
[debug] @@ -23,7 +23,7 @@
[debug] ^inherited^ final def ##(): scala.this#Int
[debug] ^inherited^ final def synchronized[ java.lang.Object.T0 >: scala.this#Nothing <: scala.this#Any](x$1: <java.lang.Object.T0>): <java.lang.Object.T0>
[debug] ^inherited^ final def $isInstanceOf[ java.lang.Object.T0 >: scala.this#Nothing <: scala.this#Any](): scala.this#Boolean
[debug] ^inherited^ final def $asInstanceOf[ java.lang.Object.T0 >: scala.this#Nothing <: scala.this#Any](): <java.lang.Object.T0>
[debug] def <init>(): this#A
[debug] -def b: scala.this#Int
[debug] +def b: java.lang.this#String
[debug] }
2つのインターフェースのテキスト表現の統一されたdiffが表示されます。ご覧のとおり、増分コンパイラはb
メソッドの戻り値型の変更を検出しました。
このセクションでは、公開メソッドの戻り値型に型推論を頼ることは常に適切ではない理由を説明します。しかし、これは重要な設計上の問題であるため、固定されたルールを与えることはできません。さらに、この変更は多くの場合侵襲的であり、コンパイル時間の短縮は多くの場合十分な動機とはなりません。そのため、バイナリ互換性とソフトウェアエンジニアリングの観点からいくつかの影響についても説明します。
次のソースファイルA.scala
を検討してください。
import java.io._
object A {
def openFiles(list: List[File]) =
list.map(name => new FileWriter(name))
}
次に、トレイトA
の公開インターフェースを検討してみましょう。メソッドopenFiles
の戻り値型は明示的に指定されていませんが、型推論によってList[FileWriter]
と計算されます。このソースコードを記述した後、クライアントコードを導入し、A.scala
を次のように変更するとします。
import java.io._
object A {
def openFiles(list: List[File]) =
Vector(list.map(name => new BufferedWriter(new FileWriter(name))): _*)
}
型推論により、結果型はVector[BufferedWriter]
と計算されます。つまり、実装を変更することで公開インターフェースが変更され、2つの望ましくない結果が生じます。
val res: List[FileWriter] = A.openFiles(List(new File("foo.input")))
また、次のコードも動作しなくなります。
val a: Seq[Writer] = new BufferedWriter(new FileWriter("bar.input"))
A.openFiles(List(new File("foo.input")))
これらの問題をどのように回避できますか?
もちろん、一般的に解決することはできません。モジュールのインターフェースを変更したい場合は、破損が発生する可能性があります。ただし、多くの場合、モジュールのインターフェースから実装の詳細を削除できます。たとえば、上記の例では、意図された戻り値型がより一般的である可能性があります。つまりSeq[Writer]
です。そうではない場合もあります。これはケースバイケースで決定する設計上の選択です。ただし、この例では、上記の簡略化された例と上記のコードの現実世界の拡張の両方において妥当な選択であるため、設計者がSeq[Writer]
を選択すると仮定します。
上記のクライアントスニペットは次のようになります。
val res: Seq[Writer] =
A.openFiles(List(new File("foo.input")))
val a: Seq[Writer] =
new BufferedWriter(new FileWriter("bar.input")) +:
A.openFiles(List(new File("foo.input")))
sbtは、増分コンパイラがクラスファイルのハッシュをキャッシュしようとする前に、ユーザーがJavaバイトコード(.class
ファイル)を効果的に操作できる拡張ポイントを追加しました。これにより、Ebeanなどのライブラリは、コンパイラキャッシュを破損させたり、数秒ごとにコンパイルを再実行したりすることなく、sbtで機能できるようになります。
これにより、コンパイルタスクはいくつかのサブタスクに分割されます。
previousCompile
: このタスクは、このプロジェクトの以前に永続化されたAnalysis
オブジェクトを返します。compileIncremental
: これは、Scala/Javaファイルをまとめてコンパイルするコアロジックです。このタスクは、実際にはプロジェクトを増分的にコンパイルする作業を行い、最小限のソースファイルがコンパイルされることを保証します。このメソッドの後、scalac + javacによって生成されるすべての.classファイルが使用可能になります。manipulateByteCode
: これは、compileIncremental
の結果を取得して返すスタブタスクです。バイトコードを操作する必要があるプラグインは、このタスクを独自のインプリメンテーションでオーバーライドし、以前の動作を呼び出す必要があります。compile
: このタスクはmanipulateBytecode
に依存し、すべての増分コンパイラ情報を含むAnalysis
オブジェクトを永続化します。独自のプラグインで新しいmanipulateBytecode
キーをフックする方法の例を次に示します。
Compile / manipulateBytecode := {
val previous = (Compile / manipulateBytecode).value
// Note: This must return a new Compiler.CompileResult with our changes.
doManipulateBytecode(previous)
}
増分コンパイルロジックはhttps://github.com/sbt/sbt/blob/0.13/compile/inc/src/main/scala/inc/Incremental.scalaに実装されています。増分再コンパイルポリシーに関する議論は、issue #322、#288、および#1010で確認できます。