diff --git a/.gitignore b/.gitignore index f6f7c7ae7..e762de7f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ target/ -project/boot/ -.release.sbt __pycache__ diff --git a/compile/src/main/scala/sbt/compiler/Eval.scala b/compile/src/main/scala/sbt/compiler/Eval.scala index 536cef22c..18cff9744 100644 --- a/compile/src/main/scala/sbt/compiler/Eval.scala +++ b/compile/src/main/scala/sbt/compiler/Eval.scala @@ -12,11 +12,33 @@ import Tokens.{EOF, NEWLINE, NEWLINES, SEMI} import java.io.File import java.nio.ByteBuffer import java.net.URLClassLoader +import Eval.{getModule, getValue, WrapValName} // TODO: provide a way to cleanup backing directory final class EvalImports(val strings: Seq[(String,Int)], val srcName: String) + +/** The result of evaluating a Scala expression. The inferred type of the expression is given by `tpe`. +* The value may be obtained from `getValue` by providing a parent class loader that provides the classes from the classpath +* this expression was compiled against. Each call to `getValue` constructs a new class loader and loads +* the module from that class loader. `generated` contains the compiled classes and cache files related +* to the expression. The name of the auto-generated module wrapping the expression is `enclosingModule`. */ final class EvalResult(val tpe: String, val getValue: ClassLoader => Any, val generated: Seq[File], val enclosingModule: String) + +/** The result of evaluating a group of Scala definitions. The definitions are wrapped in an auto-generated, +* top-level module named `enclosingModule`. `generated` contains the compiled classes and cache files related to the definitions. +* A new class loader containing the module may be obtained from `loader` by passing the parent class loader providing the classes +* from the classpath that the definitions were compiled against. The list of vals with the requested types is `valNames`. +* The values for these may be obtained by providing the parent class loader to `values` as is done with `loader`.*/ +final class EvalDefinitions(val loader: ClassLoader => ClassLoader, val generated: Seq[File], val enclosingModule: String, val valNames: Seq[String]) +{ + def values(parent: ClassLoader): Seq[Any] = { + val module = getModule(enclosingModule, loader(parent)) + for(n <- valNames) yield + module.getClass.getMethod(n).invoke(module) + } +} + final class EvalException(msg: String) extends RuntimeException(msg) // not thread safe, since it reuses a Global instance final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Settings => Reporter, backing: Option[File]) @@ -46,39 +68,49 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se private[this] var toUnlinkLater = List[Symbol]() private[this] def unlink(sym: Symbol) = sym.owner.info.decls.unlink(sym) - + def eval(expression: String, imports: EvalImports = noImports, tpeName: Option[String] = None, srcName: String = "", line: Int = DefaultStartLine): EvalResult = { - val ev = new EvalType { + val ev = new EvalType[String] { def makeUnit = mkUnit(srcName, line, expression) def unlink = true - def load(moduleName: String, loader: ClassLoader): Any = getValue[Any](moduleName, loader) def unitBody(unit: CompilationUnit, importTrees: Seq[Tree], moduleName: String): Tree = { val (parser, tree) = parse(unit, settingErrorStrings, _.expr()) val tpt: Tree = expectedType(tpeName) augment(parser, importTrees, tree, tpt, moduleName) } + def extra(run: Run, unit: CompilationUnit) = atPhase(run.typerPhase.next) { (new TypeExtractor).getType(unit.body) } + def read(file: File) = IO.read(file) + def write(value: String, f: File) = IO.write(f, value) } - evalCommon(expression :: Nil, imports, tpeName, ev) + val i = evalCommon(expression :: Nil, imports, tpeName, ev) + val value = (cl: ClassLoader) => getValue[Any](i.enclosingModule, i.loader(cl)) + new EvalResult(i.extra, value, i.generated, i.enclosingModule) } - private[sbt] def evalDefinitions(definitions: Seq[(String,Range)], imports: EvalImports, srcName: String): EvalResult = + def evalDefinitions(definitions: Seq[(String,Range)], imports: EvalImports, srcName: String, valTypes: Seq[String]): EvalDefinitions = { require(definitions.nonEmpty, "Definitions to evaluate cannot be empty.") - val ev = new EvalType { + val ev = new EvalType[Seq[String]] { lazy val (fullUnit, defUnits) = mkDefsUnit(srcName, definitions) def makeUnit = fullUnit def unlink = false - def load(moduleName: String, loader: ClassLoader): Any = getModule(moduleName, loader) def unitBody(unit: CompilationUnit, importTrees: Seq[Tree], moduleName: String): Tree = { val fullParser = new syntaxAnalyzer.UnitParser(unit) val trees = defUnits flatMap parseDefinitions syntheticModule(fullParser, importTrees, trees.toList, moduleName) } + def extra(run: Run, unit: CompilationUnit) = { + val tpes = valTypes.map(tpe => rootMirror.getRequiredClass(tpe).tpe) + atPhase(run.typerPhase.next) { (new ValExtractor(tpes)).getVals(unit.body) } + } + def read(file: File) = IO.readLines(file) + def write(value: Seq[String], file: File) = IO.writeLines(file, value) } - evalCommon(definitions.map(_._1), imports, Some(""), ev) + val i = evalCommon(definitions.map(_._1), imports, Some(""), ev) + new EvalDefinitions(i.loader, i.generated, i.enclosingModule, i.extra) } - private[this] def evalCommon(content: Seq[String], imports: EvalImports, tpeName: Option[String], ev: EvalType): EvalResult = + private[this] def evalCommon[T](content: Seq[String], imports: EvalImports, tpeName: Option[String], ev: EvalType[T]): EvalIntermediate[T] = { import Eval._ val hash = Hash.toHex(Hash(bytes( stringSeqBytes(content) :: optBytes(backing)(fileExistsBytes) :: stringSeqBytes(options) :: @@ -94,21 +126,22 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se } def unlinkAll(): Unit = for( (sym, _) <- run.symSource ) if(ev.unlink) unlink(sym) else toUnlinkLater ::= sym - val (tpe, value) = - (tpeName, backing) match { - case (Some(tpe), Some(back)) if classExists(back, moduleName) => - val loader = (parent: ClassLoader) => ev.load(moduleName, new URLClassLoader(Array(back.toURI.toURL), parent)) - (tpe, loader) - case _ => - try { compileAndLoad(run, unit, imports, backing, moduleName, ev) } - finally { unlinkAll() } - } + val (extra, loader) = backing match { + case Some(back) if classExists(back, moduleName) => + val loader = (parent: ClassLoader) => new URLClassLoader(Array(back.toURI.toURL), parent) + val extra = ev.read(cacheFile(back,moduleName)) + (extra, loader) + case _ => + try { compileAndLoad(run, unit, imports, backing, moduleName, ev) } + finally { unlinkAll() } + } val classFiles = getClassFiles(backing, moduleName) - new EvalResult(tpe, value, classFiles, moduleName) + new EvalIntermediate(extra, loader, classFiles, moduleName) } - - private[this] def compileAndLoad(run: Run, unit: CompilationUnit, imports: EvalImports, backing: Option[File], moduleName: String, ev: EvalType): (String, ClassLoader => Any) = + // location of the cached type or definition information + private[this] def cacheFile(base: File, moduleName: String): File = new File(base, moduleName + ".cache") + private[this] def compileAndLoad[T](run: Run, unit: CompilationUnit, imports: EvalImports, backing: Option[File], moduleName: String, ev: EvalType[T]): (T, ClassLoader => ClassLoader) = { val dir = outputDirectory(backing) settings.outputDirs setSingleOutput dir @@ -130,10 +163,11 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se compile(run.namerPhase) checkError("Type error in expression") - val tpe = atPhase(run.typerPhase.next) { (new TypeExtractor).getType(unit.body) } - val loader = (parent: ClassLoader) => ev.load(moduleName, new AbstractFileClassLoader(dir, parent)) - (tpe, loader) + val extra = ev.extra(run, unit) + for(f <- backing) ev.write(extra, cacheFile(f, moduleName)) + val loader = (parent: ClassLoader) => new AbstractFileClassLoader(dir, parent) + (extra, loader) } private[this] def expectedType(tpeName: Option[String]): Tree = @@ -148,7 +182,6 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se def load(dir: AbstractFile, moduleName: String): ClassLoader => Any = parent => getValue[Any](moduleName, new AbstractFileClassLoader(dir, parent)) def loadPlain(dir: File, moduleName: String): ClassLoader => Any = parent => getValue[Any](moduleName, new URLClassLoader(Array(dir.toURI.toURL), parent)) - val WrapValName = "$sbtdef" //wrap tree in object objectName { def WrapValName = } def augment(parser: global.syntaxAnalyzer.UnitParser, imports: Seq[Tree], tree: Tree, tpt: Tree, objectName: String): Tree = { @@ -173,20 +206,7 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se parser.makePackaging(0, emptyPkg, (imports :+ moduleDef).toList) } - def getValue[T](objectName: String, loader: ClassLoader): T = - { - val module = getModule(objectName, loader) - val accessor = module.getClass.getMethod(WrapValName) - val value = accessor.invoke(module) - value.asInstanceOf[T] - } - private[this] def getModule(moduleName: String, loader: ClassLoader): Any = - { - val clazz = Class.forName(moduleName + "$", true, loader) - clazz.getField("MODULE$").get(null) - } - - final class TypeExtractor extends Traverser { + private[this] final class TypeExtractor extends Traverser { private[this] var result = "" def getType(t: Tree) = { result = ""; traverse(t); result } override def traverse(tree: Tree): Unit = tree match { @@ -194,6 +214,18 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se case _ => super.traverse(tree) } } + /** Tree traverser that obtains the names of vals in a top-level module whose type is a subtype of one of `types`.*/ + private[this] final class ValExtractor(types: Seq[Type]) extends Traverser { + private[this] var vals = List[String]() + def getVals(t: Tree): List[String] = { vals = Nil; traverse(t); vals } + override def traverse(tree: Tree): Unit = tree match { + case ValDef(_, n, actualTpe, _) if tree.symbol.owner.isTopLevelModule && types.exists(_ <:< actualTpe.tpe) => + vals ::= nme.localToGetter(n).encoded + case _ => super.traverse(tree) + } + } + private[this] final class EvalIntermediate[T](val extra: T, val loader: ClassLoader => ClassLoader, val generated: Seq[File], val enclosingModule: String) + private[this] def classExists(dir: File, name: String) = (new File(dir, name + ".class")).exists // TODO: use the code from Analyzer private[this] def getClassFiles(backing: Option[File], moduleName: String): Seq[File] = @@ -276,7 +308,17 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se defs } - private[this] trait EvalType { + private[this] trait EvalType[T] + { + /** Extracts additional information after the compilation unit is evaluated.*/ + def extra(run: Run, unit: CompilationUnit): T + + /** Deserializes the extra information for unchanged inputs from a cache file.*/ + def read(file: File): T + + /** Serializes the extra information to a cache file, where it can be `read` back if inputs haven't changed.*/ + def write(value: T, file: File): Unit + /** Constructs the full compilation unit for this evaluation. * This is used for error reporting during compilation. * The `unitBody` method actually does the parsing and may parse the Tree from another source. */ @@ -284,10 +326,6 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se /** If true, all top-level symbols from this evaluation will be unlinked.*/ def unlink: Boolean - - /** Gets the value for this evaluation. - * The enclosing `moduleName` and the `parent` class loader containing classes on the classpath are provided. */ - def load(moduleName: String, parent: ClassLoader): Any /** Constructs the Tree to be compiled. The full compilation `unit` from `makeUnit` is provided along with the * parsed imports `importTrees` to be used. `moduleName` should be name of the enclosing module. @@ -367,5 +405,25 @@ private object Eval buffer.array } + /** The name of the synthetic val in the synthetic module that an expression is assigned to. */ + final val WrapValName = "$sbtdef" + + /** Gets the value of the expression wrapped in module `objectName`, which is accessible via `loader`. + * The module name should not include the trailing `$`. */ + def getValue[T](objectName: String, loader: ClassLoader): T = + { + val module = getModule(objectName, loader) + val accessor = module.getClass.getMethod(WrapValName) + val value = accessor.invoke(module) + value.asInstanceOf[T] + } + + /** Gets the top-level module `moduleName` from the provided class `loader`. The module name should not include the trailing `$`.*/ + def getModule(moduleName: String, loader: ClassLoader): Any = + { + val clazz = Class.forName(moduleName + "$", true, loader) + clazz.getField("MODULE$").get(null) + } + private val classDirFilter: FileFilter = DirectoryFilter || GlobFilter("*.class") } diff --git a/compile/src/test/scala/EvalTest.scala b/compile/src/test/scala/EvalTest.scala index 7f8995bf0..520e16546 100644 --- a/compile/src/test/scala/EvalTest.scala +++ b/compile/src/test/scala/EvalTest.scala @@ -43,6 +43,27 @@ object EvalTest extends Properties("eval") } } + val ValTestNames = Set("x", "a") + val ValTestContent = """ +val x: Int = { + val y: Int = 4 + y +} +val z: Double = 3.0 +val a = 9 +val p = { + object B { val i = 3 } + class C { val j = 4 } + "asdf" +} +""" + + property("val test") = secure { + val defs = (ValTestContent, 1 to 7) :: Nil + val res = eval.evalDefinitions(defs, new EvalImports(Nil, ""), "", "scala.Int" :: Nil) + label("Val names", res.valNames) |: (res.valNames.toSet == ValTestNames) + } + property("explicit import") = forAll(testImport("import math.abs" :: Nil)) property("wildcard import") = forAll(testImport("import math._" :: Nil)) @@ -53,7 +74,7 @@ object EvalTest extends Properties("eval") value(eval.eval("abs("+i+")", new EvalImports(imports.zipWithIndex, "imp"))) == math.abs(i) private[this] def local(i: Int) = "{ class ETest(val i: Int); new ETest(" + i + ") }" - val LocalType = "java.lang.Object with ScalaObject{val i: Int}" + val LocalType = "Object{val i: Int}" private[this] def value(r: EvalResult) = r.getValue(getClass.getClassLoader) private[this] def hasErrors(line: Int, src: String) = diff --git a/main/src/main/scala/sbt/Build.scala b/main/src/main/scala/sbt/Build.scala index cc01ab8a4..9acdf6500 100644 --- a/main/src/main/scala/sbt/Build.scala +++ b/main/src/main/scala/sbt/Build.scala @@ -36,6 +36,7 @@ trait Plugin object Build { val default: Build = new Build { override def projectDefinitions(base: File) = defaultProject(base) :: Nil } + def defaultID(base: File): String = "default-" + Hash.trimHashString(base.getAbsolutePath, 6) def defaultProject(base: File): Project = Project(defaultID(base), base).settings( // if the user has overridden the name, use the normal organization that is derived from the name. diff --git a/main/src/main/scala/sbt/BuildPaths.scala b/main/src/main/scala/sbt/BuildPaths.scala index c160d5ea0..478753ea4 100644 --- a/main/src/main/scala/sbt/BuildPaths.scala +++ b/main/src/main/scala/sbt/BuildPaths.scala @@ -39,13 +39,11 @@ object BuildPaths private[this] def defaultStaging(globalBase: File) = globalBase / "staging" private[this] def defaultGlobalPlugins(globalBase: File) = globalBase / PluginsDirectoryName - def definitionSources(base: File): Seq[File] = (base * "*.scala").get def configurationSources(base: File): Seq[File] = (base * (GlobFilter("*.sbt") - ".sbt")).get def pluginDirectory(definitionBase: File) = definitionBase / PluginsDirectoryName def evalOutputDirectory(base: File) = outputDirectory(base) / "config-classes" def outputDirectory(base: File) = base / DefaultTargetName - def buildOutputDirectory(base: File, scalaInstance: xsbti.compile.ScalaInstance) = crossPath(outputDirectory(base), scalaInstance) def projectStandard(base: File) = base / "project" def projectHidden(base: File) = base / ConfigDirectoryName diff --git a/main/src/main/scala/sbt/BuildStructure.scala b/main/src/main/scala/sbt/BuildStructure.scala index eacbdc59b..663c91216 100644 --- a/main/src/main/scala/sbt/BuildStructure.scala +++ b/main/src/main/scala/sbt/BuildStructure.scala @@ -43,7 +43,7 @@ final class LoadedBuildUnit(val unit: BuildUnit, val defined: Map[String, Resolv override def toString = unit.toString } -final class LoadedDefinitions(val base: File, val target: Seq[File], val loader: ClassLoader, val builds: Seq[Build], val buildNames: Seq[String]) +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]) { def fullClasspath: Seq[Attributed[File]] = pluginData.classpath diff --git a/main/src/main/scala/sbt/BuildUtil.scala b/main/src/main/scala/sbt/BuildUtil.scala index 09463e1c3..3bc5cc9aa 100644 --- a/main/src/main/scala/sbt/BuildUtil.scala +++ b/main/src/main/scala/sbt/BuildUtil.scala @@ -59,11 +59,12 @@ object BuildUtil deps(proj)(_.aggregate) } } - def baseImports = "import sbt._, Process._, Keys._" :: Nil - def getImports(unit: BuildUnit) = baseImports ++ importAllRoot(unit.plugins.pluginNames ++ unit.definitions.buildNames) - def importAll(values: Seq[String]) = if(values.isEmpty) Nil else values.map( _ + "._" ).mkString("import ", ", ", "") :: Nil - def importAllRoot(values: Seq[String]) = importAll(values map rootedName) - def rootedName(s: String) = if(s contains '.') "_root_." + s else s + def baseImports: Seq[String] = "import sbt._, Process._, 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 importAllRoot(values: Seq[String]): Seq[String] = importAll(values map rootedName) + def rootedName(s: String): String = if(s contains '.') "_root_." + s else s def aggregationRelation(units: Map[URI, LoadedBuildUnit]): Relation[ProjectRef, ProjectRef] = { diff --git a/main/src/main/scala/sbt/EvaluateConfigurations.scala b/main/src/main/scala/sbt/EvaluateConfigurations.scala index ecc4c1c2f..2d3ba0483 100644 --- a/main/src/main/scala/sbt/EvaluateConfigurations.scala +++ b/main/src/main/scala/sbt/EvaluateConfigurations.scala @@ -14,14 +14,17 @@ package sbt object EvaluateConfigurations { private[this] final class ParsedFile(val imports: Seq[(String,Int)], val definitions: Seq[(String,LineRange)], val settings: Seq[(String,LineRange)]) - private[this] final class Definitions(val loader: ClassLoader => ClassLoader, val moduleNames: Seq[String]) private[this] val DefinitionKeywords = Seq("lazy val ", "def ", "val ") - def apply(eval: Eval, srcs: Seq[File], imports: Seq[String]): ClassLoader => Seq[Setting[_]] = - flatten(srcs.sortBy(_.getName) map { src => evaluateConfiguration(eval, src, imports) }) + def apply(eval: Eval, srcs: Seq[File], imports: Seq[String]): ClassLoader => LoadedSbtFile = + { + val loadFiles = srcs.sortBy(_.getName) map { src => evaluateSbtFile(eval, src, IO.readLines(src), imports, 0) } + loader => (LoadedSbtFile.empty /: loadFiles) { (loaded, load) => loaded merge load(loader) } + } + def evaluateConfiguration(eval: Eval, src: File, imports: Seq[String]): ClassLoader => Seq[Setting[_]] = - evaluateConfiguration(eval, src.getPath, IO.readLines(src), imports, 0) + evaluateConfiguration(eval, src, IO.readLines(src), imports, 0) private[this] def parseConfiguration(lines: Seq[String], builtinImports: Seq[String], offset: Int): ParsedFile = { @@ -31,20 +34,31 @@ object EvaluateConfigurations new ParsedFile(allImports, definitions, settings) } - def evaluateConfiguration(eval: Eval, name: String, lines: Seq[String], imports: Seq[String], offset: Int): ClassLoader => Seq[Setting[_]] = + def evaluateConfiguration(eval: Eval, file: File, lines: Seq[String], imports: Seq[String], offset: Int): ClassLoader => Seq[Setting[_]] = { + val l = evaluateSbtFile(eval, file, lines, imports, offset) + loader => l(loader).settings + } + + private[this] def evaluateSbtFile(eval: Eval, file: File, lines: Seq[String], imports: Seq[String], offset: Int): ClassLoader => LoadedSbtFile = + { + val name = file.getPath val parsed = parseConfiguration(lines, imports, offset) - val importDefs = if(parsed.definitions.isEmpty) Nil else { + val (importDefs, projects) = if(parsed.definitions.isEmpty) (Nil, (l: ClassLoader) => Nil) else { val definitions = evaluateDefinitions(eval, name, parsed.imports, parsed.definitions) - Load.importAllRoot(definitions.moduleNames).map(s => (s, -1)) + val imp = Load.importAllRoot(definitions.enclosingModule :: Nil) + val projs = (loader: ClassLoader) => definitions.values(loader).map(p => resolveBase(file.getParentFile, p.asInstanceOf[Project])) + (imp, projs) } - val allImports = importDefs ++ parsed.imports + val allImports = importDefs.map(s => (s, -1)) ++ parsed.imports val settings = parsed.settings map { case (settingExpression,range) => evaluateSetting(eval, name, allImports, settingExpression, range) } eval.unlinkDeferred() - flatten(settings) + val loadSettings = flatten(settings) + loader => new LoadedSbtFile(loadSettings(loader), projects(loader), importDefs) } + private[this] def resolveBase(f: File, p: Project) = p.copy(base = IO.resolve(f, p.base)) def flatten(mksettings: Seq[ClassLoader => Seq[Setting[_]]]): ClassLoader => Seq[Setting[_]] = loader => mksettings.flatMap(_ apply loader) def addOffset(offset: Int, lines: Seq[(String,Int)]): Seq[(String,Int)] = @@ -53,7 +67,7 @@ object EvaluateConfigurations ranges.map { case (s, r) => (s, r shift offset) } val SettingsDefinitionName = { - val _ = classOf[SettingsDefinition] // this line exists to try to provide a compile-time error when the following line needs to be changed + val _ = classOf[sbt.Def.SettingsDefinition] // this line exists to try to provide a compile-time error when the following line needs to be changed "sbt.Def.SettingsDefinition" } def evaluateSetting(eval: Eval, name: String, imports: Seq[(String,Int)], expression: String, range: LineRange): ClassLoader => Seq[Setting[_]] = @@ -104,11 +118,11 @@ object EvaluateConfigurations val trimmed = line.trim DefinitionKeywords.exists(trimmed startsWith _) } - private[this] def evaluateDefinitions(eval: Eval, name: String, imports: Seq[(String,Int)], definitions: Seq[(String,LineRange)]): Definitions = + private[this] def evaluateDefinitions(eval: Eval, name: String, imports: Seq[(String,Int)], definitions: Seq[(String,LineRange)]) = { val convertedRanges = definitions.map { case (s, r) => (s, r.start to r.end) } - val res = eval.evalDefinitions(convertedRanges, new EvalImports(imports, name), name) - new Definitions(loader => res.getValue(loader).getClass.getClassLoader, res.enclosingModule :: Nil) + val findTypes = (classOf[Project] :: /*classOf[Build] :: */ Nil).map(_.getName) + eval.evalDefinitions(convertedRanges, new EvalImports(imports, name), name, findTypes) } } object Index diff --git a/main/src/main/scala/sbt/Load.scala b/main/src/main/scala/sbt/Load.scala index 54a5ff5b5..b8857e76d 100755 --- a/main/src/main/scala/sbt/Load.scala +++ b/main/src/main/scala/sbt/Load.scala @@ -76,7 +76,7 @@ object Load { val eval = mkEval(data(config.globalPluginClasspath), base, defaultEvalOptions) val imports = baseImports ++ importAllRoot(config.globalPluginNames) - EvaluateConfigurations(eval, files, imports) + loader => EvaluateConfigurations(eval, files, imports)(loader).settings } def loadGlobal(state: State, base: File, global: File, config: LoadBuildConfiguration): LoadBuildConfiguration = if(base != global && global.exists) @@ -124,7 +124,7 @@ object Load val loaded = resolveProjects(load(rootBase, s, config)) val projects = loaded.units lazy val rootEval = lazyEval(loaded.units(loaded.root).unit) - val settings = finalTransforms(buildConfigurations(loaded, getRootProject(projects), rootEval, config.injectSettings)) + val settings = finalTransforms(buildConfigurations(loaded, getRootProject(projects), config.injectSettings)) val delegates = config.delegates(loaded) val data = makeSettings(settings, delegates, config.scopeLocal)( Project.showLoadingKey( loaded ) ) val index = structureIndex(data, settings, loaded.extra(data)) @@ -187,33 +187,20 @@ object Load } def isProjectThis(s: Setting[_]) = s.key.scope.project match { case This | Select(ThisProject) => true; case _ => false } - def buildConfigurations(loaded: LoadedBuild, rootProject: URI => String, rootEval: () => Eval, injectSettings: InjectSettings): Seq[Setting[_]] = + def buildConfigurations(loaded: LoadedBuild, rootProject: URI => String, injectSettings: InjectSettings): Seq[Setting[_]] = { ((loadedBuild in GlobalScope :== loaded) +: transformProjectOnly(loaded.root, rootProject, injectSettings.global)) ++ inScope(GlobalScope)( pluginGlobalSettings(loaded) ) ++ loaded.units.toSeq.flatMap { case (uri, build) => - val eval = if(uri == loaded.root) rootEval else lazyEval(build.unit) val plugins = build.unit.plugins.plugins val pluginBuildSettings = plugins.flatMap(_.buildSettings) val pluginNotThis = plugins.flatMap(_.settings) filterNot isProjectThis val projectSettings = build.defined flatMap { case (id, project) => - val loader = build.unit.definitions.loader - lazy val defaultSbtFiles = configurationSources(project.base) - - import AddSettings.{User,SbtFiles,DefaultSbtFiles,Plugins,Sequence} - def expand(auto: AddSettings): Seq[Setting[_]] = auto match { - case User => injectSettings.projectLoaded(loader) - case sf: SbtFiles => configurations( sf.files.map(f => IO.resolve(project.base, f)), eval, build.imports )(loader) - case sf: DefaultSbtFiles => configurations( defaultSbtFiles.filter(sf.include), eval, build.imports )(loader) - case f: Plugins => plugins.filter(f.include).flatMap(p => p.settings.filter(isProjectThis) ++ p.projectSettings) - case q: Sequence => q.sequence.flatMap(expand) - } - val ref = ProjectRef(uri, id) val defineConfig: Seq[Setting[_]] = for(c <- project.configurations) yield ( (configuration in (ref, ConfigKey(c.name))) :== c) val builtin: Seq[Setting[_]] = (thisProject :== project) +: (thisProjectRef :== ref) +: defineConfig - val settings = builtin ++ project.settings ++ expand(project.auto) ++ injectSettings.project + val settings = builtin ++ project.settings ++ injectSettings.project // map This to thisScope, Select(p) to mapRef(uri, rootProject, p) transformSettings(projectScope(ref), uri, rootProject, settings) } @@ -249,8 +236,8 @@ object Load def mkEval(classpath: Seq[File], base: File, options: Seq[String]): Eval = new Eval(options, classpath, s => new ConsoleReporter(s), Some(evalOutputDirectory(base))) - def configurations(srcs: Seq[File], eval: () => Eval, imports: Seq[String]): ClassLoader => Seq[Setting[_]] = - if(srcs.isEmpty) const(Nil) else EvaluateConfigurations(eval(), srcs, imports) + def configurations(srcs: Seq[File], eval: () => Eval, imports: Seq[String]): ClassLoader => LoadedSbtFile = + if(srcs.isEmpty) const(LoadedSbtFile.empty) else EvaluateConfigurations(eval(), srcs, imports) def load(file: File, s: State, config: LoadBuildConfiguration): PartBuild = load(file, builtinLoader(s, config.copy(pluginManagement = config.pluginManagement.shift, extraBuilds = Nil)), config.extraBuilds.toList ) @@ -387,8 +374,8 @@ object Load // we don't have the complete build graph loaded, so we don't have the rootProject function yet. // Therefore, we use resolveProjectBuild instead of resolveProjectRef. After all builds are loaded, we can fully resolve ProjectReferences. val resolveBuild = (_: Project).resolveBuild(ref => Scope.resolveProjectBuild(unit.uri, ref)) - val resolve = resolveBuild compose resolveBase(unit.localBase) - unit.definitions.builds.flatMap(_.projectDefinitions(unit.localBase) map resolve) + // although the default loader will resolve the project base directory, other loaders may not, so run resolveBase here as well + unit.definitions.projects.map(resolveBuild compose resolveBase(unit.localBase)) } def getRootProject(map: Map[URI, BuildUnitBase]): URI => String = uri => getBuild(map, uri).rootProjects.headOption getOrElse emptyBuild(uri) @@ -411,43 +398,52 @@ object Load { val normBase = localBase.getCanonicalFile val defDir = selectProjectDir(normBase, config.log) - val pluginDir = pluginDirectory(defDir) - val oldStyleExists = pluginDir.isDirectory - val newStyleExists = configurationSources(defDir).nonEmpty || projectStandard(defDir).exists - val (plugs, defs) = - if(newStyleExists || !oldStyleExists) - { - if(oldStyleExists) - config.log.warn("Detected both new and deprecated style of plugin configuration.\n Ignoring deprecated project/plugins/ directory (" + pluginDir + ").") - loadUnitNew(defDir, s, config) - } - else - loadUnitOld(defDir, pluginDir, s, config) - new BuildUnit(uri, normBase, defs, plugs) - } - def loadUnitNew(defDir: File, s: State, config: LoadBuildConfiguration): (LoadedPlugins, LoadedDefinitions) = - { val plugs = plugins(defDir, s, config) val defNames = analyzed(plugs.fullClasspath) flatMap findDefinitions val defs = if(defNames.isEmpty) Build.default :: Nil else loadDefinitions(plugs.loader, defNames) - val loadedDefs = new LoadedDefinitions(defDir, Nil, plugs.loader, defs, defNames) - (plugs, loadedDefs) + val imports = BuildUtil.getImports(plugs.pluginNames, defNames) + + lazy val eval = mkEval(plugs.classpath, defDir, Nil) + val initialProjects = defs.flatMap(_.projectDefinitions(normBase).map(resolveBase(normBase))) + + val loadedProjects = loadTransitive(initialProjects, imports, plugs, () => eval, config.injectSettings, Nil) + val loadedDefs = new LoadedDefinitions(defDir, Nil, plugs.loader, defs, loadedProjects, defNames) + new BuildUnit(uri, normBase, loadedDefs, plugs) } - def loadUnitOld(defDir: File, pluginDir: File, s: State, config: LoadBuildConfiguration): (LoadedPlugins, LoadedDefinitions) = + + private[this] def loadTransitive(newProjects: Seq[Project], imports: Seq[String], plugins: LoadedPlugins, eval: () => Eval, injectSettings: InjectSettings, acc: Seq[Project]): Seq[Project] = { - config.log.warn("Using project/plugins/ (" + pluginDir + ") for plugin configuration is deprecated.\n" + - "Put .sbt plugin definitions directly in project/,\n .scala plugin definitions in project/project/,\n and remove the project/plugins/ directory.") - val plugs = plugins(pluginDir, s, config) - val defs = definitionSources(defDir) - val target = buildOutputDirectory(defDir, config.compilers.scalac.scalaInstance) - IO.createDirectory(target) - val loadedDefs = - if(defs.isEmpty) - new LoadedDefinitions(defDir, target :: Nil, plugs.loader, Build.default :: Nil, Nil) - else - definitions(defDir, target, defs, plugs, config.definesClass, config.compilers, config.log) - (plugs, loadedDefs) + val loaded = newProjects map { project => + val loadedSbtFiles = loadSettings(project.auto, project.base, imports, plugins, eval, injectSettings) + val transformed = project.copy(settings = (project.settings: Seq[Setting[_]]) ++ loadedSbtFiles.settings) + (transformed, loadedSbtFiles.projects) + } + val (transformed, np) = loaded.unzip + val nextProjects = np.flatten + val loadedProjects = transformed ++ acc + + if(nextProjects.isEmpty) + loadedProjects + else + loadTransitive(nextProjects, imports, plugins, eval, injectSettings, loadedProjects) + } + + private[this] def loadSettings(auto: AddSettings, projectBase: File, buildImports: Seq[String], loadedPlugins: LoadedPlugins, eval: ()=>Eval, injectSettings: InjectSettings): LoadedSbtFile = + { + lazy val defaultSbtFiles = configurationSources(projectBase) + def settings(ss: Seq[Setting[_]]) = new LoadedSbtFile(ss, Nil, Nil) + val loader = loadedPlugins.loader + + import AddSettings.{User,SbtFiles,DefaultSbtFiles,Plugins,Sequence} + def expand(auto: AddSettings): LoadedSbtFile = auto match { + case User => settings(injectSettings.projectLoaded(loader)) + case sf: SbtFiles => configurations( sf.files.map(f => IO.resolve(projectBase, f)), eval, buildImports )(loader) + case sf: DefaultSbtFiles => configurations( defaultSbtFiles.filter(sf.include), eval, buildImports )(loader) + case f: Plugins => settings(loadedPlugins.plugins.filter(f.include).flatMap(p => p.settings.filter(isProjectThis) ++ p.projectSettings)) + case q: Sequence => (LoadedSbtFile.empty /: q.sequence) { (b,add) => b.merge( expand(add) ) } + } + expand(auto) } def globalPluginClasspath(globalPlugin: Option[GlobalPlugin]): Seq[Attributed[File]] = @@ -533,29 +529,11 @@ object Load config.evalPluginDef(pluginDef, pluginState) } - def definitions(base: File, targetBase: File, srcs: Seq[File], plugins: LoadedPlugins, definesClass: DefinesClass, compilers: Compilers, log: Logger): LoadedDefinitions = - { - val (inputs, defAnalysis) = build(plugins.fullClasspath, srcs, targetBase, compilers, definesClass, log) - val target = inputs.config.classesDirectory - val definitionLoader = ClasspathUtilities.toLoader(target :: Nil, plugins.loader) - val defNames = findDefinitions(defAnalysis) - val defs = if(defNames.isEmpty) Build.default :: Nil else loadDefinitions(definitionLoader, defNames) - new LoadedDefinitions(base, target :: Nil, definitionLoader, defs, defNames) - } - def loadDefinitions(loader: ClassLoader, defs: Seq[String]): Seq[Build] = defs map { definition => loadDefinition(loader, definition) } def loadDefinition(loader: ClassLoader, definition: String): Build = ModuleUtilities.getObject(definition, loader).asInstanceOf[Build] - def build(classpath: Seq[Attributed[File]], sources: Seq[File], target: File, compilers: Compilers, definesClass: DefinesClass, log: Logger): (Inputs, inc.Analysis) = - { - // TODO: make used of classpath metadata for recompilation - val inputs = Compiler.inputs(data(classpath), sources, target, Nil, Nil, definesClass, Compiler.DefaultMaxErrors, CompileOrder.Mixed)(compilers, log) - val analysis = Compiler(inputs, log) - (inputs, analysis) - } - def loadPlugins(dir: File, data: PluginData, loader: ClassLoader): LoadedPlugins = { val (pluginNames, plugins) = if(data.classpath.isEmpty) (Nil, Nil) else { diff --git a/main/src/main/scala/sbt/LoadedSbtFile.scala b/main/src/main/scala/sbt/LoadedSbtFile.scala new file mode 100644 index 000000000..b8c86dd2e --- /dev/null +++ b/main/src/main/scala/sbt/LoadedSbtFile.scala @@ -0,0 +1,17 @@ +package sbt + + import Def.Setting + +/** Represents the exported contents of a .sbt file. Currently, that includes the list of settings, +* the values of Project vals, and the import statements for all defined vals/defs. */ +private[sbt] final class LoadedSbtFile(val settings: Seq[Setting[_]], val projects: Seq[Project], val importedDefs: Seq[String]) +{ + def merge(o: LoadedSbtFile): LoadedSbtFile = + new LoadedSbtFile(settings ++ o.settings, projects ++ o.projects, importedDefs ++ o.importedDefs) +} +private[sbt] object LoadedSbtFile +{ + /** Represents an empty .sbt file: no Projects, imports, or settings.*/ + def empty = new LoadedSbtFile(Nil, Nil, Nil) +} + diff --git a/main/src/main/scala/sbt/Script.scala b/main/src/main/scala/sbt/Script.scala index e315f623d..da7546b4e 100644 --- a/main/src/main/scala/sbt/Script.scala +++ b/main/src/main/scala/sbt/Script.scala @@ -25,7 +25,7 @@ object Script import extracted._ val embeddedSettings = blocks(script).flatMap { block => - evaluate(eval(), script.getPath, block.lines, currentUnit.imports, block.offset+1)(currentLoader) + evaluate(eval(), script, block.lines, currentUnit.imports, block.offset+1)(currentLoader) } val scriptAsSource = sources in Compile := script :: Nil val asScript = scalacOptions ++= Seq("-Xscript", script.getName.stripSuffix(".scala")) diff --git a/sbt/src/sbt-test/project/Class.forName/test b/sbt/src/sbt-test/project/Class.forName/test index 60331d0c1..7f9c0a4e2 100644 --- a/sbt/src/sbt-test/project/Class.forName/test +++ b/sbt/src/sbt-test/project/Class.forName/test @@ -2,13 +2,13 @@ > package $ delete build.sbt -$ copy-file target/definition-lib-forname-test-1.0.jar project/plugins/lib/test.jar +$ copy-file target/definition-lib-forname-test-1.0.jar project/lib/test.jar $ copy-file changes/build2.sbt build.sbt # the copied project definition depends on the Test module in test.jar and will -# fail to compile if sbt did not put the jars in project/plugins/lib/ on the compile classpath +# fail to compile if sbt did not put the jars in project/lib/ on the compile classpath > reload # The project definition uses the class in test.jar and will fail here if sbt did not put the -# jars in project/plugins/lib on the runtime classpath +# jars in project/lib on the runtime classpath > use-jar \ No newline at end of file diff --git a/sbt/src/sbt-test/project/plugins/project/plugins/p.sbt b/sbt/src/sbt-test/project/plugins/project/p.sbt similarity index 100% rename from sbt/src/sbt-test/project/plugins/project/plugins/p.sbt rename to sbt/src/sbt-test/project/plugins/project/p.sbt diff --git a/sbt/src/sbt-test/project/sbt-file-projects/a/A.scala b/sbt/src/sbt-test/project/sbt-file-projects/a/A.scala new file mode 100644 index 000000000..528ffce71 --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-file-projects/a/A.scala @@ -0,0 +1 @@ +object A \ No newline at end of file diff --git a/sbt/src/sbt-test/project/sbt-file-projects/a/a.sbt b/sbt/src/sbt-test/project/sbt-file-projects/a/a.sbt new file mode 100644 index 000000000..dd33a306a --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-file-projects/a/a.sbt @@ -0,0 +1,4 @@ + +val aa = taskKey[Unit]("A task in the 'a' project") + +aa := println("Hello.") diff --git a/sbt/src/sbt-test/project/sbt-file-projects/b/build.sbt b/sbt/src/sbt-test/project/sbt-file-projects/b/build.sbt new file mode 100644 index 000000000..0c5fae53d --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-file-projects/b/build.sbt @@ -0,0 +1,4 @@ + +val h = taskKey[Unit]("A task in project 'b'") + +h := println("Hello.") diff --git a/sbt/src/sbt-test/project/sbt-file-projects/build.sbt b/sbt/src/sbt-test/project/sbt-file-projects/build.sbt new file mode 100644 index 000000000..0afc94a34 --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-file-projects/build.sbt @@ -0,0 +1,9 @@ +val a = "a" +val f = file("a") +val g = taskKey[Unit]("A task in the root project") + +val p = Project(a, f).autoSettings(AddSettings.sbtFiles( file("a.sbt") )) + +val b = Project("b", file("b")) + +g := println("Hello.") diff --git a/sbt/src/sbt-test/project/sbt-file-projects/changes/Basic.scala b/sbt/src/sbt-test/project/sbt-file-projects/changes/Basic.scala new file mode 100644 index 000000000..379e8b7eb --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-file-projects/changes/Basic.scala @@ -0,0 +1,6 @@ +import sbt._ +import Keys._ + +object B extends Build { + lazy val root = Project("root", file(".")) +} \ No newline at end of file diff --git a/sbt/src/sbt-test/project/sbt-file-projects/changes/Restricted.scala b/sbt/src/sbt-test/project/sbt-file-projects/changes/Restricted.scala new file mode 100644 index 000000000..c7a3ee533 --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-file-projects/changes/Restricted.scala @@ -0,0 +1,7 @@ +import sbt._ +import Keys._ + +object B extends Build { + lazy val root = Project("root", file(".")).autoSettings( + AddSettings.sbtFiles( file("other.sbt") )) // ignore build.sbt +} \ No newline at end of file diff --git a/sbt/src/sbt-test/project/sbt-file-projects/other.sbt b/sbt/src/sbt-test/project/sbt-file-projects/other.sbt new file mode 100644 index 000000000..c9166192a --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-file-projects/other.sbt @@ -0,0 +1 @@ +val c = Project("c", file("c")) \ No newline at end of file diff --git a/sbt/src/sbt-test/project/sbt-file-projects/test b/sbt/src/sbt-test/project/sbt-file-projects/test new file mode 100644 index 000000000..07d74c340 --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-file-projects/test @@ -0,0 +1,29 @@ +# nothing in project/ +> g +-> root/compile +> a/compile +> a/aa +> b/compile +> b/h +> c/compile + +$ copy-file changes/Basic.scala project/Build.scala +> reload +> g +> root/compile +> a/compile +> a/aa +> b/compile +> b/h +> c/compile + +$ copy-file changes/Restricted.scala project/Build.scala +> reload +> root/compile +-> g +-> h +-> a/compile +-> a/aa +-> b/compile +-> b/h +> c/compile \ No newline at end of file diff --git a/sbt/src/sbt-test/project/src-plugins/plugin/build.sbt b/sbt/src/sbt-test/project/src-plugins/plugin/build.sbt index 047198974..039c85119 100644 --- a/sbt/src/sbt-test/project/src-plugins/plugin/build.sbt +++ b/sbt/src/sbt-test/project/src-plugins/plugin/build.sbt @@ -1,3 +1,2 @@ -libraryDependencies <<= (libraryDependencies, appConfiguration) { (deps, conf) => - deps :+ ("org.scala-sbt" % "sbt" % conf.provider.id.version) -} \ No newline at end of file +libraryDependencies += + "org.scala-sbt" % "sbt" % appConfiguration.value.provider.id.version diff --git a/sbt/src/sbt-test/project/src-plugins/project/plugins/project/P.scala b/sbt/src/sbt-test/project/src-plugins/project/plugins/project/P.scala index 96c335289..0696a6f02 100644 --- a/sbt/src/sbt-test/project/src-plugins/project/plugins/project/P.scala +++ b/sbt/src/sbt-test/project/src-plugins/project/plugins/project/P.scala @@ -2,5 +2,5 @@ import sbt._ object B extends Build { - lazy val root = Project("root", file(".")).dependsOn( file("../../plugin") ) + lazy val root = Project("root", file(".")).dependsOn( file("../plugin") ) } \ No newline at end of file diff --git a/sbt/src/sbt-test/project/src-plugins/project/project/P.scala b/sbt/src/sbt-test/project/src-plugins/project/project/P.scala new file mode 100644 index 000000000..0696a6f02 --- /dev/null +++ b/sbt/src/sbt-test/project/src-plugins/project/project/P.scala @@ -0,0 +1,6 @@ +import sbt._ + +object B extends Build +{ + lazy val root = Project("root", file(".")).dependsOn( file("../plugin") ) +} \ No newline at end of file