Merge branch '0.13' into follow-deprecation

This commit is contained in:
Jacek Laskowski 2014-04-07 22:20:55 +02:00
commit b75442b4a3
220 changed files with 6001 additions and 1211 deletions

19
.travis.yml Normal file
View File

@ -0,0 +1,19 @@
language: scala
script:
- sbt "scripted $SCRIPTED_TEST"
env:
- SCRIPTED_TEST=actions/*
- SCRIPTED_TEST=api/*
- SCRIPTED_TEST=compiler-project/*
- SCRIPTED_TEST=dependency-management/*
- SCRIPTED_TEST=java/*
- SCRIPTED_TEST=package/*
- SCRIPTED_TEST=reporter/*
- SCRIPTED_TEST=run/*
- SCRIPTED_TEST=source-dependencies/*
- SCRIPTED_TEST=tests/*
jdk:
- openjdk6
notifications:
email:
- qbranch@typesafe.com

View File

@ -29,6 +29,14 @@ object APIUtil
{
var hasMacro = false
// Don't visit inherited definitions since we consider that a class
// that inherits a macro does not have a macro.
override def visitStructure0(structure: Structure)
{
visitTypes(structure.parents)
visitDefinitions(structure.declared)
}
override def visitModifiers(m: Modifiers)
{
hasMacro ||= m.isMacro

View File

@ -11,7 +11,13 @@ package sbt
// because complexity(Equiv[Seq[String]]) > complexity(Equiv[CompileSetup])
// (6 > 4)
final class CompileOptions(val options: Seq[String], val javacOptions: Seq[String])
final class CompileSetup(val output: APIOutput, val options: CompileOptions, val compilerVersion: String, val order: CompileOrder)
final class CompileSetup(val output: APIOutput, val options: CompileOptions, val compilerVersion: String,
val order: CompileOrder, val nameHashing: Boolean) {
@deprecated("Use the other overloaded variant of the constructor that takes `nameHashing` value, instead.", "0.13.2")
def this(output: APIOutput, options: CompileOptions, compilerVersion: String, order: CompileOrder) = {
this(output, options, compilerVersion, order, false)
}
}
object CompileSetup
{
@ -21,18 +27,21 @@ object CompileSetup
equivOutput.equiv(a.output, b.output) &&
equivOpts.equiv(a.options, b.options) &&
equivComp.equiv(a.compilerVersion, b.compilerVersion) &&
a.order == b.order // equivOrder.equiv(a.order, b.order)
a.order == b.order && // equivOrder.equiv(a.order, b.order)
a.nameHashing == b.nameHashing
}
implicit val equivFile: Equiv[File] = new Equiv[File] {
def equiv(a: File, b: File) = a.getAbsoluteFile == b.getAbsoluteFile
}
implicit val equivOutput: Equiv[APIOutput] = new Equiv[APIOutput] {
implicit val outputGroupsOrdering = Ordering.by((og: MultipleOutput.OutputGroup) => og.sourceDirectory)
def equiv(out1: APIOutput, out2: APIOutput) = (out1, out2) match {
case (m1: MultipleOutput, m2: MultipleOutput) =>
m1.outputGroups zip (m2.outputGroups) forall {
case (a,b) =>
(m1.outputGroups.length == m2.outputGroups.length) &&
(m1.outputGroups.sorted zip m2.outputGroups.sorted forall {
case (a,b) =>
equivFile.equiv(a.sourceDirectory, b.sourceDirectory) && equivFile.equiv(a.outputDirectory, b.outputDirectory)
}
})
case (s1: SingleOutput, s2: SingleOutput) => equivFile.equiv(s1.outputDirectory, s2.outputDirectory)
case _ => false
}
@ -40,12 +49,12 @@ object CompileSetup
implicit val equivOpts: Equiv[CompileOptions] = new Equiv[CompileOptions] {
def equiv(a: CompileOptions, b: CompileOptions) =
(a.options sameElements b.options) &&
(a.javacOptions sameElements b.javacOptions)
(a.javacOptions sameElements b.javacOptions)
}
implicit val equivCompilerVersion: Equiv[String] = new Equiv[String] {
def equiv(a: String, b: String) = a == b
}
implicit val equivOrder: Equiv[CompileOrder] = new Equiv[CompileOrder] {
def equiv(a: CompileOrder, b: CompileOrder) = a == b
}

View File

@ -56,6 +56,8 @@ trait Analysis
object Analysis
{
lazy val Empty: Analysis = new MAnalysis(Stamps.empty, APIs.empty, Relations.empty, SourceInfos.empty, Compilations.empty)
private[sbt] def empty(nameHashing: Boolean): Analysis = new MAnalysis(Stamps.empty, APIs.empty,
Relations.empty(nameHashing = nameHashing), SourceInfos.empty, Compilations.empty)
/** Merge multiple analysis objects into one. Deps will be internalized as needed. */
def merge(analyses: Traversable[Analysis]): Analysis = {

View File

@ -6,6 +6,8 @@ package inc
import xsbt.api.NameChanges
import java.io.File
import xsbti.api.{_internalOnly_NameHashes => NameHashes}
import xsbti.api.{_internalOnly_NameHash => NameHash}
final case class InitialChanges(internalSrc: Changes[File], removedProducts: Set[File], binaryDeps: Set[File], external: APIChanges[String])
final class APIChanges[T](val apiChanges: Iterable[APIChange[T]])
@ -20,8 +22,39 @@ sealed abstract class APIChange[T](val modified: T)
* api has changed. The reason is that there's no way to determine if changes to macros implementation
* are affecting its users or not. Therefore we err on the side of caution.
*/
case class APIChangeDueToMacroDefinition[T](modified0: T) extends APIChange(modified0)
case class SourceAPIChange[T](modified0: T) extends APIChange(modified0)
final case class APIChangeDueToMacroDefinition[T](modified0: T) extends APIChange(modified0)
final case class SourceAPIChange[T](modified0: T) extends APIChange(modified0)
/**
* An APIChange that carries information about modified names.
*
* This class is used only when name hashing algorithm is enabled.
*/
final case class NamesChange[T](modified0: T, modifiedNames: ModifiedNames) extends APIChange(modified0)
/**
* ModifiedNames are determined by comparing name hashes in two versions of an API representation.
*
* Note that we distinguish between sets of regular (non-implicit) and implicit modified names.
* This distinction is needed because the name hashing algorithm makes different decisions based
* on whether modified name is implicit or not. Implicit names are much more difficult to handle
* due to difficulty of reasoning about the implicit scope.
*/
final case class ModifiedNames(regularNames: Set[String], implicitNames: Set[String]) {
override def toString: String =
s"ModifiedNames(regularNames = ${regularNames mkString ", "}, implicitNames = ${implicitNames mkString ", "})"
}
object ModifiedNames {
def compareTwoNameHashes(a: NameHashes, b: NameHashes): ModifiedNames = {
val modifiedRegularNames = calculateModifiedNames(a.regularMembers.toSet, b.regularMembers.toSet)
val modifiedImplicitNames = calculateModifiedNames(a.implicitMembers.toSet, b.implicitMembers.toSet)
ModifiedNames(modifiedRegularNames, modifiedImplicitNames)
}
private def calculateModifiedNames(xs: Set[NameHash], ys: Set[NameHash]): Set[String] = {
val differentNameHashes = (xs union ys) diff (xs intersect ys)
differentNameHashes.map(_.name)
}
}
trait Changes[A]
{

View File

@ -146,10 +146,18 @@ private final class AnalysisCallback(internalMap: File => Option[File], external
classToSource.put(module, source)
}
// empty value used when name hashing algorithm is disabled
private val emptyNameHashes = new xsbti.api._internalOnly_NameHashes(Array.empty, Array.empty)
def api(sourceFile: File, source: SourceAPI) {
import xsbt.api.{APIUtil, HashAPI}
if (APIUtil.isScalaSourceName(sourceFile.getName) && APIUtil.hasMacro(source)) macroSources += sourceFile
publicNameHashes(sourceFile) = (new NameHashing).nameHashes(source)
publicNameHashes(sourceFile) = {
if (nameHashing)
(new NameHashing).nameHashes(source)
else
emptyNameHashes
}
val shouldMinimize = !Incremental.apiDebug(options)
val savedSource = if (shouldMinimize) APIUtil.minimize(source) else source
apis(sourceFile) = (HashAPI(source), savedSource)
@ -157,9 +165,9 @@ private final class AnalysisCallback(internalMap: File => Option[File], external
def usedName(sourceFile: File, name: String) = add(usedNames, sourceFile, name)
def nameHashing: Boolean = false // TODO: define the flag in IncOptions which controls this
def nameHashing: Boolean = options.nameHashing
def get: Analysis = addUsedNames( addCompilation( addExternals( addBinaries( addProducts( addSources(Analysis.Empty) ) ) ) ) )
def get: Analysis = addUsedNames( addCompilation( addExternals( addBinaries( addProducts( addSources(Analysis.empty(nameHashing = nameHashing)) ) ) ) ) )
def addProducts(base: Analysis): Analysis = addAll(base, classes) { case (a, src, (prod, name)) => a.addProduct(src, prod, current product prod, name ) }
def addBinaries(base: Analysis): Analysis = addAll(base, binaryDeps)( (a, src, bin) => a.addBinaryDep(src, bin, binaryClassName(bin), current binary bin) )
def addSources(base: Analysis): Analysis =

View File

@ -51,57 +51,76 @@ final class IncOptions(
* Determines whether incremental compiler should recompile all dependencies of a file
* that contains a macro definition.
*/
val recompileOnMacroDef: Boolean
val recompileOnMacroDef: Boolean,
/**
* Determines whether incremental compiler uses the new algorithm known as name hashing.
*
* This flag is disabled by default so incremental compiler's behavior is the same as in sbt 0.13.0.
*
* IMPLEMENTATION NOTE:
* Enabling this flag enables a few additional functionalities that are needed by the name hashing algorithm:
*
* 1. New dependency source tracking is used. See `sbt.inc.Relations` for details.
* 2. Used names extraction and tracking is enabled. See `sbt.inc.Relations` for details as well.
* 3. Hashing of public names is enabled. See `sbt.inc.AnalysisCallback` for details.
*
*/
val nameHashing: Boolean
) extends Product with Serializable {
/**
* Secondary constructor introduced to make IncOptions to be binary compatible with version that didn't have
* `recompileOnMacroDef` filed defined.
* `recompileOnMacroDef` and `nameHashing` fields defined.
*/
def this(transitiveStep: Int, recompileAllFraction: Double, relationsDebug: Boolean, apiDebug: Boolean,
apiDiffContextSize: Int, apiDumpDirectory: Option[java.io.File], newClassfileManager: () => ClassfileManager) = {
this(transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize,
apiDumpDirectory, newClassfileManager, IncOptions.recompileOnMacroDefDefault)
apiDumpDirectory, newClassfileManager, IncOptions.recompileOnMacroDefDefault, IncOptions.nameHashingDefault)
}
def withTransitiveStep(transitiveStep: Int): IncOptions = {
new IncOptions(transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize,
apiDumpDirectory, newClassfileManager, recompileOnMacroDef)
apiDumpDirectory, newClassfileManager, recompileOnMacroDef, nameHashing)
}
def withRecompileAllFraction(recompileAllFraction: Double): IncOptions = {
new IncOptions(transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize,
apiDumpDirectory, newClassfileManager, recompileOnMacroDef)
apiDumpDirectory, newClassfileManager, recompileOnMacroDef, nameHashing)
}
def withRelationsDebug(relationsDebug: Boolean): IncOptions = {
new IncOptions(transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize,
apiDumpDirectory, newClassfileManager, recompileOnMacroDef)
apiDumpDirectory, newClassfileManager, recompileOnMacroDef, nameHashing)
}
def withApiDebug(apiDebug: Boolean): IncOptions = {
new IncOptions(transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize,
apiDumpDirectory, newClassfileManager, recompileOnMacroDef)
apiDumpDirectory, newClassfileManager, recompileOnMacroDef, nameHashing)
}
def withApiDiffContextSize(apiDiffContextSize: Int): IncOptions = {
new IncOptions(transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize,
apiDumpDirectory, newClassfileManager, recompileOnMacroDef)
apiDumpDirectory, newClassfileManager, recompileOnMacroDef, nameHashing)
}
def withApiDumpDirectory(apiDumpDirectory: Option[File]): IncOptions = {
new IncOptions(transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize,
apiDumpDirectory, newClassfileManager, recompileOnMacroDef)
apiDumpDirectory, newClassfileManager, recompileOnMacroDef, nameHashing)
}
def withNewClassfileManager(newClassfileManager: () => ClassfileManager): IncOptions = {
new IncOptions(transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize,
apiDumpDirectory, newClassfileManager, recompileOnMacroDef)
apiDumpDirectory, newClassfileManager, recompileOnMacroDef, nameHashing)
}
def withRecompileOnMacroDef(recompileOnMacroDef: Boolean): IncOptions = {
new IncOptions(transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize,
apiDumpDirectory, newClassfileManager, recompileOnMacroDef)
apiDumpDirectory, newClassfileManager, recompileOnMacroDef, nameHashing)
}
def withNameHashing(nameHashing: Boolean): IncOptions = {
new IncOptions(transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize,
apiDumpDirectory, newClassfileManager, recompileOnMacroDef, nameHashing)
}
//- EXPANDED CASE CLASS METHOD BEGIN -//
@ -112,14 +131,14 @@ final class IncOptions(
apiDumpDirectory: Option[java.io.File] = this.apiDumpDirectory,
newClassfileManager: () => ClassfileManager = this.newClassfileManager): IncOptions = {
new IncOptions(transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize,
apiDumpDirectory, newClassfileManager)
apiDumpDirectory, newClassfileManager, recompileOnMacroDef, nameHashing)
}
@deprecated("Methods generated for case class will be removed in the future.", "0.13.2")
override def productPrefix: String = "IncOptions"
@deprecated("Methods generated for case class will be removed in the future.", "0.13.2")
def productArity: Int = 7
def productArity: Int = 9
@deprecated("Methods generated for case class will be removed in the future.", "0.13.2")
def productElement(x$1: Int): Any = x$1 match {
@ -130,6 +149,8 @@ final class IncOptions(
case 4 => IncOptions.this.apiDiffContextSize
case 5 => IncOptions.this.apiDumpDirectory
case 6 => IncOptions.this.newClassfileManager
case 7 => IncOptions.this.recompileOnMacroDef
case 8 => IncOptions.this.nameHashing
case _ => throw new IndexOutOfBoundsException(x$1.toString())
}
@ -149,7 +170,9 @@ final class IncOptions(
acc = Statics.mix(acc, apiDiffContextSize)
acc = Statics.mix(acc, Statics.anyHash(apiDumpDirectory))
acc = Statics.mix(acc, Statics.anyHash(newClassfileManager))
Statics.finalizeHash(acc, 7)
acc = Statics.mix(acc, if (recompileOnMacroDef) 1231 else 1237)
acc = Statics.mix(acc, if (nameHashing) 1231 else 1237)
Statics.finalizeHash(acc, 9)
}
override def toString(): String = scala.runtime.ScalaRunTime._toString(IncOptions.this)
@ -160,7 +183,8 @@ final class IncOptions(
transitiveStep == IncOptions$1.transitiveStep && recompileAllFraction == IncOptions$1.recompileAllFraction &&
relationsDebug == IncOptions$1.relationsDebug && apiDebug == IncOptions$1.apiDebug &&
apiDiffContextSize == IncOptions$1.apiDiffContextSize && apiDumpDirectory == IncOptions$1.apiDumpDirectory &&
newClassfileManager == IncOptions$1.newClassfileManager
newClassfileManager == IncOptions$1.newClassfileManager &&
recompileOnMacroDef == IncOptions$1.recompileOnMacroDef && nameHashing == IncOptions$1.nameHashing
}))
}
//- EXPANDED CASE CLASS METHOD END -//
@ -168,6 +192,7 @@ final class IncOptions(
object IncOptions extends Serializable {
private val recompileOnMacroDefDefault: Boolean = true
private val nameHashingDefault: Boolean = false
val Default = IncOptions(
// 1. recompile changed sources
// 2(3). recompile direct dependencies and transitive public inheritance dependencies of sources with API changes in 1(2).
@ -179,7 +204,8 @@ object IncOptions extends Serializable {
apiDiffContextSize = 5,
apiDumpDirectory = None,
newClassfileManager = ClassfileManager.deleteImmediately,
recompileOnMacroDef = recompileOnMacroDefDefault
recompileOnMacroDef = recompileOnMacroDefDefault,
nameHashing = nameHashingDefault
)
//- EXPANDED CASE CLASS METHOD BEGIN -//
final override def toString(): String = "IncOptions"
@ -192,9 +218,10 @@ object IncOptions extends Serializable {
}
def apply(transitiveStep: Int, recompileAllFraction: Double, relationsDebug: Boolean, apiDebug: Boolean,
apiDiffContextSize: Int, apiDumpDirectory: Option[java.io.File],
newClassfileManager: () => ClassfileManager, recompileOnMacroDef: Boolean): IncOptions = {
newClassfileManager: () => ClassfileManager, recompileOnMacroDef: Boolean,
nameHashing: Boolean): IncOptions = {
new IncOptions(transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize,
apiDumpDirectory, newClassfileManager, recompileOnMacroDef)
apiDumpDirectory, newClassfileManager, recompileOnMacroDef, nameHashing)
}
@deprecated("Methods generated for case class will be removed in the future.", "0.13.2")
def unapply(x$0: IncOptions): Option[(Int, Double, Boolean, Boolean, Int, Option[java.io.File], () => AnyRef)] = {
@ -217,7 +244,8 @@ object IncOptions extends Serializable {
private val apiDebugKey = "apiDebug"
private val apiDumpDirectoryKey = "apiDumpDirectory"
private val apiDiffContextSizeKey = "apiDiffContextSize"
private val recompileOnMacroDefKey = "recompileOnMacroDefKey"
private val recompileOnMacroDefKey = "recompileOnMacroDef"
private val nameHashingKey = "nameHashing"
def fromStringMap(m: java.util.Map[String, String]): IncOptions = {
// all the code below doesn't look like idiomatic Scala for a good reason: we are working with Java API
@ -251,9 +279,13 @@ object IncOptions extends Serializable {
val k = recompileOnMacroDefKey
if (m.containsKey(k)) m.get(k).toBoolean else Default.recompileOnMacroDef
}
def getNameHashing: Boolean = {
val k = nameHashingKey
if (m.containsKey(k)) m.get(k).toBoolean else Default.nameHashing
}
new IncOptions(getTransitiveStep, getRecompileAllFraction, getRelationsDebug, getApiDebug, getApiDiffContextSize,
getApiDumpDirectory, ClassfileManager.deleteImmediately, getRecompileOnMacroDef)
getApiDumpDirectory, ClassfileManager.deleteImmediately, getRecompileOnMacroDef, getNameHashing)
}
def toStringMap(o: IncOptions): java.util.Map[String, String] = {
@ -265,6 +297,7 @@ object IncOptions extends Serializable {
o.apiDumpDirectory.foreach(f => m.put(apiDumpDirectoryKey, f.toString))
m.put(apiDiffContextSizeKey, o.apiDiffContextSize.toString)
m.put(recompileOnMacroDefKey, o.recompileOnMacroDef.toString)
m.put(nameHashingKey, o.nameHashing.toString)
m
}
}

View File

@ -21,20 +21,44 @@ object Incremental
log: Logger,
options: IncOptions)(implicit equivS: Equiv[Stamp]): (Boolean, Analysis) =
{
val initialChanges = changedInitial(entry, sources, previous, current, forEntry, options, log)
val incremental: IncrementalCommon =
if (!options.nameHashing)
new IncrementalDefaultImpl(log, options)
else
new IncrementalNameHashing(log, options)
val initialChanges = incremental.changedInitial(entry, sources, previous, current, forEntry)
val binaryChanges = new DependencyChanges {
val modifiedBinaries = initialChanges.binaryDeps.toArray
val modifiedClasses = initialChanges.external.allModified.toArray
def isEmpty = modifiedBinaries.isEmpty && modifiedClasses.isEmpty
}
val initialInv = invalidateInitial(previous.relations, initialChanges, log)
val initialInv = incremental.invalidateInitial(previous.relations, initialChanges)
log.debug("All initially invalidated sources: " + initialInv + "\n")
val analysis = manageClassfiles(options) { classfileManager =>
cycle(initialInv, sources, binaryChanges, previous, doCompile, classfileManager, 1, log, options)
incremental.cycle(initialInv, sources, binaryChanges, previous, doCompile, classfileManager, 1)
}
(!initialInv.isEmpty, analysis)
}
// the name of system property that was meant to enable debugging mode of incremental compiler but
// it ended up being used just to enable debugging of relations. That's why if you migrate to new
// API for configuring incremental compiler (IncOptions) it's enough to control value of `relationsDebug`
// flag to achieve the same effect as using `incDebugProp`.
@deprecated("Use `IncOptions.relationsDebug` flag to enable debugging of relations.", "0.13.2")
val incDebugProp = "xsbt.inc.debug"
private[inc] val apiDebugProp = "xsbt.api.debug"
private[inc] def apiDebug(options: IncOptions): Boolean = options.apiDebug || java.lang.Boolean.getBoolean(apiDebugProp)
private[sbt] def prune(invalidatedSrcs: Set[File], previous: Analysis): Analysis =
prune(invalidatedSrcs, previous, ClassfileManager.deleteImmediately())
private[sbt] def prune(invalidatedSrcs: Set[File], previous: Analysis, classfileManager: ClassfileManager): Analysis =
{
classfileManager.delete( invalidatedSrcs.flatMap(previous.relations.products) )
previous -- invalidatedSrcs
}
private[this] def manageClassfiles[T](options: IncOptions)(run: ClassfileManager => T): T =
{
val classfileManager = options.newClassfileManager()
@ -46,10 +70,12 @@ object Incremental
result
}
val incDebugProp = "xsbt.inc.debug"
private def incDebug(options: IncOptions): Boolean = options.relationsDebug || java.lang.Boolean.getBoolean(incDebugProp)
val apiDebugProp = "xsbt.api.debug"
def apiDebug(options: IncOptions): Boolean = options.apiDebug || java.lang.Boolean.getBoolean(apiDebugProp)
}
private abstract class IncrementalCommon(log: Logger, options: IncOptions) {
private def incDebug(options: IncOptions): Boolean = options.relationsDebug || java.lang.Boolean.getBoolean(Incremental.incDebugProp)
// setting the related system property to true will skip checking that the class name
// still comes from the same classpath entry. This can workaround bugs in classpath construction,
@ -58,16 +84,16 @@ object Incremental
// TODO: the Analysis for the last successful compilation should get returned + Boolean indicating success
// TODO: full external name changes, scopeInvalidations
@tailrec def cycle(invalidatedRaw: Set[File], allSources: Set[File], binaryChanges: DependencyChanges, previous: Analysis,
doCompile: (Set[File], DependencyChanges) => Analysis, classfileManager: ClassfileManager, cycleNum: Int, log: Logger, options: IncOptions): Analysis =
@tailrec final def cycle(invalidatedRaw: Set[File], allSources: Set[File], binaryChanges: DependencyChanges, previous: Analysis,
doCompile: (Set[File], DependencyChanges) => Analysis, classfileManager: ClassfileManager, cycleNum: Int): Analysis =
if(invalidatedRaw.isEmpty)
previous
else
{
def debug(s: => String) = if (incDebug(options)) log.debug(s) else ()
val withPackageObjects = invalidatedRaw ++ invalidatedPackageObjects(invalidatedRaw, previous.relations)
val invalidated = expand(withPackageObjects, allSources, log, options)
val pruned = prune(invalidated, previous, classfileManager)
val invalidated = expand(withPackageObjects, allSources)
val pruned = Incremental.prune(invalidated, previous, classfileManager)
debug("********* Pruned: \n" + pruned.relations + "\n*********")
val fresh = doCompile(invalidated, binaryChanges)
@ -76,18 +102,18 @@ object Incremental
val merged = pruned ++ fresh//.copy(relations = pruned.relations ++ fresh.relations, apis = pruned.apis ++ fresh.apis)
debug("********* Merged: \n" + merged.relations + "\n*********")
val incChanges = changedIncremental(invalidated, previous.apis.internalAPI _, merged.apis.internalAPI _, log, options)
val incChanges = changedIncremental(invalidated, previous.apis.internalAPI _, merged.apis.internalAPI _)
debug("\nChanges:\n" + incChanges)
val transitiveStep = options.transitiveStep
val incInv = invalidateIncremental(merged.relations, merged.apis, incChanges, invalidated, cycleNum >= transitiveStep, log)
cycle(incInv, allSources, emptyChanges, merged, doCompile, classfileManager, cycleNum+1, log, options)
val incInv = invalidateIncremental(merged.relations, merged.apis, incChanges, invalidated, cycleNum >= transitiveStep)
cycle(incInv, allSources, emptyChanges, merged, doCompile, classfileManager, cycleNum+1)
}
private[this] def emptyChanges: DependencyChanges = new DependencyChanges {
val modifiedBinaries = new Array[File](0)
val modifiedClasses = new Array[String](0)
def isEmpty = true
}
private[this] def expand(invalidated: Set[File], all: Set[File], log: Logger, options: IncOptions): Set[File] = {
private[this] def expand(invalidated: Set[File], all: Set[File]): Set[File] = {
val recompileAllFraction = options.recompileAllFraction
if(invalidated.size > all.size * recompileAllFraction) {
log.debug("Recompiling all " + all.size + " sources: invalidated sources (" + invalidated.size + ") exceeded " + (recompileAllFraction*100.0) + "% of all sources")
@ -96,10 +122,7 @@ object Incremental
else invalidated
}
// Package objects are fragile: if they inherit from an invalidated source, get "class file needed by package is missing" error
// This might be too conservative: we probably only need package objects for packages of invalidated sources.
private[this] def invalidatedPackageObjects(invalidated: Set[File], relations: Relations): Set[File] =
invalidated flatMap relations.publicInherited.internal.reverse filter { _.getName == "package.scala" }
protected def invalidatedPackageObjects(invalidated: Set[File], relations: Relations): Set[File]
/**
* Logs API changes using debug-level logging. The API are obtained using the APIDiff class.
@ -107,14 +130,15 @@ object Incremental
* NOTE: This method creates a new APIDiff instance on every invocation.
*/
private def logApiChanges[T](apiChanges: Iterable[APIChange[T]], oldAPIMapping: T => Source,
newAPIMapping: T => Source, log: Logger, options: IncOptions): Unit = {
newAPIMapping: T => Source): Unit = {
val contextSize = options.apiDiffContextSize
try {
val apiDiff = new APIDiff
apiChanges foreach {
case APIChangeDueToMacroDefinition(src) =>
log.debug(s"Public API is considered to be changed because $src contains a macro definition.")
case SourceAPIChange(src) =>
case apiChange@(_: SourceAPIChange[T] | _: NamesChange[T]) =>
val src = apiChange.modified
val oldApi = oldAPIMapping(src)
val newApi = newAPIMapping(src)
val apiUnifiedPatch = apiDiff.generateApiDiff(src.toString, oldApi.api, newApi.api, contextSize)
@ -138,19 +162,19 @@ object Incremental
* providing the API before and after the last step. The functions should return
* an empty API if the file did not/does not exist.
*/
def changedIncremental[T](lastSources: collection.Set[T], oldAPI: T => Source, newAPI: T => Source, log: Logger, options: IncOptions): APIChanges[T] =
def changedIncremental[T](lastSources: collection.Set[T], oldAPI: T => Source, newAPI: T => Source): APIChanges[T] =
{
val oldApis = lastSources.toSeq map oldAPI
val newApis = lastSources.toSeq map newAPI
val apiChanges = (lastSources, oldApis, newApis).zipped.flatMap { (src, oldApi, newApi) => sameSource(src, oldApi, newApi, log, options) }
val apiChanges = (lastSources, oldApis, newApis).zipped.flatMap { (src, oldApi, newApi) => sameSource(src, oldApi, newApi) }
if (apiDebug(options) && apiChanges.nonEmpty) {
logApiChanges(apiChanges, oldAPI, newAPI, log, options)
if (Incremental.apiDebug(options) && apiChanges.nonEmpty) {
logApiChanges(apiChanges, oldAPI, newAPI)
}
new APIChanges(apiChanges)
}
def sameSource[T](src: T, a: Source, b: Source, log: Logger, options: IncOptions): Option[APIChange[T]] = {
def sameSource[T](src: T, a: Source, b: Source): Option[APIChange[T]] = {
// Clients of a modified source file (ie, one that doesn't satisfy `shortcutSameSource`) containing macros must be recompiled.
val hasMacro = a.hasMacro || b.hasMacro
if (shortcutSameSource(a, b)) {
@ -158,18 +182,11 @@ object Incremental
} else {
if (hasMacro && options.recompileOnMacroDef) {
Some(APIChangeDueToMacroDefinition(src))
} else sameAPI(src, a, b, log)
} else sameAPI(src, a, b)
}
}
def sameAPI[T](src: T, a: Source, b: Source, log: Logger): Option[SourceAPIChange[T]] = {
if (SameAPI(a,b))
None
else {
val sourceApiChange = SourceAPIChange(src)
Some(sourceApiChange)
}
}
protected def sameAPI[T](src: T, a: Source, b: Source): Option[APIChange[T]]
def shortcutSameSource(a: Source, b: Source): Boolean = !a.hash.isEmpty && !b.hash.isEmpty && sameCompilation(a.compilation, b.compilation) && (a.hash.deep equals b.hash.deep)
def sameCompilation(a: Compilation, b: Compilation): Boolean = a.startTime == b.startTime && a.outputs.corresponds(b.outputs){
@ -177,15 +194,15 @@ object Incremental
}
def changedInitial(entry: String => Option[File], sources: Set[File], previousAnalysis: Analysis, current: ReadStamps,
forEntry: File => Option[Analysis], options: IncOptions, log: Logger)(implicit equivS: Equiv[Stamp]): InitialChanges =
forEntry: File => Option[Analysis])(implicit equivS: Equiv[Stamp]): InitialChanges =
{
val previous = previousAnalysis.stamps
val previousAPIs = previousAnalysis.apis
val srcChanges = changes(previous.allInternalSources.toSet, sources, f => !equivS.equiv( previous.internalSource(f), current.internalSource(f) ) )
val removedProducts = previous.allProducts.filter( p => !equivS.equiv( previous.product(p), current.product(p) ) ).toSet
val binaryDepChanges = previous.allBinaries.filter( externalBinaryModified(entry, forEntry, previous, current, log)).toSet
val extChanges = changedIncremental(previousAPIs.allExternals, previousAPIs.externalAPI _, currentExternalAPI(entry, forEntry), log, options)
val binaryDepChanges = previous.allBinaries.filter( externalBinaryModified(entry, forEntry, previous, current)).toSet
val extChanges = changedIncremental(previousAPIs.allExternals, previousAPIs.externalAPI _, currentExternalAPI(entry, forEntry))
InitialChanges(srcChanges, removedProducts, binaryDepChanges, extChanges )
}
@ -199,14 +216,14 @@ object Incremental
val (changed, unmodified) = inBoth.partition(existingModified)
}
def invalidateIncremental(previous: Relations, apis: APIs, changes: APIChanges[File], recompiledSources: Set[File], transitive: Boolean, log: Logger): Set[File] =
def invalidateIncremental(previous: Relations, apis: APIs, changes: APIChanges[File], recompiledSources: Set[File], transitive: Boolean): Set[File] =
{
val dependsOnSrc = previous.usesInternalSrc _
val propagated =
if(transitive)
transitiveDependencies(dependsOnSrc, changes.allModified.toSet, log)
transitiveDependencies(dependsOnSrc, changes.allModified.toSet)
else
invalidateIntermediate(previous, changes, log)
invalidateIntermediate(previous, changes)
val dups = invalidateDuplicates(previous)
if(dups.nonEmpty)
@ -227,28 +244,27 @@ object Incremental
/** Returns the transitive source dependencies of `initial`.
* Because the intermediate steps do not pull in cycles, this result includes the initial files
* if they are part of a cycle containing newly invalidated files . */
def transitiveDependencies(dependsOnSrc: File => Set[File], initial: Set[File], log: Logger): Set[File] =
def transitiveDependencies(dependsOnSrc: File => Set[File], initial: Set[File]): Set[File] =
{
val transitiveWithInitial = transitiveDeps(initial, log)(dependsOnSrc)
val transitivePartial = includeInitialCond(initial, transitiveWithInitial, dependsOnSrc, log)
val transitiveWithInitial = transitiveDeps(initial)(dependsOnSrc)
val transitivePartial = includeInitialCond(initial, transitiveWithInitial, dependsOnSrc)
log.debug("Final step, transitive dependencies:\n\t" + transitivePartial)
transitivePartial
}
/** Invalidates sources based on initially detected 'changes' to the sources, products, and dependencies.*/
def invalidateInitial(previous: Relations, changes: InitialChanges, log: Logger): Set[File] =
def invalidateInitial(previous: Relations, changes: InitialChanges): Set[File] =
{
val srcChanges = changes.internalSrc
val srcDirect = srcChanges.removed ++ srcChanges.removed.flatMap(previous.usesInternalSrc) ++ srcChanges.added ++ srcChanges.changed
val byProduct = changes.removedProducts.flatMap(previous.produced)
val byBinaryDep = changes.binaryDeps.flatMap(previous.usesBinary)
val externalModifiedSources = changes.external.allModified.toSet
val byExtSrcDep = invalidateByExternal(previous, externalModifiedSources, log) //changes.external.modified.flatMap(previous.usesExternal) // ++ scopeInvalidations
val byExtSrcDep = invalidateByAllExternal(previous, changes.external) //changes.external.modified.flatMap(previous.usesExternal) // ++ scopeInvalidations
checkAbsolute(srcChanges.added.toList)
log.debug(
"\nInitial source changes: \n\tremoved:" + srcChanges.removed + "\n\tadded: " + srcChanges.added + "\n\tmodified: " + srcChanges.changed +
"\nRemoved products: " + changes.removedProducts +
"\nModified external sources: " + externalModifiedSources +
"\nExternal API changes: " + changes.external +
"\nModified binary dependencies: " + changes.binaryDeps +
"\nInitial directly invalidated sources: " + srcDirect +
"\n\nSources indirectly invalidated by:" +
@ -273,64 +289,48 @@ object Incremental
}
}
/** Sources invalidated by `external` sources in other projects according to the previous `relations`. */
def invalidateByExternal(relations: Relations, external: Set[String], log: Logger): Set[File] =
{
// Propagate public inheritance dependencies transitively.
// This differs from normal because we need the initial crossing from externals to sources in this project.
val externalInheritedR = relations.publicInherited.external
val byExternalInherited = external flatMap externalInheritedR.reverse
val internalInheritedR = relations.publicInherited.internal
val transitiveInherited = transitiveDeps(byExternalInherited, log)(internalInheritedR.reverse _)
// Get the direct dependencies of all sources transitively invalidated by inheritance
val directA = transitiveInherited flatMap relations.direct.internal.reverse
// Get the sources that directly depend on externals. This includes non-inheritance dependencies and is not transitive.
val directB = external flatMap relations.direct.external.reverse
transitiveInherited ++ directA ++ directB
def invalidateByAllExternal(relations: Relations, externalAPIChanges: APIChanges[String]): Set[File] = {
(externalAPIChanges.apiChanges.flatMap { externalAPIChange =>
invalidateByExternal(relations, externalAPIChange)
}).toSet
}
/** Sources invalidated by `external` sources in other projects according to the previous `relations`. */
protected def invalidateByExternal(relations: Relations, externalAPIChange: APIChange[String]): Set[File]
/** Intermediate invalidation step: steps after the initial invalidation, but before the final transitive invalidation. */
def invalidateIntermediate(relations: Relations, changes: APIChanges[File], log: Logger): Set[File] =
def invalidateIntermediate(relations: Relations, changes: APIChanges[File]): Set[File] =
{
def reverse(r: Relations.Source) = r.internal.reverse _
invalidateSources(reverse(relations.direct), reverse(relations.publicInherited), changes, log)
invalidateSources(relations, changes)
}
/** Invalidates inheritance dependencies, transitively. Then, invalidates direct dependencies. Finally, excludes initial dependencies not
* included in a cycle with newly invalidated sources. */
private[this] def invalidateSources(directDeps: File => Set[File], publicInherited: File => Set[File], changes: APIChanges[File], log: Logger): Set[File] =
private[this] def invalidateSources(relations: Relations, changes: APIChanges[File]): Set[File] =
{
val initial = changes.allModified.toSet
log.debug("Invalidating by inheritance (transitively)...")
val transitiveInherited = transitiveDeps(initial, log)(publicInherited)
log.debug("Invalidated by transitive public inheritance: " + transitiveInherited)
val direct = transitiveInherited flatMap directDeps
log.debug("Invalidated by direct dependency: " + direct)
val all = transitiveInherited ++ direct
includeInitialCond(initial, all, f => directDeps(f) ++ publicInherited(f), log)
val all = (changes.apiChanges flatMap { change =>
invalidateSource(relations, change)
}).toSet
includeInitialCond(initial, all, allDeps(relations))
}
protected def allDeps(relations: Relations): File => Set[File]
protected def invalidateSource(relations: Relations, change: APIChange[File]): Set[File]
/** Conditionally include initial sources that are dependencies of newly invalidated sources.
** Initial sources included in this step can be because of a cycle, but not always. */
private[this] def includeInitialCond(initial: Set[File], currentInvalidations: Set[File], allDeps: File => Set[File], log: Logger): Set[File] =
private[this] def includeInitialCond(initial: Set[File], currentInvalidations: Set[File], allDeps: File => Set[File]): Set[File] =
{
val newInv = currentInvalidations -- initial
log.debug("New invalidations:\n\t" + newInv)
val transitiveOfNew = transitiveDeps(newInv, log)(allDeps)
val transitiveOfNew = transitiveDeps(newInv)(allDeps)
val initialDependsOnNew = transitiveOfNew & initial
log.debug("Previously invalidated, but (transitively) depend on new invalidations:\n\t" + initialDependsOnNew)
newInv ++ initialDependsOnNew
}
def prune(invalidatedSrcs: Set[File], previous: Analysis): Analysis =
prune(invalidatedSrcs, previous, ClassfileManager.deleteImmediately())
def prune(invalidatedSrcs: Set[File], previous: Analysis, classfileManager: ClassfileManager): Analysis =
{
classfileManager.delete( invalidatedSrcs.flatMap(previous.relations.products) )
previous -- invalidatedSrcs
}
def externalBinaryModified(entry: String => Option[File], analysis: File => Option[Analysis], previous: Stamps, current: ReadStamps, log: Logger)(implicit equivS: Equiv[Stamp]): File => Boolean =
def externalBinaryModified(entry: String => Option[File], analysis: File => Option[Analysis], previous: Stamps, current: ReadStamps)(implicit equivS: Equiv[Stamp]): File => Boolean =
dependsOn =>
{
def inv(reason: String): Boolean = {
@ -382,7 +382,7 @@ object Incremental
def orEmpty(o: Option[Source]): Source = o getOrElse APIs.emptySource
def orTrue(o: Option[Boolean]): Boolean = o getOrElse true
private[this] def transitiveDeps[T](nodes: Iterable[T], log: Logger)(dependencies: T => Iterable[T]): Set[T] =
protected def transitiveDeps[T](nodes: Iterable[T])(dependencies: T => Iterable[T]): Set[T] =
{
val xs = new collection.mutable.HashSet[T]
def all(from: T, tos: Iterable[T]): Unit = tos.foreach(to => visit(from, to))
@ -442,3 +442,133 @@ object Incremental
def properSubPkg(testParent: Seq[String], testSub: Seq[String]) = testParent.length < testSub.length && testSub.startsWith(testParent)
def pkgs(api: Source) = names(api :: Nil).map(pkg)*/
}
private final class IncrementalDefaultImpl(log: Logger, options: IncOptions) extends IncrementalCommon(log, options) {
// Package objects are fragile: if they inherit from an invalidated source, get "class file needed by package is missing" error
// This might be too conservative: we probably only need package objects for packages of invalidated sources.
override protected def invalidatedPackageObjects(invalidated: Set[File], relations: Relations): Set[File] =
invalidated flatMap relations.publicInherited.internal.reverse filter { _.getName == "package.scala" }
override protected def sameAPI[T](src: T, a: Source, b: Source): Option[SourceAPIChange[T]] = {
if (SameAPI(a,b))
None
else {
val sourceApiChange = SourceAPIChange(src)
Some(sourceApiChange)
}
}
/** Invalidates sources based on initially detected 'changes' to the sources, products, and dependencies.*/
override protected def invalidateByExternal(relations: Relations, externalAPIChange: APIChange[String]): Set[File] = {
val modified = externalAPIChange.modified
// Propagate public inheritance dependencies transitively.
// This differs from normal because we need the initial crossing from externals to sources in this project.
val externalInheritedR = relations.publicInherited.external
val byExternalInherited = externalInheritedR.reverse(modified)
val internalInheritedR = relations.publicInherited.internal
val transitiveInherited = transitiveDeps(byExternalInherited)(internalInheritedR.reverse _)
// Get the direct dependencies of all sources transitively invalidated by inheritance
val directA = transitiveInherited flatMap relations.direct.internal.reverse
// Get the sources that directly depend on externals. This includes non-inheritance dependencies and is not transitive.
val directB = relations.direct.external.reverse(modified)
transitiveInherited ++ directA ++ directB
}
override protected def invalidateSource(relations: Relations, change: APIChange[File]): Set[File] = {
def reverse(r: Relations.Source) = r.internal.reverse _
val directDeps: File => Set[File] = reverse(relations.direct)
val publicInherited: File => Set[File] = reverse(relations.publicInherited)
log.debug("Invalidating by inheritance (transitively)...")
val transitiveInherited = transitiveDeps(Set(change.modified))(publicInherited)
log.debug("Invalidated by transitive public inheritance: " + transitiveInherited)
val direct = transitiveInherited flatMap directDeps
log.debug("Invalidated by direct dependency: " + direct)
transitiveInherited ++ direct
}
override protected def allDeps(relations: Relations): File => Set[File] =
f => relations.direct.internal.reverse(f)
}
/**
* Implementation of incremental algorithm known as "name hashing". It differs from the default implementation
* by applying pruning (filter) of member reference dependencies based on used and modified simple names.
*
* See MemberReferenceInvalidationStrategy for some more information.
*/
private final class IncrementalNameHashing(log: Logger, options: IncOptions) extends IncrementalCommon(log, options) {
private val memberRefInvalidator = new MemberRefInvalidator(log)
// Package objects are fragile: if they inherit from an invalidated source, get "class file needed by package is missing" error
// This might be too conservative: we probably only need package objects for packages of invalidated sources.
override protected def invalidatedPackageObjects(invalidated: Set[File], relations: Relations): Set[File] =
invalidated flatMap relations.inheritance.internal.reverse filter { _.getName == "package.scala" }
override protected def sameAPI[T](src: T, a: Source, b: Source): Option[APIChange[T]] = {
if (SameAPI(a,b))
None
else {
val aNameHashes = a._internalOnly_nameHashes
val bNameHashes = b._internalOnly_nameHashes
val modifiedNames = ModifiedNames.compareTwoNameHashes(aNameHashes, bNameHashes)
val apiChange = NamesChange(src, modifiedNames)
Some(apiChange)
}
}
/** Invalidates sources based on initially detected 'changes' to the sources, products, and dependencies.*/
override protected def invalidateByExternal(relations: Relations, externalAPIChange: APIChange[String]): Set[File] = {
val modified = externalAPIChange.modified
val invalidationReason = memberRefInvalidator.invalidationReason(externalAPIChange)
log.debug(s"$invalidationReason\nAll member reference dependencies will be considered within this context.")
// Propagate inheritance dependencies transitively.
// This differs from normal because we need the initial crossing from externals to sources in this project.
val externalInheritanceR = relations.inheritance.external
val byExternalInheritance = externalInheritanceR.reverse(modified)
log.debug(s"Files invalidated by inheriting from (external) $modified: $byExternalInheritance; now invalidating by inheritance (internally).")
val transitiveInheritance = byExternalInheritance flatMap { file =>
invalidateByInheritance(relations, file)
}
val memberRefInvalidationInternal = memberRefInvalidator.get(relations.memberRef.internal,
relations.names, externalAPIChange)
val memberRefInvalidationExternal = memberRefInvalidator.get(relations.memberRef.external,
relations.names, externalAPIChange)
// Get the member reference dependencies of all sources transitively invalidated by inheritance
log.debug("Getting direct dependencies of all sources transitively invalidated by inheritance.")
val memberRefA = transitiveInheritance flatMap memberRefInvalidationInternal
// Get the sources that depend on externals by member reference.
// This includes non-inheritance dependencies and is not transitive.
log.debug(s"Getting sources that directly depend on (external) $modified.")
val memberRefB = memberRefInvalidationExternal(modified)
transitiveInheritance ++ memberRefA ++ memberRefB
}
private def invalidateByInheritance(relations: Relations, modified: File): Set[File] = {
val inheritanceDeps = relations.inheritance.internal.reverse _
log.debug(s"Invalidating (transitively) by inheritance from $modified...")
val transitiveInheritance = transitiveDeps(Set(modified))(inheritanceDeps)
log.debug("Invalidated by transitive inheritance dependency: " + transitiveInheritance)
transitiveInheritance
}
override protected def invalidateSource(relations: Relations, change: APIChange[File]): Set[File] = {
log.debug(s"Invalidating ${change.modified}...")
val transitiveInheritance = invalidateByInheritance(relations, change.modified)
val reasonForInvalidation = memberRefInvalidator.invalidationReason(change)
log.debug(s"$reasonForInvalidation\nAll member reference dependencies will be considered within this context.")
val memberRefInvalidation = memberRefInvalidator.get(relations.memberRef.internal,
relations.names, change)
val memberRef = transitiveInheritance flatMap memberRefInvalidation
val all = transitiveInheritance ++ memberRef
all
}
override protected def allDeps(relations: Relations): File => Set[File] =
f => relations.memberRef.internal.reverse(f)
}

View File

@ -0,0 +1,124 @@
package sbt.inc
import sbt.Relation
import java.io.File
import sbt.Logger
import xsbt.api.APIUtil
/**
* Implements various strategies for invalidating dependencies introduced by member reference.
*
* The strategy is represented as function T => Set[File] where T is a source file that other
* source files depend on. When you apply that function to given element `src` you get set of
* files that depend on `src` by member reference and should be invalidated due to api change
* that was passed to a method constructing that function. There are two questions that arise:
*
* 1. Why is signature T => Set[File] and not T => Set[T] or File => Set[File]?
* 2. Why would we apply that function to any other `src` that then one that got modified
* and the modification is described by APIChange?
*
* Let's address the second question with the following example of source code structure:
*
* // A.scala
* class A
*
* // B.scala
* class B extends A
*
* // C.scala
* class C { def foo(a: A) = ??? }
*
* // D.scala
* class D { def bar(b: B) = ??? }
*
* Member reference dependencies on A.scala are B.scala, C.scala. When the api of A changes
* then we would consider B and C for invalidation. However, B is also a dependency by inheritance
* so we always invalidate it. The api change to A is relevant when B is considered (because
* of how inheritance works) so we would invalidate B by inheritance and then we would like to
* invalidate member reference dependencies of B as well. In other words, we have a function
* because we want to apply it (with the same api change in mind) to all src files invalidated
* by inheritance of the originally modified file.
*
* The first question is a bit more straightforward to answer. We always invalidate internal
* source files (in given project) that are represented as File but they might depend either on
* internal source files (then T=File) or they can depend on external class name (then T=String).
*
* The specific invalidation strategy is determined based on APIChange that describes a change to api
* of a single source file.
*
* For example, if we get APIChangeDueToMacroDefinition then we invalidate all member reference
* dependencies unconditionally. On the other hand, if api change is due to modified name hashes
* of regular members then we'll invalidate sources that use those names.
*/
private[inc] class MemberRefInvalidator(log: Logger) {
def get[T](memberRef: Relation[File, T], usedNames: Relation[File, String], apiChange: APIChange[_]):
T => Set[File] = apiChange match {
case _: APIChangeDueToMacroDefinition[_] =>
new InvalidateUnconditionally(memberRef)
case NamesChange(_, modifiedNames) if !modifiedNames.implicitNames.isEmpty =>
new InvalidateUnconditionally(memberRef)
case NamesChange(modifiedSrcFile, modifiedNames) =>
new NameHashFilteredInvalidator[T](usedNames, memberRef, modifiedNames.regularNames)
case _: SourceAPIChange[_] =>
sys.error(wrongAPIChangeMsg)
}
def invalidationReason(apiChange: APIChange[_]): String = apiChange match {
case APIChangeDueToMacroDefinition(modifiedSrcFile) =>
s"The $modifiedSrcFile source file declares a macro."
case NamesChange(modifiedSrcFile, modifiedNames) if !modifiedNames.implicitNames.isEmpty =>
s"""|The $modifiedSrcFile source file has the following implicit definitions changed:
|\t${modifiedNames.implicitNames.mkString(", ")}.""".stripMargin
case NamesChange(modifiedSrcFile, modifiedNames) =>
s"""|The $modifiedSrcFile source file has the following regular definitions changed:
|\t${modifiedNames.regularNames.mkString(", ")}.""".stripMargin
case _: SourceAPIChange[_] =>
sys.error(wrongAPIChangeMsg)
}
private val wrongAPIChangeMsg =
"MemberReferenceInvalidator.get should be called when name hashing is enabled " +
"and in that case we shouldn't have SourceAPIChange as an api change."
private class InvalidateUnconditionally[T](memberRef: Relation[File, T]) extends (T => Set[File]) {
def apply(from: T): Set[File] = {
val invalidated = memberRef.reverse(from)
if (!invalidated.isEmpty)
log.debug(s"The following member ref dependencies of $from are invalidated:\n" +
formatInvalidated(invalidated))
invalidated
}
private def formatInvalidated(invalidated: Set[File]): String = {
val sortedFiles = invalidated.toSeq.sortBy(_.getAbsolutePath)
sortedFiles.map(file => "\t"+file).mkString("\n")
}
}
private class NameHashFilteredInvalidator[T](
usedNames: Relation[File, String],
memberRef: Relation[File, T],
modifiedNames: Set[String]) extends (T => Set[File]) {
def apply(to: T): Set[File] = {
val dependent = memberRef.reverse(to)
filteredDependencies(dependent)
}
private def filteredDependencies(dependent: Set[File]): Set[File] = {
dependent.filter {
case from if APIUtil.isScalaSourceName(from.getName) =>
val usedNamesInDependent = usedNames.forward(from)
val modifiedAndUsedNames = modifiedNames intersect usedNamesInDependent
if (modifiedAndUsedNames.isEmpty) {
log.debug(s"None of the modified names appears in $from. This dependency is not being considered for invalidation.")
false
} else {
log.debug(s"The following modified names cause invalidation of $from: $modifiedAndUsedNames")
true
}
case from =>
log.debug(s"Name hashing optimization doesn't apply to non-Scala dependency: $from")
true
}
}
}
}

View File

@ -41,7 +41,8 @@ class AggressiveCompile(cacheFile: File)
skip: Boolean = false,
incrementalCompilerOptions: IncOptions)(implicit log: Logger): Analysis =
{
val setup = new CompileSetup(output, new CompileOptions(options, javacOptions), compiler.scalaInstance.actualVersion, compileOrder)
val setup = new CompileSetup(output, new CompileOptions(options, javacOptions),
compiler.scalaInstance.actualVersion, compileOrder, incrementalCompilerOptions.nameHashing)
compile1(sources, classpath, setup, progress, store, analysisMap, definesClass,
compiler, javac, reporter, skip, cache, incrementalCompilerOptions)
}
@ -61,7 +62,7 @@ class AggressiveCompile(cacheFile: File)
cache: GlobalsCache,
incrementalCompilerOptions: IncOptions)(implicit log: Logger): Analysis =
{
val (previousAnalysis, previousSetup) = extract(store.get())
val (previousAnalysis, previousSetup) = extract(store.get(), incrementalCompilerOptions)
if(skip)
previousAnalysis
else {
@ -144,6 +145,12 @@ class AggressiveCompile(cacheFile: File)
val sourcesSet = sources.toSet
val analysis = previousSetup match {
case Some(previous) if previous.nameHashing != currentSetup.nameHashing =>
// if the value of `nameHashing` flag has changed we have to throw away
// previous Analysis completely and start with empty Analysis object
// that supports the particular value of the `nameHashing` flag.
// Otherwise we'll be getting UnsupportedOperationExceptions
Analysis.empty(currentSetup.nameHashing)
case Some(previous) if equiv.equiv(previous, currentSetup) => previousAnalysis
case _ => Incremental.prune(sourcesSet, previousAnalysis)
}
@ -169,11 +176,11 @@ class AggressiveCompile(cacheFile: File)
if(!combined.isEmpty)
log.info(combined.mkString("Compiling ", " and ", " to " + outputDirs.map(_.getAbsolutePath).mkString(",") + "..."))
}
private def extract(previous: Option[(Analysis, CompileSetup)]): (Analysis, Option[CompileSetup]) =
private def extract(previous: Option[(Analysis, CompileSetup)], incOptions: IncOptions): (Analysis, Option[CompileSetup]) =
previous match
{
case Some((an, setup)) => (an, Some(setup))
case None => (Analysis.Empty, None)
case None => (Analysis.empty(nameHashing = incOptions.nameHashing), None)
}
def javaOnly(f: File) = f.getName.endsWith(".java")

View File

@ -36,9 +36,22 @@ object IC extends IncrementalCompiler[Analysis, AnalyzingCompiler]
def readCache(file: File): Maybe[(Analysis, CompileSetup)] =
try { Maybe.just(readCacheUncaught(file)) } catch { case _: Exception => Maybe.nothing() }
@deprecated("Use overloaded variant which takes `IncOptions` as parameter.", "0.13.2")
def readAnalysis(file: File): Analysis =
try { readCacheUncaught(file)._1 } catch { case _: Exception => Analysis.Empty }
def readAnalysis(file: File, incOptions: IncOptions): Analysis =
try { readCacheUncaught(file)._1 } catch {
case _: Exception => Analysis.empty(nameHashing = incOptions.nameHashing)
}
def readCacheUncaught(file: File): (Analysis, CompileSetup) =
Using.fileReader(IO.utf8)(file) { reader => TextAnalysisFormat.read(reader) }
Using.fileReader(IO.utf8)(file) { reader =>
try {
TextAnalysisFormat.read(reader)
} catch {
case ex: sbt.inc.ReadException =>
throw new java.io.IOException(s"Error while reading $file", ex)
}
}
}

View File

@ -91,4 +91,42 @@ abstract class Compat
private[this] def sourceCompatibilityOnly: Nothing = throw new RuntimeException("For source compatibility only: should not get here.")
private[this] final implicit def miscCompat(n: AnyRef): MiscCompat = new MiscCompat
object MacroExpansionOf {
def unapply(tree: Tree): Option[Tree] = {
// MacroExpansionAttachment (MEA) compatibility for 2.8.x and 2.9.x
object Compat {
class MacroExpansionAttachment(val original: Tree)
// Trees have no attachments in 2.8.x and 2.9.x
implicit def withAttachments(tree: Tree): WithAttachments = new WithAttachments(tree)
class WithAttachments(val tree: Tree) {
object EmptyAttachments {
def all = Set.empty[Any]
}
val attachments = EmptyAttachments
}
}
import Compat._
locally {
// Wildcard imports are necessary since 2.8.x and 2.9.x don't have `MacroExpansionAttachment` at all
import global._ // this is where MEA lives in 2.10.x
// `original` has been renamed to `expandee` in 2.11.x
implicit def withExpandee(att: MacroExpansionAttachment): WithExpandee = new WithExpandee(att)
class WithExpandee(att: MacroExpansionAttachment) {
def expandee: Tree = att.original
}
locally {
import analyzer._ // this is where MEA lives in 2.11.x
tree.attachments.all.collect {
case att: MacroExpansionAttachment => att.expandee
} headOption
}
}
}
}
}

View File

@ -146,6 +146,8 @@ final class Dependency(val global: CallbackGlobal) extends LocateClassFile
deps.foreach(addDependency)
case Template(parents, self, body) =>
traverseTrees(body)
case MacroExpansionOf(original) =>
this.traverse(original)
case other => ()
}
super.traverse(tree)

View File

@ -38,7 +38,7 @@ import scala.tools.nsc._
* The tree walking algorithm walks into TypeTree.original explicitly.
*
*/
class ExtractUsedNames[GlobalType <: CallbackGlobal](val global: GlobalType) {
class ExtractUsedNames[GlobalType <: CallbackGlobal](val global: GlobalType) extends Compat {
import global._
def extract(unit: CompilationUnit): Set[String] = {
@ -53,30 +53,44 @@ class ExtractUsedNames[GlobalType <: CallbackGlobal](val global: GlobalType) {
val symbolNameAsString = symbol.name.decode.trim
namesBuffer += symbolNameAsString
}
def handleTreeNode(node: Tree): Unit = node match {
case _: DefTree | _: Template => ()
// turns out that Import node has a TermSymbol associated with it
// I (Grzegorz) tried to understand why it's there and what does it represent but
// that logic was introduced in 2005 without any justification I'll just ignore the
// import node altogether and just process the selectors in the import node
case Import(_, selectors: List[ImportSelector]) =>
def usedNameInImportSelector(name: Name): Unit =
if ((name != null) && (name != nme.WILDCARD)) namesBuffer += name.toString
selectors foreach { selector =>
usedNameInImportSelector(selector.name)
usedNameInImportSelector(selector.rename)
}
// TODO: figure out whether we should process the original tree or walk the type
// the argument for processing the original tree: we process what user wrote
// the argument for processing the type: we catch all transformations that typer applies
// to types but that might be a bad thing because it might expand aliases eagerly which
// not what we need
case t: TypeTree if t.original != null =>
t.original.foreach(handleTreeNode)
case t if t.hasSymbol && eligibleAsUsedName(t.symbol) =>
addSymbol(t.symbol)
case _ => ()
def handleTreeNode(node: Tree): Unit = {
def handleMacroExpansion(original: Tree): Unit = original.foreach(handleTreeNode)
def handleClassicTreeNode(node: Tree): Unit = node match {
case _: DefTree | _: Template => ()
// turns out that Import node has a TermSymbol associated with it
// I (Grzegorz) tried to understand why it's there and what does it represent but
// that logic was introduced in 2005 without any justification I'll just ignore the
// import node altogether and just process the selectors in the import node
case Import(_, selectors: List[ImportSelector]) =>
def usedNameInImportSelector(name: Name): Unit =
if ((name != null) && (name != nme.WILDCARD)) namesBuffer += name.toString
selectors foreach { selector =>
usedNameInImportSelector(selector.name)
usedNameInImportSelector(selector.rename)
}
// TODO: figure out whether we should process the original tree or walk the type
// the argument for processing the original tree: we process what user wrote
// the argument for processing the type: we catch all transformations that typer applies
// to types but that might be a bad thing because it might expand aliases eagerly which
// not what we need
case t: TypeTree if t.original != null =>
t.original.foreach(handleTreeNode)
case t if t.hasSymbol && eligibleAsUsedName(t.symbol) =>
addSymbol(t.symbol)
case _ => ()
}
node match {
case MacroExpansionOf(original) =>
handleClassicTreeNode(node)
handleMacroExpansion(original)
case _ =>
handleClassicTreeNode(node)
}
}
tree.foreach(handleTreeNode)
namesBuffer.toSet
}

View File

@ -65,6 +65,19 @@ class DependencySpecification extends Specification {
inheritance('D) === Set('A, 'C)
}
"Extracted source dependencies from macro arguments" in {
val sourceDependencies = extractSourceDependenciesFromMacroArgument
val memberRef = sourceDependencies.memberRef
val inheritance = sourceDependencies.inheritance
memberRef('A) === Set('B, 'C)
inheritance('A) === Set.empty
memberRef('B) === Set.empty
inheritance('B) === Set.empty
memberRef('C) === Set.empty
inheritance('C) === Set.empty
}
private def extractSourceDependenciesPublic: ExtractedSourceDependencies = {
val srcA = "class A"
val srcB = "class B extends D[A]"
@ -109,4 +122,25 @@ class DependencySpecification extends Specification {
compilerForTesting.extractDependenciesFromSrcs('A -> srcA, 'B -> srcB, 'C -> srcC, 'D -> srcD)
sourceDependencies
}
private def extractSourceDependenciesFromMacroArgument: ExtractedSourceDependencies = {
val srcA = "class A { println(B.printTree(C.foo)) }"
val srcB = """
|import scala.language.experimental.macros
|import scala.reflect.macros._
|object B {
| def printTree(arg: Any) = macro printTreeImpl
| def printTreeImpl(c: Context)(arg: c.Expr[Any]): c.Expr[String] = {
| val argStr = arg.tree.toString
| val literalStr = c.universe.Literal(c.universe.Constant(argStr))
| c.Expr[String](literalStr)
| }
|}""".stripMargin
val srcC = "object C { val foo = 1 }"
val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true)
val sourceDependencies =
compilerForTesting.extractDependenciesFromSrcs(List(Map('B -> srcB, 'C -> srcC), Map('A -> srcA)))
sourceDependencies
}
}

View File

@ -53,15 +53,19 @@ class ScalaCompilerForUnitTesting(nameHashing: Boolean = false) {
* dependencies between snippets. Source code snippets are identified by symbols. Each symbol should
* be associated with one snippet only.
*
* Snippets can be grouped to be compiled together in the same compiler run. This is
* useful to compile macros, which cannot be used in the same compilation run that
* defines them.
*
* Symbols are used to express extracted dependencies between source code snippets. This way we have
* file system-independent way of testing dependencies between source code "files".
*/
def extractDependenciesFromSrcs(srcs: (Symbol, String)*): ExtractedSourceDependencies = {
val (symbolsForSrcs, rawSrcs) = srcs.unzip
assert(symbolsForSrcs.distinct.size == symbolsForSrcs.size,
s"Duplicate symbols for srcs detected: $symbolsForSrcs")
val (tempSrcFiles, testCallback) = compileSrcs(rawSrcs: _*)
val fileToSymbol = (tempSrcFiles zip symbolsForSrcs).toMap
def extractDependenciesFromSrcs(srcs: List[Map[Symbol, String]]): ExtractedSourceDependencies = {
val rawGroupedSrcs = srcs.map(_.values.toList).toList
val symbols = srcs.map(_.keys).flatten
val (tempSrcFiles, testCallback) = compileSrcs(rawGroupedSrcs)
val fileToSymbol = (tempSrcFiles zip symbols).toMap
val memberRefFileDeps = testCallback.sourceDependencies collect {
// false indicates that those dependencies are not introduced by inheritance
case (target, src, false) => (src, target)
@ -82,40 +86,64 @@ class ScalaCompilerForUnitTesting(nameHashing: Boolean = false) {
// convert all collections to immutable variants
multiMap.toMap.mapValues(_.toSet).withDefaultValue(Set.empty)
}
ExtractedSourceDependencies(pairsToMultiMap(memberRefDeps), pairsToMultiMap(inheritanceDeps))
}
def extractDependenciesFromSrcs(srcs: (Symbol, String)*): ExtractedSourceDependencies = {
val symbols = srcs.map(_._1)
assert(symbols.distinct.size == symbols.size,
s"Duplicate symbols for srcs detected: $symbols")
extractDependenciesFromSrcs(List(srcs.toMap))
}
/**
* Compiles given source code snippets written to a temporary files. Each snippet is
* Compiles given source code snippets written to temporary files. Each snippet is
* written to a separate temporary file.
*
* Snippets can be grouped to be compiled together in the same compiler run. This is
* useful to compile macros, which cannot be used in the same compilation run that
* defines them.
*
* The sequence of temporary files corresponding to passed snippets and analysis
* callback is returned as a result.
*/
private def compileSrcs(srcs: String*): (Seq[File], TestCallback) = {
private def compileSrcs(groupedSrcs: List[List[String]]): (Seq[File], TestCallback) = {
withTemporaryDirectory { temp =>
val analysisCallback = new TestCallback(nameHashing)
val classesDir = new File(temp, "classes")
classesDir.mkdir()
val compiler = prepareCompiler(classesDir, analysisCallback)
val run = new compiler.Run
val srcFiles = srcs.toSeq.zipWithIndex map { case (src, i) =>
val fileName = s"Test_$i.scala"
prepareSrcFile(temp, fileName, src)
val compiler = prepareCompiler(classesDir, analysisCallback, classesDir.toString)
val files = for((compilationUnit, unitId) <- groupedSrcs.zipWithIndex) yield {
val run = new compiler.Run
val srcFiles = compilationUnit.toSeq.zipWithIndex map { case (src, i) =>
val fileName = s"Test-$unitId-$i.scala"
prepareSrcFile(temp, fileName, src)
}
val srcFilePaths = srcFiles.map(srcFile => srcFile.getAbsolutePath).toList
run.compile(srcFilePaths)
srcFilePaths.foreach(f => new File(f).delete)
srcFiles
}
val srcFilePaths = srcFiles.map(srcFile => srcFile.getAbsolutePath).toList
run.compile(srcFilePaths)
(srcFiles, analysisCallback)
(files.flatten.toSeq, analysisCallback)
}
}
private def compileSrcs(srcs: String*): (Seq[File], TestCallback) = {
compileSrcs(List(srcs.toList))
}
private def prepareSrcFile(baseDir: File, fileName: String, src: String): File = {
val srcFile = new File(baseDir, fileName)
sbt.IO.write(srcFile, src)
srcFile
}
private def prepareCompiler(outputDir: File, analysisCallback: AnalysisCallback): CachedCompiler0#Compiler = {
private def prepareCompiler(outputDir: File, analysisCallback: AnalysisCallback, classpath: String = "."): CachedCompiler0#Compiler = {
val args = Array.empty[String]
object output extends SingleOutput {
def outputDirectory: File = outputDir
@ -123,6 +151,7 @@ class ScalaCompilerForUnitTesting(nameHashing: Boolean = false) {
val weakLog = new WeakLog(ConsoleLogger(), ConsoleReporter)
val cachedCompiler = new CachedCompiler0(args, output, weakLog, false)
val settings = cachedCompiler.settings
settings.classpath.value = classpath
settings.usejavacp.value = true
val scalaReporter = new ConsoleReporter(settings)
val delegatingReporter = DelegatingReporter(settings, ConsoleReporter)

View File

@ -73,8 +73,8 @@ object AnalysisFormats
wrap[Severity, Byte]( _.ordinal.toByte, b => Severity.values.apply(b.toInt) )
implicit def setupFormat(implicit outputF: Format[APIOutput], optionF: Format[CompileOptions], compilerVersion: Format[String], orderF: Format[CompileOrder]): Format[CompileSetup] =
asProduct4[CompileSetup, APIOutput, CompileOptions, String, CompileOrder]( (a,b,c,d) => new CompileSetup(a,b,c,d) )(s => (s.output, s.options, s.compilerVersion, s.order))(outputF, optionF, compilerVersion, orderF)
implicit def setupFormat(implicit outputF: Format[APIOutput], optionF: Format[CompileOptions], compilerVersion: Format[String], orderF: Format[CompileOrder], nameHashingF: Format[Boolean]): Format[CompileSetup] =
asProduct5[CompileSetup, APIOutput, CompileOptions, String, CompileOrder, Boolean]( (a,b,c,d,e) => new CompileSetup(a,b,c,d,e) )(s => (s.output, s.options, s.compilerVersion, s.order, s.nameHashing))(outputF, optionF, compilerVersion, orderF, nameHashingF)
implicit val outputGroupFormat: Format[OutputGroup] =
asProduct2((a: File,b: File) => new OutputGroup{def sourceDirectory = a; def outputDirectory = b}) { out => (out.sourceDirectory, out.outputDirectory) }(fileFormat, fileFormat)

View File

@ -4,6 +4,7 @@ package inc
import java.io._
import sbt.{CompileSetup, Relation}
import xsbti.api.{Compilation, Source}
import xsbti.compile.{MultipleOutput, SingleOutput}
import javax.xml.bind.DatatypeConverter
@ -55,52 +56,54 @@ object TextAnalysisFormat {
implicit val compilationF = xsbt.api.CompilationFormat
def write(out: Writer, analysis: Analysis, setup: CompileSetup) {
VersionF.write(out)
// We start with relations because that's the part of greatest interest to external readers,
VersionF.write(out)
// We start with writing compile setup which contains value of the `nameHashing`
// flag that is needed to properly deserialize relations
FormatTimer.time("write setup") { CompileSetupF.write(out, setup) }
// Next we write relations because that's the part of greatest interest to external readers,
// who can abort reading early once they're read them.
FormatTimer.time("write relations") { RelationsF.write(out, analysis.relations) }
FormatTimer.time("write stamps") { StampsF.write(out, analysis.stamps) }
FormatTimer.time("write apis") { APIsF.write(out, analysis.apis) }
FormatTimer.time("write sourceinfos") { SourceInfosF.write(out, analysis.infos) }
FormatTimer.time("write compilations") { CompilationsF.write(out, analysis.compilations) }
FormatTimer.time("write setup") { CompileSetupF.write(out, setup) }
out.flush()
}
def read(in: BufferedReader): (Analysis, CompileSetup) = {
VersionF.read(in)
val relations = FormatTimer.time("read relations") { RelationsF.read(in) }
VersionF.read(in)
val setup = FormatTimer.time("read setup") { CompileSetupF.read(in) }
val relations = FormatTimer.time("read relations") { RelationsF.read(in, setup.nameHashing) }
val stamps = FormatTimer.time("read stamps") { StampsF.read(in) }
val apis = FormatTimer.time("read apis") { APIsF.read(in) }
val infos = FormatTimer.time("read sourceinfos") { SourceInfosF.read(in) }
val compilations = FormatTimer.time("read compilations") { CompilationsF.read(in) }
val setup = FormatTimer.time("read setup") { CompileSetupF.read(in) }
(Analysis.Empty.copy(stamps, apis, relations, infos, compilations), setup)
}
private[this] object VersionF {
val currentVersion = "2"
val currentVersion = "5"
def write(out: Writer) {
out.write("format version: %s\n".format(currentVersion))
}
def write(out: Writer) {
out.write("format version: %s\n".format(currentVersion))
}
private val versionPattern = """format version: (\w+)""".r
def read(in: BufferedReader) {
in.readLine() match {
case versionPattern(version) => validateVersion(version)
case s: String => throw new ReadException("\"format version: <version>\"", s)
case null => throw new EOFException
}
}
private val versionPattern = """format version: (\w+)""".r
def read(in: BufferedReader) {
in.readLine() match {
case versionPattern(version) => validateVersion(version)
case s: String => throw new ReadException("\"format version: <version>\"", s)
case null => throw new EOFException
}
}
def validateVersion(version: String) {
// TODO: Support backwards compatibility?
if (version != currentVersion) {
throw new ReadException("File uses format version %s, but we are compatible with version %s only.".format(version, currentVersion))
}
}
def validateVersion(version: String) {
// TODO: Support backwards compatibility?
if (version != currentVersion) {
throw new ReadException("File uses format version %s, but we are compatible with version %s only.".format(version, currentVersion))
}
}
}
private[this] object RelationsF {
@ -128,8 +131,8 @@ object TextAnalysisFormat {
// We sort for ease of debugging and for more efficient reconstruction when reading.
// Note that we don't share code with writeMap. Each is implemented more efficiently
// than the shared code would be, and the difference is measurable on large analyses.
rel.forwardMap.toSeq.sortBy(_._1).foreach { case (k, vs) =>
val kStr = k.toString
rel.forwardMap.toSeq.sortBy(_._1).foreach { case (k, vs) =>
val kStr = k.toString
vs.toSeq.sorted foreach { v =>
out.write(kStr); out.write(" -> "); out.write(v.toString); out.write("\n")
}
@ -137,8 +140,8 @@ object TextAnalysisFormat {
}
val nameHashing = relations.nameHashing
writeRelation(Headers.srcProd, relations.srcProd)
writeRelation(Headers.binaryDep, relations.binaryDep)
writeRelation(Headers.srcProd, relations.srcProd)
writeRelation(Headers.binaryDep, relations.binaryDep)
val direct = if (nameHashing) Relations.emptySource else relations.direct
val publicInherited = if (nameHashing)
@ -160,11 +163,11 @@ object TextAnalysisFormat {
writeRelation(Headers.inheritanceInternalDep, inheritance.internal)
writeRelation(Headers.inheritanceExternalDep, inheritance.external)
writeRelation(Headers.classes, relations.classes)
writeRelation(Headers.classes, relations.classes)
writeRelation(Headers.usedNames, names)
}
def read(in: BufferedReader): Relations = {
def read(in: BufferedReader, nameHashing: Boolean): Relations = {
def readRelation[T](expectedHeader: String, s2t: String => T): Relation[File, T] = {
val items = readPairs(in)(expectedHeader, new File(_), s2t).toIterator
// Reconstruct the forward map. This is more efficient than Relation.empty ++ items.
@ -188,19 +191,19 @@ object TextAnalysisFormat {
def readFileRelation(expectedHeader: String) = readRelation(expectedHeader, { new File(_) })
def readStringRelation(expectedHeader: String) = readRelation(expectedHeader, identity[String])
val srcProd = readFileRelation(Headers.srcProd)
val binaryDep = readFileRelation(Headers.binaryDep)
val srcProd = readFileRelation(Headers.srcProd)
val binaryDep = readFileRelation(Headers.binaryDep)
import sbt.inc.Relations.{Source, SourceDependencies, makeSourceDependencies, emptySource,
makeSource, emptySourceDependencies}
val directSrcDeps: Source = {
val internalSrcDep = readFileRelation(Headers.directSrcDep)
val externalDep = readStringRelation(Headers.directExternalDep)
val internalSrcDep = readFileRelation(Headers.directSrcDep)
val externalDep = readStringRelation(Headers.directExternalDep)
makeSource(internalSrcDep, externalDep)
}
val publicInheritedSrcDeps: Source = {
val internalSrcDepPI = readFileRelation(Headers.internalSrcDepPI)
val externalDepPI = readStringRelation(Headers.externalDepPI)
val externalDepPI = readStringRelation(Headers.externalDepPI)
makeSource(internalSrcDepPI, externalDepPI)
}
val memberRefSrcDeps: SourceDependencies = {
@ -215,17 +218,18 @@ object TextAnalysisFormat {
}
// we don't check for emptiness of publicInherited/inheritance relations because
// we assume that invariant that says they are subsets of direct/memberRef holds
assert((directSrcDeps == emptySource) || (memberRefSrcDeps == emptySourceDependencies),
"One mechanism is supported for tracking source dependencies at the time")
val nameHashing = memberRefSrcDeps != emptySourceDependencies
val classes = readStringRelation(Headers.classes)
assert(nameHashing || (memberRefSrcDeps == emptySourceDependencies),
"When name hashing is disabled the `memberRef` relation should be empty.")
assert(!nameHashing || (directSrcDeps == emptySource),
"When name hashing is enabled the `direct` relation should be empty.")
val classes = readStringRelation(Headers.classes)
val names = readStringRelation(Headers.usedNames)
if (nameHashing)
Relations.make(srcProd, binaryDep, memberRefSrcDeps, inheritanceSrcDeps, classes, names)
else {
assert(names.all.isEmpty, s"When `nameHashing` is disabled `names` relation " +
"should be empty: $names")
assert(names.all.isEmpty, "When `nameHashing` is disabled `names` relation " +
s"should be empty: $names")
Relations.make(srcProd, binaryDep, directSrcDeps, publicInheritedSrcDeps, classes)
}
}
@ -250,9 +254,9 @@ object TextAnalysisFormat {
def read(in: BufferedReader): Stamps = {
def doReadMap[V](expectedHeader: String, s2v: String => V) = readMap(in)(expectedHeader, new File(_), s2v)
val products = doReadMap(Headers.products, Stamp.fromString)
val sources = doReadMap(Headers.sources, Stamp.fromString)
val binaries = doReadMap(Headers.binaries, Stamp.fromString)
val products = doReadMap(Headers.products, Stamp.fromString)
val sources = doReadMap(Headers.sources, Stamp.fromString)
val binaries = doReadMap(Headers.binaries, Stamp.fromString)
val classNames = doReadMap(Headers.classNames, identity[String])
Stamps(products, sources, binaries, classNames)
@ -260,10 +264,10 @@ object TextAnalysisFormat {
}
private[this] object APIsF {
object Headers {
val internal = "internal apis"
val external = "external apis"
}
object Headers {
val internal = "internal apis"
val external = "external apis"
}
val stringToSource = ObjectStringifier.stringToObj[Source] _
val sourceToString = ObjectStringifier.objToString[Source] _
@ -286,9 +290,9 @@ object TextAnalysisFormat {
}
private[this] object SourceInfosF {
object Headers {
val infos = "source infos"
}
object Headers {
val infos = "source infos"
}
val stringToSourceInfo = ObjectStringifier.stringToObj[SourceInfo] _
val sourceInfoToString = ObjectStringifier.objToString[SourceInfo] _
@ -298,31 +302,83 @@ object TextAnalysisFormat {
}
private[this] object CompilationsF {
object Headers {
val compilations = "compilations"
}
object Headers {
val compilations = "compilations"
}
val stringToCompilation = ObjectStringifier.stringToObj[Compilation] _
val compilationToString = ObjectStringifier.objToString[Compilation] _
def write(out: Writer, compilations: Compilations) {
def toMapEntry(x: (Compilation, Int)): (String, Compilation) = "%03d".format(x._2) -> x._1
writeMap(out)(Headers.compilations, compilations.allCompilations.zipWithIndex.map(toMapEntry).toMap, compilationToString, inlineVals=false)
writeSeq(out)(Headers.compilations, compilations.allCompilations, compilationToString)
}
def read(in: BufferedReader): Compilations =
Compilations.make(readMap(in)(Headers.compilations, identity[String], stringToCompilation).values.toSeq)
def read(in: BufferedReader): Compilations = Compilations.make(
readSeq[Compilation](in)(Headers.compilations, stringToCompilation))
}
private[this] object CompileSetupF {
object Headers {
val setup = "compile setup"
}
object Headers {
val outputMode = "output mode"
val outputDir = "output directories"
val compileOptions = "compile options"
val javacOptions = "javac options"
val compilerVersion = "compiler version"
val compileOrder = "compile order"
val nameHashing = "name hashing"
}
val stringToSetup = ObjectStringifier.stringToObj[CompileSetup] _
val setupToString = ObjectStringifier.objToString[CompileSetup] _
private[this] val singleOutputMode = "single"
private[this] val multipleOutputMode = "multiple"
private[this] val singleOutputKey = new File("output dir")
def write(out: Writer, setup: CompileSetup) { writeMap(out)(Headers.setup, Map("1" -> setup), setupToString, inlineVals=false)}
def read(in: BufferedReader): CompileSetup = readMap(in)(Headers.setup, identity[String], stringToSetup).head._2
def write(out: Writer, setup: CompileSetup) {
val (mode, outputAsMap) = setup.output match {
case s: SingleOutput => (singleOutputMode, Map(singleOutputKey -> s.outputDirectory))
case m: MultipleOutput => (multipleOutputMode, m.outputGroups.map(x => x.sourceDirectory -> x.outputDirectory).toMap)
}
writeSeq(out)(Headers.outputMode, mode :: Nil, identity[String])
writeMap(out)(Headers.outputDir, outputAsMap, { f: File => f.getPath })
writeSeq(out)(Headers.compileOptions, setup.options.options, identity[String])
writeSeq(out)(Headers.javacOptions, setup.options.javacOptions, identity[String])
writeSeq(out)(Headers.compilerVersion, setup.compilerVersion :: Nil, identity[String])
writeSeq(out)(Headers.compileOrder, setup.order.name :: Nil, identity[String])
writeSeq(out)(Headers.nameHashing, setup.nameHashing :: Nil, (b: Boolean) => b.toString)
}
def read(in: BufferedReader): CompileSetup = {
def s2f(s: String) = new File(s)
def s2b(s: String): Boolean = s.toBoolean
val outputDirMode = readSeq(in)(Headers.outputMode, identity[String]).headOption
val outputAsMap = readMap(in)(Headers.outputDir, s2f, s2f)
val compileOptions = readSeq(in)(Headers.compileOptions, identity[String])
val javacOptions = readSeq(in)(Headers.javacOptions, identity[String])
val compilerVersion = readSeq(in)(Headers.compilerVersion, identity[String]).head
val compileOrder = readSeq(in)(Headers.compileOrder, identity[String]).head
val nameHashing = readSeq(in)(Headers.nameHashing, s2b).head
val output = outputDirMode match {
case Some(s) => s match {
case `singleOutputMode` => new SingleOutput {
val outputDirectory = outputAsMap(singleOutputKey)
}
case `multipleOutputMode` => new MultipleOutput {
val outputGroups: Array[MultipleOutput.OutputGroup] = outputAsMap.toArray.map {
case (src: File, out: File) => new MultipleOutput.OutputGroup {
val sourceDirectory = src
val outputDirectory = out
}
}
}
case str: String => throw new ReadException("Unrecognized output mode: " + str)
}
case None => throw new ReadException("No output mode specified")
}
new CompileSetup(output, new CompileOptions(compileOptions, javacOptions), compilerVersion,
xsbti.compile.CompileOrder.valueOf(compileOrder), nameHashing)
}
}
private[this] object ObjectStringifier {
@ -348,8 +404,8 @@ object TextAnalysisFormat {
}
private[this] def expectHeader(in: BufferedReader, expectedHeader: String) {
val header = in.readLine()
if (header != expectedHeader + ":") throw new ReadException(expectedHeader, if (header == null) "EOF" else header)
val header = in.readLine()
if (header != expectedHeader + ":") throw new ReadException(expectedHeader, if (header == null) "EOF" else header)
}
private[this] def writeSize(out: Writer, n: Int) {
@ -361,10 +417,23 @@ object TextAnalysisFormat {
in.readLine() match {
case itemsPattern(nStr) => Integer.parseInt(nStr)
case s: String => throw new ReadException("\"<n> items\"", s)
case null => throw new EOFException
case null => throw new EOFException
}
}
private[this] def writeSeq[T](out: Writer)(header: String, s: Seq[T], t2s: T => String) {
// We write sequences as idx -> element maps, for uniformity with maps/relations.
def n = s.length
val numDigits = if (n < 2) 1 else math.log10(n - 1).toInt + 1
val fmtStr = "%%0%dd".format(numDigits)
// We only use this for relatively short seqs, so creating this extra map won't be a performance hit.
val m: Map[String, T] = s.zipWithIndex.map(x => fmtStr.format(x._2) -> x._1).toMap
writeMap(out)(header, m, t2s)
}
private[this] def readSeq[T](in: BufferedReader)(expectedHeader: String, s2t: String => T): Seq[T] =
(readPairs(in)(expectedHeader, identity[String], s2t) map(_._2)).toSeq
private[this] def writeMap[K, V](out: Writer)(header: String, m: Map[K, V], v2s: V => String, inlineVals: Boolean=true)(implicit ord: Ordering[K]) {
writeHeader(out, header)
writeSize(out, m.size)
@ -379,7 +448,7 @@ object TextAnalysisFormat {
private[this] def readPairs[K, V](in: BufferedReader)(expectedHeader: String, s2k: String => K, s2v: String => V): Traversable[(K, V)] = {
def toPair(s: String): (K, V) = {
if (s == null) throw new EOFException
if (s == null) throw new EOFException
val p = s.indexOf(" -> ")
val k = s2k(s.substring(0, p))
// Pair is either "a -> b" or "a -> \nb". This saves us a lot of substring munging when b is a large blob.
@ -387,8 +456,8 @@ object TextAnalysisFormat {
(k, v)
}
expectHeader(in, expectedHeader)
val n = readSize(in)
for (i <- 0 until n) yield toPair(in.readLine())
val n = readSize(in)
for (i <- 0 until n) yield toPair(in.readLine())
}
private[this] def readMap[K, V](in: BufferedReader)(expectedHeader: String, s2k: String => K, s2v: String => V): Map[K, V] = {

View File

@ -10,13 +10,91 @@ import core.module.id.ModuleRevisionId
import core.module.descriptor.DependencyDescriptor
import core.resolve.ResolveData
import core.settings.IvySettings
import plugins.resolver.{BasicResolver, DependencyResolver, IBiblioResolver}
import plugins.resolver.{BasicResolver, DependencyResolver, IBiblioResolver, RepositoryResolver}
import plugins.resolver.{AbstractPatternsBasedResolver, AbstractSshBasedResolver, FileSystemResolver, SFTPResolver, SshResolver, URLResolver}
import plugins.repository.url.{URLRepository => URLRepo}
import plugins.repository.file.{FileRepository => FileRepo, FileResource}
import java.io.File
import org.apache.ivy.util.ChecksumHelper
import org.apache.ivy.core.module.descriptor.{Artifact=>IArtifact}
private object ConvertResolver
{
/** This class contains all the reflective lookups used in the
* checksum-friendly URL publishing shim.
*/
private object ChecksumFriendlyURLResolver {
// TODO - When we dump JDK6 support we can remove this hackery
// import java.lang.reflect.AccessibleObject
type AccessibleObject = {
def setAccessible(value: Boolean): Unit
}
private def reflectiveLookup[A <: AccessibleObject](f: Class[_] => A): Option[A] =
try {
val cls = classOf[RepositoryResolver]
val thing = f(cls)
import scala.language.reflectiveCalls
thing.setAccessible(true)
Some(thing)
} catch {
case (_: java.lang.NoSuchFieldException) |
(_: java.lang.SecurityException) |
(_: java.lang.NoSuchMethodException) => None
}
private val signerNameField: Option[java.lang.reflect.Field] =
reflectiveLookup(_.getDeclaredField("signerName"))
private val putChecksumMethod: Option[java.lang.reflect.Method] =
reflectiveLookup(_.getDeclaredMethod("putChecksum",
classOf[IArtifact], classOf[File], classOf[String],
classOf[Boolean], classOf[String]))
private val putSignatureMethod: Option[java.lang.reflect.Method] =
reflectiveLookup(_.getDeclaredMethod("putSignature",
classOf[IArtifact], classOf[File], classOf[String],
classOf[Boolean]))
}
/**
* The default behavior of ivy's overwrite flags ignores the fact that a lot of repositories
* will autogenerate checksums *for* an artifact if it doesn't already exist. Therefore
* if we succeed in publishing an artifact, we need to just blast the checksums in place.
* This acts as a "shim" on RepositoryResolvers so that we can hook our methods into
* both the IBiblioResolver + URLResolver without having to duplicate the code in two
* places. However, this does mean our use of reflection is awesome.
*
* TODO - See about contributing back to ivy.
*/
private trait ChecksumFriendlyURLResolver extends RepositoryResolver {
import ChecksumFriendlyURLResolver._
private def signerName: String = signerNameField match {
case Some(field) => field.get(this).asInstanceOf[String]
case None => null
}
override protected def put(artifact: IArtifact, src: File, dest: String, overwrite: Boolean): Unit = {
// verify the checksum algorithms before uploading artifacts!
val checksums = getChecksumAlgorithms()
val repository = getRepository()
for {
checksum <- checksums
if !ChecksumHelper.isKnownAlgorithm(checksum)
} throw new IllegalArgumentException("Unknown checksum algorithm: " + checksum)
repository.put(artifact, src, dest, overwrite);
// Fix for sbt#1156 - Artifactory will auto-generate MD5/sha1 files, so
// we need to overwrite what it has.
for (checksum <- checksums) {
putChecksumMethod match {
case Some(method) => method.invoke(this, artifact, src, dest, true: java.lang.Boolean, checksum)
case None => // TODO - issue warning?
}
}
if (signerName != null) {
putSignatureMethod match {
case None => ()
case Some(method) => method.invoke(artifact, src, dest, true: java.lang.Boolean)
}
}
}
}
/** Converts the given sbt resolver into an Ivy resolver..*/
def apply(r: Resolver, settings: IvySettings, log: Logger) =
{
@ -25,7 +103,7 @@ private object ConvertResolver
case repo: MavenRepository =>
{
val pattern = Collections.singletonList(Resolver.resolvePattern(repo.root, Resolver.mavenStyleBasePattern))
final class PluginCapableResolver extends IBiblioResolver with DescriptorRequired {
final class PluginCapableResolver extends IBiblioResolver with ChecksumFriendlyURLResolver with DescriptorRequired {
def setPatterns() { // done this way for access to protected methods.
setArtifactPatterns(pattern)
setIvyPatterns(pattern)
@ -61,7 +139,13 @@ private object ConvertResolver
}
case repo: FileRepository =>
{
val resolver = new FileSystemResolver with DescriptorRequired
val resolver = new FileSystemResolver with DescriptorRequired {
// Workaround for #1156
// Temporarily in sbt 0.13.x we deprecate overwriting
// in local files for non-changing revisions.
// This will be fully enforced in sbt 1.0.
setRepository(new WarnOnOverwriteFileRepo())
}
resolver.setName(repo.name)
initializePatterns(resolver, repo.patterns, settings)
import repo.configuration.{isLocal, isTransactional}
@ -71,7 +155,7 @@ private object ConvertResolver
}
case repo: URLRepository =>
{
val resolver = new URLResolver with DescriptorRequired
val resolver = new URLResolver with ChecksumFriendlyURLResolver with DescriptorRequired
resolver.setName(repo.name)
initializePatterns(resolver, repo.patterns, settings)
resolver
@ -135,7 +219,7 @@ private object ConvertResolver
/** A custom Ivy URLRepository that returns FileResources for file URLs.
* This allows using the artifacts from the Maven local repository instead of copying them to the Ivy cache. */
private[this] final class LocalIfFileRepo extends URLRepo {
private[this] val repo = new FileRepo
private[this] val repo = new WarnOnOverwriteFileRepo()
override def getResource(source: String) = {
val url = new URL(source)
if(url.getProtocol == IO.FileScheme)
@ -144,4 +228,16 @@ private object ConvertResolver
super.getResource(source)
}
}
private[this] final class WarnOnOverwriteFileRepo extends FileRepo() {
override def put(source: java.io.File, destination: String, overwrite: Boolean): Unit = {
try super.put(source, destination, overwrite)
catch {
case e: java.io.IOException if e.getMessage.contains("destination already exists") =>
import org.apache.ivy.util.Message
Message.warn(s"Attempting to overwrite $destination\n\tThis usage is deprecated and will be removed in sbt 1.0.")
super.put(source, destination, true)
}
}
}
}

View File

@ -43,7 +43,7 @@ object CustomPomParser
val JarPackagings = Set("eclipse-plugin", "hk2-jar", "orbit")
val default = new CustomPomParser(PomModuleDescriptorParser.getInstance, defaultTransform)
private[this] val TransformedHashKey = "sbtTransformHash"
private[this] val TransformedHashKey = "e:sbtTransformHash"
// A hash of the parameters transformation is based on.
// If a descriptor has a different hash, we need to retransform it.
private[this] val TransformHash: String = hash((unqualifiedKeys ++ JarPackagings).toSeq.sorted)
@ -57,8 +57,14 @@ object CustomPomParser
private[this] def transformedByThisVersion(md: ModuleDescriptor): Boolean =
{
val oldTransformedHashKey = "sbtTransformHash"
val extraInfo = md.getExtraInfo
extraInfo != null && extraInfo.get(TransformedHashKey) == TransformHash
// sbt 0.13.1 used "sbtTransformHash" instead of "e:sbtTransformHash" until #1192 so read both
Option(extraInfo).isDefined &&
((Option(extraInfo get TransformedHashKey) orElse Option(extraInfo get oldTransformedHashKey)) match {
case Some(TransformHash) => true
case _ => false
})
}
private[this] def defaultTransformImpl(parser: ModuleDescriptorParser, md: ModuleDescriptor): ModuleDescriptor =

View File

@ -7,7 +7,6 @@ import Resolver.PluginPattern
import java.io.File
import java.net.URI
import java.text.ParseException
import java.util.concurrent.Callable
import java.util.{Collection, Collections => CS}
import CS.singleton
@ -24,9 +23,7 @@ import core.settings.IvySettings
import plugins.latest.LatestRevisionStrategy
import plugins.matcher.PatternMatcher
import plugins.parser.m2.PomModuleDescriptorParser
import plugins.repository.ResourceDownloader
import plugins.resolver.{ChainResolver, DependencyResolver}
import plugins.resolver.util.ResolvedResource
import util.{Message, MessageLogger}
import util.extendable.ExtendableItem
@ -99,6 +96,8 @@ final class IvySbt(val configuration: IvyConfiguration)
def withIvy[T](log: MessageLogger)(f: Ivy => T): T =
withDefaultLogger(log)
{
// See #429 - We always insert a helper authenticator here which lets us get more useful authentication errors.
ivyint.ErrorMessageAuthenticator.install()
ivy.pushContext()
ivy.getLoggerEngine.pushLogger(log)
try { f(ivy) }
@ -356,41 +355,8 @@ private object IvySbt
case pr: ProjectResolver => true
case _ => false
}
/** This is overridden to delete outofdate artifacts of changing modules that are not listed in the metadata.
* This occurs for artifacts with classifiers, for example. */
@throws(classOf[ParseException])
override def cacheModuleDescriptor(resolver: DependencyResolver, mdRef: ResolvedResource, dd: DependencyDescriptor, moduleArtifact: IArtifact, downloader: ResourceDownloader, options: CacheMetadataOptions): ResolvedModuleRevision =
{
val rmrRaw = super.cacheModuleDescriptor(null, mdRef, dd, moduleArtifact, downloader, options)
val rmr = resetArtifactResolver(rmrRaw)
val mrid = moduleArtifact.getModuleRevisionId
def shouldClear(): Boolean = rmr != null &&
( (rmr.getReport != null && rmr.getReport.isSearched && isChanging(dd, mrid)) ||
isProjectResolver(rmr.getResolver) )
// only handle changing modules whose metadata actually changed.
// Typically, the publication date in the metadata has to change to get here.
if(shouldClear()) {
// this is the locally cached metadata as originally retrieved (e.g. the pom)
val original = rmr.getReport.getOriginalLocalFile
if(original != null) {
// delete all files in subdirectories that are older than the original metadata file's publication date
// The publication date is used because the metadata will be redownloaded for changing files,
// so the last modified time changes, but the publication date doesn't
val pubDate = rmrRaw.getPublicationDate
val lm = if(pubDate eq null) original.lastModified else pubDate.getTime
val indirectFiles = PathFinder(original.getParentFile).*(DirectoryFilter).**(-DirectoryFilter).get.toList
val older = indirectFiles.filter(f => f.lastModified < lm).toList
Message.verbose("Deleting additional old artifacts from cache for changed module " + mrid + older.mkString(":\n\t", "\n\t", ""))
IO.delete(older)
}
}
rmr
}
// ignore the original resolver wherever possible to avoid issues like #704
override def saveResolvers(descriptor: ModuleDescriptor, metadataResolverName: String, artifactResolverName: String) {}
def isChanging(dd: DependencyDescriptor, requestedRevisionId: ModuleRevisionId): Boolean =
!localOnly && (dd.isChanging || requestedRevisionId.getRevision.contains("-SNAPSHOT"))
}
manager.setArtifactPattern(PluginPattern + manager.getArtifactPattern)
manager.setDataFilePattern(PluginPattern + manager.getDataFilePattern)

View File

@ -16,7 +16,11 @@ import core.resolve.ResolveOptions
import plugins.resolver.{BasicResolver, DependencyResolver}
final class DeliverConfiguration(val deliverIvyPattern: String, val status: String, val configurations: Option[Seq[Configuration]], val logging: UpdateLogging.Value)
final class PublishConfiguration(val ivyFile: Option[File], val resolverName: String, val artifacts: Map[Artifact, File], val checksums: Seq[String], val logging: UpdateLogging.Value)
final class PublishConfiguration(val ivyFile: Option[File], val resolverName: String, val artifacts: Map[Artifact, File], val checksums: Seq[String], val logging: UpdateLogging.Value,
val overwrite: Boolean) {
def this(ivyFile: Option[File], resolverName: String, artifacts: Map[Artifact, File], checksums: Seq[String], logging: UpdateLogging.Value) =
this(ivyFile, resolverName, artifacts, checksums, logging, false)
}
final class UpdateConfiguration(val retrieve: Option[RetrieveConfiguration], val missingOk: Boolean, val logging: UpdateLogging.Value)
final class RetrieveConfiguration(val retrieveDirectory: File, val outputPattern: String)
@ -86,11 +90,11 @@ object IvyActions
import configuration._
module.withModule(log) { case (ivy, md, default) =>
val resolver = ivy.getSettings.getResolver(resolverName)
if(resolver eq null) error("Undefined resolver '" + resolverName + "'")
if(resolver eq null) sys.error("Undefined resolver '" + resolverName + "'")
val ivyArtifact = ivyFile map { file => (MDArtifact.newIvyArtifact(md), file) }
val cross = crossVersionMap(module.moduleSettings)
val as = mapArtifacts(md, cross, artifacts) ++ ivyArtifact.toList
withChecksums(resolver, checksums) { publish(md, as, resolver, overwrite = true) }
val as = mapArtifacts(md, cross, artifacts) ++ ivyArtifact.toSeq
withChecksums(resolver, checksums) { publish(md, as, resolver, overwrite = overwrite) }
}
}
private[this] def withChecksums[T](resolver: DependencyResolver, checksums: Seq[String])(act: => T): T =

View File

@ -188,7 +188,7 @@ class MakePom(val log: Logger)
<dependency>
<groupId>{mrid.getOrganisation}</groupId>
<artifactId>{mrid.getName}</artifactId>
<version>{mrid.getRevision}</version>
<version>{makeDependencyVersion(mrid.getRevision)}</version>
{ scopeElem(scope) }
{ optionalElem(optional) }
{ classifierElem(classifier) }
@ -197,6 +197,44 @@ class MakePom(val log: Logger)
</dependency>
}
def makeDependencyVersion(revision: String): String = {
def plusRange(s:String, shift:Int = 0) = {
def pow(i:Int):Int = if (i>0) 10 * pow(i-1) else 1
val (prefixVersion, lastVersion) = (s+"0"*shift).reverse.split("\\.",2) match {
case Array(revLast,revRest) =>
( revRest.reverse + ".", revLast.reverse )
case Array(revLast) => ("", revLast.reverse)
}
val lastVersionInt = lastVersion.toInt
s"[${prefixVersion}${lastVersion},${prefixVersion}${lastVersionInt+pow(shift)})"
}
val startSym=Set(']','[','(')
val stopSym=Set(']','[',')')
try {
if (revision endsWith ".+") {
plusRange(revision.substring(0,revision.length-2))
} else if (revision endsWith "+") {
val base = revision.take(revision.length-1)
// This is a heuristic. Maven just doesn't support Ivy's notions of 1+, so
// we assume version ranges never go beyond 5 siginificant digits.
(0 to 5).map(plusRange(base,_)).mkString(",")
} else if (startSym(revision(0)) && stopSym(revision(revision.length-1))) {
val start = revision(0)
val stop = revision(revision.length-1)
val mid = revision.substring(1,revision.length-1)
(if (start == ']') "(" else start) + mid + (if (stop == '[') ")" else stop)
} else revision
} catch {
case e: NumberFormatException =>
// TODO - if the version doesn't meet our expectations, maybe we just issue a hard
// error instead of softly ignoring the attempt to rewrite.
//sys.error(s"Could not fix version [$revision] into maven style version")
revision
}
}
@deprecated("No longer used and will be removed.", "0.12.1")
def classifier(dependency: DependencyDescriptor, includeTypes: Set[String]): NodeSeq =
{

View File

@ -0,0 +1,128 @@
package sbt
package ivyint
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.net.Authenticator
import java.net.PasswordAuthentication
import org.apache.ivy.util.Credentials
import org.apache.ivy.util.Message
import org.apache.ivy.util.url.IvyAuthenticator
import org.apache.ivy.util.url.CredentialsStore
/**
* Helper to install an Authenticator that works with the IvyAuthenticator to provide better error messages when
* credentials don't line up.
*/
object ErrorMessageAuthenticator {
private var securityWarningLogged = false
private def originalAuthenticator: Option[Authenticator] = {
try {
val f = classOf[Authenticator].getDeclaredField("theAuthenticator");
f.setAccessible(true);
Option(f.get(null).asInstanceOf[Authenticator])
} catch {
// TODO - Catch more specific errors.
case t: Throwable =>
Message.debug("Error occurred while getting the original authenticator: " + t.getMessage)
None
}
}
private lazy val ivyOriginalField = {
val field = classOf[IvyAuthenticator].getDeclaredField("original")
field.setAccessible(true)
field
}
// Attempts to get the original authenticator form the ivy class or returns null.
private def installIntoIvy(ivy: IvyAuthenticator): Option[Authenticator] = {
// Here we install ourselves as the IvyAuthenticator's default so we get called AFTER Ivy has a chance to run.
def installIntoIvyImpl(original: Option[Authenticator]): Unit = {
val newOriginal = new ErrorMessageAuthenticator(original)
ivyOriginalField.set(ivy, newOriginal)
}
try Option(ivyOriginalField.get(ivy).asInstanceOf[Authenticator]) match {
case Some(alreadyThere: ErrorMessageAuthenticator) => // We're already installed, no need to do the work again.
case originalOpt => installIntoIvyImpl(originalOpt)
} catch {
case t: Throwable =>
Message.debug("Error occurred will trying to install debug messages into Ivy Authentication" + t.getMessage)
}
Some(ivy)
}
/** Installs the error message authenticator so we have nicer error messages when using java's URL for downloading. */
def install() {
// Actually installs the error message authenticator.
def doInstall(original: Option[Authenticator]): Unit =
try Authenticator.setDefault(new ErrorMessageAuthenticator(original))
catch {
case e: SecurityException if !securityWarningLogged =>
securityWarningLogged = true;
Message.warn("Not enough permissions to set the ErorrMessageAuthenticator. "
+ "Helpful debug messages disabled!");
}
// We will try to use the original authenticator as backup authenticator.
// Since there is no getter available, so try to use some reflection to
// obtain it. If that doesn't work, assume there is no original authenticator
def doInstallIfIvy(original: Option[Authenticator]): Unit =
original match {
case Some(installed: ErrorMessageAuthenticator) => // Ignore, we're already installed
case Some(ivy: IvyAuthenticator) => installIntoIvy(ivy)
case original => doInstall(original)
}
doInstallIfIvy(originalAuthenticator)
}
}
/**
* An authenticator which just delegates to a previous authenticator and issues *nice*
* error messages on failure to find credentials.
*
* Since ivy installs its own credentials handler EVERY TIME it resolves or publishes, we want to
* install this one at some point and eventually ivy will capture it and use it.
*/
private[sbt] final class ErrorMessageAuthenticator(original: Option[Authenticator]) extends Authenticator {
protected override def getPasswordAuthentication(): PasswordAuthentication = {
// We're guaranteed to only get here if Ivy's authentication fails
if (!isProxyAuthentication) {
val host = getRequestingHost
// TODO - levenshtein distance "did you mean" message.
Message.error(s"Unable to find credentials for [${getRequestingPrompt} @ ${host}].")
val configuredRealms = IvyCredentialsLookup.realmsForHost.getOrElse(host, Set.empty)
if(!configuredRealms.isEmpty) {
Message.error(s" Is one of these realms mispelled for host [${host}]:")
configuredRealms foreach { realm =>
Message.error(s" * ${realm}")
}
}
}
// TODO - Maybe we should work on a helpful proxy message...
// TODO - To be more maven friendly, we may want to also try to grab the "first" authentication that shows up for a server and try it.
// or maybe allow that behavior to be configured, since maven users aren't used to realms (which they should be).
// Grabs the authentication that would have been provided had we not been installed...
def originalAuthentication: Option[PasswordAuthentication] = {
Authenticator.setDefault(original.getOrElse(null))
try Option(Authenticator.requestPasswordAuthentication(
getRequestingHost,
getRequestingSite,
getRequestingPort,
getRequestingProtocol,
getRequestingPrompt,
getRequestingScheme))
finally Authenticator.setDefault(this)
}
originalAuthentication.getOrElse(null)
}
/** Returns true if this authentication if for a proxy and not for an HTTP server.
* We want to display different error messages, depending.
*/
private def isProxyAuthentication: Boolean =
getRequestorType == Authenticator.RequestorType.PROXY
}

View File

@ -0,0 +1,63 @@
package sbt
package ivyint
import org.apache.ivy.util.url.CredentialsStore
import collection.JavaConverters._
/** A key used to store credentials in the ivy credentials store. */
private[sbt] sealed trait CredentialKey
/** Represents a key in the ivy credentials store that is only specific to a host. */
private[sbt] case class Host(name: String) extends CredentialKey
/** Represents a key in the ivy credentials store that is keyed to both a host and a "realm". */
private[sbt] case class Realm(host: String, realm: String) extends CredentialKey
/**
* Helper mechanism to improve credential related error messages.
*
* This evil class exposes to us the necessary information to warn on credential failure and offer
* spelling/typo suggestions.
*/
private[sbt] object IvyCredentialsLookup {
/** Helper extractor for Ivy's key-value store of credentials. */
private object KeySplit {
def unapply(key: String): Option[(String,String)] = {
key.indexOf('@') match {
case -1 => None
case n => Some(key.take(n) -> key.drop(n+1))
}
}
}
/** Here we cheat runtime private so we can look in the credentials store.
*
* TODO - Don't bomb at class load time...
*/
private val credKeyringField = {
val tmp = classOf[CredentialsStore].getDeclaredField("KEYRING")
tmp.setAccessible(true)
tmp
}
/** All the keys for credentials in the ivy configuration store. */
def keyringKeys: Set[CredentialKey] = {
val map = credKeyringField.get(null).asInstanceOf[java.util.HashMap[String, Any]]
// make a clone of the set...
(map.keySet.asScala.map {
case KeySplit(realm, host) => Realm(host, realm)
case host => Host(host)
})(collection.breakOut)
}
/**
* A mapping of host -> realms in the ivy credentials store.
*/
def realmsForHost: Map[String, Set[String]] =
keyringKeys collect {
case x: Realm => x
} groupBy { realm =>
realm.host
} mapValues { realms =>
realms map (_.realm)
}
}

View File

@ -0,0 +1,29 @@
package sbt
import java.io.File
import org.specs2._
import mutable.Specification
object MakePomTest extends Specification
{
val mp = new MakePom(ConsoleLogger())
import mp.{makeDependencyVersion=>v}
"MakePom makeDependencyVersion" should {
"Handle .+ in versions" in {
v("1.+") must_== "[1,2)"
v("1.2.3.4.+") must_== "[1.2.3.4,1.2.3.5)"
v("12.31.42.+") must_== "[12.31.42,12.31.43)"
}
/* TODO - do we care about this case?
* 1+ --> [1,2),[10,20),[100,200),[1000,2000),[10000,20000),[100000,200000)
*/
"Handle ]* bracket in version ranges" in {
v("]1,3]") must_== "(1,3]"
v("]1.1,1.3]") must_== "(1.1,1.3]"
}
"Handle *[ bracket in version ranges" in {
v("[1,3[") must_== "[1,3)"
v("[1.1,1.3[") must_== "[1.1,1.3)"
}
}
}

View File

@ -1,6 +1,25 @@
package xsbti;
/**
* The main entry interface for launching applications. Classes which implement this interface
* can be launched via the sbt launcher.
*
* In addition, classes can be adapted into this interface by the launcher if they have a static method
* matching one of these signatures:
*
* - public static void main(String[] args)
* - public static int main(String[] args)
* - public static xsbti.Exit main(String[] args)
*
*/
public interface AppMain
{
/** Run the application and return the result.
*
* @param configuration The configuration used to run the application. Includes arguments and access to launcher features.
* @return
* The result of running this app.
* Note: the result can be things like "Please reboot this application".
*/
public MainResult run(AppConfiguration configuration);
}

View File

@ -3,9 +3,10 @@ package xsbti;
import java.io.File;
/**
* This represents an interface that can generate applications.
* This represents an interface that can generate applications or servers.
*
* An application is somethign which will run and return an exit value.
* This provider grants access to launcher related features associated with
* the id.
*/
public interface AppProvider
{
@ -33,6 +34,8 @@ public interface AppProvider
* It is NOT guaranteed that newMain().getClass() == mainClass().
* The sbt launcher can wrap generic static main methods. In this case, there will be a wrapper class,
* and you must use the `entryPoint` method.
* @throws IncompatibleClassChangeError if the configuration used for this Application does not
* represent a launched application.
*/
public AppMain newMain();

View File

@ -1,7 +1,9 @@
package xsbti;
/** A launched application returns an instance of this class in order to communicate to the launcher
* that the application is completely finished and the launcher should exit with the given exit code.*/
/**
* A launched application returns an instance of this class in order to communicate to the launcher
* that the application finished and the launcher should exit with the given exit code.
*/
public interface Exit extends MainResult
{
public int code();

View File

@ -1,8 +1,12 @@
package xsbti;
/** A launched application should return an instance of this from its 'run' method
* to communicate to the launcher what should be done now that the application
* has competed. This interface should be treated as 'sealed', with Exit and Reboot the only
* direct subtypes.
*/
/**
* A launched application should return an instance of this from its 'run' method
* to communicate to the launcher what should be done now that the application
* has completed. This interface should be treated as 'sealed', with Exit and Reboot the only
* direct subtypes.
*
* @see xsbti.Exit
* @see xsbti.Reboot
*/
public interface MainResult {}

View File

@ -2,9 +2,11 @@ package xsbti;
import java.io.File;
/** A launched application returns an instance of this class in order to communicate to the launcher
* that the application should be restarted. Different versions of the application and Scala can be used.
* The application can be given different arguments and a new working directory as well.*/
/**
* A launched application returns an instance of this class in order to communicate to the launcher
* that the application should be restarted. Different versions of the application and Scala can be used.
* The application can be given different arguments as well as a new working directory.
*/
public interface Reboot extends MainResult
{
public String[] arguments();

View File

@ -0,0 +1,36 @@
package xsbti;
/** A running server.
*
* A class implementing this must:
*
* 1. Expose an HTTP port that clients can connect to, returned via the uri method.
* 2. Accept HTTP HEAD requests against the returned URI. These are used as "ping" messages to ensure
* a server is still alive, when new clients connect.
* 3. Create a new thread to execute its service
* 4. Block the calling thread until the server is shutdown via awaitTermination()
*/
public interface Server {
/**
* @return
* A URI denoting the Port which clients can connect to.
*
* Note: we use a URI so that the server can bind to different IP addresses (even a public one) if desired.
* Note: To verify that a server is "up", the sbt launcher will attempt to connect to
* this URI's address and port with a socket. If the connection is accepted, the server is assumed to
* be working.
*/
public java.net.URI uri();
/**
* This should block the calling thread until the server is shutdown.
*
* @return
* The result that should occur from the server.
* Can be:
* - xsbti.Exit: Shutdown this launch
* - xsbti.Reboot: Restart the server
*
*
*/
public xsbti.MainResult awaitTermination();
}

View File

@ -0,0 +1,17 @@
package xsbti;
/** The main entry point for a launched service. This allows applciations
* to instantiate server instances.
*/
public interface ServerMain {
/**
* This method should launch one or more thread(s) which run the service. After the service has
* been started, it should return the port/URI it is listening for connections on.
*
* @param configuration
* The configuration used to launch this service.
* @return
* A running server.
*/
public Server start(AppConfiguration configuration);
}

View File

@ -5,36 +5,48 @@
import java.io.File
// The entry point to the launcher
object Boot
{
def main(args: Array[String])
{
args match {
case Array("--version") =>
println("sbt launcher version " + Package.getPackage("xsbt.boot").getImplementationVersion)
case _ =>
System.clearProperty("scala.home") // avoid errors from mixing Scala versions in the same JVM
System.setProperty("jline.shutdownhook", "false") // shutdown hooks cause class loader leaks
System.setProperty("jline.esc.timeout", "0") // starts up a thread otherwise
CheckProxy()
run(args)
}
val config = parseArgs(args)
// If we havne't exited, we set up some hooks and launch
System.clearProperty("scala.home") // avoid errors from mixing Scala versions in the same JVM
System.setProperty("jline.shutdownhook", "false") // shutdown hooks cause class loader leaks
System.setProperty("jline.esc.timeout", "0") // starts up a thread otherwise
CheckProxy()
run(config)
}
def parseArgs(args: Array[String]): LauncherArguments = {
@annotation.tailrec
def parse(args: List[String], isLocate: Boolean, remaining: List[String]): LauncherArguments =
args match {
case "--version" :: rest =>
println("sbt launcher version " + Package.getPackage("xsbt.boot").getImplementationVersion)
exit(1)
case "--locate" :: rest => parse(rest, true, remaining)
case next :: rest => parse(rest, isLocate, next :: remaining)
case Nil => new LauncherArguments(remaining.reverse, isLocate)
}
parse(args.toList, false, Nil)
}
// this arrangement is because Scala does not always properly optimize away
// the tail recursion in a catch statement
final def run(args: Array[String]): Unit = runImpl(args) match {
final def run(args: LauncherArguments): Unit = runImpl(args) match {
case Some(newArgs) => run(newArgs)
case None => ()
}
private def runImpl(args: Array[String]): Option[Array[String]] =
private def runImpl(args: LauncherArguments): Option[LauncherArguments] =
try
Launch(args.toList) map exit
Launch(args) map exit
catch
{
case b: BootException => errorAndExit(b.toString)
case r: xsbti.RetrieveException => errorAndExit("Error: " + r.getMessage)
case r: xsbti.FullReload => Some(r.arguments)
case r: xsbti.FullReload => Some(new LauncherArguments(r.arguments.toList, false))
case e: Throwable =>
e.printStackTrace
errorAndExit(Pre.prefixError(e.toString))

View File

@ -10,21 +10,34 @@ import java.util.regex.Pattern
import scala.collection.immutable.List
import annotation.tailrec
object ConfigurationStorageState extends Enumeration {
val PropertiesFile = value("properties-file")
val SerializedFile = value("serialized-file")
}
object Configuration
{
import ConfigurationStorageState._
final val SysPropPrefix = "-D"
def parse(file: URL, baseDirectory: File) = Using( new InputStreamReader(file.openStream, "utf8") )( (new ConfigurationParser).apply )
@tailrec def find(args: List[String], baseDirectory: File): (URL, List[String]) =
/**
* Finds the configuration location.
*
* Note: Configuration may be previously serialized by a launcher.
*/
@tailrec def find(args: List[String], baseDirectory: File): (URL, List[String], ConfigurationStorageState.Value) =
args match
{
case head :: tail if head.startsWith("@") => (directConfiguration(head.substring(1), baseDirectory), tail)
case head :: tail if head.startsWith("@load:") => (directConfiguration(head.substring(6), baseDirectory), tail, SerializedFile)
case head :: tail if head.startsWith("@") => (directConfiguration(head.substring(1), baseDirectory), tail, PropertiesFile)
case head :: tail if head.startsWith(SysPropPrefix) =>
setProperty(head stripPrefix SysPropPrefix)
find(tail, baseDirectory)
case _ =>
val propertyConfigured = System.getProperty("sbt.boot.properties")
val url = if(propertyConfigured == null) configurationOnClasspath else configurationFromFile(propertyConfigured, baseDirectory)
(url , args)
(url, args, PropertiesFile)
}
def setProperty(head: String)
{
@ -108,7 +121,7 @@ object Configuration
// We have to hard code them here in order to use them to determine the location of sbt.boot.properties itself
def guessSbtVersion: Option[String] =
{
val props = ResolveValues.readProperties(new File(DefaultBuildProperties))
val props = Pre.readProperties(new File(DefaultBuildProperties))
Option(props.getProperty(SbtVersionProperty))
}

View File

@ -78,11 +78,14 @@ class ConfigurationParser
val (logging, m5) = processSection(m4, "log", getLogging)
val (properties, m6) = processSection(m5, "app-properties", getAppProperties)
val ((ivyHome, checksums, isOverrideRepos, rConfigFile), m7) = processSection(m6, "ivy", getIvy)
check(m7, "section")
val (serverOptions, m8) = processSection(m7, "server", getServer)
check(m8, "section")
val classifiers = Classifiers(scalaClassifiers, appClassifiers)
val repositories = rConfigFile map readRepositoriesConfig getOrElse defaultRepositories
val ivyOptions = IvyOptions(ivyHome, classifiers, repositories, checksums, isOverrideRepos)
new LaunchConfiguration(scalaVersion, ivyOptions, app, boot, logging, properties)
// TODO - Read server properties...
new LaunchConfiguration(scalaVersion, ivyOptions, app, boot, logging, properties, serverOptions)
}
def getScala(m: LabelMap) =
{
@ -178,6 +181,16 @@ class ConfigurationParser
val app = new Application(org, name, rev, main, components, LaunchCrossVersion(crossVersioned), classpathExtra)
(app, classifiers)
}
def getServer(m: LabelMap): (Option[ServerConfiguration]) =
{
val (lock, m1) = optfile(m, "lock")
// TODO - JVM args
val (args, m2) = optfile(m1, "jvmargs")
val (props, m3) = optfile(m2, "jvmprops")
lock map { file =>
ServerConfiguration(file, args, props)
}
}
def getRepositories(m: LabelMap): List[Repository.Repository] =
{
import Repository.{Ivy, Maven, Predefined}

View File

@ -33,17 +33,11 @@ object Initialize
def fill(file: File, spec: List[AppProperty]): Unit = process(file, spec, selectFill)
def process(file: File, appProperties: List[AppProperty], select: AppProperty => Option[PropertyInit])
{
val properties = new Properties
if(file.exists)
Using(new FileInputStream(file))( properties.load )
val properties = readProperties(file)
val uninitialized =
for(property <- appProperties; init <- select(property) if properties.getProperty(property.name) == null) yield
initialize(properties, property.name, init)
if(!uninitialized.isEmpty)
{
file.getParentFile.mkdirs()
Using(new FileOutputStream(file))( out => properties.store(out, "") )
}
if(!uninitialized.isEmpty) writeProperties(properties, file, "")
}
def initialize(properties: Properties, name: String, init: PropertyInit)
{

View File

@ -6,7 +6,7 @@ package xsbt.boot
import Pre._
import scala.collection.immutable.List
class Enumeration
class Enumeration extends Serializable
{
def elements: List[Value] = members
private lazy val members: List[Value] =
@ -25,6 +25,6 @@ class Enumeration
}
def value(s: String) = new Value(s, 0)
def value(s: String, i: Int) = new Value(s, i)
final class Value(override val toString: String, val id: Int)
final class Value(override val toString: String, val id: Int) extends Serializable
def toValue(s: String): Value = elements.find(_.toString == s).getOrElse(error("Expected one of " + elements.mkString(",") + " (got: " + s + ")"))
}

View File

@ -6,20 +6,64 @@ package xsbt.boot
import Pre._
import BootConfiguration.{CompilerModuleName, JAnsiVersion, LibraryModuleName}
import java.io.File
import java.net.{URL, URLClassLoader}
import java.net.{URL, URLClassLoader, URI}
import java.util.concurrent.Callable
import scala.collection.immutable.List
import scala.annotation.tailrec
import ConfigurationStorageState._
class LauncherArguments(val args: List[String], val isLocate: Boolean)
object Launch
{
def apply(arguments: List[String]): Option[Int] = apply( (new File("")).getAbsoluteFile , arguments )
def apply(arguments: LauncherArguments): Option[Int] = apply( (new File("")).getAbsoluteFile , arguments )
def apply(currentDirectory: File, arguments: List[String]): Option[Int] = {
val (configLocation, newArguments) = Configuration.find(arguments, currentDirectory)
val config = parseAndInitializeConfig(configLocation, currentDirectory)
launch(run(Launcher(config)))(makeRunConfig(currentDirectory, config, newArguments))
def apply(currentDirectory: File, arguments: LauncherArguments): Option[Int] = {
val (configLocation, newArgs2, state) = Configuration.find(arguments.args, currentDirectory)
val config = state match {
case SerializedFile => LaunchConfiguration.restore(configLocation)
case PropertiesFile => parseAndInitializeConfig(configLocation, currentDirectory)
}
if(arguments.isLocate) {
if(!newArgs2.isEmpty) {
// TODO - Print the arguments without exploding proguard size.
System.err.println("Warning: --locate option ignores arguments.")
}
locate(currentDirectory, config)
} else {
// First check to see if there are java system properties we need to set. Then launch the application.
updateProperties(config)
launch(run(Launcher(config)))(makeRunConfig(currentDirectory, config, newArgs2))
}
}
/** Locate a server, print where it is, and exit. */
def locate(currentDirectory: File, config: LaunchConfiguration): Option[Int] = {
config.serverConfig match {
case Some(_) =>
val uri = ServerLocator.locate(currentDirectory, config)
System.out.println(uri.toASCIIString)
Some(0)
case None => sys.error(s"${config.app.groupID}-${config.app.main} is not configured as a server.")
}
}
/** Some hackery to allow sys.props to be configured via a file. If this launch config has
* a valid file configured, we load the properties and and apply them to this jvm.
*/
def updateProperties(config: LaunchConfiguration): Unit = {
config.serverConfig match {
case Some(config) =>
config.jvmPropsFile match {
case Some(file) if file.exists =>
try setSystemProperties(readProperties(file))
catch {
case e: Exception => throw new RuntimeException(s"Unable to load server properties file: ${file}", e)
}
case _ =>
}
case None =>
}
}
/** Parses the configuration *and* runs the initialization code that will remove variable references. */
def parseAndInitializeConfig(configLocation: URL, currentDirectory: File): LaunchConfiguration =
{
@ -84,6 +128,10 @@ object Launch
Thread.currentThread.setContextClassLoader(loader)
try { eval } finally { Thread.currentThread.setContextClassLoader(oldLoader) }
}
// Cache of classes for lookup later.
val ServerMainClass = classOf[xsbti.ServerMain]
val AppMainClass = classOf[xsbti.AppMain]
}
final class RunConfiguration(val scalaVersion: Option[String], val app: xsbti.ApplicationID, val workingDirectory: File, val arguments: List[String])
@ -152,28 +200,34 @@ class Launch private[xsbt](val bootDirectory: File, val lockBoot: Boolean, val i
@tailrec private[this] final def getAppProvider0(id: xsbti.ApplicationID, explicitScalaVersion: Option[String], forceAppUpdate: Boolean): xsbti.AppProvider =
{
val app = appModule(id, explicitScalaVersion, true, "app")
val baseDirs = (base: File) => appBaseDirs(base, id)
/** Replace the version of an ApplicationID with the given one, if set. */
def resolveId(appVersion: Option[String], id: xsbti.ApplicationID) = appVersion map { v =>
import id._
AppID(groupID(), name(), v, mainClass(), mainComponents(), crossVersionedValue(), classpathExtra())
} getOrElse id
val baseDirs = (resolvedVersion: Option[String]) => (base: File) => appBaseDirs(base, resolveId(resolvedVersion, id))
def retrieve() = {
val sv = update(app, "")
val (appv, sv) = update(app, "")
val scalaVersion = strictOr(explicitScalaVersion, sv)
new RetrievedModule(true, app, sv, baseDirs(scalaHome(ScalaOrg, scalaVersion)))
new RetrievedModule(true, app, sv, appv, baseDirs(appv)(scalaHome(ScalaOrg, scalaVersion)))
}
val retrievedApp =
if(forceAppUpdate)
retrieve()
else
existing(app, ScalaOrg, explicitScalaVersion, baseDirs) getOrElse retrieve()
existing(app, ScalaOrg, explicitScalaVersion, baseDirs(None)) getOrElse retrieve()
val scalaVersion = getOrError(strictOr(explicitScalaVersion, retrievedApp.detectedScalaVersion), "No Scala version specified or detected")
val scalaProvider = getScala(scalaVersion, "(for " + id.name + ")")
val resolvedId = resolveId(retrievedApp.resolvedAppVersion, id)
val (missing, appProvider) = checkedAppProvider(id, retrievedApp, scalaProvider)
val (missing, appProvider) = checkedAppProvider(resolvedId, retrievedApp, scalaProvider)
if(missing.isEmpty)
appProvider
else if(retrievedApp.fresh)
app.retrieveCorrupt(missing)
else
getAppProvider0(id, explicitScalaVersion, true)
getAppProvider0(resolvedId, explicitScalaVersion, true)
}
def scalaHome(scalaOrg: String, scalaVersion: Option[String]): File = new File(bootDirectory, baseDirectoryName(scalaOrg, scalaVersion))
def appHome(id: xsbti.ApplicationID, scalaVersion: Option[String]): File = appDirectory(scalaHome(ScalaOrg, scalaVersion), id)
@ -200,7 +254,7 @@ class Launch private[xsbt](val bootDirectory: File, val lockBoot: Boolean, val i
try Some(provider(mod))
catch { case e: Exception => None }
} getOrElse {
val scalaVersion = update(scalaM, reason)
val (_, scalaVersion) = update(scalaM, reason)
provider( new RetrievedModule(true, scalaM, scalaVersion, baseDirs) )
}
}
@ -240,27 +294,32 @@ class Launch private[xsbt](val bootDirectory: File, val lockBoot: Boolean, val i
(scalaHome, libDirectory)
}
def appProvider(appID: xsbti.ApplicationID, app: RetrievedModule, scalaProvider0: xsbti.ScalaProvider, appHome: File): xsbti.AppProvider = new xsbti.AppProvider
{
def appProvider(appID: xsbti.ApplicationID, app: RetrievedModule, scalaProvider0: xsbti.ScalaProvider, appHome: File): xsbti.AppProvider =
new xsbti.AppProvider {
import Launch.{ServerMainClass,AppMainClass}
val scalaProvider = scalaProvider0
val id = appID
def mainClasspath = app.fullClasspath
lazy val loader = app.createLoader(scalaProvider.loader)
// TODO - For some reason we can't call this from vanilla scala. We get a
// no such method exception UNLESS we're in the same project.
lazy val entryPoint: Class[T] forSome { type T } =
{
val c = Class.forName(id.mainClass, true, loader)
if(classOf[xsbti.AppMain].isAssignableFrom(c)) c
else if(PlainApplication.isPlainApplication(c)) c
else sys.error(s"Class: ${c} is not an instance of xsbti.AppMain nor does it have one of these static methods:\n"+
" * void main(String[] args)\n * int main(String[] args)\n * xsbti.Exit main(String[] args)")
else if(ServerApplication.isServerApplication(c)) c
else sys.error(s"${c} is not an instance of xsbti.AppMain, xsbti.ServerMain nor does it have one of these static methods:\n"+
" * void main(String[] args)\n * int main(String[] args)\n * xsbti.Exit main(String[] args)\n")
}
// Deprecated API. Remove when we can.
def mainClass: Class[T] forSome { type T <: xsbti.AppMain } = entryPoint.asSubclass(classOf[xsbti.AppMain])
def mainClass: Class[T] forSome { type T <: xsbti.AppMain } = entryPoint.asSubclass(AppMainClass)
def newMain(): xsbti.AppMain = {
if(PlainApplication.isPlainApplication(entryPoint)) PlainApplication(entryPoint)
else mainClass.newInstance
if(ServerApplication.isServerApplication(entryPoint)) ServerApplication(this)
else if(PlainApplication.isPlainApplication(entryPoint)) PlainApplication(entryPoint)
else if(AppMainClass.isAssignableFrom(entryPoint)) mainClass.newInstance
else throw new IncompatibleClassChangeError(s"Main class ${entryPoint.getName} is not an instance of xsbti.AppMain, xsbti.ServerMain nor does it have a valid `main` method.")
}
lazy val components = componentProvider(appHome)
}
def componentProvider(appHome: File) = new ComponentProvider(appHome, lockBoot)
@ -290,10 +349,11 @@ class Launch private[xsbt](val bootDirectory: File, val lockBoot: Boolean, val i
failLabel = "Scala " + version,
extraClasspath = array()
)
def update(mm: ModuleDefinition, reason: String): Option[String] =
/** Returns the resolved appVersion (if this was an App), as well as the scalaVersion. */
def update(mm: ModuleDefinition, reason: String): (Option[String], Option[String]) =
{
val result = ( new Update(mm.configuration) )(mm.target, reason)
if(result.success) result.scalaVersion else mm.retrieveFailed
if(result.success) result.appVersion -> result.scalaVersion else mm.retrieveFailed
}
}
object Launcher

View File

@ -9,27 +9,46 @@ import java.net.URL
import scala.collection.immutable.List
//TODO: use copy constructor, check size change
final case class LaunchConfiguration(scalaVersion: Value[String], ivyConfiguration: IvyOptions, app: Application, boot: BootSetup, logging: Logging, appProperties: List[AppProperty])
final case class LaunchConfiguration(scalaVersion: Value[String], ivyConfiguration: IvyOptions, app: Application, boot: BootSetup, logging: Logging, appProperties: List[AppProperty], serverConfig: Option[ServerConfiguration])
{
def isServer: Boolean = serverConfig.isDefined
def getScalaVersion = {
val sv = Value.get(scalaVersion)
if(sv == "auto") None else Some(sv)
}
def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration, app, boot, logging, appProperties)
def withApp(app: Application) = LaunchConfiguration(scalaVersion, ivyConfiguration, app, boot, logging, appProperties)
def withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, ivyConfiguration, app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties)
def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration, app, boot, logging, appProperties, serverConfig)
def withApp(app: Application) = LaunchConfiguration(scalaVersion, ivyConfiguration, app, boot, logging, appProperties, serverConfig)
def withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, ivyConfiguration, app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties, serverConfig)
// TODO: withExplicit
def withVersions(newScalaVersion: String, newAppVersion: String, classifiers0: Classifiers) =
LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration.copy(classifiers = classifiers0), app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties)
LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration.copy(classifiers = classifiers0), app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties, serverConfig)
def map(f: File => File) = LaunchConfiguration(scalaVersion, ivyConfiguration.map(f), app.map(f), boot.map(f), logging, appProperties)
def map(f: File => File) = LaunchConfiguration(scalaVersion, ivyConfiguration.map(f), app.map(f), boot.map(f), logging, appProperties, serverConfig.map(_ map f))
}
object LaunchConfiguration {
// Saves a launch configuration into a file. This is only safe if it is loaded by the *same* launcher version.
def save(config: LaunchConfiguration, f: File): Unit = {
val out = new java.io.ObjectOutputStream(new java.io.FileOutputStream(f))
try out.writeObject(config)
finally out.close()
}
// Restores a launch configuration from a file. This is only safe if it is loaded by the *same* launcher version.
def restore(url: URL): LaunchConfiguration = {
val in = new java.io.ObjectInputStream(url.openConnection.getInputStream)
try in.readObject.asInstanceOf[LaunchConfiguration]
finally in.close()
}
}
final case class ServerConfiguration(lockFile: File, jvmArgs: Option[File], jvmPropsFile: Option[File]) {
def map(f: File => File) =
ServerConfiguration(f(lockFile), jvmArgs map f, jvmPropsFile map f)
}
final case class IvyOptions(ivyHome: Option[File], classifiers: Classifiers, repositories: List[Repository.Repository], checksums: List[String], isOverrideRepositories: Boolean)
{
def map(f: File => File) = IvyOptions(ivyHome.map(f), classifiers, repositories, checksums, isOverrideRepositories)
}
sealed trait Value[T]
sealed trait Value[T] extends Serializable
final class Explicit[T](val value: T) extends Value[T] {
override def toString = value.toString
}
@ -130,7 +149,7 @@ sealed trait PropertyInit
final class SetProperty(val value: String) extends PropertyInit
final class PromptProperty(val label: String, val default: Option[String]) extends PropertyInit
final class Logging(level: LogLevel.Value)
final class Logging(level: LogLevel.Value) extends Serializable
{
def log(s: => String, at: LogLevel.Value) = if(level.id <= at.id) stream(at).println("[" + at + "] " + s)
def debug(s: => String) = log(s, LogLevel.Debug)

View File

@ -13,8 +13,12 @@ final class ModuleDefinition(val configuration: UpdateConfiguration, val extraCl
private def versionString: String = target match { case _: UpdateScala => configuration.getScalaVersion; case a: UpdateApp => Value.get(a.id.version) }
}
final class RetrievedModule(val fresh: Boolean, val definition: ModuleDefinition, val detectedScalaVersion: Option[String], val baseDirectories: List[File])
final class RetrievedModule(val fresh: Boolean, val definition: ModuleDefinition, val detectedScalaVersion: Option[String], val resolvedAppVersion: Option[String], val baseDirectories: List[File])
{
/** Use this constructor only when the module exists already, or when its version is not dynamic (so its resolved version would be the same) */
def this(fresh: Boolean, definition: ModuleDefinition, detectedScalaVersion: Option[String], baseDirectories: List[File]) =
this(fresh, definition, detectedScalaVersion, None, baseDirectories)
lazy val classpath: Array[File] = getJars(baseDirectories)
lazy val fullClasspath: Array[File] = concat(classpath, definition.extraClasspath)

View File

@ -70,6 +70,10 @@ object Pre
classes.toList.filter(classMissing)
}
def toURLs(files: Array[File]): Array[URL] = files.map(_.toURI.toURL)
def toFile(url: URL): File =
try { new File(url.toURI) }
catch { case _: java.net.URISyntaxException => new File(url.getPath) }
def delete(f: File)
{
@ -82,4 +86,25 @@ object Pre
}
final val isWindows: Boolean = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows")
final val isCygwin: Boolean = isWindows && java.lang.Boolean.getBoolean("sbt.cygwin")
import java.util.Properties
import java.io.{FileInputStream,FileOutputStream}
private[boot] def readProperties(propertiesFile: File) =
{
val properties = new Properties
if(propertiesFile.exists)
Using( new FileInputStream(propertiesFile) )( properties.load )
properties
}
private[boot] def writeProperties(properties: Properties, file: File, msg: String): Unit = {
file.getParentFile.mkdirs()
Using(new FileOutputStream(file))( out => properties.store(out, msg) )
}
private[boot] def setSystemProperties(properties: Properties): Unit = {
val nameItr = properties.stringPropertyNames.iterator
while(nameItr.hasNext) {
val propName = nameItr.next
System.setProperty(propName, properties.getProperty(propName))
}
}
}

View File

@ -12,16 +12,9 @@ object ResolveValues
def apply(conf: LaunchConfiguration): LaunchConfiguration = (new ResolveValues(conf))()
private def trim(s: String) = if(s eq null) None else notEmpty(s.trim)
private def notEmpty(s: String) = if(isEmpty(s)) None else Some(s)
private[boot] def readProperties(propertiesFile: File) =
{
val properties = new Properties
if(propertiesFile.exists)
Using( new FileInputStream(propertiesFile) )( properties.load )
properties
}
}
import ResolveValues.{readProperties, trim}
import ResolveValues.{trim}
final class ResolveValues(conf: LaunchConfiguration)
{
private def propertiesFile = conf.boot.properties

View File

@ -0,0 +1,200 @@
package xsbt
package boot
import java.io.File
import scala.util.control.NonFatal
import java.net.URI
import java.io.IOException
import Pre._
import scala.annotation.tailrec
/** A wrapper around 'raw' static methods to meet the sbt application interface. */
class ServerApplication private (provider: xsbti.AppProvider) extends xsbti.AppMain {
import ServerApplication._
override def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = {
val serverMain = provider.entryPoint.asSubclass(ServerMainClass).newInstance
val server = serverMain.start(configuration)
System.out.println(s"${SERVER_SYNCH_TEXT}${server.uri}")
server.awaitTermination()
}
}
/** An object that lets us detect compatible "plain" applications and launch them reflectively. */
object ServerApplication {
val SERVER_SYNCH_TEXT = "[SERVER-URI]"
val ServerMainClass = classOf[xsbti.ServerMain]
// TODO - We should also adapt friendly static methods into servers, perhaps...
// We could even structurally type things that have a uri + awaitTermination method...
def isServerApplication(clazz: Class[_]): Boolean =
ServerMainClass.isAssignableFrom(clazz)
def apply(provider: xsbti.AppProvider): xsbti.AppMain =
new ServerApplication(provider)
}
object ServerLocator {
// TODO - Probably want to drop this to reduce classfile size
private def locked[U](file: File)(f: => U): U = {
Locks(file, new java.util.concurrent.Callable[U] {
def call(): U = f
})
}
// We use the lock file they give us to write the server info. However,
// it seems we cannot both use the server info file for locking *and*
// read from it successfully. Locking seems to blank the file. SO, we create
// another file near the info file to lock.a
def makeLockFile(f: File): File =
new File(f.getParentFile, s"${f.getName}.lock")
// Launch the process and read the port...
def locate(currentDirectory: File, config: LaunchConfiguration): URI =
config.serverConfig match {
case None => sys.error("No server lock file configured. Cannot locate server.")
case Some(sc) => locked(makeLockFile(sc.lockFile)) {
readProperties(sc.lockFile) match {
case Some(uri) if isReachable(uri) => uri
case _ =>
val uri = ServerLauncher.startServer(currentDirectory, config)
writeProperties(sc.lockFile, uri)
uri
}
}
}
private val SERVER_URI_PROPERTY = "server.uri"
def readProperties(f: File): Option[java.net.URI] = {
try {
val props = Pre.readProperties(f)
props.getProperty(SERVER_URI_PROPERTY) match {
case null => None
case uri => Some(new java.net.URI(uri))
}
} catch {
case e: IOException => None
}
}
def writeProperties(f: File, uri: URI): Unit = {
val props = new java.util.Properties
props.setProperty(SERVER_URI_PROPERTY, uri.toASCIIString)
val output = new java.io.FileOutputStream(f)
val df = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ")
df.setTimeZone(java.util.TimeZone.getTimeZone("UTC"))
Pre.writeProperties(props, f, s"Server Startup at ${df.format(new java.util.Date)}")
}
def isReachable(uri: java.net.URI): Boolean =
try {
// TODO - For now we assume if we can connect, it means
// that the server is working...
val socket = new java.net.Socket(uri.getHost, uri.getPort)
try socket.isConnected
finally socket.close()
} catch {
case e: IOException => false
}
}
/** A helper class that dumps incoming values into a print stream. */
class StreamDumper(in: java.io.BufferedReader, out: java.io.PrintStream) extends Thread {
// Don't block the application for this thread.
setDaemon(true)
private val running = new java.util.concurrent.atomic.AtomicBoolean(true)
override def run(): Unit = {
def read(): Unit = if(running.get) in.readLine match {
case null => ()
case line =>
out.println(line)
read()
}
read()
out.close()
}
def close(): Unit = running.set(false)
}
object ServerLauncher {
import ServerApplication.SERVER_SYNCH_TEXT
def startServer(currentDirectory: File, config: LaunchConfiguration): URI = {
val serverConfig = config.serverConfig match {
case Some(c) => c
case None => throw new RuntimeException("Logic Failure: Attempting to start a server that isn't configured to be a server. Please report a bug.")
}
val launchConfig = java.io.File.createTempFile("sbtlaunch", "config")
launchConfig.deleteOnExit()
LaunchConfiguration.save(config, launchConfig)
val jvmArgs: List[String] = serverConfig.jvmArgs map readLines match {
case Some(args) => args
case None => Nil
}
val cmd: List[String] =
("java" :: jvmArgs) ++
("-jar" :: defaultLauncherLookup.getCanonicalPath :: s"@load:${launchConfig.toURI.toURL.toString}" :: Nil)
launchProcessAndGetUri(cmd, currentDirectory)
}
// Here we try to isolate all the stupidity of dealing with Java processes.
def launchProcessAndGetUri(cmd: List[String], cwd: File): URI = {
// TODO - Handle windows path stupidity in arguments.
val pb = new java.lang.ProcessBuilder()
pb.command(cmd:_*)
pb.directory(cwd)
val process = pb.start()
// First we need to grab all the input streams, and close the ones we don't care about.
process.getOutputStream.close()
val stderr = process.getErrorStream
val stdout = process.getInputStream
// Now we start dumping out errors.
val errorDumper = new StreamDumper(new java.io.BufferedReader(new java.io.InputStreamReader(stderr)), System.err)
errorDumper.start()
// Now we look for the URI synch value, and then make sure we close the output files.
try readUntilSynch(new java.io.BufferedReader(new java.io.InputStreamReader(stdout))) match {
case Some(uri) => uri
case _ => sys.error("Failed to start server!")
} finally {
errorDumper.close()
stdout.close()
stderr.close()
}
}
object ServerUriLine {
def unapply(in: String): Option[URI] =
if(in startsWith SERVER_SYNCH_TEXT) {
Some(new URI(in.substring(SERVER_SYNCH_TEXT.size)))
} else None
}
/** Reads an input steam until it hits the server synch text and server URI. */
def readUntilSynch(in: java.io.BufferedReader): Option[URI] = {
@tailrec
def read(): Option[URI] = in.readLine match {
case null => None
case ServerUriLine(uri) => Some(uri)
case line => read()
}
try read()
finally in.close()
}
/** Reads all the lines in a file. If it doesn't exist, returns an empty list. Forces UTF-8 strings. */
def readLines(f: File): List[String] =
if(!f.exists) Nil else {
val reader = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(f), "UTF-8"))
@tailrec
def read(current: List[String]): List[String] =
reader.readLine match {
case null => current.reverse
case line => read(line :: current)
}
try read(Nil)
finally reader.close()
}
def defaultLauncherLookup: File =
try {
val classInLauncher = classOf[AppConfiguration]
val fileOpt = for {
domain <- Option(classInLauncher.getProtectionDomain)
source <- Option(domain.getCodeSource)
location = source.getLocation
} yield toFile(location)
fileOpt.getOrElse(throw new RuntimeException("Could not inspect protection domain or code source"))
} catch {
case e: Throwable => throw new RuntimeException("Unable to find sbt-launch.jar.", e)
}
}

View File

@ -39,7 +39,10 @@ final class UpdateConfiguration(val bootDirectory: File, val ivyHome: Option[Fil
def getScalaVersion = scalaVersion match { case Some(sv) => sv; case None => "" }
}
final class UpdateResult(val success: Boolean, val scalaVersion: Option[String])
final class UpdateResult(val success: Boolean, val scalaVersion: Option[String], val appVersion: Option[String]) {
@deprecated("0.13.2", "Please use the other constructor providing appVersion.")
def this(success: Boolean, scalaVersion: Option[String]) = this(success, scalaVersion, None)
}
/** Ensures that the Scala and application jars exist for the given versions or else downloads them.*/
final class Update(config: UpdateConfiguration)
@ -55,7 +58,7 @@ final class Update(config: UpdateConfiguration)
val optionProps =
Option(System.getProperty("sbt.boot.credentials")) orElse
Option(System.getenv("SBT_CREDENTIALS")) map ( path =>
ResolveValues.readProperties(new File(path))
Pre.readProperties(new File(path))
)
optionProps match {
case Some(props) => extractCredentials("realm","host","user","password")(props)
@ -109,7 +112,7 @@ final class Update(config: UpdateConfiguration)
e.printStackTrace(logWriter)
log(e.toString)
System.out.println(" (see " + logFile + " for complete log)")
new UpdateResult(false, None)
new UpdateResult(false, None, None)
}
finally
{
@ -127,15 +130,16 @@ final class Update(config: UpdateConfiguration)
moduleID.setLastModified(System.currentTimeMillis)
moduleID.addConfiguration(new IvyConfiguration(DefaultIvyConfiguration, PUBLIC, "", new Array(0), true, null))
// add dependencies based on which target needs updating
target match
val dep = target match
{
case u: UpdateScala =>
val scalaVersion = getScalaVersion
addDependency(moduleID, scalaOrg, CompilerModuleName, scalaVersion, "default;optional(default)", u.classifiers)
addDependency(moduleID, scalaOrg, LibraryModuleName, scalaVersion, "default", u.classifiers)
val ddesc = addDependency(moduleID, scalaOrg, LibraryModuleName, scalaVersion, "default", u.classifiers)
excludeJUnit(moduleID)
val scalaOrgString = if (scalaOrg != ScalaOrg) " " + scalaOrg else ""
System.out.println("Getting" + scalaOrgString + " Scala " + scalaVersion + " " + reason + "...")
ddesc.getDependencyId
case u: UpdateApp =>
val app = u.id
val resolvedName = (app.crossVersioned, scalaVersion) match {
@ -143,24 +147,31 @@ final class Update(config: UpdateConfiguration)
case (xsbti.CrossValue.Binary, Some(sv)) => app.name + "_" + CrossVersionUtil.binaryScalaVersion(sv)
case _ => app.name
}
addDependency(moduleID, app.groupID, resolvedName, app.getVersion, "default(compile)", u.classifiers)
val ddesc = addDependency(moduleID, app.groupID, resolvedName, app.getVersion, "default(compile)", u.classifiers)
System.out.println("Getting " + app.groupID + " " + resolvedName + " " + app.getVersion + " " + reason + "...")
ddesc.getDependencyId
}
update(moduleID, target)
update(moduleID, target, dep)
}
/** Runs the resolve and retrieve for the given moduleID, which has had its dependencies added already. */
private def update(moduleID: DefaultModuleDescriptor, target: UpdateTarget): UpdateResult =
private def update(moduleID: DefaultModuleDescriptor, target: UpdateTarget, dep: ModuleId): UpdateResult =
{
val eventManager = new EventManager
val autoScalaVersion = resolve(eventManager, moduleID)
val (autoScalaVersion, depVersion) = resolve(eventManager, moduleID, dep)
// Fix up target.id with the depVersion that we know for sure is resolved (not dynamic) -- this way, `retrieve`
// will put them in the right version directory.
val target1 = (depVersion, target) match {
case (Some(dv), u: UpdateApp) => import u._; new UpdateApp(id.copy(version = new Explicit(dv)), classifiers, tpe)
case _ => target
}
setScalaVariable(settings, autoScalaVersion)
retrieve(eventManager, moduleID, target, autoScalaVersion)
new UpdateResult(true, autoScalaVersion)
retrieve(eventManager, moduleID, target1, autoScalaVersion)
new UpdateResult(true, autoScalaVersion, depVersion)
}
private def createID(organization: String, name: String, revision: String) =
ModuleRevisionId.newInstance(organization, name, revision)
/** Adds the given dependency to the default configuration of 'moduleID'. */
private def addDependency(moduleID: DefaultModuleDescriptor, organization: String, name: String, revision: String, conf: String, classifiers: List[String])
private def addDependency(moduleID: DefaultModuleDescriptor, organization: String, name: String, revision: String, conf: String, classifiers: List[String]) =
{
val dep = new DefaultDependencyDescriptor(moduleID, createID(organization, name, revision), false, false, true)
for(c <- conf.split(";"))
@ -168,6 +179,7 @@ final class Update(config: UpdateConfiguration)
for(classifier <- classifiers)
addClassifier(dep, name, classifier)
moduleID.addDependency(dep)
dep
}
private def addClassifier(dep: DefaultDependencyDescriptor, name: String, classifier: String)
{
@ -186,8 +198,9 @@ final class Update(config: UpdateConfiguration)
rule.addConfiguration(DefaultIvyConfiguration)
rule
}
// returns the version of any Scala dependency
private def resolve(eventManager: EventManager, module: ModuleDescriptor): Option[String] =
val scalaLibraryId = ModuleId.newInstance(ScalaOrg, LibraryModuleName)
// Returns the version of the scala library, as well as `dep` (a dependency of `module`) after it's been resolved
private def resolve(eventManager: EventManager, module: ModuleDescriptor, dep: ModuleId): (Option[String], Option[String]) =
{
val resolveOptions = new ResolveOptions
// this reduces the substantial logging done by Ivy, including the progress dots when downloading artifacts
@ -203,18 +216,18 @@ final class Update(config: UpdateConfiguration)
System.out.println(seen.toArray.mkString(System.getProperty("line.separator")))
error("Error retrieving required libraries")
}
scalaDependencyVersion(resolveReport).headOption
val modules = moduleRevisionIDs(resolveReport)
extractVersion(modules, scalaLibraryId) -> extractVersion(modules, dep)
}
private[this] def scalaDependencyVersion(report: ResolveReport): List[String] =
private[this] def extractVersion(modules: Seq[ModuleRevisionId], dep: ModuleId): Option[String] =
{
val modules = report.getConfigurations.toList flatMap { config =>
report.getConfigurationReport(config).getModuleRevisionIds.toArray
}
modules flatMap {
case module: ModuleRevisionId if module.getOrganisation == ScalaOrg && module.getName == LibraryModuleName =>
module.getRevision :: Nil
case _ => Nil
}
modules collectFirst { case m if m.getModuleId.equals(dep) => m.getRevision }
}
private[this] def moduleRevisionIDs(report: ResolveReport): Seq[ModuleRevisionId] =
{
import collection.JavaConverters._
import org.apache.ivy.core.resolve.IvyNode
report.getDependencies.asInstanceOf[java.util.List[IvyNode]].asScala map (_.getResolvedId)
}
/** Exceptions are logged to the update log file. */
@ -244,7 +257,8 @@ final class Update(config: UpdateConfiguration)
val filter = (a: IArtifact) => retrieveType(a.getType) && a.getExtraAttribute("classifier") == null && extraFilter(a)
retrieveOptions.setArtifactFilter(new ArtifactFilter(filter))
val scalaV = strictOr(scalaVersion, autoScalaVersion)
retrieveEngine.retrieve(module.getModuleRevisionId, baseDirectoryName(scalaOrg, scalaV) + "/" + pattern, retrieveOptions)
retrieveOptions.setDestArtifactPattern(baseDirectoryName(scalaOrg, scalaV) + "/" + pattern)
retrieveEngine.retrieve(module.getModuleRevisionId, retrieveOptions)
}
private[this] def notCoreScala(a: IArtifact) = a.getName match {
case LibraryModuleName | CompilerModuleName => false

View File

@ -0,0 +1,53 @@
package xsbt.boot
import java.io.{File,InputStream}
import java.net.URL
import java.util.Properties
import xsbti._
import org.specs2._
import mutable.Specification
import LaunchTest._
import sbt.IO.{createDirectory, touch,withTemporaryDirectory}
import java.net.URI
object ServerLocatorTest extends Specification
{
"ServerLocator" should {
// TODO - Maybe use scalacheck to randomnly generate URIs
"read and write server URI properties" in {
withTemporaryDirectory { dir =>
val propFile = new File(dir, "server.properties")
val expected = new java.net.URI("http://localhost:8080")
ServerLocator.writeProperties(propFile, expected)
ServerLocator.readProperties(propFile) must equalTo(Some(expected))
}
}
"detect listening ports" in {
val serverSocket = new java.net.ServerSocket(0)
object serverThread extends Thread {
override def run(): Unit = {
// Accept one connection.
val result = serverSocket.accept()
result.close()
serverSocket.close()
}
}
serverThread.start()
val uri = new java.net.URI(s"http://${serverSocket.getInetAddress.getHostAddress}:${serverSocket.getLocalPort}")
ServerLocator.isReachable(uri) must beTrue
}
}
"ServerLauncher" should {
"detect start URI from reader" in {
val expected = new java.net.URI("http://localhost:8080")
val input = s"""|Some random text
|to start the server
|${ServerApplication.SERVER_SYNCH_TEXT}${expected.toASCIIString}
|Some more output.""".stripMargin
val inputStream = new java.io.BufferedReader(new java.io.StringReader(input))
val result = try ServerLauncher.readUntilSynch(inputStream)
finally inputStream.close()
result must equalTo(Some(expected))
}
}
}

View File

@ -8,14 +8,23 @@ import java.net.URI
object URITests extends Properties("URI Tests")
{
// Need a platform-specific root, otherwise URI will not be absolute (e.g. if we use a "/a/b/c" path in Windows)
// Note:
// If I use "C:" instead of "/C:", then isAbsolute == true for the resulting URI, but resolve is broken:
// e.g. scala> new URI("file", "c:/a/b'/has spaces", null).resolve("a") broken
// res0: java.net.URI = a
// scala> new URI("file", "/c:/a/b'/has spaces", null).resolve("a") working
// res1: java.net.URI = file:/c:/a/b'/a
val Root = if (xsbt.boot.Pre.isWindows) "/C:/" else "/"
val FileProtocol = "file"
property("directoryURI adds trailing slash") = secure {
val dirURI = directoryURI(new File("/a/b/c"))
val directURI = filePathURI("/a/b/c/")
val dirURI = directoryURI(new File(Root + "a/b/c"))
val directURI = filePathURI(Root + "a/b/c/")
dirURI == directURI
}
property("directoryURI preserves trailing slash") = secure {
directoryURI(new File("/a/b/c/")) == filePathURI("/a/b/c/")
directoryURI(new File(Root + "a/b/c/")) == filePathURI(Root + "a/b/c/")
}
property("filePathURI encodes spaces") = secure {
@ -33,18 +42,18 @@ object URITests extends Properties("URI Tests")
}
property("filePathURI and File.toURI agree for absolute file") = secure {
val s = "/a/b'/has spaces"
val s = Root + "a/b'/has spaces"
val viaPath = filePathURI(s)
val viaFile = (new File(s)).toURI
val viaFile = new File(s).toURI
s"via path: $viaPath" |:
s"via file: $viaFile" |:
(viaPath == viaFile)
}
property("filePathURI supports URIs") = secure {
val s = "file:///is/a/uri/with%20spaces"
val decoded = "/is/a/uri/with spaces"
val encoded = "/is/a/uri/with%20spaces"
val s = s"file://${Root}is/a/uri/with%20spaces"
val decoded = Root + "is/a/uri/with spaces"
val encoded = Root + "is/a/uri/with%20spaces"
val fpURI = filePathURI(s)
val directURI = new URI(s)
s"filePathURI: $fpURI" |:

View File

@ -0,0 +1,74 @@
/** These are packaged and published locally and the resulting artifact is used to test the launcher.*/
package xsbt.boot.test
import java.net.Socket
import java.net.SocketTimeoutException
class EchoServer extends xsbti.ServerMain
{
def start(configuration: xsbti.AppConfiguration): xsbti.Server =
{
object server extends xsbti.Server {
// TODO - Start a server.
val serverSocket = new java.net.ServerSocket(0)
val port = serverSocket.getLocalPort
val addr = serverSocket.getInetAddress.getHostAddress
override val uri =new java.net.URI(s"http://${addr}:${port}")
// Check for stop every second.
serverSocket.setSoTimeout(1000)
object serverThread extends Thread {
private val running = new java.util.concurrent.atomic.AtomicBoolean(true)
override def run(): Unit = {
while(running.get) try {
val clientSocket = serverSocket.accept()
// Handle client connections
object clientSocketThread extends Thread {
override def run(): Unit = {
echoTo(clientSocket)
}
}
clientSocketThread.start()
} catch {
case e: SocketTimeoutException => // Ignore
}
}
// Simple mechanism to dump input to output.
private def echoTo(socket: Socket): Unit = {
val input = new java.io.BufferedReader(new java.io.InputStreamReader(socket.getInputStream))
val output = new java.io.BufferedWriter(new java.io.OutputStreamWriter(socket.getOutputStream))
import scala.util.control.Breaks._
try {
// Lame way to break out.
breakable {
def read(): Unit = input.readLine match {
case null => ()
case "kill" =>
running.set(false)
serverSocket.close()
break()
case line =>
output.write(line)
output.flush()
read()
}
read()
}
} finally {
output.close()
input.close()
socket.close()
}
}
}
// Start the thread immediately
serverThread.start()
override def awaitTermination(): xsbti.MainResult = {
serverThread.join()
new Exit(0)
}
}
server
}
}

View File

@ -3,7 +3,7 @@
*/
package sbt
import Predef.{conforms => _, _}
import Predef.{Map, Set, implicitly} // excludes *both 2.10.x conforms and 2.11.x $conforms in source compatible manner.
import FileInfo.{exists, hash}
import java.io.File

View File

@ -58,16 +58,21 @@ object Compiler
val provider = ComponentCompiler.interfaceProvider(componentManager)
new AnalyzingCompiler(instance, provider, cpOptions, log)
}
def apply(in: Inputs, log: Logger): Analysis =
def apply(in: Inputs, log: Logger): Analysis =
{
import in.compilers._
import in.config._
import in.incSetup._
import in.compilers._
import in.config._
import in.incSetup._
apply(in, log, new LoggerReporter(maxErrors, log, sourcePositionMapper))
}
def apply(in: Inputs, log: Logger, reporter: xsbti.Reporter): Analysis =
{
import in.compilers._
import in.config._
import in.incSetup._
val agg = new AggressiveCompile(cacheFile)
agg(scalac, javac, sources, classpath, CompileOutput(classesDirectory), cache, None, options, javacOptions,
analysisMap, definesClass, new LoggerReporter(maxErrors, log, sourcePositionMapper), order, skip, incOptions)(log)
analysisMap, definesClass, reporter, order, skip, incOptions)(log)
}
private[sbt] def foldMappers[A](mappers: Seq[A => Option[A]]) =

View File

@ -220,7 +220,9 @@ object Tests
def processResults(results: Iterable[(String, SuiteResult)]): Output =
Output(overall(results.map(_._2.result)), results.toMap, Iterable.empty)
def foldTasks(results: Seq[Task[Output]], parallel: Boolean): Task[Output] =
if (parallel)
if (results.isEmpty)
task { Output(TestResult.Passed, Map.empty, Nil) }
else if (parallel)
reduced(results.toIndexedSeq, {
case (Output(v1, m1, _), Output(v2, m2, _)) => Output(if (v1.id < v2.id) v2 else v1, m1 ++ m2, Iterable.empty)
})
@ -336,4 +338,4 @@ object Tests
}
}
final class TestsFailedException extends RuntimeException("Tests unsuccessful") with FeedbackProvidedException
final class TestsFailedException extends RuntimeException("Tests unsuccessful") with FeedbackProvidedException

View File

@ -87,7 +87,7 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se
val value = (cl: ClassLoader) => getValue[Any](i.enclosingModule, i.loader(cl))
new EvalResult(i.extra, value, i.generated, i.enclosingModule)
}
def evalDefinitions(definitions: Seq[(String,Range)], imports: EvalImports, srcName: String, valTypes: Seq[String]): EvalDefinitions =
def evalDefinitions(definitions: Seq[(String,scala.Range)], imports: EvalImports, srcName: String, valTypes: Seq[String]): EvalDefinitions =
{
require(definitions.nonEmpty, "Definitions to evaluate cannot be empty.")
val ev = new EvalType[Seq[String]] {
@ -349,7 +349,7 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se
}
/** Constructs a CompilationUnit for each definition, which can be used to independently parse the definition into a Tree.
* Additionally, a CompilationUnit for the combined definitions is constructed for use by combined compilation after parsing. */
private[this] def mkDefsUnit(srcName: String, definitions: Seq[(String,Range)]): (CompilationUnit, Seq[CompilationUnit]) =
private[this] def mkDefsUnit(srcName: String, definitions: Seq[(String,scala.Range)]): (CompilationUnit, Seq[CompilationUnit]) =
{
def fragmentUnit(content: String, lineMap: Array[Int]) = new CompilationUnit(fragmentSourceFile(srcName, content, lineMap))

View File

@ -19,7 +19,7 @@ object BasicCommandStrings
/** The command name to terminate the program.*/
val TerminateAction: String = Exit
def helpBrief = (HelpCommand, "Displays this help message or prints detailed help on requested commands (run 'help <command>').")
def helpBrief = (HelpCommand, s"Displays this help message or prints detailed help on requested commands (run '$HelpCommand <command>').")
def helpDetailed = HelpCommand + """
Prints a help summary.

View File

@ -27,7 +27,7 @@ sealed trait ScopedTaskable[T] extends Scoped {
/** Identifies a setting. It consists of three parts: the scope, the name, and the type of a value associated with this key.
* The scope is represented by a value of type Scope.
* The name and the type are represented by a value of type AttributeKey[T].
* The name and the type are represented by a value of type `AttributeKey[T]`.
* Instances are constructed using the companion object. */
sealed abstract class SettingKey[T] extends ScopedTaskable[T] with KeyedInitialize[T] with Scoped.ScopingSetting[SettingKey[T]] with Scoped.DefinableSetting[T]
{
@ -52,7 +52,7 @@ sealed abstract class SettingKey[T] extends ScopedTaskable[T] with KeyedInitiali
/** Identifies a task. It consists of three parts: the scope, the name, and the type of the value computed by a task associated with this key.
* The scope is represented by a value of type Scope.
* The name and the type are represented by a value of type AttributeKey[Task[T]].
* The name and the type are represented by a value of type `AttributeKey[Task[T]]`.
* Instances are constructed using the companion object. */
sealed abstract class TaskKey[T] extends ScopedTaskable[T] with KeyedInitialize[Task[T]] with Scoped.ScopingSetting[TaskKey[T]] with Scoped.DefinableTask[T]
{
@ -76,7 +76,7 @@ sealed abstract class TaskKey[T] extends ScopedTaskable[T] with KeyedInitialize[
/** Identifies an input task. An input task parses input and produces a task to run.
* It consists of three parts: the scope, the name, and the type of the value produced by an input task associated with this key.
* The scope is represented by a value of type Scope.
* The name and the type are represented by a value of type AttributeKey[InputTask[T]].
* The name and the type are represented by a value of type `AttributeKey[InputTask[T]]`.
* Instances are constructed using the companion object. */
sealed trait InputKey[T] extends Scoped with KeyedInitialize[InputTask[T]] with Scoped.ScopingSetting[InputKey[T]] with Scoped.DefinableSetting[InputTask[T]]
{
@ -95,6 +95,21 @@ object Scoped
implicit def taskScopedToKey[T](s: TaskKey[T]): ScopedKey[Task[T]] = ScopedKey(s.scope, s.key)
implicit def inputScopedToKey[T](s: InputKey[T]): ScopedKey[InputTask[T]] = ScopedKey(s.scope, s.key)
/**
* Mixin trait for adding convenience vocabulary associated with specifiying the [[Scope]] of a setting.
* Allows specification of the Scope or part of the [[Scope]] of a setting being referenced.
* @example
* {{{
* name in Global := "hello Global scope"
*
* name in (Compile, packageBin) := "hello Compile scope packageBin"
*
* name in Compile := "hello Compile scope"
* name.in(Compile).:=("hello ugly syntax")
* }}}
*
*/
sealed trait ScopingSetting[Result]
{
def in(s: Scope): Result
@ -113,18 +128,50 @@ object Scoped
def scopedInput[T](s: Scope, k: AttributeKey[InputTask[T]]): InputKey[T] = new InputKey[T] { val scope = s; val key = k }
def scopedTask[T](s: Scope, k: AttributeKey[Task[T]]): TaskKey[T] = new TaskKey[T] { val scope = s; val key = k }
/**
* Mixin trait for adding convenience vocabulary associated with applying a setting to a configuration item.
*/
sealed trait DefinableSetting[S]
{
def scopedKey: ScopedKey[S]
private[sbt] final def :==(app: S): Setting[S] = macro std.TaskMacro.settingAssignPure[S]
/** Binds a single value to this. A new [Def.Setting] is defined using the value(s) of `app`.
* @param app value to bind to this key
* @return setting binding this key to the given value.
*/
final def <<= (app: Initialize[S]): Setting[S] = macro std.TaskMacro.settingAssignPosition[S]
/** Internally used function for setting a value along with the `.sbt` file location where it is defined. */
final def set (app: Initialize[S], source: SourcePosition): Setting[S] = setting(scopedKey, app, source)
/** From the given [[Settings]], extract the value bound to this key. */
final def get(settings: Settings[Scope]): Option[S] = settings.get(scopedKey.scope, scopedKey.key)
/** Creates an [[Def.Initialize]] with value [[scala.None]] if there was no previous definition of this key,
* and `[[scala.Some]](value)` if a definition exists. Useful for when you want to use the ''existence'' of
* one setting in order to define another setting.
* @return currently bound value wrapped in `Initialize[Some[T]]`, or `Initialize[None]` if unbound. */
final def ? : Initialize[Option[S]] = Def.optional(scopedKey)(idFun)
/** Creates an [[Def.Initialize]] with value bound to this key, or returns `i` parameter if unbound.
* @param i value to return if this setting doesn't have a value.
* @return currently bound setting value, or `i` if unbound.
*/
final def or[T >: S](i: Initialize[T]): Initialize[T] = (this.?, i)(_ getOrElse _ )
/** Like [[?]], but with a call-by-name parameter rather than an existing [[Def.Initialize]].
* Useful when you want to have a value computed when no value is bound to this key.
* @param or by-name expression evaluated when a value is needed.
* @return currently bound setting value, or the result of `or` if unbound.
*/
final def ??[T >: S](or: => T): Initialize[T] = Def.optional(scopedKey)(_ getOrElse or )
}
/**
* Wraps an [[sbt.Def.Initialize]] instance to provide `map` and `flatMap` symantics.
*/
final class RichInitialize[S](init: Initialize[S])
{
def map[T](f: S => T): Initialize[Task[T]] = init(s => mktask(f(s)) )

View File

@ -59,6 +59,9 @@ object InputWrapper
private[std] def wrapPrevious[T: c.WeakTypeTag](c: Context)(ts: c.Expr[Any], pos: c.Position): c.Expr[Option[T]] =
wrapImpl[Option[T],InputWrapper.type](c, InputWrapper, WrapPreviousName)(ts, pos)
// TODO 2.11 Remove this after dropping 2.10.x support.
private object HasCompat { val compat = ??? }; import HasCompat._
/** Wraps an arbitrary Tree in a call to the `<s>.<wrapName>` method of this module for later processing by an enclosing macro.
* The resulting Tree is the manually constructed version of:
*
@ -67,6 +70,7 @@ object InputWrapper
def wrapImpl[T: c.WeakTypeTag, S <: AnyRef with Singleton](c: Context, s: S, wrapName: String)(ts: c.Expr[Any], pos: c.Position)(implicit it: c.TypeTag[s.type]): c.Expr[T] =
{
import c.universe.{Apply=>ApplyTree,_}
import compat._
val util = new ContextUtil[c.type](c)
val iw = util.singleton(s)
val tpe = c.weakTypeOf[T]
@ -75,8 +79,16 @@ object InputWrapper
sel.setPos(pos) // need to set the position on Select, because that is where the compileTimeOnly check looks
val tree = ApplyTree(TypeApply(sel, TypeTree(tpe) :: Nil), ts.tree :: Nil)
tree.setPos(ts.tree.pos)
tree.setType(tpe)
c.Expr[T](tree)
// JZ: I'm not sure why we need to do this. Presumably a caller is wrapping this tree in a
// typed tree *before* handing the whole thing back to the macro engine. One must never splice
// untyped trees under typed trees, as the type checker doesn't descend if `tree.tpe == null`.
//
// #1031 The previous attempt to fix this just set the type on `tree`, which worked in cases when the
// call to `.value` was inside a the task macro and eliminated before the end of the typer phase.
// But, if a "naked" call to `.value` left the typer, the superaccessors phase would freak out when
// if hit the untyped trees, before we could get to refchecks and the desired @compileTimeOnly warning.
val typedTree = c.typeCheck(tree)
c.Expr[T](typedTree)
}
def valueMacroImpl[T: c.WeakTypeTag](c: Context): c.Expr[T] =

View File

@ -281,9 +281,13 @@ object TaskMacro
private[this] def iTaskMacro[T: c.WeakTypeTag](c: Context)(t: c.Expr[T]): c.Expr[Task[T]] =
Instance.contImpl[T,Id](c, TaskInstance, TaskConvert, MixedBuilder)(Left(t), Instance.idTransform)
// TODO 2.11 Remove this after dropping 2.10.x support.
private object HasCompat { val compat = ??? }; import HasCompat._
private[this] def inputTaskDynMacro0[T: c.WeakTypeTag](c: Context)(t: c.Expr[Initialize[Task[T]]]): c.Expr[Initialize[InputTask[T]]] =
{
import c.universe.{Apply=>ApplyTree,_}
import compat._
val tag = implicitly[c.WeakTypeTag[T]]
val util = ContextUtil[c.type](c)

View File

@ -12,11 +12,27 @@ object AddSettings
private[sbt] final class Sequence(val sequence: Seq[AddSettings]) extends AddSettings
private[sbt] final object User extends AddSettings
private[sbt] final class Plugins(val include: Plugin => Boolean) extends AddSettings
private[sbt] final class AutoPlugins(val include: AutoPlugin => Boolean) extends AddSettings
private[sbt] final class DefaultSbtFiles(val include: File => Boolean) extends AddSettings
private[sbt] final class SbtFiles(val files: Seq[File]) extends AddSettings
private[sbt] final object BuildScalaFiles extends AddSettings
/** Adds all settings from autoplugins. */
val autoPlugins: AddSettings = new AutoPlugins(const(true)) // Note: We do not expose fine-grained autoplugins because
// it's dangerous to control at that level right now.
// Leaving the hook in place in case we need to expose
// it, but most likely it will remain locked out
// for users with an alternative ordering feature
// in place.
/** Settings specified in Build.scala `Project` constructors. */
val buildScalaFiles: AddSettings = BuildScalaFiles
/** All plugins that aren't auto plugins. */
val nonAutoPlugins: AddSettings = plugins(const(true))
/** Adds all settings from a plugin to a project. */
val allPlugins: AddSettings = plugins(const(true))
val allPlugins: AddSettings = seq(autoPlugins, nonAutoPlugins)
/** Allows the plugins whose names match the `names` filter to automatically add settings to a project. */
def plugins(include: Plugin => Boolean): AddSettings = new Plugins(include)
@ -33,7 +49,8 @@ object AddSettings
/** Includes settings automatically*/
def seq(autos: AddSettings*): AddSettings = new Sequence(autos)
val allDefaults: AddSettings = seq(userSettings, allPlugins, defaultSbtFiles)
/** The default inclusion of settings. */
val allDefaults: AddSettings = seq(autoPlugins, buildScalaFiles, userSettings, nonAutoPlugins, defaultSbtFiles)
/** Combines two automatic setting configurations. */
def append(a: AddSettings, b: AddSettings): AddSettings = (a,b) match {

View File

@ -59,7 +59,7 @@ final object Aggregation
import extracted.structure
val toRun = ts map { case KeyValue(k,t) => t.map(v => KeyValue(k,v)) } join;
val roots = ts map { case KeyValue(k,_) => k }
val config = extractedConfig(extracted, structure)
val config = extractedConfig(extracted, structure, s)
val start = System.currentTimeMillis
val (newS, result) = withStreams(structure, s){ str =>
@ -211,4 +211,4 @@ final object Aggregation
@deprecated("Use BuildUtil.aggregationRelation", "0.13.0")
def relation(units: Map[URI, LoadedBuildUnit]): Relation[ProjectRef, ProjectRef] =
BuildUtil.aggregationRelation(units)
}
}

View File

@ -12,12 +12,14 @@ trait Build
{
def projectDefinitions(baseDirectory: File): Seq[Project] = projects
def projects: Seq[Project] = ReflectUtilities.allVals[Project](this).values.toSeq
// TODO: Should we grab the build core setting shere or in a plugin?
def settings: Seq[Setting[_]] = Defaults.buildCore
def buildLoaders: Seq[BuildLoader.Components] = Nil
/** Explicitly defines the root project.
* If None, the root project is the first project in the build's root directory or just the first project if none are in the root directory.*/
def rootProject: Option[Project] = None
}
// TODO 0.14.0: decide if Plugin should be deprecated in favor of AutoPlugin
trait Plugin
{
@deprecated("Override projectSettings or buildSettings instead.", "0.12.0")
@ -45,8 +47,16 @@ object Build
@deprecated("Explicitly specify the ID", "0.13.0")
def defaultProject(base: File): Project = defaultProject(defaultID(base), base)
def defaultProject(id: String, base: File): Project = Project(id, base).settings(
// TODO - Can we move this somewhere else? ordering of settings is causing this to get borked.
// if the user has overridden the name, use the normal organization that is derived from the name.
organization <<= (thisProject, organization, name) { (p, o, n) => if(p.id == n) "default" else o }
organization := {
val overridden = thisProject.value.id == name.value
organization.?.value match {
case Some(o) if !overridden => o
case _ => "default"
}
//(thisProject, organization, name) { (p, o, n) => if(p.id == n) "default" else o }
}
)
def defaultAggregatedProject(id: String, base: File, agg: Seq[ProjectRef]): Project =
defaultProject(id, base).aggregate(agg : _*)

View File

@ -30,23 +30,116 @@ final class StructureIndex(
val keyIndex: KeyIndex,
val aggregateKeyIndex: KeyIndex
)
/** A resolved build unit. (`ResolvedBuildUnit` would be a better name to distinguish it from the loaded, but unresolved `BuildUnit`.)
* @param unit The loaded, but unresolved [[BuildUnit]] this was resolved from.
* @param defined The definitive map from project IDs to resolved projects.
* These projects have had [[Reference]]s resolved and [[AutoPlugin]]s evaluated.
* @param rootProjects The list of project IDs for the projects considered roots of this build.
* The first root project is used as the default in several situations where a project is not otherwise selected.
*/
final class LoadedBuildUnit(val unit: BuildUnit, val defined: Map[String, ResolvedProject], val rootProjects: Seq[String], val buildSettings: Seq[Setting[_]]) extends BuildUnitBase
{
assert(!rootProjects.isEmpty, "No root projects defined for build unit " + unit)
/** The project to use as the default when one is not otherwise selected.
* [[LocalRootProject]] resolves to this from within the same build.*/
val root = rootProjects.head
/** The base directory of the build unit (not the build definition).*/
def localBase = unit.localBase
/** The classpath to use when compiling against this build unit's publicly visible code.
* It includes build definition and plugin classes, but not classes for .sbt file statements and expressions. */
def classpath: Seq[File] = unit.definitions.target ++ unit.plugins.classpath
/** The class loader to use for this build unit's publicly visible code.
* It includes build definition and plugin classes, but not classes for .sbt file statements and expressions. */
def loader = unit.definitions.loader
/** The imports to use for .sbt files, `consoleProject` and other contexts that use code from the build definition. */
def imports = BuildUtil.getImports(unit)
override def toString = unit.toString
}
// TODO: figure out how to deprecate and drop buildNames
/** The built and loaded build definition, including loaded but unresolved [[Project]]s, for a build unit (for a single URI).
*
* @param base The base directory of the build definition, typically `<build base>/project/`.
* @param loader The ClassLoader containing all classes and plugins for the build definition project.
* Note that this does not include classes for .sbt files.
* @param builds The list of [[Build]]s for the build unit.
* In addition to auto-discovered [[Build]]s, this includes any auto-generated default [[Build]]s.
* @param projects The list of all [[Project]]s from all [[Build]]s.
* These projects have not yet been resolved, but they have had auto-plugins applied.
* In particular, each [[Project]]'s `autoPlugins` field is populated according to their configured `plugins`
* and their `settings` and `configurations` updated as appropriate.
* @param buildNames No longer used and will be deprecated once feasible.
*/
final class LoadedDefinitions(val base: File, val target: Seq[File], val loader: ClassLoader, val builds: Seq[Build], val projects: Seq[Project], val buildNames: Seq[String])
final class LoadedPlugins(val base: File, val pluginData: PluginData, val loader: ClassLoader, val plugins: Seq[Plugin], val pluginNames: Seq[String])
/** Auto-detected top-level modules (as in `object X`) of type `T` paired with their source names. */
final class DetectedModules[T](val modules: Seq[(String, T)])
{
/** The source names of the modules. This is "X" in `object X`, as opposed to the implementation class name "X$".
* The names are returned in a stable order such that `names zip values` pairs a name with the actual module. */
def names: Seq[String] = modules.map(_._1)
/** The singleton value of the module.
* The values are returned in a stable order such that `names zip values` pairs a name with the actual module. */
def values: Seq[T] = modules.map(_._2)
}
/** Auto-detected auto plugin. */
case class DetectedAutoPlugin(val name: String, val value: AutoPlugin, val hasAutoImport: Boolean)
/** Auto-discovered modules for the build definition project. These include modules defined in build definition sources
* as well as modules in binary dependencies.
*
* @param builds The [[Build]]s detected in the build definition. This does not include the default [[Build]] that sbt creates if none is defined.
*/
final class DetectedPlugins(val plugins: DetectedModules[Plugin], val autoPlugins: Seq[DetectedAutoPlugin], val builds: DetectedModules[Build])
{
/** Sequence of import expressions for the build definition. This includes the names of the [[Plugin]], [[Build]], and [[AutoImport]] modules, but not the [[AutoPlugin]] modules. */
lazy val imports: Seq[String] = BuildUtil.getImports(plugins.names ++ builds.names ++
(autoPlugins flatMap { case DetectedAutoPlugin(name, ap, hasAutoImport) =>
if (hasAutoImport) Some(name + ".autoImport")
else None
})) ++
BuildUtil.importNamesRoot(autoPlugins map { _.name })
/** A function to select the right [[AutoPlugin]]s from [[autoPlugins]] for a [[Project]]. */
lazy val deducePlugins: (Plugins, Logger) => Seq[AutoPlugin] = Plugins.deducer(autoPlugins.toList map {_.value})
}
/** The built and loaded build definition project.
* @param base The base directory for the build definition project (not the base of the project itself).
* @param pluginData Evaluated tasks/settings from the build definition for later use.
* This is necessary because the build definition project is discarded.
* @param loader The class loader for the build definition project, notably excluding classes used for .sbt files.
* @param detected Auto-detected modules in the build definition.
*/
final class LoadedPlugins(val base: File, val pluginData: PluginData, val loader: ClassLoader, val detected: DetectedPlugins)
{
@deprecated("Use the primary constructor.", "0.13.2")
def this(base: File, pluginData: PluginData, loader: ClassLoader, plugins: Seq[Plugin], pluginNames: Seq[String]) =
this(base, pluginData, loader,
new DetectedPlugins(new DetectedModules(pluginNames zip plugins), Nil, new DetectedModules(Nil))
)
@deprecated("Use detected.plugins.values.", "0.13.2")
val plugins: Seq[Plugin] = detected.plugins.values
@deprecated("Use detected.plugins.names.", "0.13.2")
val pluginNames: Seq[String] = detected.plugins.names
def fullClasspath: Seq[Attributed[File]] = pluginData.classpath
def classpath = data(fullClasspath)
}
/** The loaded, but unresolved build unit.
* @param uri The uniquely identifying URI for the build.
* @param localBase The working location of the build on the filesystem.
* For local URIs, this is the same as `uri`, but for remote URIs, this is the local copy or workspace allocated for the build.
*/
final class BuildUnit(val uri: URI, val localBase: File, val definitions: LoadedDefinitions, val plugins: LoadedPlugins)
{
override def toString = if(uri.getScheme == "file") localBase.toString else (uri + " (locally: " + localBase +")")
@ -57,6 +150,8 @@ final class LoadedBuild(val root: URI, val units: Map[URI, LoadedBuildUnit])
BuildUtil.checkCycles(units)
def allProjectRefs: Seq[(ProjectRef, ResolvedProject)] = for( (uri, unit) <- units.toSeq; (id, proj) <- unit.defined ) yield ProjectRef(uri, id) -> proj
def extra(data: Settings[Scope])(keyIndex: KeyIndex): BuildUtil[ResolvedProject] = BuildUtil(root, units, keyIndex, data)
private[sbt] def autos = GroupedAutoPlugins(units)
}
final class PartBuild(val root: URI, val units: Map[URI, PartBuildUnit])
sealed trait BuildUnitBase { def rootProjects: Seq[String]; def buildSettings: Seq[Setting[_]] }

View File

@ -35,7 +35,7 @@ final class BuildUtil[Proj](
case _ => None
}
val configurationsForAxis: Option[ResolvedReference] => Seq[String] =
val configurationsForAxis: Option[ResolvedReference] => Seq[String] =
refOpt => configurations(projectForAxis(refOpt)).map(_.name)
}
object BuildUtil
@ -48,6 +48,20 @@ object BuildUtil
new BuildUtil(keyIndex, data, root, Load getRootProject units, getp, configs, aggregates)
}
def dependencies(units: Map[URI, LoadedBuildUnit]): BuildDependencies =
{
import collection.mutable.HashMap
val agg = new HashMap[ProjectRef, Seq[ProjectRef]]
val cp = new HashMap[ProjectRef, Seq[ClasspathDep[ProjectRef]]]
for(lbu <- units.values; rp <- lbu.defined.values)
{
val ref = ProjectRef(lbu.unit.uri, rp.id)
cp(ref) = rp.dependencies
agg(ref) = rp.aggregate
}
BuildDependencies(cp.toMap, agg.toMap)
}
def checkCycles(units: Map[URI, LoadedBuildUnit])
{
def getRef(pref: ProjectRef) = units(pref.build).defined(pref.project)
@ -60,9 +74,22 @@ object BuildUtil
}
}
def baseImports: Seq[String] = "import sbt._, Keys._" :: Nil
def getImports(unit: BuildUnit): Seq[String] = getImports(unit.plugins.pluginNames, unit.definitions.buildNames)
def getImports(pluginNames: Seq[String], buildNames: Seq[String]): Seq[String] = baseImports ++ importAllRoot(pluginNames ++ buildNames)
def importAll(values: Seq[String]): Seq[String] = if(values.isEmpty) Nil else values.map( _ + "._" ).mkString("import ", ", ", "") :: Nil
def getImports(unit: BuildUnit): Seq[String] = unit.plugins.detected.imports
@deprecated("Use getImports(Seq[String]).", "0.13.2")
def getImports(pluginNames: Seq[String], buildNames: Seq[String]): Seq[String] = getImports(pluginNames ++ buildNames)
/** `import sbt._, Keys._`, and wildcard import `._` for all names. */
def getImports(names: Seq[String]): Seq[String] = baseImports ++ importAllRoot(names)
/** Import just the names. */
def importNames(names: Seq[String]): Seq[String] = if (names.isEmpty) Nil else names.mkString("import ", ", ", "") :: Nil
/** Prepend `_root_` and import just the names. */
def importNamesRoot(names: Seq[String]): Seq[String] = importNames(names map rootedName)
/** Wildcard import `._` for all values. */
def importAll(values: Seq[String]): Seq[String] = importNames(values map { _ + "._" })
def importAllRoot(values: Seq[String]): Seq[String] = importAll(values map rootedName)
def rootedName(s: String): String = if(s contains '.') "_root_." + s else s
@ -78,4 +105,4 @@ object BuildUtil
(ref, agg)
Relation.empty ++ depPairs
}
}
}

View File

@ -38,7 +38,7 @@ s"""$multiTaskSyntax
def multiTaskBrief = """Executes all of the specified tasks concurrently."""
def showHelp = Help(ShowCommand, (ShowCommand + " <key>", actBrief), actDetailed)
def showHelp = Help(ShowCommand, (s"$ShowCommand <key>", showBrief), showDetailed)
def showBrief = "Displays the result of evaluating the setting or task associated with 'key'."
def showDetailed =
s"""$ShowCommand <setting>
@ -49,6 +49,11 @@ $ShowCommand <task>
Evaluates the specified task and display the value returned by the task."""
val PluginsCommand = "plugins"
val PluginCommand = "plugin"
def pluginsBrief = "Lists currently available plugins."
def pluginsDetailed = pluginsBrief // TODO: expand
val LastCommand = "last"
val LastGrepCommand = "last-grep"
val ExportCommand = "export"
@ -167,7 +172,7 @@ Syntax summary
Displays the main %<s defined directly or indirectly for the current project.
-v
Displays additional tasks. More 'v's increase the number of tasks displayed.
Displays additional %<s. More 'v's increase the number of %<s displayed.
-V
displays all %<s

View File

@ -56,94 +56,107 @@ object Defaults extends BuildCommon
def thisBuildCore: Seq[Setting[_]] = inScope(GlobalScope.copy(project = Select(ThisBuild)))(Seq(
managedDirectory := baseDirectory.value / "lib_managed"
))
@deprecated("0.13.2", "Use AutoPlugins and globalSbtCore instead.")
lazy val globalCore: Seq[Setting[_]] = globalDefaults(defaultTestTasks(test) ++ defaultTestTasks(testOnly) ++ defaultTestTasks(testQuick) ++ Seq(
excludeFilter :== HiddenFileFilter
) ++ globalIvyCore ++ globalJvmCore) ++ globalSbtCore
private[sbt] lazy val globalJvmCore: Seq[Setting[_]] =
Seq(
compilerCache := state.value get Keys.stateCompilerCache getOrElse compiler.CompilerCache.fresh,
crossVersion :== CrossVersion.Disabled,
sourcesInBase :== true,
autoAPIMappings := false,
apiMappings := Map.empty,
autoScalaLibrary :== true,
managedScalaInstance :== true,
definesClass :== FileValueCache(Locate.definesClass _ ).get,
traceLevel in run :== 0,
traceLevel in runMain :== 0,
traceLevel in console :== Int.MaxValue,
traceLevel in consoleProject :== Int.MaxValue,
autoCompilerPlugins :== true,
scalaHome :== None,
apiURL := None,
javaHome :== None,
testForkedParallel :== false,
javaOptions :== Nil,
sbtPlugin :== false,
crossPaths :== true,
sourcePositionMappers :== Nil,
artifactClassifier in packageSrc :== Some(SourceClassifier),
artifactClassifier in packageDoc :== Some(DocClassifier),
includeFilter :== NothingFilter,
includeFilter in unmanagedSources :== "*.java" | "*.scala",
includeFilter in unmanagedJars :== "*.jar" | "*.so" | "*.dll" | "*.jnilib" | "*.zip",
includeFilter in unmanagedResources :== AllPassFilter
)
private[sbt] lazy val globalIvyCore: Seq[Setting[_]] =
Seq(
internalConfigurationMap :== Configurations.internalMap _,
credentials :== Nil,
exportJars :== false,
retrieveManaged :== false,
scalaOrganization :== ScalaArtifacts.Organization,
sbtResolver := { if(sbtVersion.value endsWith "-SNAPSHOT") Classpaths.typesafeSnapshots else Classpaths.typesafeReleases },
crossVersion :== CrossVersion.Disabled,
buildDependencies <<= Classpaths.constructBuildDependencies,
version :== "0.1-SNAPSHOT",
classpathTypes :== Set("jar", "bundle") ++ CustomPomParser.JarPackagings,
artifactClassifier :== None,
checksums := Classpaths.bootChecksums(appConfiguration.value),
conflictManager := ConflictManager.default,
pomExtra :== NodeSeq.Empty,
pomPostProcess :== idFun,
pomAllRepositories :== false,
pomIncludeRepository :== Classpaths.defaultRepositoryFilter
)
/** Core non-plugin settings for sbt builds. These *must* be on every build or the sbt engine will fail to run at all. */
private[sbt] lazy val globalSbtCore: Seq[Setting[_]] = globalDefaults(Seq(
outputStrategy :== None, // TODO - This might belong elsewhere.
buildStructure := Project.structure(state.value),
settingsData := buildStructure.value.data,
trapExit :== true,
connectInput :== false,
cancelable :== false,
envVars :== Map.empty,
sbtVersion := appConfiguration.value.provider.id.version,
sbtBinaryVersion := binarySbtVersion(sbtVersion.value),
watchingMessage := Watched.defaultWatchingMessage,
triggeredMessage := Watched.defaultTriggeredMessage,
onLoad := idFun[State],
onUnload := idFun[State],
onUnload := { s => try onUnload.value(s) finally IO.delete(taskTemporaryDirectory.value) },
extraLoggers :== { _ => Nil },
watchSources :== Nil,
skip :== false,
taskTemporaryDirectory := { val dir = IO.createTemporaryDirectory; dir.deleteOnExit(); dir },
onComplete := { val dir = taskTemporaryDirectory.value; () => {IO.delete(dir); IO.createDirectory(dir) }},
Previous.cache <<= Previous.cacheSetting,
Previous.references :== new Previous.References,
concurrentRestrictions <<= defaultRestrictions,
parallelExecution :== true,
sbtVersion := appConfiguration.value.provider.id.version,
sbtBinaryVersion := binarySbtVersion(sbtVersion.value),
sbtResolver := { if(sbtVersion.value endsWith "-SNAPSHOT") Classpaths.typesafeSnapshots else Classpaths.typesafeReleases },
pollInterval :== 500,
logBuffered :== false,
connectInput :== false,
cancelable :== false,
envVars :== Map.empty,
sourcesInBase :== true,
autoAPIMappings := false,
apiMappings := Map.empty,
autoScalaLibrary :== true,
managedScalaInstance :== true,
onLoad := idFun[State],
onUnload := idFun[State],
onUnload := { s => try onUnload.value(s) finally IO.delete(taskTemporaryDirectory.value) },
watchingMessage := Watched.defaultWatchingMessage,
triggeredMessage := Watched.defaultTriggeredMessage,
definesClass :== FileValueCache(Locate.definesClass _ ).get,
trapExit :== true,
traceLevel in run :== 0,
traceLevel in runMain :== 0,
traceLevel in console :== Int.MaxValue,
traceLevel in consoleProject :== Int.MaxValue,
autoCompilerPlugins :== true,
internalConfigurationMap :== Configurations.internalMap _,
initialize :== {},
credentials :== Nil,
scalaHome :== None,
apiURL := None,
javaHome :== None,
extraLoggers :== { _ => Nil },
skip :== false,
watchSources :== Nil,
version :== "0.1-SNAPSHOT",
outputStrategy :== None,
exportJars :== false,
fork :== false,
testForkedParallel :== false,
javaOptions :== Nil,
sbtPlugin :== false,
crossPaths :== true,
classpathTypes :== Set("jar", "bundle") ++ CustomPomParser.JarPackagings,
aggregate :== true,
maxErrors :== 100,
sourcePositionMappers :== Nil,
commands :== Nil,
showSuccess :== true,
showTiming :== true,
timingFormat :== Aggregation.defaultFormat,
showSuccess :== true,
commands :== Nil,
retrieveManaged :== false,
buildStructure := Project.structure(state.value),
settingsData := buildStructure.value.data,
artifactClassifier :== None,
artifactClassifier in packageSrc :== Some(SourceClassifier),
artifactClassifier in packageDoc :== Some(DocClassifier),
checksums := Classpaths.bootChecksums(appConfiguration.value),
conflictManager := ConflictManager.default,
pomExtra :== NodeSeq.Empty,
pomPostProcess :== idFun,
pomAllRepositories :== false,
includeFilter :== NothingFilter,
includeFilter in unmanagedSources :== "*.java" | "*.scala",
includeFilter in unmanagedJars :== "*.jar" | "*.so" | "*.dll" | "*.jnilib" | "*.zip",
includeFilter in unmanagedResources :== AllPassFilter,
excludeFilter :== HiddenFileFilter,
pomIncludeRepository :== Classpaths.defaultRepositoryFilter
aggregate :== true,
maxErrors :== 100,
fork :== false,
initialize :== {}
))
def defaultTestTasks(key: Scoped): Seq[Setting[_]] = inTask(key)(Seq(
tags := Seq(Tags.Test -> 1),
logBuffered := true
))
// TODO: This should be on the new default settings for a project.
def projectCore: Seq[Setting[_]] = Seq(
name := thisProject.value.id,
logManager := LogManager.defaults(extraLoggers.value, StandardMain.console),
onLoadMessage <<= onLoadMessage or (name, thisProjectRef)("Set current project to " + _ + " (in build " + _.build +")"),
runnerTask
onLoadMessage <<= onLoadMessage or (name, thisProjectRef)("Set current project to " + _ + " (in build " + _.build +")")
)
def paths = Seq(
baseDirectory := thisProject.value.base,
@ -179,7 +192,7 @@ object Defaults extends BuildCommon
unmanagedResources <<= collectFiles(unmanagedResourceDirectories, includeFilter in unmanagedResources, excludeFilter in unmanagedResources),
watchSources in ConfigGlobal ++= unmanagedResources.value,
resourceGenerators :== Nil,
resourceGenerators <+= (definedSbtPlugins, resourceManaged) map writePluginsDescriptor,
resourceGenerators <+= (discoveredSbtPlugins, resourceManaged) map PluginDiscovery.writeDescriptors,
managedResources <<= generate(resourceGenerators),
resources <<= Classpaths.concat(managedResources, unmanagedResources)
)
@ -233,6 +246,7 @@ object Defaults extends BuildCommon
consoleQuick <<= consoleQuickTask,
discoveredMainClasses <<= compile map discoverMainClasses storeAs discoveredMainClasses triggeredBy compile,
definedSbtPlugins <<= discoverPlugins,
discoveredSbtPlugins <<= discoverSbtPluginNames,
inTask(run)(runnerTask :: Nil).head,
selectMainClass := mainClass.value orElse selectRunMain(discoveredMainClasses.value),
mainClass in run := (selectMainClass in run).value,
@ -732,13 +746,16 @@ object Defaults extends BuildCommon
@deprecated("Use inTask(compile)(compileInputsSettings)", "0.13.0")
def compileTaskSettings: Seq[Setting[_]] = inTask(compile)(compileInputsSettings)
def compileTask: Initialize[Task[inc.Analysis]] = Def.task { compileTaskImpl(streams.value, (compileInputs in compile).value) }
private[this] def compileTaskImpl(s: TaskStreams, ci: Compiler.Inputs): inc.Analysis =
def compileTask: Initialize[Task[inc.Analysis]] = Def.task { compileTaskImpl(streams.value, (compileInputs in compile).value, (compilerReporter in compile).value) }
private[this] def compileTaskImpl(s: TaskStreams, ci: Compiler.Inputs, reporter: Option[xsbti.Reporter]): inc.Analysis =
{
lazy val x = s.text(ExportStream)
def onArgs(cs: Compiler.Compilers) = cs.copy(scalac = cs.scalac.onArgs(exported(x, "scalac")), javac = cs.javac.onArgs(exported(x, "javac")))
val i = ci.copy(compilers = onArgs(ci.compilers))
try Compiler(i,s.log)
try reporter match {
case Some(reporter) => Compiler(i, s.log, reporter)
case None => Compiler(i, s.log)
}
finally x.close() // workaround for #937
}
def compileIncSetupTask =
@ -749,7 +766,8 @@ object Defaults extends BuildCommon
Seq(compileInputs := {
val cp = classDirectory.value +: data(dependencyClasspath.value)
Compiler.inputs(cp, sources.value, classDirectory.value, scalacOptions.value, javacOptions.value, maxErrors.value, sourcePositionMappers.value, compileOrder.value)(compilers.value, compileIncSetup.value, streams.value.log)
})
},
compilerReporter := None)
def printWarningsTask: Initialize[Task[Unit]] =
(streams, compile, maxErrors, sourcePositionMappers) map { (s, analysis, max, spms) =>
@ -760,27 +778,21 @@ object Defaults extends BuildCommon
def sbtPluginExtra(m: ModuleID, sbtV: String, scalaV: String): ModuleID =
m.extra(CustomPomParser.SbtVersionKey -> sbtV, CustomPomParser.ScalaVersionKey -> scalaV).copy(crossVersion = CrossVersion.Disabled)
@deprecated("Use PluginDiscovery.writeDescriptor.", "0.13.2")
def writePluginsDescriptor(plugins: Set[String], dir: File): Seq[File] =
{
val descriptor: File = dir / "sbt" / "sbt.plugins"
if(plugins.isEmpty)
{
IO.delete(descriptor)
Nil
}
else
{
IO.writeLines(descriptor, plugins.toSeq.sorted)
descriptor :: Nil
}
PluginDiscovery.writeDescriptor(plugins.toSeq, dir, PluginDiscovery.Paths.Plugins).toList
def discoverSbtPluginNames: Initialize[Task[PluginDiscovery.DiscoveredNames]] = Def.task {
if(sbtPlugin.value) PluginDiscovery.discoverSourceAll(compile.value) else PluginDiscovery.emptyDiscoveredNames
}
@deprecated("Use discoverSbtPluginNames.", "0.13.2")
def discoverPlugins: Initialize[Task[Set[String]]] = (compile, sbtPlugin, streams) map { (analysis, isPlugin, s) => if(isPlugin) discoverSbtPlugins(analysis, s.log) else Set.empty }
@deprecated("Use PluginDiscovery.sourceModuleNames[Plugin].", "0.13.2")
def discoverSbtPlugins(analysis: inc.Analysis, log: Logger): Set[String] =
{
val pluginClass = classOf[Plugin].getName
val discovery = Discovery(Set(pluginClass), Set.empty)( Tests allDefs analysis )
discovery collect { case (df, disc) if (disc.baseClasses contains pluginClass) && disc.isModule => df.name } toSet;
}
PluginDiscovery.sourceModuleNames(analysis, classOf[Plugin].getName).toSet
def copyResourcesTask =
(classDirectory, resources, resourceDirectories, streams) map { (target, resrcs, dirs, s) =>
@ -853,6 +865,7 @@ object Defaults extends BuildCommon
lazy val disableAggregation = Defaults.globalDefaults( noAggregation map disableAggregate )
def disableAggregate(k: Scoped) = aggregate in k :== false
lazy val runnerSettings: Seq[Setting[_]] = Seq(runnerTask)
lazy val baseTasks: Seq[Setting[_]] = projectTasks ++ packageBase
lazy val baseClasspaths: Seq[Setting[_]] = Classpaths.publishSettings ++ Classpaths.baseSettings
@ -866,7 +879,12 @@ object Defaults extends BuildCommon
// settings that are not specific to a configuration
lazy val projectBaseSettings: Seq[Setting[_]] = projectCore ++ paths ++ baseClasspaths ++ baseTasks ++ compileBase ++ disableAggregation
@deprecated("0.13.2", "Settings now split into AutoPlugins.")
lazy val projectBaseSettings: Seq[Setting[_]] = projectCore ++ runnerSettings ++ paths ++ baseClasspaths ++ baseTasks ++ compileBase ++ disableAggregation
// These are project level settings that MUST be on every project.
lazy val coreDefaultSettings: Seq[Setting[_]] = projectCore ++ disableAggregation
@deprecated("0.13.2", "Default settings split into `coreDefaultSettings` and IvyModule/JvmModule plugins.")
lazy val defaultSettings: Seq[Setting[_]] = projectBaseSettings ++ defaultConfigs
}
object Classpaths
@ -936,9 +954,14 @@ object Classpaths
publishArtifact in Test:== false
))
val publishSettings: Seq[Setting[_]] = publishGlobalDefaults ++ Seq(
artifacts <<= artifactDefs(defaultArtifactTasks),
packagedArtifacts <<= packaged(defaultArtifactTasks),
val jvmPublishSettings: Seq[Setting[_]] = Seq(
artifacts <<= artifactDefs(defaultArtifactTasks),
packagedArtifacts <<= packaged(defaultArtifactTasks)
)
val ivyPublishSettings: Seq[Setting[_]] = publishGlobalDefaults ++ Seq(
artifacts :== Nil,
packagedArtifacts :== Map.empty,
makePom := { val config = makePomConfiguration.value; IvyActions.makePom(ivyModule.value, config, streams.value.log); config.file },
packagedArtifact in makePom := (artifact in makePom value, makePom value),
deliver <<= deliverTask(deliverConfiguration),
@ -947,6 +970,8 @@ object Classpaths
publishLocal <<= publishTask(publishLocalConfiguration, deliverLocal),
publishM2 <<= publishTask(publishM2Configuration, deliverLocal)
)
@deprecated("0.13.2", "This has been split into jvmIvySettings and ivyPublishSettings.")
val publishSettings: Seq[Setting[_]] = ivyPublishSettings ++ jvmPublishSettings
private[this] def baseGlobalDefaults = Defaults.globalDefaults(Seq(
conflictWarning :== ConflictWarning.default("global"),
@ -977,7 +1002,7 @@ object Classpaths
}
))
val baseSettings: Seq[Setting[_]] = baseGlobalDefaults ++ sbtClassifiersTasks ++ Seq(
val ivyBaseSettings: Seq[Setting[_]] = baseGlobalDefaults ++ sbtClassifiersTasks ++ Seq(
conflictWarning := conflictWarning.value.copy(label = Reference.display(thisProjectRef.value)),
unmanagedBase := baseDirectory.value / "lib",
normalizedName := Project.normalizeModuleID(name.value),
@ -1008,14 +1033,11 @@ object Classpaths
otherResolvers := Resolver.publishMavenLocal :: publishTo.value.toList,
projectResolver <<= projectResolverTask,
projectDependencies <<= projectDependenciesTask,
libraryDependencies ++= autoLibraryDependency(autoScalaLibrary.value && !scalaHome.value.isDefined && managedScalaInstance.value, sbtPlugin.value, scalaOrganization.value, scalaVersion.value),
// TODO - Is this the appropriate split? Ivy defines this simply as
// just project + library, while the JVM plugin will define it as
// having the additional sbtPlugin + autoScala magikz.
allDependencies := {
val base = projectDependencies.value ++ libraryDependencies.value
val pluginAdjust = if(sbtPlugin.value) sbtDependency.value.copy(configurations = Some(Provided.name)) +: base else base
if(scalaHome.value.isDefined || ivyScala.value.isEmpty || !managedScalaInstance.value)
pluginAdjust
else
ScalaArtifacts.toolDependencies(scalaOrganization.value, scalaVersion.value) ++ pluginAdjust
projectDependencies.value ++ libraryDependencies.value
},
ivyScala <<= ivyScala or (scalaHome, scalaVersion in update, scalaBinaryVersion in update, scalaOrganization) { (sh,fv,bv,so) =>
Some(new IvyScala(fv, bv, Nil, filterImplicit = false, checkExplicit = true, overrideScalaVersion = false, scalaOrganization = so))
@ -1039,9 +1061,9 @@ object Classpaths
makePomConfiguration := new MakePomConfiguration(artifactPath in makePom value, projectInfo.value, None, pomExtra.value, pomPostProcess.value, pomIncludeRepository.value, pomAllRepositories.value),
deliverLocalConfiguration := deliverConfig(crossTarget.value, status = if (isSnapshot.value) "integration" else "release", logging = ivyLoggingLevel.value ),
deliverConfiguration <<= deliverLocalConfiguration,
publishConfiguration := publishConfig(packagedArtifacts.in(publish).value, if(publishMavenStyle.value) None else Some(deliver.value), resolverName = getPublishTo(publishTo.value).name, checksums = checksums.in(publish).value, logging = ivyLoggingLevel.value),
publishLocalConfiguration := publishConfig(packagedArtifacts.in(publishLocal).value, Some(deliverLocal.value), checksums.in(publishLocal).value, logging = ivyLoggingLevel.value ),
publishM2Configuration := publishConfig(packagedArtifacts.in(publishM2).value, None, resolverName = Resolver.publishMavenLocal.name, checksums = checksums.in(publishM2).value, logging = ivyLoggingLevel.value),
publishConfiguration := publishConfig(packagedArtifacts.in(publish).value, if(publishMavenStyle.value) None else Some(deliver.value), resolverName = getPublishTo(publishTo.value).name, checksums = checksums.in(publish).value, logging = ivyLoggingLevel.value, overwrite = isSnapshot.value),
publishLocalConfiguration := publishConfig(packagedArtifacts.in(publishLocal).value, Some(deliverLocal.value), checksums.in(publishLocal).value, logging = ivyLoggingLevel.value, overwrite = isSnapshot.value),
publishM2Configuration := publishConfig(packagedArtifacts.in(publishM2).value, None, resolverName = Resolver.publishMavenLocal.name, checksums = checksums.in(publishM2).value, logging = ivyLoggingLevel.value, overwrite = isSnapshot.value),
ivySbt <<= ivySbt0,
ivyModule := { val is = ivySbt.value; new is.Module(moduleSettings.value) },
transitiveUpdate <<= transitiveUpdateTask,
@ -1055,6 +1077,22 @@ object Classpaths
}
} tag(Tags.Update, Tags.Network)
)
val jvmBaseSettings: Seq[Setting[_]] = Seq(
libraryDependencies ++= autoLibraryDependency(autoScalaLibrary.value && !scalaHome.value.isDefined && managedScalaInstance.value, sbtPlugin.value, scalaOrganization.value, scalaVersion.value),
// Override the default to handle mixing in the sbtPlugin + scala dependencies.
allDependencies := {
val base = projectDependencies.value ++ libraryDependencies.value
val pluginAdjust = if(sbtPlugin.value) sbtDependency.value.copy(configurations = Some(Provided.name)) +: base else base
if(scalaHome.value.isDefined || ivyScala.value.isEmpty || !managedScalaInstance.value)
pluginAdjust
else
ScalaArtifacts.toolDependencies(scalaOrganization.value, scalaVersion.value) ++ pluginAdjust
}
)
@deprecated("0.13.2", "Split into ivyBaseSettings and jvmBaseSettings.")
val baseSettings: Seq[Setting[_]] = ivyBaseSettings ++ jvmBaseSettings
def warnResolversConflict(ress: Seq[Resolver], log: Logger) {
val resset = ress.toSet
for ((name, r) <- resset groupBy (_.name) if r.size > 1) {
@ -1209,8 +1247,12 @@ object Classpaths
def deliverConfig(outputDirectory: File, status: String = "release", logging: UpdateLogging.Value = UpdateLogging.DownloadOnly) =
new DeliverConfiguration(deliverPattern(outputDirectory), status, None, logging)
def publishConfig(artifacts: Map[Artifact, File], ivyFile: Option[File], checksums: Seq[String], resolverName: String = "local", logging: UpdateLogging.Value = UpdateLogging.DownloadOnly) =
new PublishConfiguration(ivyFile, resolverName, artifacts, checksums, logging)
@deprecated("0.13.2", "Previous semantics allowed overwriting cached files, which was unsafe. Please specify overwrite parameter.")
def publishConfig(artifacts: Map[Artifact, File], ivyFile: Option[File], checksums: Seq[String], resolverName: String, logging: UpdateLogging.Value): PublishConfiguration =
publishConfig(artifacts, ivyFile, checksums, resolverName, logging, overwrite = true)
def publishConfig(artifacts: Map[Artifact, File], ivyFile: Option[File], checksums: Seq[String], resolverName: String = "local", logging: UpdateLogging.Value = UpdateLogging.DownloadOnly, overwrite: Boolean = false) =
new PublishConfiguration(ivyFile, resolverName, artifacts, checksums, logging, overwrite)
def deliverPattern(outputPath: File): String = (outputPath / "[artifact]-[revision](-[classifier]).[ext]").absolutePath
@ -1255,19 +1297,8 @@ object Classpaths
if(useJars) Seq(pkgTask).join else psTask
}
def constructBuildDependencies: Initialize[BuildDependencies] =
loadedBuild { lb =>
import collection.mutable.HashMap
val agg = new HashMap[ProjectRef, Seq[ProjectRef]]
val cp = new HashMap[ProjectRef, Seq[ClasspathDep[ProjectRef]]]
for(lbu <- lb.units.values; rp <- lbu.defined.values)
{
val ref = ProjectRef(lbu.unit.uri, rp.id)
cp(ref) = rp.dependencies
agg(ref) = rp.aggregate
}
BuildDependencies(cp.toMap, agg.toMap)
}
def constructBuildDependencies: Initialize[BuildDependencies] = loadedBuild(lb => BuildUtil.dependencies(lb.units))
def internalDependencies: Initialize[Task[Classpath]] =
(thisProjectRef, classpathConfiguration, configuration, settingsData, buildDependencies) flatMap internalDependencies0
def unmanagedDependencies: Initialize[Task[Classpath]] =

View File

@ -44,18 +44,25 @@ object EvaluateTask
def defaultConfig(state: State): EvaluateConfig =
{
val extracted = Project.extract(state)
defaultConfig(extracted, extracted.structure)
extractedConfig(extracted, extracted.structure, state)
}
@deprecated("Use extractedConfig.", "0.13.0")
def defaultConfig(extracted: Extracted, structure: BuildStructure) =
EvaluateConfig(false, restrictions(extracted, structure), progress = executeProgress(extracted, structure))
EvaluateConfig(false, restrictions(extracted, structure), progress = defaultProgress)
@deprecated("Use other extractedConfig", "0.13.2")
def extractedConfig(extracted: Extracted, structure: BuildStructure): EvaluateConfig =
{
val workers = restrictions(extracted, structure)
val canCancel = cancelable(extracted, structure)
val progress = executeProgress(extracted, structure)
EvaluateConfig(cancelable = canCancel, restrictions = workers, progress = defaultProgress)
}
def extractedConfig(extracted: Extracted, structure: BuildStructure, state: State): EvaluateConfig =
{
val workers = restrictions(extracted, structure)
val canCancel = cancelable(extracted, structure)
val progress = executeProgress(extracted, structure, state)
EvaluateConfig(cancelable = canCancel, restrictions = workers, progress = progress)
}
@ -78,8 +85,11 @@ object EvaluateTask
def cancelable(extracted: Extracted, structure: BuildStructure): Boolean =
getSetting(Keys.cancelable, false, extracted, structure)
private[sbt] def executeProgress(extracted: Extracted, structure: BuildStructure): ExecuteProgress[Task] =
getSetting(Keys.executeProgress, new Keys.TaskProgress(defaultProgress), extracted, structure).progress
private[sbt] def executeProgress(extracted: Extracted, structure: BuildStructure, state: State): ExecuteProgress[Task] = {
import Types.const
val maker: State => Keys.TaskProgress = getSetting(Keys.executeProgress, const(new Keys.TaskProgress(defaultProgress)), extracted, structure)
maker(state).progress
}
def getSetting[T](key: SettingKey[T], default: T, extracted: Extracted, structure: BuildStructure): T =
key in extracted.currentRef get structure.data getOrElse default
@ -94,7 +104,7 @@ object EvaluateTask
{
val root = ProjectRef(pluginDef.root, Load.getRootProject(pluginDef.units)(pluginDef.root))
val pluginKey = pluginData
val config = extractedConfig(Project.extract(state), pluginDef)
val config = extractedConfig(Project.extract(state), pluginDef, state)
val evaluated = apply(pluginDef, ScopedKey(pluginKey.scope, pluginKey.key), state, root, config)
val (newS, result) = evaluated getOrElse sys.error("Plugin data does not exist for plugin definition at " + pluginDef.root)
Project.runUnloadHooks(newS) // discard states

View File

@ -0,0 +1,20 @@
package sbt
import Def.Setting
import java.net.URI
private[sbt] final class GroupedAutoPlugins(val all: Seq[AutoPlugin], val byBuild: Map[URI, Seq[AutoPlugin]])
{
def globalSettings: Seq[Setting[_]] = all.flatMap(_.globalSettings)
def buildSettings(uri: URI): Seq[Setting[_]] = byBuild.getOrElse(uri, Nil).flatMap(_.buildSettings)
}
private[sbt] object GroupedAutoPlugins
{
private[sbt] def apply(units: Map[URI, LoadedBuildUnit]): GroupedAutoPlugins =
{
val byBuild: Map[URI, Seq[AutoPlugin]] = units.mapValues(unit => unit.defined.values.flatMap(_.autoPlugins).toSeq.distinct).toMap
val all: Seq[AutoPlugin] = byBuild.values.toSeq.flatten.distinct
new GroupedAutoPlugins(all, byBuild)
}
}

View File

@ -131,6 +131,7 @@ object Keys
val crossVersion = SettingKey[CrossVersion]("cross-version", "Configures handling of the Scala version when cross-building.", CSetting)
val classpathOptions = SettingKey[ClasspathOptions]("classpath-options", "Configures handling of Scala classpaths.", DSetting)
val definedSbtPlugins = TaskKey[Set[String]]("defined-sbt-plugins", "The set of names of Plugin implementations defined by this project.", CTask)
val discoveredSbtPlugins = TaskKey[PluginDiscovery.DiscoveredNames]("discovered-sbt-plugins", "The names of sbt plugin-related modules (modules that extend Build, Plugin, AutoPlugin) defined by this project.", CTask)
val sbtPlugin = SettingKey[Boolean]("sbt-plugin", "If true, enables adding sbt as a dependency and auto-generation of the plugin descriptor file.", BMinusSetting)
val printWarnings = TaskKey[Unit]("print-warnings", "Shows warnings from compilation, including ones that weren't printed initially.", BPlusTask)
val fileInputOptions = SettingKey[Seq[String]]("file-input-options", "Options that take file input, which may invalidate the cache.", CSetting)
@ -344,7 +345,10 @@ object Keys
// wrapper to work around SI-2915
private[sbt] final class TaskProgress(val progress: ExecuteProgress[Task])
private[sbt] val executeProgress = SettingKey[TaskProgress]("executeProgress", "Experimental task execution listener.", DTask)
private[sbt] val executeProgress = SettingKey[State => TaskProgress]("executeProgress", "Experimental task execution listener.", DTask)
// Experimental in sbt 0.13.2 to enable grabing semantic compile failures.
private[sbt] val compilerReporter = TaskKey[Option[xsbti.Reporter]]("compilerReporter", "Experimental hook to listen (or send) compilation failure messages.", DTask)
val triggeredBy = Def.triggeredBy
val runBefore = Def.runBefore

View File

@ -6,19 +6,16 @@ package sbt
import java.io.File
import java.net.{URI,URL}
import compiler.{Eval,EvalImports}
import xsbt.api.{Discovered,Discovery}
import xsbti.compile.CompileOrder
import classpath.ClasspathUtilities
import scala.annotation.tailrec
import collection.mutable
import Compiler.{Compilers,Inputs}
import Compiler.Compilers
import inc.{FileValueCache, Locate}
import Project.{inScope,makeSettings}
import Def.{isDummy, ScopedKey, ScopeLocal, Setting}
import Keys.{appConfiguration, baseDirectory, configuration, fullResolvers, fullClasspath, pluginData, streams, thisProject, thisProjectRef, update}
import Keys.{exportedProducts, loadedBuild, onLoadMessage, resolvedScoped, sbtPlugin, scalacOptions, taskDefinitionKey}
import tools.nsc.reporters.ConsoleReporter
import Build.analyzed
import Attributed.data
import Scope.{GlobalScope, ThisScope}
import Types.const
@ -180,7 +177,7 @@ object Load
val keys = Index.allKeys(settings)
val attributeKeys = Index.attributeKeys(data) ++ keys.map(_.key)
val scopedKeys = keys ++ data.allKeys( (s,k) => ScopedKey(s,k))
val projectsMap = projects.mapValues(_.defined.keySet)
val projectsMap = projects.mapValues(_.defined.keySet).toMap
val keyIndex = KeyIndex(scopedKeys, projectsMap)
val aggIndex = KeyIndex.aggregate(scopedKeys, extra(keyIndex), projectsMap)
new sbt.StructureIndex(Index.stringToKeyMap(attributeKeys), Index.taskToKeyMap(data), Index.triggers(data), keyIndex, aggIndex)
@ -201,10 +198,10 @@ object Load
{
((loadedBuild in GlobalScope :== loaded) +:
transformProjectOnly(loaded.root, rootProject, injectSettings.global)) ++
inScope(GlobalScope)( pluginGlobalSettings(loaded) ) ++
inScope(GlobalScope)( pluginGlobalSettings(loaded) ++ loaded.autos.globalSettings ) ++
loaded.units.toSeq.flatMap { case (uri, build) =>
val plugins = build.unit.plugins.plugins
val pluginBuildSettings = plugins.flatMap(_.buildSettings)
val plugins = build.unit.plugins.detected.plugins.values
val pluginBuildSettings = plugins.flatMap(_.buildSettings) ++ loaded.autos.buildSettings(uri)
val pluginNotThis = plugins.flatMap(_.settings) filterNot isProjectThis
val projectSettings = build.defined flatMap { case (id, project) =>
val ref = ProjectRef(uri, id)
@ -220,9 +217,10 @@ object Load
buildSettings ++ projectSettings
}
}
@deprecated("Does not account for AutoPlugins and will be made private.", "0.13.2")
def pluginGlobalSettings(loaded: sbt.LoadedBuild): Seq[Setting[_]] =
loaded.units.toSeq flatMap { case (_, build) =>
build.unit.plugins.plugins flatMap { _.globalSettings }
build.unit.plugins.detected.plugins.values flatMap { _.globalSettings }
}
@deprecated("No longer used.", "0.13.0")
@ -368,10 +366,11 @@ object Load
def resolveProjects(loaded: sbt.PartBuild): sbt.LoadedBuild =
{
val rootProject = getRootProject(loaded.units)
new sbt.LoadedBuild(loaded.root, loaded.units map { case (uri, unit) =>
val units = loaded.units map { case (uri, unit) =>
IO.assertAbsolute(uri)
(uri, resolveProjects(uri, unit, rootProject))
})
}
new sbt.LoadedBuild(loaded.root, units)
}
def resolveProjects(uri: URI, unit: sbt.PartBuildUnit, rootProject: URI => String): sbt.LoadedBuildUnit =
{
@ -399,10 +398,10 @@ object Load
def getBuild[T](map: Map[URI, T], uri: URI): T =
map.getOrElse(uri, noBuild(uri))
def emptyBuild(uri: URI) = sys.error("No root project defined for build unit '" + uri + "'")
def noBuild(uri: URI) = sys.error("Build unit '" + uri + "' not defined.")
def noProject(uri: URI, id: String) = sys.error("No project '" + id + "' defined in '" + uri + "'.")
def noConfiguration(uri: URI, id: String, conf: String) = sys.error("No configuration '" + conf + "' defined in project '" + id + "' in '" + uri +"'")
def emptyBuild(uri: URI) = sys.error(s"No root project defined for build unit '$uri'")
def noBuild(uri: URI) = sys.error(s"Build unit '$uri' not defined.")
def noProject(uri: URI, id: String) = sys.error(s"No project '$id' defined in '$uri'.")
def noConfiguration(uri: URI, id: String, conf: String) = sys.error(s"No configuration '$conf' defined in project '$id' in '$uri'")
def loadUnit(uri: URI, localBase: File, s: State, config: sbt.LoadBuildConfiguration): sbt.BuildUnit =
{
@ -410,15 +409,13 @@ object Load
val defDir = projectStandard(normBase)
val plugs = plugins(defDir, s, config.copy(pluginManagement = config.pluginManagement.forPlugin))
val defNames = analyzed(plugs.fullClasspath) flatMap findDefinitions
val defsScala = if(defNames.isEmpty) Nil else loadDefinitions(plugs.loader, defNames)
val imports = BuildUtil.getImports(plugs.pluginNames, defNames)
val defsScala = plugs.detected.builds.values
lazy val eval = mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions)
val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase))
val memoSettings = new mutable.HashMap[File, LoadedSbtFile]
def loadProjects(ps: Seq[Project]) = loadTransitive(ps, normBase, imports, plugs, () => eval, config.injectSettings, Nil, memoSettings)
def loadProjects(ps: Seq[Project]) = loadTransitive(ps, normBase, plugs, () => eval, config.injectSettings, Nil, memoSettings, config.log)
val loadedProjectsRaw = loadProjects(initialProjects)
val hasRoot = loadedProjectsRaw.exists(_.base == normBase) || defsScala.exists(_.rootProject.isDefined)
val (loadedProjects, defaultBuildIfNone) =
@ -434,7 +431,7 @@ object Load
}
val defs = if(defsScala.isEmpty) defaultBuildIfNone :: Nil else defsScala
val loadedDefs = new sbt.LoadedDefinitions(defDir, Nil, plugs.loader, defs, loadedProjects, defNames)
val loadedDefs = new sbt.LoadedDefinitions(defDir, Nil, plugs.loader, defs, loadedProjects, plugs.detected.builds.names)
new sbt.BuildUnit(uri, normBase, loadedDefs, plugs)
}
@ -460,16 +457,22 @@ object Load
private[this] def projectsFromBuild(b: Build, base: File): Seq[Project] =
b.projectDefinitions(base).map(resolveBase(base))
private[this] def loadTransitive(newProjects: Seq[Project], buildBase: File, imports: Seq[String], plugins: sbt.LoadedPlugins, eval: () => Eval, injectSettings: InjectSettings, acc: Seq[Project], memoSettings: mutable.Map[File, LoadedSbtFile]): Seq[Project] =
private[this] def loadTransitive(newProjects: Seq[Project], buildBase: File, plugins: sbt.LoadedPlugins, eval: () => Eval, injectSettings: InjectSettings,
acc: Seq[Project], memoSettings: mutable.Map[File, LoadedSbtFile], log: Logger): Seq[Project] =
{
def loadSbtFiles(auto: AddSettings, base: File): LoadedSbtFile =
loadSettings(auto, base, imports, plugins, eval, injectSettings, memoSettings)
def loadSbtFiles(auto: AddSettings, base: File, autoPlugins: Seq[AutoPlugin], projectSettings: Seq[Setting[_]]): LoadedSbtFile =
loadSettings(auto, base, plugins, eval, injectSettings, memoSettings, autoPlugins, projectSettings)
def loadForProjects = newProjects map { project =>
val loadedSbtFiles = loadSbtFiles(project.auto, project.base)
val transformed = project.copy(settings = (project.settings: Seq[Setting[_]]) ++ loadedSbtFiles.settings)
val autoPlugins =
try plugins.detected.deducePlugins(project.plugins, log)
catch { case e: AutoPluginException => throw translateAutoPluginException(e, project) }
val autoConfigs = autoPlugins.flatMap(_.projectConfigurations)
val loadedSbtFiles = loadSbtFiles(project.auto, project.base, autoPlugins, project.settings)
// add the automatically selected settings, record the selected AutoPlugins, and register the automatically selected configurations
val transformed = project.copy(settings = loadedSbtFiles.settings).setAutoPlugins(autoPlugins).overrideConfigs(autoConfigs : _*)
(transformed, loadedSbtFiles.projects)
}
def defaultLoad = loadSbtFiles(AddSettings.defaultSbtFiles, buildBase).projects
def defaultLoad = loadSbtFiles(AddSettings.defaultSbtFiles, buildBase, Nil, Nil).projects
val (nextProjects, loadedProjects) =
if(newProjects.isEmpty) // load the .sbt files in the root directory to look for Projects
(defaultLoad, acc)
@ -481,10 +484,12 @@ object Load
if(nextProjects.isEmpty)
loadedProjects
else
loadTransitive(nextProjects, buildBase, imports, plugins, eval, injectSettings, loadedProjects, memoSettings)
loadTransitive(nextProjects, buildBase, plugins, eval, injectSettings, loadedProjects, memoSettings, log)
}
private[this] def translateAutoPluginException(e: AutoPluginException, project: Project): AutoPluginException =
e.withPrefix(s"Error determining plugins for project '${project.id}' in ${project.base}:\n")
private[this] def loadSettings(auto: AddSettings, projectBase: File, buildImports: Seq[String], loadedPlugins: sbt.LoadedPlugins, eval: ()=>Eval, injectSettings: InjectSettings, memoSettings: mutable.Map[File, LoadedSbtFile]): LoadedSbtFile =
private[this] def loadSettings(auto: AddSettings, projectBase: File, loadedPlugins: sbt.LoadedPlugins, eval: ()=>Eval, injectSettings: InjectSettings, memoSettings: mutable.Map[File, LoadedSbtFile], autoPlugins: Seq[AutoPlugin], buildScalaFiles: Seq[Setting[_]]): LoadedSbtFile =
{
lazy val defaultSbtFiles = configurationSources(projectBase)
def settings(ss: Seq[Setting[_]]) = new LoadedSbtFile(ss, Nil, Nil)
@ -499,14 +504,25 @@ object Load
lf
}
def loadSettingsFile(src: File): LoadedSbtFile =
EvaluateConfigurations.evaluateSbtFile(eval(), src, IO.readLines(src), buildImports, 0)(loader)
EvaluateConfigurations.evaluateSbtFile(eval(), src, IO.readLines(src), loadedPlugins.detected.imports, 0)(loader)
import AddSettings.{User,SbtFiles,DefaultSbtFiles,Plugins,AutoPlugins,Sequence,BuildScalaFiles}
def pluginSettings(f: Plugins) = {
val included = loadedPlugins.detected.plugins.values.filter(f.include) // don't apply the filter to AutoPlugins, only Plugins
included.flatMap(p => p.settings.filter(isProjectThis) ++ p.projectSettings)
}
// Filter the AutoPlugin settings we included based on which ones are
// intended in the AddSettings.AutoPlugins filter.
def autoPluginSettings(f: AutoPlugins) =
autoPlugins.filter(f.include).flatMap(_.projectSettings)
import AddSettings.{User,SbtFiles,DefaultSbtFiles,Plugins,Sequence}
def expand(auto: AddSettings): LoadedSbtFile = auto match {
case BuildScalaFiles => settings(buildScalaFiles)
case User => settings(injectSettings.projectLoaded(loader))
case sf: SbtFiles => loadSettings( sf.files.map(f => IO.resolve(projectBase, f)))
case sf: DefaultSbtFiles => loadSettings( defaultSbtFiles.filter(sf.include))
case f: Plugins => settings(loadedPlugins.plugins.filter(f.include).flatMap(p => p.settings.filter(isProjectThis) ++ p.projectSettings))
case p: Plugins => settings(pluginSettings(p))
case p: AutoPlugins => settings(autoPluginSettings(p))
case q: Sequence => (LoadedSbtFile.empty /: q.sequence) { (b,add) => b.merge( expand(add) ) }
}
expand(auto)
@ -599,67 +615,46 @@ object Load
config.evalPluginDef(pluginDef, pluginState)
}
@deprecated("Use ModuleUtilities.getCheckedObjects[Build].", "0.13.2")
def loadDefinitions(loader: ClassLoader, defs: Seq[String]): Seq[Build] =
defs map { definition => loadDefinition(loader, definition) }
@deprecated("Use ModuleUtilities.getCheckedObject[Build].", "0.13.2")
def loadDefinition(loader: ClassLoader, definition: String): Build =
ModuleUtilities.getObject(definition, loader).asInstanceOf[Build]
def loadPlugins(dir: File, data: PluginData, loader: ClassLoader): sbt.LoadedPlugins =
{
val (pluginNames, plugins) = if(data.classpath.isEmpty) (Nil, Nil) else {
val names = getPluginNames(data.classpath, loader)
val loaded =
try loadPlugins(loader, names)
catch {
case e: ExceptionInInitializerError =>
val cause = e.getCause
if(cause eq null) throw e else throw cause
case e: LinkageError => incompatiblePlugins(data, e)
}
(names, loaded)
}
new sbt.LoadedPlugins(dir, data, loader, plugins, pluginNames)
}
private[this] def incompatiblePlugins(data: PluginData, t: LinkageError): Nothing =
{
val evicted = data.report.toList.flatMap(_.configurations.flatMap(_.evicted))
val evictedModules = evicted map { id => (id.organization, id.name) } distinct ;
val evictedStrings = evictedModules map { case (o,n) => o + ":" + n }
val msgBase = "Binary incompatibility in plugins detected."
val msgExtra = if(evictedStrings.isEmpty) "" else "\nNote that conflicts were resolved for some dependencies:\n\t" + evictedStrings.mkString("\n\t")
throw new IncompatiblePluginsException(msgBase + msgExtra, t)
}
new sbt.LoadedPlugins(dir, data, loader, PluginDiscovery.discoverAll(data, loader))
@deprecated("Replaced by the more general PluginDiscovery.binarySourceModuleNames and will be made private.", "0.13.2")
def getPluginNames(classpath: Seq[Attributed[File]], loader: ClassLoader): Seq[String] =
( binaryPlugins(data(classpath), loader) ++ (analyzed(classpath) flatMap findPlugins) ).distinct
PluginDiscovery.binarySourceModuleNames(classpath, loader, PluginDiscovery.Paths.Plugins, classOf[Plugin].getName)
@deprecated("Use PluginDiscovery.binaryModuleNames.", "0.13.2")
def binaryPlugins(classpath: Seq[File], loader: ClassLoader): Seq[String] =
{
import collection.JavaConversions._
loader.getResources("sbt/sbt.plugins").toSeq.filter(onClasspath(classpath)) flatMap { u =>
IO.readLinesURL(u).map( _.trim).filter(!_.isEmpty)
}
}
PluginDiscovery.binaryModuleNames(classpath, loader, PluginDiscovery.Paths.Plugins)
@deprecated("Use PluginDiscovery.onClasspath", "0.13.2")
def onClasspath(classpath: Seq[File])(url: URL): Boolean =
IO.urlAsFile(url) exists (classpath.contains _)
PluginDiscovery.onClasspath(classpath)(url)
@deprecated("Use ModuleUtilities.getCheckedObjects[Plugin].", "0.13.2")
def loadPlugins(loader: ClassLoader, pluginNames: Seq[String]): Seq[Plugin] =
pluginNames.map(pluginName => loadPlugin(pluginName, loader))
ModuleUtilities.getCheckedObjects[Plugin](pluginNames, loader).map(_._2)
@deprecated("Use ModuleUtilities.getCheckedObject[Plugin].", "0.13.2")
def loadPlugin(pluginName: String, loader: ClassLoader): Plugin =
ModuleUtilities.getObject(pluginName, loader).asInstanceOf[Plugin]
ModuleUtilities.getCheckedObject[Plugin](pluginName, loader)
@deprecated("No longer used.", "0.13.2")
def findPlugins(analysis: inc.Analysis): Seq[String] = discover(analysis, "sbt.Plugin")
@deprecated("No longer used.", "0.13.2")
def findDefinitions(analysis: inc.Analysis): Seq[String] = discover(analysis, "sbt.Build")
@deprecated("Use PluginDiscovery.sourceModuleNames", "0.13.2")
def discover(analysis: inc.Analysis, subclasses: String*): Seq[String] =
{
val subclassSet = subclasses.toSet
val ds = Discovery(subclassSet, Set.empty)(Tests.allDefs(analysis))
ds.flatMap {
case (definition, Discovered(subs,_,_,true)) =>
if((subs & subclassSet).isEmpty) Nil else definition.name :: Nil
case _ => Nil
}
}
PluginDiscovery.sourceModuleNames(analysis, subclasses : _*)
def initialSession(structure: sbt.BuildStructure, rootEval: () => Eval, s: State): SessionSettings = {
val session = s get Keys.sessionSettings
@ -748,4 +743,4 @@ final case class LoadBuildConfiguration(stagingDirectory: File, classpath: Seq[A
lazy val globalPluginNames = if(classpath.isEmpty) Nil else Load.getPluginNames(classpath, pluginManagement.initialLoader)
}
final class IncompatiblePluginsException(msg: String, cause: Throwable) extends Exception(msg, cause)
final class IncompatiblePluginsException(msg: String, cause: Throwable) extends Exception(msg, cause)

View File

@ -89,7 +89,7 @@ object BuiltinCommands
def ScriptCommands: Seq[Command] = Seq(ignore, exit, Script.command, setLogLevel, early, act, nop)
def DefaultCommands: Seq[Command] = Seq(ignore, help, completionsCommand, about, tasks, settingsCommand, loadProject,
projects, project, reboot, read, history, set, sessionCommand, inspect, loadProjectImpl, loadFailed, Cross.crossBuild, Cross.switchVersion,
setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, setLogLevel,
setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, setLogLevel, plugin, plugins,
ifLast, multi, shell, continuous, eval, alias, append, last, lastGrep, export, boot, nop, call, exit, early, initialize, act) ++
compatCommands
def DefaultBootCommands: Seq[String] = LoadProject :: (IfLast + " " + Shell) :: Nil
@ -125,7 +125,8 @@ object BuiltinCommands
def aboutPlugins(e: Extracted): String =
{
val allPluginNames = e.structure.units.values.flatMap(_.unit.plugins.pluginNames).toSeq.distinct
def list(b: BuildUnit) = b.plugins.detected.autoPlugins.map(_.value.label) ++ b.plugins.detected.plugins.names
val allPluginNames = e.structure.units.values.flatMap(u => list(u.unit)).toSeq.distinct
if(allPluginNames.isEmpty) "" else allPluginNames.mkString("Available Plugins: ", ", ", "")
}
def aboutScala(s: State, e: Extracted): String =
@ -374,6 +375,20 @@ object BuiltinCommands
Help.detailOnly(taskDetail(allTaskAndSettingKeys(s)))
else
Help.empty
def plugins = Command.command(PluginsCommand, pluginsBrief, pluginsDetailed) { s =>
val helpString = PluginsDebug.helpAll(s)
System.out.println(helpString)
s
}
val pluginParser: State => Parser[AutoPlugin] = s => {
val autoPlugins: Map[String, AutoPlugin] = PluginsDebug.autoPluginMap(s)
token(Space) ~> Act.knownIDParser(autoPlugins, "plugin")
}
def plugin = Command(PluginCommand)(pluginParser) { (s, plugin) =>
val helpString = PluginsDebug.help(plugin, s)
System.out.println(helpString)
s
}
def projects = Command(ProjectsCommand, (ProjectsCommand, projectsBrief), projectsDetailed )(s => projectsParser(s).?) {
case (s, Some(modifyBuilds)) => transformExtraBuilds(s, modifyBuilds)

View File

@ -20,7 +20,7 @@ object Output
def last(keys: Values[_], streams: Streams, printLines: Seq[String] => Unit)(implicit display: Show[ScopedKey[_]]): Unit =
last(keys, streams, printLines, None)(display)
def last(keys: Values[_], streams: Streams, printLines: Seq[String] => Unit, sid: Option[String])(implicit display: Show[ScopedKey[_]]): Unit =
def last(keys: Values[_], streams: Streams, printLines: Seq[String] => Unit, sid: Option[String])(implicit display: Show[ScopedKey[_]]): Unit =
printLines( flatLines(lastLines(keys, streams, sid))(idFun) )
def last(file: File, printLines: Seq[String] => Unit, tailDelim: String = DefaultTail): Unit =
@ -55,7 +55,17 @@ object Output
@deprecated("Explicitly provide None for the stream ID.", "0.13.0")
def lastLines(key: ScopedKey[_], mgr: Streams): Seq[String] = lastLines(key, mgr, None)
def lastLines(key: ScopedKey[_], mgr: Streams, sid: Option[String]): Seq[String] = mgr.use(key) { s => IO.readLines(s.readText( Project.fillTaskAxis(key), sid )) }
def lastLines(key: ScopedKey[_], mgr: Streams, sid: Option[String]): Seq[String] =
mgr.use(key) { s =>
// Workaround for #1155 - Keys.streams are always scoped by the task they're included in
// but are keyed by the Keys.streams key. I think this isn't actually a workaround, but
// is how things are expected to work now.
// You can see where streams are injected using their own key scope in
// EvaluateTask.injectStreams.
val streamScopedKey: ScopedKey[_] = ScopedKey(Project.fillTaskAxis(key).scope, Keys.streams.key)
val tmp = s.readText( streamScopedKey, sid )
IO.readLines(tmp)
}
def tailLines(file: File, tailDelim: String): Seq[String] = headLines(IO.readLines(file).reverse, tailDelim).reverse

View File

@ -0,0 +1,143 @@
package sbt
import java.io.File
import java.net.URL
import Attributed.data
import Build.analyzed
import xsbt.api.{Discovered,Discovery}
object PluginDiscovery
{
/** Relative paths of resources that list top-level modules that are available.
* Normally, the classes for those modules will be in the same classpath entry as the resource. */
object Paths
{
final val AutoPlugins = "sbt/sbt.autoplugins"
final val Plugins = "sbt/sbt.plugins"
final val Builds = "sbt/sbt.builds"
}
/** Names of top-level modules that subclass sbt plugin-related classes: [[Plugin]], [[AutoPlugin]], and [[Build]]. */
final class DiscoveredNames(val plugins: Seq[String], val autoPlugins: Seq[String], val builds: Seq[String])
def emptyDiscoveredNames: DiscoveredNames = new DiscoveredNames(Nil, Nil, Nil)
/** Discovers and loads the sbt-plugin-related top-level modules from the classpath and source analysis in `data` and using the provided class `loader`. */
def discoverAll(data: PluginData, loader: ClassLoader): DetectedPlugins =
{
def discover[T](resource: String)(implicit mf: reflect.ClassManifest[T]) =
binarySourceModules[T](data, loader, resource)
import Paths._
// TODO - Fix this once we can autodetect AutoPlugins defined by sbt itself.
val defaultAutoPlugins = Seq(
"sbt.plugins.IvyPlugin" -> sbt.plugins.IvyPlugin,
"sbt.plugins.JvmPlugin" -> sbt.plugins.JvmPlugin,
"sbt.plugins.CorePlugin" -> sbt.plugins.CorePlugin
)
val detectedAutoPugins = discover[AutoPlugin](AutoPlugins)
val allAutoPlugins = (defaultAutoPlugins ++ detectedAutoPugins.modules) map { case (name, value) =>
DetectedAutoPlugin(name, value, sbt.Plugins.hasAutoImportGetter(value, loader))
}
new DetectedPlugins(discover[Plugin](Plugins), allAutoPlugins, discover[Build](Builds))
}
/** Discovers the sbt-plugin-related top-level modules from the provided source `analysis`. */
def discoverSourceAll(analysis: inc.Analysis): DiscoveredNames =
{
def discover[T](implicit mf: reflect.ClassManifest[T]): Seq[String] =
sourceModuleNames(analysis, mf.erasure.getName)
new DiscoveredNames(discover[Plugin], discover[AutoPlugin], discover[Build])
}
// TODO: for 0.14.0, consider consolidating into a single file, which would make the classpath search 4x faster
/** Writes discovered module `names` to zero or more files in `dir` as per [[writeDescriptor]] and returns the list of files written. */
def writeDescriptors(names: DiscoveredNames, dir: File): Seq[File] =
{
import Paths._
val files =
writeDescriptor(names.plugins, dir, Plugins) ::
writeDescriptor(names.autoPlugins, dir, AutoPlugins) ::
writeDescriptor(names.builds, dir, Builds) ::
Nil
files.flatMap(_.toList)
}
/** Stores the module `names` in `dir / path`, one per line, unless `names` is empty and then the file is deleted and `None` returned. */
def writeDescriptor(names: Seq[String], dir: File, path: String): Option[File] =
{
val descriptor: File = new File(dir, path)
if(names.isEmpty)
{
IO.delete(descriptor)
None
}
else
{
IO.writeLines(descriptor, names.distinct.sorted)
Some(descriptor)
}
}
/** Discovers the names of top-level modules listed in resources named `resourceName` as per [[binaryModuleNames]] or
* available as analyzed source and extending from any of `subclasses` as per [[sourceModuleNames]]. */
def binarySourceModuleNames(classpath: Seq[Attributed[File]], loader: ClassLoader, resourceName: String, subclasses: String*): Seq[String] =
(
binaryModuleNames(data(classpath), loader, resourceName) ++
(analyzed(classpath) flatMap ( a => sourceModuleNames(a, subclasses : _*) ))
).distinct
/** Discovers top-level modules in `analysis` that inherit from any of `subclasses`. */
def sourceModuleNames(analysis: inc.Analysis, subclasses: String*): Seq[String] =
{
val subclassSet = subclasses.toSet
val ds = Discovery(subclassSet, Set.empty)(Tests.allDefs(analysis))
ds.flatMap {
case (definition, Discovered(subs,_,_,true)) =>
if((subs & subclassSet).isEmpty) Nil else definition.name :: Nil
case _ => Nil
}
}
/** Obtains the list of modules identified in all resource files `resourceName` from `loader` that are on `classpath`.
* `classpath` and `loader` are both required to ensure that `loader`
* doesn't bring in any resources outside of the intended `classpath`, such as from parent loaders. */
def binaryModuleNames(classpath: Seq[File], loader: ClassLoader, resourceName: String): Seq[String] =
{
import collection.JavaConversions._
loader.getResources(resourceName).toSeq.filter(onClasspath(classpath)) flatMap { u =>
IO.readLinesURL(u).map( _.trim).filter(!_.isEmpty)
}
}
/** Returns `true` if `url` is an entry in `classpath`.*/
def onClasspath(classpath: Seq[File])(url: URL): Boolean =
IO.urlAsFile(url) exists (classpath.contains _)
private[sbt] def binarySourceModules[T](data: PluginData, loader: ClassLoader, resourceName: String)(implicit mf: reflect.ClassManifest[T]): DetectedModules[T] =
{
val classpath = data.classpath
val namesAndValues = if(classpath.isEmpty) Nil else {
val names = binarySourceModuleNames(classpath, loader, resourceName, mf.erasure.getName)
loadModules[T](data, names, loader)
}
new DetectedModules(namesAndValues)
}
private[this] def loadModules[T: ClassManifest](data: PluginData, names: Seq[String], loader: ClassLoader): Seq[(String,T)] =
try ModuleUtilities.getCheckedObjects[T](names, loader)
catch {
case e: ExceptionInInitializerError =>
val cause = e.getCause
if(cause eq null) throw e else throw cause
case e: LinkageError => incompatiblePlugins(data, e)
}
private[this] def incompatiblePlugins(data: PluginData, t: LinkageError): Nothing =
{
val evicted = data.report.toList.flatMap(_.configurations.flatMap(_.evicted))
val evictedModules = evicted map { id => (id.organization, id.name) } distinct ;
val evictedStrings = evictedModules map { case (o,n) => o + ":" + n }
val msgBase = "Binary incompatibility in plugins detected."
val msgExtra = if(evictedStrings.isEmpty) "" else "\nNote that conflicts were resolved for some dependencies:\n\t" + evictedStrings.mkString("\n\t")
throw new IncompatiblePluginsException(msgBase + msgExtra, t)
}
}

View File

@ -0,0 +1,327 @@
package sbt
/*
TODO:
- index all available AutoPlugins to get the tasks that will be added
- error message when a task doesn't exist that it would be provided by plugin x, enabled by natures y,z, blocked by a, b
*/
import logic.{Atom, Clause, Clauses, Formula, Literal, Logic, Negated}
import Logic.{CyclicNegation, InitialContradictions, InitialOverlap, LogicException}
import Def.Setting
import Plugins._
import annotation.tailrec
/**
An AutoPlugin defines a group of settings and the conditions where the settings are automatically added to a build (called "activation").
The `requires` and `trigger` methods together define the conditions, and a method like `projectSettings` defines the settings to add.
Steps for plugin authors:
1. Determine if the AutoPlugin should automatically be activated when all requirements are met, or should be opt-in.
2. Determine the [[AutoPlugins]]s that, when present (or absent), act as the requirements for the AutoPlugin.
3. Determine the settings/configurations to that the AutoPlugin injects when activated.
4. Determine the keys and other names to be automatically imported to *.sbt scripts.
For example, the following will automatically add the settings in `projectSettings`
to a project that has both the `Web` and `Javascript` plugins enabled.
object Plugin extends sbt.AutoPlugin {
override def requires = Web && Javascript
override def trigger = allRequirements
override def projectSettings = Seq(...)
object autoImport {
lazy val obfuscate = taskKey[Seq[File]]("Obfuscates the source.")
}
}
Steps for users:
1. Add dependencies on plugins in `project/plugins.sbt` as usual with `addSbtPlugin`
2. Add key plugins to Projects, which will automatically select the plugin + dependent plugin settings to add for those Projects.
3. Exclude plugins, if desired.
For example, given plugins Web and Javascript (perhaps provided by plugins added with addSbtPlugin),
<Project>.addPlugins( Web && Javascript )
will activate `MyPlugin` defined above and have its settings automatically added. If the user instead defines
<Project>.addPlugins( Web && Javascript ).disablePlugins(MyPlugin)
then the `MyPlugin` settings (and anything that activates only when `MyPlugin` is activated) will not be added.
*/
abstract class AutoPlugin extends Plugins.Basic with PluginsFunctions
{
/** Determines whether this AutoPlugin will be activated for this project when the `requires` clause is satisfied.
*
* When this method returns `allRequirements`, and `requires` method returns `Web && Javascript`, this plugin
* instance will be added automatically if the `Web` and `Javascript` plugins are enbled.
*
* When this method returns `noTrigger`, and `requires` method returns `Web && Javascript`, this plugin
* instance will be added only if the build user enables it, but it will automatically add both `Web` and `Javascript`. */
def trigger: PluginTrigger = noTrigger
/** This AutoPlugin requires the plugins the [[Plugins]] matcher returned by this method. See [[trigger]].
*/
def requires: Plugins = empty
val label: String = getClass.getName.stripSuffix("$")
override def toString: String = label
/** The [[Configuration]]s to add to each project that activates this AutoPlugin.*/
def projectConfigurations: Seq[Configuration] = Nil
/** The [[Setting]]s to add in the scope of each project that activates this AutoPlugin. */
def projectSettings: Seq[Setting[_]] = Nil
/** The [[Setting]]s to add to the build scope for each project that activates this AutoPlugin.
* The settings returned here are guaranteed to be added to a given build scope only once
* regardless of how many projects for that build activate this AutoPlugin. */
def buildSettings: Seq[Setting[_]] = Nil
/** The [[Setting]]s to add to the global scope exactly once if any project activates this AutoPlugin. */
def globalSettings: Seq[Setting[_]] = Nil
// TODO?: def commands: Seq[Command]
private[sbt] def unary_! : Exclude = Exclude(this)
/** If this plugin does not have any requirements, it means it is actually a root plugin. */
private[sbt] final def isRoot: Boolean =
requires match {
case Empty => true
case _ => false
}
/** If this plugin does not have any requirements, it means it is actually a root plugin. */
private[sbt] final def isAlwaysEnabled: Boolean =
isRoot && (trigger == AllRequirements)
}
/** An error that occurs when auto-plugins aren't configured properly.
* It translates the error from the underlying logic system to be targeted at end users. */
final class AutoPluginException private(val message: String, val origin: Option[LogicException]) extends RuntimeException(message)
{
/** Prepends `p` to the error message derived from `origin`. */
def withPrefix(p: String) = new AutoPluginException(p + message, origin)
}
object AutoPluginException
{
def apply(msg: String): AutoPluginException = new AutoPluginException(msg, None)
def apply(origin: LogicException): AutoPluginException = new AutoPluginException(Plugins.translateMessage(origin), Some(origin))
}
sealed trait PluginTrigger
case object AllRequirements extends PluginTrigger
case object NoTrigger extends PluginTrigger
/** An expression that matches `AutoPlugin`s. */
sealed trait Plugins {
def && (o: Basic): Plugins
}
sealed trait PluginsFunctions
{
/** [[Plugins]] instance that doesn't require any [[Plugins]]s. */
def empty: Plugins = Plugins.Empty
/** This plugin is activated when all required plugins are present. */
def allRequirements: PluginTrigger = AllRequirements
/** This plugin is activated only when it is manually activated. */
def noTrigger: PluginTrigger = NoTrigger
}
object Plugins extends PluginsFunctions
{
/** Given the available auto plugins `defined`, returns a function that selects [[AutoPlugin]]s for the provided [[AutoPlugin]]s.
* The [[AutoPlugin]]s are topologically sorted so that a required [[AutoPlugin]] comes before its requiring [[AutoPlugin]].*/
def deducer(defined0: List[AutoPlugin]): (Plugins, Logger) => Seq[AutoPlugin] =
if(defined0.isEmpty) (_, _) => Nil
else
{
// TODO: defined should return all the plugins
val allReqs = (defined0 flatMap { asRequirements }).toSet
val diff = allReqs diff defined0.toSet
val defined = if (!diff.isEmpty) diff.toList ::: defined0
else defined0
val byAtom = defined map { x => (Atom(x.label), x) }
val byAtomMap = byAtom.toMap
if(byAtom.size != byAtomMap.size) duplicateProvidesError(byAtom)
// Ignore clauses for plugins that does not require anything else.
// Avoids the requirement for pure Nature strings *and* possible
// circular dependencies in the logic.
val allRequirementsClause = defined.filterNot(_.isRoot).flatMap(d => asRequirementsClauses(d))
val allEnabledByClause = defined.filterNot(_.isRoot).flatMap(d => asEnabledByClauses(d))
(requestedPlugins, log) => {
val alwaysEnabled: List[AutoPlugin] = defined.filter(_.isAlwaysEnabled)
val knowlege0: Set[Atom] = ((flatten(requestedPlugins) ++ alwaysEnabled) collect {
case x: AutoPlugin => Atom(x.label)
}).toSet
val clauses = Clauses((allRequirementsClause ::: allEnabledByClause) filterNot { _.head subsetOf knowlege0 })
log.debug(s"deducing auto plugins based on known facts ${knowlege0.toString} and clauses ${clauses.toString}")
Logic.reduce(clauses, (flattenConvert(requestedPlugins) ++ convertAll(alwaysEnabled)).toSet) match {
case Left(problem) => throw AutoPluginException(problem)
case Right(results) =>
log.debug(s" :: deduced result: ${results}")
val selectedAtoms: List[Atom] = results.ordered
val selectedPlugins = selectedAtoms map { a =>
byAtomMap.getOrElse(a, throw AutoPluginException(s"${a} was not found in atom map."))
}
val forbidden: Set[AutoPlugin] = (selectedPlugins flatMap { Plugins.asExclusions }).toSet
val c = selectedPlugins.toSet & forbidden
if (!c.isEmpty) {
exlusionConflictError(requestedPlugins, selectedPlugins, c.toSeq sortBy {_.label})
}
val retval = topologicalSort(selectedPlugins, log)
log.debug(s" :: sorted deduced result: ${retval.toString}")
retval
}
}
}
private[sbt] def topologicalSort(ns: List[AutoPlugin], log: Logger): List[AutoPlugin] = {
log.debug(s"sorting: ns: ${ns.toString}")
@tailrec def doSort(found0: List[AutoPlugin], notFound0: List[AutoPlugin], limit0: Int): List[AutoPlugin] = {
log.debug(s" :: sorting:: found: ${found0.toString} not found ${notFound0.toString}")
if (limit0 < 0) throw AutoPluginException(s"Failed to sort ${ns} topologically")
else if (notFound0.isEmpty) found0
else {
val (found1, notFound1) = notFound0 partition { n => asRequirements(n).toSet subsetOf found0.toSet }
doSort(found0 ::: found1, notFound1, limit0 - 1)
}
}
val (roots, nonRoots) = ns partition (_.isRoot)
doSort(roots, nonRoots, ns.size * ns.size + 1)
}
private[sbt] def translateMessage(e: LogicException) = e match {
case ic: InitialContradictions => s"Contradiction in selected plugins. These plugins were both included and excluded: ${literalsString(ic.literals.toSeq)}"
case io: InitialOverlap => s"Cannot directly enable plugins. Plugins are enabled when their required plugins are satisifed. The directly selected plugins were: ${literalsString(io.literals.toSeq)}"
case cn: CyclicNegation => s"Cycles in plugin requirements cannot involve excludes. The problematic cycle is: ${literalsString(cn.cycle)}"
}
private[this] def literalsString(lits: Seq[Literal]): String =
lits map { case Atom(l) => l; case Negated(Atom(l)) => l } mkString(", ")
private[this] def duplicateProvidesError(byAtom: Seq[(Atom, AutoPlugin)]) {
val dupsByAtom = byAtom.groupBy(_._1).mapValues(_.map(_._2))
val dupStrings = for( (atom, dups) <- dupsByAtom if dups.size > 1 ) yield
s"${atom.label} by ${dups.mkString(", ")}"
val (ns, nl) = if(dupStrings.size > 1) ("s", "\n\t") else ("", " ")
val message = s"Plugin$ns provided by multiple AutoPlugins:$nl${dupStrings.mkString(nl)}"
throw AutoPluginException(message)
}
private[this] def exlusionConflictError(requested: Plugins, selected: Seq[AutoPlugin], conflicting: Seq[AutoPlugin]) {
def listConflicts(ns: Seq[AutoPlugin]) = (ns map { c =>
val reasons = (if (flatten(requested) contains c) List("requested")
else Nil) ++
(if (c.requires != empty && c.trigger == allRequirements) List(s"enabled by ${c.requires.toString}")
else Nil) ++
{
val reqs = selected filter { x => asRequirements(x) contains c }
if (!reqs.isEmpty) List(s"""required by ${reqs.mkString(", ")}""")
else Nil
} ++
{
val exs = selected filter { x => asExclusions(x) contains c }
if (!exs.isEmpty) List(s"""excluded by ${exs.mkString(", ")}""")
else Nil
}
s""" - conflict: ${c.label} is ${reasons.mkString("; ")}"""
}).mkString("\n")
throw AutoPluginException(s"""Contradiction in enabled plugins:
- requested: ${requested.toString}
- enabled: ${selected.mkString(", ")}
${listConflicts(conflicting)}""")
}
private[sbt] final object Empty extends Plugins {
def &&(o: Basic): Plugins = o
override def toString = "<none>"
}
/** An included or excluded Nature/Plugin. TODO: better name than Basic. Also, can we dump
* this class.
*/
sealed abstract class Basic extends Plugins {
def &&(o: Basic): Plugins = And(this :: o :: Nil)
}
private[sbt] final case class Exclude(n: AutoPlugin) extends Basic {
override def toString = s"!$n"
}
private[sbt] final case class And(plugins: List[Basic]) extends Plugins {
def &&(o: Basic): Plugins = And(o :: plugins)
override def toString = plugins.mkString(" && ")
}
private[sbt] def and(a: Plugins, b: Plugins) = b match {
case Empty => a
case And(ns) => (a /: ns)(_ && _)
case b: Basic => a && b
}
private[sbt] def remove(a: Plugins, del: Set[Basic]): Plugins = a match {
case b: Basic => if(del(b)) Empty else b
case Empty => Empty
case And(ns) =>
val removed = ns.filterNot(del)
if(removed.isEmpty) Empty else And(removed)
}
/** Defines enabled-by clauses for `ap`. */
private[sbt] def asEnabledByClauses(ap: AutoPlugin): List[Clause] =
// `ap` is the head and the required plugins for `ap` is the body.
if (ap.trigger == AllRequirements) Clause( convert(ap.requires), Set(Atom(ap.label)) ) :: Nil
else Nil
/** Defines requirements clauses for `ap`. */
private[sbt] def asRequirementsClauses(ap: AutoPlugin): List[Clause] =
// required plugin is the head and `ap` is the body.
asRequirements(ap) map { x => Clause( convert(ap), Set(Atom(x.label)) ) }
private[sbt] def asRequirements(ap: AutoPlugin): List[AutoPlugin] = flatten(ap.requires).toList collect {
case x: AutoPlugin => x
}
private[sbt] def asExclusions(ap: AutoPlugin): List[AutoPlugin] = flatten(ap.requires).toList collect {
case Exclude(x) => x
}
private[this] def flattenConvert(n: Plugins): Seq[Literal] = n match {
case And(ns) => convertAll(ns)
case b: Basic => convertBasic(b) :: Nil
case Empty => Nil
}
private[sbt] def flatten(n: Plugins): Seq[Basic] = n match {
case And(ns) => ns
case b: Basic => b :: Nil
case Empty => Nil
}
private[this] def convert(n: Plugins): Formula = n match {
case And(ns) => convertAll(ns).reduce[Formula](_ && _)
case b: Basic => convertBasic(b)
case Empty => Formula.True
}
private[this] def convertBasic(b: Basic): Literal = b match {
case Exclude(n) => !convertBasic(n)
case a: AutoPlugin => Atom(a.label)
}
private[this] def convertAll(ns: Seq[Basic]): Seq[Literal] = ns map convertBasic
/** True if the trigger clause `n` is satisifed by `model`. */
def satisfied(n: Plugins, model: Set[AutoPlugin]): Boolean =
flatten(n) forall {
case Exclude(a) => !model(a)
case ap: AutoPlugin => model(ap)
}
private[sbt] def hasAutoImportGetter(ap: AutoPlugin, loader: ClassLoader): Boolean = {
import reflect.runtime.{universe => ru}
import util.control.Exception.catching
val m = ru.runtimeMirror(loader)
val im = m.reflect(ap)
val hasGetterOpt = catching(classOf[ScalaReflectionException]) opt {
im.symbol.asType.toType.declaration(ru.newTermName("autoImport")) match {
case ru.NoSymbol => false
case sym => sym.asTerm.isGetter
}
}
hasGetterOpt getOrElse false
}
}

View File

@ -0,0 +1,382 @@
package sbt
import Def.Setting
import Plugins._
import PluginsDebug._
import java.net.URI
private[sbt] class PluginsDebug(val available: List[AutoPlugin], val nameToKey: Map[String, AttributeKey[_]], val provided: Relation[AutoPlugin, AttributeKey[_]])
{
/** The set of [[AutoPlugin]]s that might define a key named `keyName`.
* Because plugins can define keys in different scopes, this should only be used as a guideline. */
def providers(keyName: String): Set[AutoPlugin] = nameToKey.get(keyName) match {
case None => Set.empty
case Some(key) => provided.reverse(key)
}
/** Describes alternative approaches for defining key [[keyName]] in [[context]].*/
def toEnable(keyName: String, context: Context): List[PluginEnable] =
providers(keyName).toList.map(plugin => pluginEnable(context, plugin))
/** Provides text to suggest how [[notFoundKey]] can be defined in [[context]]. */
def debug(notFoundKey: String, context: Context): String =
{
val (activated, deactivated) = Util.separate(toEnable(notFoundKey, context)) {
case pa: PluginActivated => Left(pa)
case pd: EnableDeactivated => Right(pd)
}
val activePrefix = if(activated.nonEmpty) s"Some already activated plugins define $notFoundKey: ${activated.mkString(", ")}\n" else ""
activePrefix + debugDeactivated(notFoundKey, deactivated)
}
private[this] def debugDeactivated(notFoundKey: String, deactivated: Seq[EnableDeactivated]): String =
{
val (impossible, possible) = Util.separate(deactivated) {
case pi: PluginImpossible => Left(pi)
case pr: PluginRequirements => Right(pr)
}
if(possible.nonEmpty) {
val explained = possible.map(explainPluginEnable)
val possibleString =
if(explained.size > 1) explained.zipWithIndex.map{case (s,i) => s"$i. $s"}.mkString("Multiple plugins are available that can provide $notFoundKey:\n", "\n", "")
else s"$notFoundKey is provided by an available (but not activated) plugin:\n${explained.mkString}"
def impossiblePlugins = impossible.map(_.plugin.label).mkString(", ")
val imPostfix = if(impossible.isEmpty) "" else s"\n\nThere are other available plugins that provide $notFoundKey, but they are impossible to add: $impossiblePlugins"
possibleString + imPostfix
}
else if(impossible.isEmpty)
s"No available plugin provides key $notFoundKey."
else {
val explanations = impossible.map(explainPluginEnable)
explanations.mkString(s"Plugins are available that could provide $notFoundKey, but they are impossible to add:\n\t", "\n\t", "")
}
}
/** Text that suggests how to activate [[plugin]] in [[context]] if possible and if it is not already activated.*/
def help(plugin: AutoPlugin, context: Context): String =
if(context.enabled.contains(plugin))
activatedHelp(plugin)
else
deactivatedHelp(plugin, context)
private def activatedHelp(plugin: AutoPlugin): String =
{
val prefix = s"${plugin.label} is activated."
val keys = provided.forward(plugin)
val keysString = if(keys.isEmpty) "" else s"\nIt may affect these keys: ${multi(keys.toList.map(_.label))}"
val configs = plugin.projectConfigurations
val confsString = if(configs.isEmpty) "" else s"\nIt defines these configurations: ${multi(configs.map(_.name))}"
prefix + keysString + confsString
}
private def deactivatedHelp(plugin: AutoPlugin, context: Context): String =
{
val prefix = s"${plugin.label} is NOT activated."
val keys = provided.forward(plugin)
val keysString = if(keys.isEmpty) "" else s"\nActivating it may affect these keys: ${multi(keys.toList.map(_.label))}"
val configs = plugin.projectConfigurations
val confsString = if(configs.isEmpty) "" else s"\nActivating it will define these configurations: ${multi(configs.map(_.name))}"
val toActivate = explainPluginEnable(pluginEnable(context, plugin))
s"$prefix$keysString$confsString\n$toActivate"
}
private[this] def multi(strs: Seq[String]): String = strs.mkString(if(strs.size > 4) "\n\t" else ", ")
}
private[sbt] object PluginsDebug
{
def helpAll(s: State): String =
if(Project.isProjectLoaded(s))
{
val extracted = Project.extract(s)
import extracted._
def helpBuild(uri: URI, build: LoadedBuildUnit): String =
{
val pluginStrings = for(plugin <- availableAutoPlugins(build)) yield {
val activatedIn = build.defined.values.toList.filter(_.autoPlugins.contains(plugin)).map(_.id)
val actString = if(activatedIn.nonEmpty) activatedIn.mkString(": enabled in ", ", ", "") else "" // TODO: deal with large builds
s"\n\t${plugin.label}$actString"
}
s"In $uri${pluginStrings.mkString}"
}
val buildStrings = for((uri, build) <- structure.units) yield helpBuild(uri, build)
buildStrings.mkString("\n")
}
else
"No project is currently loaded."
def autoPluginMap(s: State): Map[String, AutoPlugin] =
{
val extracted = Project.extract(s)
import extracted._
structure.units.values.toList.flatMap(availableAutoPlugins).map(plugin => (plugin.label, plugin)).toMap
}
private[this] def availableAutoPlugins(build: LoadedBuildUnit): Seq[AutoPlugin] =
build.unit.plugins.detected.autoPlugins map {_.value}
def help(plugin: AutoPlugin, s: State): String =
{
val extracted = Project.extract(s)
import extracted._
def definesPlugin(p: ResolvedProject): Boolean = p.autoPlugins.contains(plugin)
def projectForRef(ref: ProjectRef): ResolvedProject = get(Keys.thisProject in ref)
val perBuild: Map[URI, Set[AutoPlugin]] = structure.units.mapValues(unit => availableAutoPlugins(unit).toSet)
val pluginsThisBuild = perBuild.getOrElse(currentRef.build, Set.empty).toList
lazy val context = Context(currentProject.plugins, currentProject.autoPlugins, Plugins.deducer(pluginsThisBuild), pluginsThisBuild, s.log)
lazy val debug = PluginsDebug(context.available)
if(!pluginsThisBuild.contains(plugin)) {
val availableInBuilds: List[URI] = perBuild.toList.filter(_._2(plugin)).map(_._1)
s"Plugin ${plugin.label} is only available in builds:\n\t${availableInBuilds.mkString("\n\t")}\nSwitch to a project in one of those builds using `project` and rerun this command for more information."
} else if(definesPlugin(currentProject))
debug.activatedHelp(plugin)
else {
val thisAggregated = BuildUtil.dependencies(structure.units).aggregateTransitive.getOrElse(currentRef, Nil)
val definedInAggregated = thisAggregated.filter(ref => definesPlugin(projectForRef(ref)))
if(definedInAggregated.nonEmpty) {
val projectNames = definedInAggregated.map(_.project) // TODO: usually in this build, but could technically require the build to be qualified
s"Plugin ${plugin.label} is not activated on this project, but this project aggregates projects where it is activated:\n\t${projectNames.mkString("\n\t")}"
} else {
val base = debug.deactivatedHelp(plugin, context)
val aggNote = if(thisAggregated.nonEmpty) "Note: This project aggregates other projects and this" else "Note: This"
val common = " information is for this project only."
val helpOther = "To see how to activate this plugin for another project, change to the project using `project <name>` and rerun this command."
s"$base\n$aggNote$common\n$helpOther"
}
}
}
/** Precomputes information for debugging plugins. */
def apply(available: List[AutoPlugin]): PluginsDebug =
{
val keyR = definedKeys(available)
val nameToKey: Map[String, AttributeKey[_]] = keyR._2s.toList.map(key => (key.label, key)).toMap
new PluginsDebug(available, nameToKey, keyR)
}
/** The context for debugging a plugin (de)activation.
* @param initial The initially defined [[AutoPlugin]]s.
* @param enabled The resulting model.
* @param deducePlugin The function used to compute the model.
* @param available All [[AutoPlugin]]s available for consideration. */
final case class Context(initial: Plugins, enabled: Seq[AutoPlugin], deducePlugin: (Plugins, Logger) => Seq[AutoPlugin], available: List[AutoPlugin], log: Logger)
/** Describes the steps to activate a plugin in some context. */
sealed abstract class PluginEnable
/** Describes a [[plugin]] that is already activated in the [[context]].*/
final case class PluginActivated(plugin: AutoPlugin, context: Context) extends PluginEnable
sealed abstract class EnableDeactivated extends PluginEnable
/** Describes a [[plugin]] that cannot be activated in a [[context]] due to [[contradictions]] in requirements. */
final case class PluginImpossible(plugin: AutoPlugin, context: Context, contradictions: Set[AutoPlugin]) extends EnableDeactivated
/** Describes the requirements for activating [[plugin]] in [[context]].
* @param context The base plugins, exclusions, and ultimately activated plugins
* @param blockingExcludes Existing exclusions that prevent [[plugin]] from being activated and must be dropped
* @param enablingPlugins [[AutoPlugin]]s that are not currently enabled, but need to be enabled for [[plugin]] to activate
* @param extraEnabledPlugins Plugins that will be enabled as a result of [[plugin]] activating, but are not required for [[plugin]] to activate
* @param willRemove Plugins that will be deactivated as a result of [[plugin]] activating
* @param deactivate Describes plugins that must be deactivated for [[plugin]] to activate. These require an explicit exclusion or dropping a transitive [[AutoPlugin]].*/
final case class PluginRequirements(plugin: AutoPlugin, context: Context, blockingExcludes: Set[AutoPlugin], enablingPlugins: Set[AutoPlugin], extraEnabledPlugins: Set[AutoPlugin], willRemove: Set[AutoPlugin], deactivate: List[DeactivatePlugin]) extends EnableDeactivated
/** Describes a [[plugin]] that must be removed in order to activate another plugin in some context.
* The [[plugin]] can always be directly, explicitly excluded.
* @param removeOneOf If non-empty, removing one of these [[AutoPlugin]]s will deactivate [[plugin]] without affecting the other plugin. If empty, a direct exclusion is required.
* @param newlySelected If false, this plugin was selected in the original context. */
final case class DeactivatePlugin(plugin: AutoPlugin, removeOneOf: Set[AutoPlugin], newlySelected: Boolean)
/** Determines how to enable [[plugin]] in [[context]]. */
def pluginEnable(context: Context, plugin: AutoPlugin): PluginEnable =
if(context.enabled.contains(plugin))
PluginActivated(plugin, context)
else
enableDeactivated(context, plugin)
private[this] def enableDeactivated(context: Context, plugin: AutoPlugin): PluginEnable =
{
// deconstruct the context
val initialModel = context.enabled.toSet
val initial = flatten(context.initial)
val initialPlugins = plugins(initial)
val initialExcludes = excludes(initial)
val minModel = minimalModel(plugin)
/* example 1
A :- B, not C
C :- D, E
initial: B, D, E
propose: drop D or E
initial: B, not A
propose: drop 'not A'
example 2
A :- B, not C
C :- B
initial: <empty>
propose: B, exclude C
*/
// `plugin` will only be activated when all of these plugins are activated
// Deactivating any one of these would deactivate `plugin`.
val minRequiredPlugins = plugins(minModel)
// The presence of any one of these plugins would deactivate `plugin`
val minAbsentPlugins = excludes(minModel).toSet
// Plugins that must be both activated and deactivated for `plugin` to activate.
// A non-empty list here cannot be satisfied and is an error.
val contradictions = minAbsentPlugins & minRequiredPlugins
if(contradictions.nonEmpty)
PluginImpossible(plugin, context, contradictions)
else
{
// Plguins that the user has to add to the currently selected plugins in order to enable `plugin`.
val addToExistingPlugins = minRequiredPlugins -- initialPlugins
// Plugins that are currently excluded that need to be allowed.
val blockingExcludes = initialExcludes & minRequiredPlugins
// The model that results when the minimal plugins are enabled and the minimal plugins are excluded.
// This can include more plugins than just `minRequiredPlugins` because the plguins required for `plugin`
// might activate other plugins as well.
val modelForMin = context.deducePlugin(and(includeAll(minRequiredPlugins), excludeAll(minAbsentPlugins)), context.log)
val incrementalInputs = and( includeAll(minRequiredPlugins ++ initialPlugins), excludeAll(minAbsentPlugins ++ initialExcludes -- minRequiredPlugins))
val incrementalModel = context.deducePlugin(incrementalInputs, context.log).toSet
// Plugins that are newly enabled as a result of selecting the plugins needed for `plugin`, but aren't strictly required for `plugin`.
// These could be excluded and `plugin` and the user's current plugins would still be activated.
val extraPlugins = incrementalModel.toSet -- minRequiredPlugins -- initialModel
// Plugins that will no longer be enabled as a result of enabling `plugin`.
val willRemove = initialModel -- incrementalModel
// Determine the plugins that must be independently deactivated.
// If both A and B must be deactivated, but A transitively depends on B, deactivating B will deactivate A.
// If A must be deactivated, but one if its (transitively) required plugins isn't present, it won't be activated.
// So, in either of these cases, A doesn't need to be considered further and won't be included in this set.
val minDeactivate = minAbsentPlugins.filter(p => Plugins.satisfied(p.requires, incrementalModel))
val deactivate = for(d <- minDeactivate.toList) yield {
// removing any one of these plugins will deactivate `d`. TODO: This is not an especially efficient implementation.
val removeToDeactivate = plugins(minimalModel(d)) -- minRequiredPlugins
val newlySelected = !initialModel(d)
// a. suggest removing a plugin in removeOneToDeactivate to deactivate d
// b. suggest excluding `d` to directly deactivate it in any case
// c. note whether d was already activated (in context.enabled) or is newly selected
DeactivatePlugin(d, removeToDeactivate, newlySelected)
}
PluginRequirements(plugin, context, blockingExcludes, addToExistingPlugins, extraPlugins, willRemove, deactivate)
}
}
private[this] def includeAll[T <: Basic](basic: Set[T]): Plugins = And(basic.toList)
private[this] def excludeAll(plugins: Set[AutoPlugin]): Plugins = And(plugins map (p => Exclude(p)) toList)
private[this] def excludes(bs: Seq[Basic]): Set[AutoPlugin] = bs.collect { case Exclude(b) => b }.toSet
private[this] def plugins(bs: Seq[Basic]): Set[AutoPlugin] = bs.collect { case n: AutoPlugin => n }.toSet
// If there is a model that includes `plugin`, it includes at least what is returned by this method.
// This is the list of plugins that must be included as well as list of plugins that must not be present.
// It might not be valid, such as if there are contradictions or if there are cycles that are unsatisfiable.
// The actual model might be larger, since other plugins might be enabled by the selected plugins.
private[this] def minimalModel(plugin: AutoPlugin): Seq[Basic] = Dag.topologicalSortUnchecked(plugin: Basic) {
case _: Exclude => Nil
case ap: AutoPlugin => Plugins.flatten(ap.requires) :+ plugin
}
/** String representation of [[PluginEnable]], intended for end users. */
def explainPluginEnable(ps: PluginEnable): String =
ps match {
case PluginRequirements(plugin, context, blockingExcludes, enablingPlugins, extraEnabledPlugins, toBeRemoved, deactivate) =>
def indent(str: String) = if(str.isEmpty) "" else s"\t$str"
def note(str: String) = if(str.isEmpty) "" else s"Note: $str"
val parts =
indent(excludedError(false /* TODO */, blockingExcludes.toList)) ::
indent(required(enablingPlugins.toList)) ::
indent(needToDeactivate(deactivate)) ::
note(willAdd(plugin, extraEnabledPlugins.toList)) ::
note(willRemove(plugin, toBeRemoved.toList)) ::
Nil
parts.filterNot(_.isEmpty).mkString("\n")
case PluginImpossible(plugin, context, contradictions) => pluginImpossible(plugin, contradictions)
case PluginActivated(plugin, context) => s"Plugin ${plugin.label} already activated."
}
/** Provides a [[Relation]] between plugins and the keys they potentially define.
* Because plugins can define keys in different scopes and keys can be overridden, this is not definitive.*/
def definedKeys(available: List[AutoPlugin]): Relation[AutoPlugin, AttributeKey[_]] =
{
def extractDefinedKeys(ss: Seq[Setting[_]]): Seq[AttributeKey[_]] =
ss.map(_.key.key)
def allSettings(p: AutoPlugin): Seq[Setting[_]] = p.projectSettings ++ p.buildSettings ++ p.globalSettings
val empty = Relation.empty[AutoPlugin, AttributeKey[_]]
(empty /: available)( (r,p) => r + (p, extractDefinedKeys(allSettings(p))) )
}
private[this] def excludedError(transitive: Boolean, dependencies: List[AutoPlugin]): String =
str(dependencies)(excludedPluginError(transitive), excludedPluginsError(transitive))
private[this] def excludedPluginError(transitive: Boolean)(dependency: AutoPlugin) =
s"Required ${transitiveString(transitive)}dependency ${dependency.label} was excluded."
private[this] def excludedPluginsError(transitive: Boolean)(dependencies: List[AutoPlugin]) =
s"Required ${transitiveString(transitive)}dependencies were excluded:\n\t${labels(dependencies).mkString("\n\t")}"
private[this] def transitiveString(transitive: Boolean) =
if(transitive) "(transitive) " else ""
private[this] def required(plugins: List[AutoPlugin]): String =
str(plugins)(requiredPlugin, requiredPlugins)
private[this] def requiredPlugin(plugin: AutoPlugin) =
s"Required plugin ${plugin.label} not present."
private[this] def requiredPlugins(plugins: List[AutoPlugin]) =
s"Required plugins not present:\n\t${plugins.map(_.label).mkString("\n\t")}"
private[this] def str[A](list: List[A])(f: A => String, fs: List[A] => String): String = list match {
case Nil => ""
case single :: Nil => f(single)
case _ => fs(list)
}
private[this] def willAdd(base: AutoPlugin, plugins: List[AutoPlugin]): String =
str(plugins)(willAddPlugin(base), willAddPlugins(base))
private[this] def willAddPlugin(base: AutoPlugin)(plugin: AutoPlugin) =
s"Enabling ${base.label} will also enable ${plugin.label}"
private[this] def willAddPlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) =
s"Enabling ${base.label} will also enable:\n\t${labels(plugins).mkString("\n\t")}"
private[this] def willRemove(base: AutoPlugin, plugins: List[AutoPlugin]): String =
str(plugins)(willRemovePlugin(base), willRemovePlugins(base))
private[this] def willRemovePlugin(base: AutoPlugin)(plugin: AutoPlugin) =
s"Enabling ${base.label} will disable ${plugin.label}"
private[this] def willRemovePlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) =
s"Enabling ${base.label} will disable:\n\t${labels(plugins).mkString("\n\t")}"
private[this] def labels(plugins: List[AutoPlugin]): List[String] =
plugins.map(_.label)
private[this] def needToDeactivate(deactivate: List[DeactivatePlugin]): String =
str(deactivate)(deactivate1, deactivateN)
private[this] def deactivateN(plugins: List[DeactivatePlugin]): String =
plugins.map(deactivateString).mkString("These plugins need to be deactivated:\n\t", "\n\t", "")
private[this] def deactivate1(deactivate: DeactivatePlugin): String =
s"Need to deactivate ${deactivateString(deactivate)}"
private[this] def deactivateString(d: DeactivatePlugin): String =
{
val removePluginsString: String =
d.removeOneOf.toList match {
case Nil => ""
case x :: Nil => s" or no longer include $x"
case xs => s" or remove one of ${xs.mkString(", ")}"
}
s"${d.plugin.label}: directly exclude it${removePluginsString}"
}
private[this] def pluginImpossible(plugin: AutoPlugin, contradictions: Set[AutoPlugin]): String =
str(contradictions.toList)(pluginImpossible1(plugin), pluginImpossibleN(plugin))
private[this] def pluginImpossible1(plugin: AutoPlugin)(contradiction: AutoPlugin): String =
s"There is no way to enable plugin ${plugin.label}. It (or its dependencies) requires plugin ${contradiction.label} to both be present and absent. Please report the problem to the plugin's author."
private[this] def pluginImpossibleN(plugin: AutoPlugin)(contradictions: List[AutoPlugin]): String =
s"There is no way to enable plugin ${plugin.label}. It (or its dependencies) requires these plugins to be both present and absent:\n\t${labels(contradictions).mkString("\n\t")}\nPlease report the problem to the plugin's author."
}

View File

@ -50,33 +50,52 @@ sealed trait ProjectDefinition[PR <: ProjectReference]
/** Configures the sources of automatically appended settings.*/
def auto: AddSettings
/** The defined [[Plugins]] associated with this project.
A [[AutoPlugin]] is a common label that is used by plugins to determine what settings, if any, to add to a project. */
def plugins: Plugins
/** The [[AutoPlugin]]s enabled for this project. This value is only available on a loaded Project. */
private[sbt] def autoPlugins: Seq[AutoPlugin]
override final def hashCode: Int = id.hashCode ^ base.hashCode ^ getClass.hashCode
override final def equals(o: Any) = o match {
case p: ProjectDefinition[_] => p.getClass == this.getClass && p.id == id && p.base == base
case _ => false
}
override def toString = "Project(id: " + id + ", base: " + base + ", aggregate: " + aggregate + ", dependencies: " + dependencies + ", configurations: " + configurations + ")"
override def toString =
{
val agg = ifNonEmpty("aggregate", aggregate)
val dep = ifNonEmpty("dependencies", dependencies)
val conf = ifNonEmpty("configurations", configurations)
val autos = ifNonEmpty("autoPlugins", autoPlugins.map(_.label))
val fields = s"id $id" :: s"base: $base" :: agg ::: dep ::: conf ::: (s"plugins: List($plugins)" :: autos)
s"Project(${fields.mkString(", ")})"
}
private[this] def ifNonEmpty[T](label: String, ts: Iterable[T]): List[String] = if(ts.isEmpty) Nil else s"$label: $ts" :: Nil
}
sealed trait Project extends ProjectDefinition[ProjectReference]
{
// TODO: add parameters for plugins in 0.14.0 (not reasonable to do in a binary compatible way in 0.13)
def copy(id: String = id, base: File = base, aggregate: => Seq[ProjectReference] = aggregate, dependencies: => Seq[ClasspathDep[ProjectReference]] = dependencies,
delegates: => Seq[ProjectReference] = delegates, settings: => Seq[Setting[_]] = settings, configurations: Seq[Configuration] = configurations,
auto: AddSettings = auto): Project =
Project(id, base, aggregate = aggregate, dependencies = dependencies, delegates = delegates, settings, configurations, auto)
unresolved(id, base, aggregate = aggregate, dependencies = dependencies, delegates = delegates, settings, configurations, auto, plugins, autoPlugins)
def resolve(resolveRef: ProjectReference => ProjectRef): ResolvedProject =
{
def resolveRefs(prs: Seq[ProjectReference]) = prs map resolveRef
def resolveDeps(ds: Seq[ClasspathDep[ProjectReference]]) = ds map resolveDep
def resolveDep(d: ClasspathDep[ProjectReference]) = ResolvedClasspathDependency(resolveRef(d.project), d.configuration)
resolved(id, base, aggregate = resolveRefs(aggregate), dependencies = resolveDeps(dependencies), delegates = resolveRefs(delegates), settings, configurations, auto)
resolved(id, base, aggregate = resolveRefs(aggregate), dependencies = resolveDeps(dependencies), delegates = resolveRefs(delegates),
settings, configurations, auto, plugins, autoPlugins)
}
def resolveBuild(resolveRef: ProjectReference => ProjectReference): Project =
{
def resolveRefs(prs: Seq[ProjectReference]) = prs map resolveRef
def resolveDeps(ds: Seq[ClasspathDep[ProjectReference]]) = ds map resolveDep
def resolveDep(d: ClasspathDep[ProjectReference]) = ClasspathDependency(resolveRef(d.project), d.configuration)
apply(id, base, aggregate = resolveRefs(aggregate), dependencies = resolveDeps(dependencies), delegates = resolveRefs(delegates), settings, configurations, auto)
unresolved(id, base, aggregate = resolveRefs(aggregate), dependencies = resolveDeps(dependencies), delegates = resolveRefs(delegates),
settings, configurations, auto, plugins, autoPlugins)
}
/** Applies the given functions to this Project.
@ -106,8 +125,11 @@ sealed trait Project extends ProjectDefinition[ProjectReference]
/** Appends settings to the current settings sequence for this project. */
def settings(ss: Setting[_]*): Project = copy(settings = (settings: Seq[Setting[_]]) ++ ss)
@deprecated("Use settingSets method.", "0.13.5")
def autoSettings(select: AddSettings*): Project = settingSets(select.toSeq: _*)
/** Configures how settings from other sources, such as .sbt files, are appended to the explicitly specified settings for this project. */
def autoSettings(select: AddSettings*): Project = copy(auto = AddSettings.seq(select : _*))
def settingSets(select: AddSettings*): Project = copy(auto = AddSettings.seq(select : _*))
/** Adds a list of .sbt files whose settings will be appended to the settings of this project.
* They will be appended after the explicit settings and already defined automatic settings sources. */
@ -116,8 +138,30 @@ sealed trait Project extends ProjectDefinition[ProjectReference]
/** Sets the list of .sbt files to parse for settings to be appended to this project's settings.
* Any configured .sbt files are removed from this project's list.*/
def setSbtFiles(files: File*): Project = copy(auto = AddSettings.append( AddSettings.clearSbtFiles(auto), AddSettings.sbtFiles(files: _*)) )
/** Sets the [[AutoPlugin]]s of this project.
A [[AutoPlugin]] is a common label that is used by plugins to determine what settings, if any, to add to a project. */
def addPlugins(ns: Plugins*): Project = setPlugins(ns.foldLeft(plugins)(Plugins.and))
/** Disable the given plugins on this project. */
def disablePlugins(ps: AutoPlugin*): Project =
setPlugins(Plugins.and(plugins, Plugins.And(ps.map(p => Plugins.Exclude(p)).toList)))
private[this] def setPlugins(ns: Plugins): Project = {
// TODO: for 0.14.0, use copy when it has the additional `plugins` parameter
unresolved(id, base, aggregate = aggregate, dependencies = dependencies, delegates = delegates, settings, configurations, auto, ns, autoPlugins)
}
/** Definitively set the [[AutoPlugin]]s for this project. */
private[sbt] def setAutoPlugins(autos: Seq[AutoPlugin]): Project = {
// TODO: for 0.14.0, use copy when it has the additional `autoPlugins` parameter
unresolved(id, base, aggregate = aggregate, dependencies = dependencies, delegates = delegates, settings, configurations, auto, plugins, autos)
}
}
sealed trait ResolvedProject extends ProjectDefinition[ProjectRef] {
/** The [[AutoPlugin]]s enabled for this project as computed from [[plugins]].*/
def autoPlugins: Seq[AutoPlugin]
}
sealed trait ResolvedProject extends ProjectDefinition[ProjectRef]
sealed trait ClasspathDep[PR <: ProjectReference] { def project: PR; def configuration: Option[String] }
final case class ResolvedClasspathDependency(project: ProjectRef, configuration: Option[String]) extends ClasspathDep[ProjectRef]
@ -150,23 +194,23 @@ object Project extends ProjectExtra
Def.showRelativeKey( ProjectRef(loaded.root, loaded.units(loaded.root).rootProjects.head), loaded.allProjectRefs.size > 1, keyNameColor)
private abstract class ProjectDef[PR <: ProjectReference](val id: String, val base: File, aggregate0: => Seq[PR], dependencies0: => Seq[ClasspathDep[PR]],
delegates0: => Seq[PR], settings0: => Seq[Def.Setting[_]], val configurations: Seq[Configuration], val auto: AddSettings) extends ProjectDefinition[PR]
delegates0: => Seq[PR], settings0: => Seq[Def.Setting[_]], val configurations: Seq[Configuration], val auto: AddSettings,
val plugins: Plugins, val autoPlugins: Seq[AutoPlugin]) extends ProjectDefinition[PR]
{
lazy val aggregate = aggregate0
lazy val dependencies = dependencies0
lazy val delegates = delegates0
lazy val settings = settings0
Dag.topologicalSort(configurations)(_.extendsConfigs) // checks for cyclic references here instead of having to do it in Scope.delegates
}
// TODO: add parameter for plugins in 0.14.0
// TODO: Modify default settings to be the core settings, and automatically add the IvyModule + JvmPlugins.
def apply(id: String, base: File, aggregate: => Seq[ProjectReference] = Nil, dependencies: => Seq[ClasspathDep[ProjectReference]] = Nil,
delegates: => Seq[ProjectReference] = Nil, settings: => Seq[Def.Setting[_]] = defaultSettings, configurations: Seq[Configuration] = Configurations.default,
delegates: => Seq[ProjectReference] = Nil, settings: => Seq[Def.Setting[_]] = Nil, configurations: Seq[Configuration] = Nil,
auto: AddSettings = AddSettings.allDefaults): Project =
{
validProjectID(id).foreach(errMsg => sys.error("Invalid project ID: " + errMsg))
new ProjectDef[ProjectReference](id, base, aggregate, dependencies, delegates, settings, configurations, auto) with Project
}
unresolved(id, base, aggregate, dependencies, delegates, settings, configurations, auto, Plugins.empty, Nil) // Note: JvmModule/IvyModule auto included...
/** Returns None if `id` is a valid Project ID or Some containing the parser error message if it is not.*/
def validProjectID(id: String): Option[String] = DefaultParsers.parse(id, DefaultParsers.ID).left.toOption
@ -185,10 +229,25 @@ object Project extends ProjectExtra
* This is a best effort implementation, since valid characters are not documented or consistent.*/
def normalizeModuleID(id: String): String = normalizeBase(id)
@deprecated("Will be removed.", "0.13.2")
def resolved(id: String, base: File, aggregate: => Seq[ProjectRef], dependencies: => Seq[ResolvedClasspathDependency], delegates: => Seq[ProjectRef],
settings: Seq[Def.Setting[_]], configurations: Seq[Configuration], auto: AddSettings): ResolvedProject =
new ProjectDef[ProjectRef](id, base, aggregate, dependencies, delegates, settings, configurations, auto) with ResolvedProject
resolved(id, base, aggregate, dependencies, delegates, settings, configurations, auto, Plugins.empty, Nil)
private def resolved(id: String, base: File, aggregate: => Seq[ProjectRef], dependencies: => Seq[ClasspathDep[ProjectRef]],
delegates: => Seq[ProjectRef], settings: Seq[Def.Setting[_]], configurations: Seq[Configuration], auto: AddSettings,
plugins: Plugins, autoPlugins: Seq[AutoPlugin]): ResolvedProject =
new ProjectDef[ProjectRef](id, base, aggregate, dependencies, delegates, settings, configurations, auto, plugins, autoPlugins) with ResolvedProject
private def unresolved(id: String, base: File, aggregate: => Seq[ProjectReference], dependencies: => Seq[ClasspathDep[ProjectReference]],
delegates: => Seq[ProjectReference], settings: => Seq[Def.Setting[_]], configurations: Seq[Configuration], auto: AddSettings,
plugins: Plugins, autoPlugins: Seq[AutoPlugin]): Project =
{
validProjectID(id).foreach(errMsg => sys.error("Invalid project ID: " + errMsg))
new ProjectDef[ProjectReference](id, base, aggregate, dependencies, delegates, settings, configurations, auto, plugins, autoPlugins) with Project
}
@deprecated("0.13.2", "Use Defaults.coreDefaultSettings instead, combined with AutoPlugins.")
def defaultSettings: Seq[Def.Setting[_]] = Defaults.defaultSettings
final class Constructor(p: ProjectReference) {
@ -307,7 +366,7 @@ object Project extends ProjectExtra
def details(structure: BuildStructure, actual: Boolean, scope: Scope, key: AttributeKey[_])(implicit display: Show[ScopedKey[_]]): String =
{
val scoped = ScopedKey(scope,key)
val data = scopedKeyData(structure, scope, key) map {_.description} getOrElse {"No entry for key."}
val description = key.description match { case Some(desc) => "Description:\n\t" + desc + "\n"; case None => "" }
@ -413,7 +472,7 @@ object Project extends ProjectExtra
import DefaultParsers._
val loadActionParser = token(Space ~> ("plugins" ^^^ Plugins | "return" ^^^ Return)) ?? Current
val ProjectReturn = AttributeKey[List[File]]("project-return", "Maintains a stack of builds visited using reload.")
def projectReturn(s: State): List[File] = getOrNil(s, ProjectReturn)
def inPluginProject(s: State): Boolean = projectReturn(s).toList.length > 1

View File

@ -110,7 +110,7 @@ object SessionSettings
val RangePosition(_, r@LineRange(start, end)) = s.pos
settings find (_._1.key == s.key) match {
case Some(ss@(ns, newLines)) if !ns.init.dependencies.contains(ns.key) =>
val shifted = ns withPos RangePosition(path, LineRange(start - offs, start - offs + 1))
val shifted = ns withPos RangePosition(path, LineRange(start - offs, start - offs + newLines.size))
(offs + end - start - newLines.size, shifted::olds, ss::repl, lineMap + (start -> (end, newLines)))
case _ =>
val shifted = s withPos RangePosition(path, r shift -offs)

View File

@ -0,0 +1,19 @@
package sbt
package plugins
import Def.Setting
/**
* Plugin for core sbt-isms.
*
* Can control task-level paralleism, logging, etc.
*/
object CorePlugin extends AutoPlugin {
// This is included by default
override def trigger = allRequirements
override lazy val projectSettings: Seq[Setting[_]] =
Defaults.coreDefaultSettings
override lazy val globalSettings: Seq[Setting[_]] =
Defaults.globalSbtCore
}

View File

@ -0,0 +1,26 @@
package sbt
package plugins
import Def.Setting
/**
* Plugin that enables resolving artifacts via ivy.
*
* Core Tasks
* - `update`
* - `makePom`
* - `publish`
* - `artifacts`
* - `publishedArtifacts`
*/
object IvyPlugin extends AutoPlugin {
// We are automatically included on everything that has the global module,
// which is automatically included on everything.
override def requires = CorePlugin
override def trigger = allRequirements
override lazy val projectSettings: Seq[Setting[_]] =
Classpaths.ivyPublishSettings ++ Classpaths.ivyBaseSettings
override lazy val globalSettings: Seq[Setting[_]] =
Defaults.globalIvyCore
}

View File

@ -0,0 +1,37 @@
package sbt
package plugins
import Def.Setting
/** A plugin representing the ability to build a JVM project.
*
* Core tasks/keys:
* - `run`
* - `test`
* - `compile`
* - `fullClasspath`
* Core configurations
* - `Test`
* - `Compile`
*/
object JvmPlugin extends AutoPlugin {
// We are automatically enabled for any IvyModule project. We also require its settings
// for ours to work.
override def requires = IvyPlugin
override def trigger = allRequirements
override lazy val projectSettings: Seq[Setting[_]] =
Defaults.runnerSettings ++
Defaults.paths ++
Classpaths.jvmPublishSettings ++
Classpaths.jvmBaseSettings ++
Defaults.projectTasks ++
Defaults.packageBase ++
Defaults.compileBase ++
Defaults.defaultConfigs
override lazy val globalSettings: Seq[Setting[_]] =
Defaults.globalJvmCore
override def projectConfigurations: Seq[Configuration] =
Configurations.default
}

View File

@ -0,0 +1,90 @@
package sbt
import java.io.File
import org.specs2._
import mutable.Specification
object PluginsTest extends Specification
{
import AI._
"Auto plugin" should {
"enable plugins with trigger=allRequirements AND requirements met" in {
deducePlugin(A && B, log) must contain(Q)
}
"enable transive plugins with trigger=allRequirements AND requirements met" in {
deducePlugin(A && B, log) must contain(R)
}
"order enable plugins after required plugins" in {
val ns = deducePlugin(A && B, log)
( (ns indexOf Q) must beGreaterThan(ns indexOf A) ) and
( (ns indexOf Q) must beGreaterThan(ns indexOf B) ) and
( (ns indexOf R) must beGreaterThan(ns indexOf A) ) and
( (ns indexOf R) must beGreaterThan(ns indexOf B) ) and
( (ns indexOf R) must beGreaterThan(ns indexOf Q) )
}
"not enable plugins with trigger=allRequirements but conflicting requirements" in {
deducePlugin(A && B, log) must not contain(S)
}
"enable plugins that are required by the requested plugins" in {
val ns = deducePlugin(Q, log)
(ns must contain(A)) and
(ns must contain(B))
}
"throw an AutoPluginException on conflicting requirements" in {
deducePlugin(S, log) must throwAn[AutoPluginException](message = """Contradiction in enabled plugins:
- requested: sbt.AI\$S
- enabled: sbt.AI\$S, sbt.AI\$Q, sbt.AI\$R, sbt.AI\$B, sbt.AI\$A
- conflict: sbt.AI\$R is enabled by sbt.AI\$Q; excluded by sbt.AI\$S""")
}
"generates a detailed report on conflicting requirements" in {
deducePlugin(T && U, log) must throwAn[AutoPluginException](message = """Contradiction in enabled plugins:
- requested: sbt.AI\$T && sbt.AI\$U
- enabled: sbt.AI\$U, sbt.AI\$T, sbt.AI\$A, sbt.AI\$Q, sbt.AI\$R, sbt.AI\$B
- conflict: sbt.AI\$Q is enabled by sbt.AI\$A && sbt.AI\$B; required by sbt.AI\$T, sbt.AI\$R; excluded by sbt.AI\$U
- conflict: sbt.AI\$R is enabled by sbt.AI\$Q; excluded by sbt.AI\$T""")
}
}
}
object AI
{
lazy val allPlugins: List[AutoPlugin] = List(A, B, Q, R, S, T, U)
lazy val deducePlugin = Plugins.deducer(allPlugins)
lazy val log = Logger.Null
object A extends AutoPlugin
object B extends AutoPlugin
object Q extends AutoPlugin
{
override def requires: Plugins = A && B
override def trigger = allRequirements
}
object R extends AutoPlugin
{
override def requires = Q
override def trigger = allRequirements
}
object S extends AutoPlugin
{
override def requires = Q && !R
override def trigger = allRequirements
}
// This is an opt-in plugin with a requirement
// Unless explicitly loaded by the build user, this will not be activated.
object T extends AutoPlugin
{
override def requires = Q && !R
}
// This is an opt-in plugin with a requirement
// Unless explicitly loaded by the build user, this will not be activated.
object U extends AutoPlugin
{
override def requires = A && !Q
}
}

View File

@ -14,9 +14,9 @@ object Sbt extends Build
override lazy val settings = super.settings ++ buildSettings ++ Status.settings ++ nightlySettings
def buildSettings = Seq(
organization := "org.scala-sbt",
version := "0.13.2-SNAPSHOT",
version := "0.13.5-SNAPSHOT",
publishArtifact in packageDoc := false,
scalaVersion := "2.10.3",
scalaVersion := "2.10.4",
publishMavenStyle := false,
componentID := None,
crossPaths := false,
@ -73,6 +73,8 @@ object Sbt extends Build
lazy val datatypeSub = baseProject(utilPath /"datatype", "Datatype Generator") dependsOn(ioSub)
// cross versioning
lazy val crossSub = baseProject(utilPath / "cross", "Cross") settings(inConfig(Compile)(Transform.crossGenSettings): _*)
// A logic with restricted negation as failure for a unique, stable model
lazy val logicSub = testedBaseProject(utilPath / "logic", "Logic").dependsOn(collectionSub, relationSub)
/* **** Intermediate-level Modules **** */
@ -130,7 +132,7 @@ object Sbt extends Build
completeSub, classpathSub, stdTaskSub, processSub) settings( sbinary )
// The main integration project for sbt. It brings all of the subsystems together, configures them, and provides for overriding conventions.
lazy val mainSub = testedBaseProject(mainPath, "Main") dependsOn(actionsSub, mainSettingsSub, interfaceSub, ioSub, ivySub, launchInterfaceSub, logSub, processSub, runSub, commandSub) settings(scalaXml)
lazy val mainSub = testedBaseProject(mainPath, "Main") dependsOn(actionsSub, mainSettingsSub, interfaceSub, ioSub, ivySub, launchInterfaceSub, logSub, logicSub, processSub, runSub, commandSub) settings(scalaXml)
// Strictly for bringing implicits and aliases from subsystems into the top-level sbt namespace through a single package object
// technically, we need a dependency on all of mainSub's dependencies, but we don't do that since this is strictly an integration project
@ -276,7 +278,7 @@ object Sbt extends Build
artifact in (Compile, packageSrc) := Artifact(srcID).copy(configurations = Compile :: Nil).extra("e:component" -> srcID)
)
def compilerSettings = Seq(
libraryDependencies <+= scalaVersion( "org.scala-lang" % "scala-compiler" % _ % "test"),
libraryDependencies <+= scalaVersion("org.scala-lang" % "scala-compiler" % _ % "test"),
unmanagedJars in Test <<= (packageSrc in compileInterfaceSub in Compile).map(x => Seq(x).classpath)
)
def precompiled(scalav: String): Project = baseProject(compilePath / "interface", "Precompiled " + scalav.replace('.', '_')) dependsOn(interfaceSub) settings(precompiledSettings : _*) settings(

View File

@ -172,13 +172,15 @@ object Common
lazy val httpclient = lib("commons-httpclient" % "commons-httpclient" % "3.1")
lazy val jsch = lib("com.jcraft" % "jsch" % "0.1.46" intransitive() )
lazy val sbinary = libraryDependencies <+= Util.nightly211(n => "org.scala-tools.sbinary" % "sbinary" % "0.4.2" cross(if(n) CrossVersion.full else CrossVersion.binary))
lazy val scalaCompiler = libraryDependencies <+= scalaVersion("org.scala-lang" % "scala-compiler" % _ )
lazy val scalaCompiler = libraryDependencies <+= scalaVersion(sv => "org.scala-lang" % "scala-compiler" % sv)
lazy val testInterface = lib("org.scala-sbt" % "test-interface" % "1.0")
def libModular(name: String) = libraryDependencies <++= (scalaVersion, scalaOrganization)( (sv,o) =>
if(sv.startsWith("2.11.")) (o % name % sv) :: Nil else Nil
)
lazy val scalaXml = libModular("scala-xml")
lazy val scalaParsers = libModular("scala-parser-combinators")
private def scala211Module(name: String, moduleVersion: String) =
libraryDependencies <++= (scalaVersion)( scalaVersion =>
if (scalaVersion startsWith "2.11.") ("org.scala-lang.modules" %% name % moduleVersion) :: Nil
else Nil
)
lazy val scalaXml = scala211Module("scala-xml", "1.0.0-RC7")
lazy val scalaParsers = scala211Module("scala-parser-combinators", "1.0.0-RC5")
}
object Licensed
{

View File

@ -413,6 +413,15 @@ private final class TrapExit(delegateManager: SecurityManager) extends SecurityM
private def isRealExit(element: StackTraceElement): Boolean =
element.getClassName == "java.lang.Runtime" && element.getMethodName == "exit"
// These are overridden to do nothing because there is a substantial filesystem performance penalty
// when there is a SecurityManager defined. The default implementations of these construct a
// FilePermission, and its initialization involves canonicalization, which is expensive.
override def checkRead(file: String) {}
override def checkRead(file: String, context: AnyRef) {}
override def checkWrite(file: String) {}
override def checkDelete(file: String) {}
override def checkExec(cmd: String) {}
override def checkPermission(perm: Permission)
{
if(delegateManager ne null)

View File

@ -5,7 +5,7 @@ object B extends Build {
override def settings = super.settings ++ Seq(
organization := "org.example",
version := "2.0"
version := "2.0-SNAPSHOT"
)
lazy val root = proj("root", ".") aggregate(a,b)

View File

@ -4,6 +4,6 @@ organization := "org.example"
version := "1.0"
libraryDependencies += "org.example" % "b" % "2.0"
libraryDependencies += "org.example" % "b" % "2.0-SNAPSHOT"
ivyPaths <<= ivyPaths in ThisBuild

View File

@ -8,8 +8,10 @@ object MakePomTest extends Build
readPom <<= makePom map XML.loadFile,
TaskKey[Unit]("check-pom") <<= checkPom,
TaskKey[Unit]("check-extra") <<= checkExtra,
TaskKey[Unit]("check-version-plus-mapping") <<= checkVersionPlusMapping,
resolvers += Resolver.sonatypeRepo("snapshots"),
makePomConfiguration ~= { _.copy(extra = <extra-tag/>) }
makePomConfiguration ~= { _.copy(extra = <extra-tag/>) },
libraryDependencies += "com.google.code.findbugs" % "jsr305" % "1.3.+"
)
val readPom = TaskKey[Elem]("read-pom")
@ -33,6 +35,17 @@ object MakePomTest extends Build
if(extra.isEmpty) error("'" + extraTagName + "' not found in generated pom.xml.") else ()
}
lazy val checkVersionPlusMapping = (readPom) map { (pomXml) =>
var found = false
for {
dep <- pomXml \ "dependencies" \ "dependency"
if (dep \ "artifactId").text == "jsr305"
// TODO - Ignore space here.
if (dep \ "version").text != "[1.3,1.4)"
} sys.error(s"Found dependency with invalid maven version: $dep")
()
}
lazy val checkPom = (readPom, fullResolvers) map { (pomXML, ivyRepositories) =>
checkProject(pomXML)
withRepositories(pomXML) { repositoriesElement =>

View File

@ -1,2 +1,3 @@
> check-pom
> check-extra
> check-extra
> check-version-plus-mapping

View File

@ -0,0 +1,38 @@
// disablePlugins(Q) will prevent R from being auto-added
lazy val projA = project.addPlugins(A, B).disablePlugins(Q)
// without B, Q is not added
lazy val projB = project.addPlugins(A)
// with both A and B, Q is selected, which in turn selects R, but not S
lazy val projC = project.addPlugins(A, B)
// with no natures defined, nothing is auto-added
lazy val projD = project
// with S selected, Q is loaded automatically, which in turn selects R
lazy val projE = project.addPlugins(S)
check := {
val adel = (del in projA).?.value // should be None
same(adel, None, "del in projA")
val bdel = (del in projB).?.value // should be None
same(bdel, None, "del in projB")
val ddel = (del in projD).?.value // should be None
same(ddel, None, "del in projD")
//
val buildValue = (demo in ThisBuild).value
same(buildValue, "build 0", "demo in ThisBuild")
val globalValue = (demo in Global).value
same(globalValue, "global 0", "demo in Global")
val projValue = (demo in projC).value
same(projValue, "project projC Q R", "demo in projC")
val qValue = (del in projC in q).value
same(qValue, " Q R", "del in projC in q")
val optInValue = (del in projE in q).value
same(optInValue, " Q S R", "del in projE in q")
}
def same[T](actual: T, expected: T, label: String) {
assert(actual == expected, s"Expected '$expected' for `$label`, got '$actual'")
}

View File

@ -0,0 +1,85 @@
package sbttest // you need package http://stackoverflow.com/questions/9822008/
import sbt._
import sbt.Keys.{name, resolvedScoped}
import java.util.concurrent.atomic.{AtomicInteger => AInt}
object Imports
{
object A extends AutoPlugin
object B extends AutoPlugin
object E extends AutoPlugin
lazy val q = config("q")
lazy val p = config("p").extend(q)
lazy val demo = settingKey[String]("A demo setting.")
lazy val del = settingKey[String]("Another demo setting.")
lazy val check = settingKey[Unit]("Verifies settings are as they should be.")
}
object X extends AutoPlugin {
val autoImport = Imports
}
import Imports._
object D extends AutoPlugin {
override def requires: Plugins = E
override def trigger = allRequirements
}
object Q extends AutoPlugin
{
override def requires: Plugins = A && B
override def trigger = allRequirements
override def projectConfigurations: Seq[Configuration] =
p ::
q ::
Nil
override def projectSettings: Seq[Setting[_]] =
(demo := s"project ${name.value}") ::
(del in q := " Q") ::
Nil
override def buildSettings: Seq[Setting[_]] =
(demo := s"build ${buildCount.getAndIncrement}") ::
Nil
override def globalSettings: Seq[Setting[_]] =
(demo := s"global ${globalCount.getAndIncrement}") ::
Nil
// used to ensure the build-level and global settings are only added once
private[this] val buildCount = new AInt(0)
private[this] val globalCount = new AInt(0)
}
object R extends AutoPlugin
{
// NOTE - Only plugins themselves support exclusions...
override def requires = Q
override def trigger = allRequirements
override def projectSettings = Seq(
// tests proper ordering: R requires Q, so Q settings should come first
del in q += " R",
// tests that configurations are properly registered, enabling delegation from p to q
demo += (del in p).value
)
}
// This is an opt-in plugin with a requirement
// Unless explicitly loaded by the build user, this will not be activated.
object S extends AutoPlugin
{
override def requires = Q
override def trigger = noTrigger
override def projectSettings = Seq(
del in q += " S"
)
}

View File

@ -0,0 +1 @@
> check

View File

@ -6,22 +6,22 @@
object B extends Build
{
// version should be from explicit/a.txt
lazy val root = project("root", "1.4") autoSettings( userSettings, sbtFiles(file("explicit/a.txt")) )
lazy val root = project("root", "1.4") settingSets( buildScalaFiles, userSettings, sbtFiles(file("explicit/a.txt")) )
// version should be from global/user.sbt
lazy val a = project("a", "1.1") autoSettings( userSettings )
lazy val a = project("a", "1.1") settingSets( buildScalaFiles, userSettings )
// version should be the default 0.1-SNAPSHOT
lazy val b = project("b", "0.1-SNAPSHOT") autoSettings()
lazy val b = project("b", "0.1-SNAPSHOT") settingSets(buildScalaFiles)
// version should be from the explicit settings call
lazy val c = project("c", "0.9") settings(version := "0.9") autoSettings()
lazy val c = project("c", "0.9") settings(version := "0.9") settingSets(buildScalaFiles)
// version should be from d/build.sbt
lazy val d = project("d", "1.3") settings(version := "0.9") autoSettings( defaultSbtFiles )
lazy val d = project("d", "1.3") settings(version := "0.9") settingSets( buildScalaFiles, defaultSbtFiles )
// version should be from global/user.sbt
lazy val e = project("e", "1.1") settings(version := "0.9") autoSettings( defaultSbtFiles, sbtFiles(file("../explicit/a.txt")), userSettings )
lazy val e = project("e", "1.1") settings(version := "0.9") settingSets( buildScalaFiles, defaultSbtFiles, sbtFiles(file("../explicit/a.txt")), userSettings )
def project(id: String, expectedVersion: String): Project = Project(id, if(id == "root") file(".") else file(id)) settings(
TaskKey[Unit]("check") <<= version map { v =>

View File

@ -1,3 +1,5 @@
> plugins
> root/check
> a/check

View File

@ -0,0 +1,27 @@
package sbttest // you need package http://stackoverflow.com/questions/9822008/
import sbt._
import Keys._
object C extends AutoPlugin {
object autoImport {
object bN extends AutoPlugin {
override def trigger = allRequirements
}
lazy val check = taskKey[Unit]("Checks that the AutoPlugin and Build are automatically added.")
}
}
import C.autoImport._
object A extends AutoPlugin {
override def requires = bN
override def trigger = allRequirements
override def projectSettings = Seq(
check := {}
)
}
object B extends Build {
lazy val extra = project.addPlugins(bN)
}

View File

@ -0,0 +1,3 @@
sbtPlugin := true
name := "demo-plugin"

View File

@ -0,0 +1 @@
addSbtPlugin("org.example" % "demo-plugin" % "3.4-SNAPSHOT")

View File

@ -0,0 +1,7 @@
organization in ThisBuild := "org.example"
// We have to use snapshot because this is publishing to our local ivy cache instead of
// an integration cache, so we're in danger land.
version in ThisBuild := "3.4-SNAPSHOT"

View File

@ -0,0 +1,12 @@
# First we define the plugin project and publish it
$ copy-file changes/define/build.sbt build.sbt
$ copy-file changes/define/A.scala A.scala
# reload implied
> publishLocal
# Now we remove the source code and define a project which uses the build.
$ delete build.sbt A.scala
$ copy-file changes/use/plugins.sbt project/plugins.sbt
> reload
> check

View File

@ -0,0 +1,8 @@
val root = Project("root", file("."), settings=Defaults.defaultSettings)
TaskKey[Unit]("checkArtifacts", "test") := {
val arts = packagedArtifacts.value
assert(!arts.isEmpty, "Packaged artifacts must not be empty!")
}

View File

@ -0,0 +1 @@
> checkArtifacts

View File

@ -1,6 +1,7 @@
import sbt._
import complete.DefaultParsers._
import Keys._
import AddSettings._
object B extends Build
{
@ -11,8 +12,11 @@ object B extends Build
val sample = SettingKey[Int]("sample")
val check = TaskKey[Unit]("check")
lazy val root = Project("root", file("."), settings = Nil)
lazy val sub = Project("sub", file("."), delegates = root :: Nil, configurations = newConfig :: Nil, settings = incSample :: checkTask(4) :: Nil)
lazy val root = Project("root", file("."), settings = Nil).settingSets()
lazy val sub = Project("sub", file("."),
delegates = root :: Nil,
configurations = newConfig :: Nil,
settings = incSample :: checkTask(4) :: Nil).settingSets(buildScalaFiles)
override lazy val settings =
(sample in newConfig := 3) ::
checkTask(3) ::

View File

@ -1,5 +1,6 @@
import sbt._
import Keys.name
import AddSettings._
object TestBuild extends Build
{
@ -7,5 +8,5 @@ object TestBuild extends Build
proj("a", "."),
proj("b", "b")
)
def proj(id: String, dir: String) = Project(id, file(dir), settings = Seq( name := id ) )
}
def proj(id: String, dir: String) = Project(id, file(dir), settings = Seq( name := id ) ).settingSets(buildScalaFiles)
}

View File

@ -11,5 +11,6 @@ object SecondBuild extends MakeBuild
}
trait MakeBuild extends Build
{
def proj(id: String, dir: String) = Project(id, file(dir), settings = Seq( name := id ) )
}
import AddSettings._
def proj(id: String, dir: String) = Project(id, file(dir), settings = Seq( name := id ) ).settingSets(buildScalaFiles, defaultSbtFiles)
}

Some files were not shown because too many files have changed in this diff Show More