Export build.sbt values inside sbt.

* Expose the values PAST the Eval/sbt.compiler package.
* Find projects using the name API rather than finding htem and dropping all values immediately.
* Adds a test to make sure the .sbt values are discovered and set-able
* Expose .sbt values in Set command and inside BuildUnit methods.
* Ensure `consoleProject` can see build.sbt values.
* Add notes for where we can look in the build if we want to expose .sbt values between files.
This commit is contained in:
Josh Suereth 2014-07-16 20:58:33 -04:00
parent 4f3da04515
commit 13fc1114de
10 changed files with 160 additions and 35 deletions

View File

@ -88,7 +88,7 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se
val value = (cl: ClassLoader) => getValue[Any](i.enclosingModule, i.loader(cl)) val value = (cl: ClassLoader) => getValue[Any](i.enclosingModule, i.loader(cl))
new EvalResult(i.extra, value, i.generated, i.enclosingModule) new EvalResult(i.extra, value, i.generated, i.enclosingModule)
} }
def evalDefinitions(definitions: Seq[(String, scala.Range)], imports: EvalImports, srcName: String, valTypes: Seq[String]): EvalDefinitions = def evalDefinitions(definitions: Seq[(String, scala.Range)], imports: EvalImports, srcName: String): EvalDefinitions =
{ {
require(definitions.nonEmpty, "Definitions to evaluate cannot be empty.") require(definitions.nonEmpty, "Definitions to evaluate cannot be empty.")
val ev = new EvalType[Seq[String]] { val ev = new EvalType[Seq[String]] {
@ -101,8 +101,7 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se
syntheticModule(fullParser, importTrees, trees.toList, moduleName) syntheticModule(fullParser, importTrees, trees.toList, moduleName)
} }
def extra(run: Run, unit: CompilationUnit) = { def extra(run: Run, unit: CompilationUnit) = {
val tpes = valTypes.map(tpe => rootMirror.getRequiredClass(tpe).tpe) atPhase(run.typerPhase.next) { (new ValExtractor()).getVals(unit.body) }
atPhase(run.typerPhase.next) { (new ValExtractor(tpes)).getVals(unit.body) }
} }
def read(file: File) = IO.readLines(file) def read(file: File) = IO.readLines(file)
def write(value: Seq[String], file: File) = IO.writeLines(file, value) def write(value: Seq[String], file: File) = IO.writeLines(file, value)
@ -215,11 +214,18 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se
} }
} }
/** Tree traverser that obtains the names of vals in a top-level module whose type is a subtype of one of `types`.*/ /** 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] final class ValExtractor() extends Traverser {
private[this] var vals = List[String]() private[this] var vals = List[String]()
def getVals(t: Tree): List[String] = { vals = Nil; traverse(t); vals } def getVals(t: Tree): List[String] = { vals = Nil; traverse(t); vals }
override def traverse(tree: Tree): Unit = tree match { override def traverse(tree: Tree): Unit = tree match {
case ValDef(_, n, actualTpe, _) if isTopLevelModule(tree.symbol.owner) && types.exists(_ <:< actualTpe.tpe) => // TODO - We really need to clean this up so that we can filter by type and
// track which vals are projects vs. other vals. It's important so that we avoid
// instantiating values more than absolutely necessary on different classloaders.
// However, it's not terrible right now if we do, as most likely the values
// are used to instantiate each other i.e. a valuing in a build.sbt file is most likely
// used in something which is contained in a `Project` vaue, therefore it will be
// instantiated anyway.
case ValDef(_, n, actualTpe, _) if isTopLevelModule(tree.symbol.owner) =>
vals ::= nme.localToGetter(n).encoded vals ::= nme.localToGetter(n).encoded
case _ => super.traverse(tree) case _ => super.traverse(tree)
} }
@ -387,7 +393,7 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se
private[this] def index(a: Array[Int], i: Int): Int = if (i < 0 || i >= a.length) 0 else a(i) private[this] def index(a: Array[Int], i: Int): Int = if (i < 0 || i >= a.length) 0 else a(i)
} }
} }
private object Eval { private[sbt] object Eval {
def optBytes[T](o: Option[T])(f: T => Array[Byte]): Array[Byte] = seqBytes(o.toSeq)(f) def optBytes[T](o: Option[T])(f: T => Array[Byte]): Array[Byte] = seqBytes(o.toSeq)(f)
def stringSeqBytes(s: Seq[String]): Array[Byte] = seqBytes(s)(bytes) def stringSeqBytes(s: Seq[String]): Array[Byte] = seqBytes(s)(bytes)
def seqBytes[T](s: Seq[T])(f: T => Array[Byte]): Array[Byte] = bytes(s map f) def seqBytes[T](s: Seq[T])(f: T => Array[Byte]): Array[Byte] = bytes(s map f)

View File

@ -50,15 +50,15 @@ final class LoadedBuildUnit(val unit: BuildUnit, val defined: Map[String, Resolv
/** /**
* The classpath to use when compiling against this build unit's publicly visible code. * 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. * It includes build definition and plugin classes and classes for .sbt file statements and expressions.
*/ */
def classpath: Seq[File] = unit.definitions.target ++ unit.plugins.classpath def classpath: Seq[File] = unit.definitions.target ++ unit.plugins.classpath ++ unit.definitions.dslDefinitions.classpath
/** /**
* The class loader to use for this build unit's publicly visible code. * 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. * It includes build definition and plugin classes and classes for .sbt file statements and expressions.
*/ */
def loader = unit.definitions.loader def loader = unit.definitions.dslDefinitions.classloader(unit.definitions.loader)
/** The imports to use for .sbt files, `consoleProject` and other contexts that use code from the build definition. */ /** The imports to use for .sbt files, `consoleProject` and other contexts that use code from the build definition. */
def imports = BuildUtil.getImports(unit) def imports = BuildUtil.getImports(unit)
@ -80,7 +80,21 @@ final class LoadedBuildUnit(val unit: BuildUnit, val defined: Map[String, Resolv
* and their `settings` and `configurations` updated as appropriate. * and their `settings` and `configurations` updated as appropriate.
* @param buildNames No longer used and will be deprecated once feasible. * @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 LoadedDefinitions(
val base: File,
val target: Seq[File],
val loader: ClassLoader,
val builds: Seq[Build],
val projects: Seq[Project],
val buildNames: Seq[String],
val dslDefinitions: DefinedSbtValues) {
def this(base: File,
target: Seq[File],
loader: ClassLoader,
builds: Seq[Build],
projects: Seq[Project],
buildNames: Seq[String]) = this(base, target, loader, builds, projects, buildNames, DefinedSbtValues.empty)
}
/** Auto-detected top-level modules (as in `object X`) of type `T` paired with their source names. */ /** 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)]) { final class DetectedModules[T](val modules: Seq[(String, T)]) {

View File

@ -71,7 +71,7 @@ object BuildUtil {
def baseImports: Seq[String] = "import sbt._, Keys._, dsl._" :: Nil def baseImports: Seq[String] = "import sbt._, Keys._, dsl._" :: Nil
def getImports(unit: BuildUnit): Seq[String] = unit.plugins.detected.imports def getImports(unit: BuildUnit): Seq[String] = unit.plugins.detected.imports ++ unit.definitions.dslDefinitions.imports
@deprecated("Use getImports(Seq[String]).", "0.13.2") @deprecated("Use getImports(Seq[String]).", "0.13.2")
def getImports(pluginNames: Seq[String], buildNames: Seq[String]): Seq[String] = getImports(pluginNames ++ buildNames) def getImports(pluginNames: Seq[String], buildNames: Seq[String]): Seq[String] = getImports(pluginNames ++ buildNames)

View File

@ -9,14 +9,16 @@ object ConsoleProject {
def apply(state: State, extra: String, cleanupCommands: String = "", options: Seq[String] = Nil)(implicit log: Logger) { def apply(state: State, extra: String, cleanupCommands: String = "", options: Seq[String] = Nil)(implicit log: Logger) {
val extracted = Project extract state val extracted = Project extract state
val cpImports = new Imports(extracted, state) val cpImports = new Imports(extracted, state)
val bindings = ("currentState" -> state) :: ("extracted" -> extracted) :: ("cpHelpers" -> cpImports) :: Nil val bindings = ("currentState" -> state) :: ("extracted" -> extracted) :: ("cpHelpers" -> cpImports) :: Nil
val unit = extracted.currentUnit val unit = extracted.currentUnit
val compiler = Compiler.compilers(ClasspathOptions.repl)(state.configuration, log).scalac val compiler = Compiler.compilers(ClasspathOptions.repl)(state.configuration, log).scalac
val imports = BuildUtil.getImports(unit.unit) ++ BuildUtil.importAll(bindings.map(_._1)) val imports = BuildUtil.getImports(unit.unit) ++ BuildUtil.importAll(bindings.map(_._1))
val importString = imports.mkString("", ";\n", ";\n\n") val importString = imports.mkString("", ";\n", ";\n\n")
val initCommands = importString + extra val initCommands = importString + extra
(new Console(compiler))(unit.classpath, options, initCommands, cleanupCommands)(Some(unit.loader), bindings) // TODO - Hook up dsl classpath correctly...
(new Console(compiler))(
unit.classpath, options, initCommands, cleanupCommands
)(Some(unit.loader), bindings)
} }
/** Conveniences for consoleProject that shouldn't normally be used for builds. */ /** Conveniences for consoleProject that shouldn't normally be used for builds. */
final class Imports private[sbt] (extracted: Extracted, state: State) { final class Imports private[sbt] (extracted: Extracted, state: State) {

View File

@ -99,11 +99,12 @@ object EvaluateConfigurations {
// detection for which project project manipulations should be applied. // detection for which project project manipulations should be applied.
val name = file.getPath val name = file.getPath
val parsed = parseConfiguration(lines, imports, offset) val parsed = parseConfiguration(lines, imports, offset)
val (importDefs, projects) = if (parsed.definitions.isEmpty) (Nil, (l: ClassLoader) => Nil) else { val (importDefs, definitions) =
if (parsed.definitions.isEmpty) (Nil, DefinedSbtValues.empty) else {
val definitions = evaluateDefinitions(eval, name, parsed.imports, parsed.definitions) val definitions = evaluateDefinitions(eval, name, parsed.imports, parsed.definitions)
val imp = BuildUtil.importAllRoot(definitions.enclosingModule :: Nil) val imp = BuildUtil.importAllRoot(definitions.enclosingModule :: Nil)
val projs = (loader: ClassLoader) => definitions.values(loader).map(p => resolveBase(file.getParentFile, p.asInstanceOf[Project])) val projs = (loader: ClassLoader) => definitions.values(loader).map(p => resolveBase(file.getParentFile, p.asInstanceOf[Project]))
(imp, projs) (imp, DefinedSbtValues(definitions))
} }
val allImports = importDefs.map(s => (s, -1)) ++ parsed.imports val allImports = importDefs.map(s => (s, -1)) ++ parsed.imports
val dslEntries = parsed.settings map { val dslEntries = parsed.settings map {
@ -112,6 +113,10 @@ object EvaluateConfigurations {
} }
eval.unlinkDeferred() eval.unlinkDeferred()
loader => { loader => {
val projects =
definitions.values(loader).collect {
case p: Project => resolveBase(file.getParentFile, p)
}
val (settingsRaw, manipulationsRaw) = val (settingsRaw, manipulationsRaw) =
dslEntries map (_ apply loader) partition { dslEntries map (_ apply loader) partition {
case internals.ProjectSettings(_) => true case internals.ProjectSettings(_) => true
@ -124,9 +129,8 @@ object EvaluateConfigurations {
val manipulations = manipulationsRaw map { val manipulations = manipulationsRaw map {
case internals.ProjectManipulation(f) => f case internals.ProjectManipulation(f) => f
} }
val ps = projects(loader)
// TODO -get project manipulations. // TODO -get project manipulations.
new LoadedSbtFile(settings, ps, importDefs, manipulations) new LoadedSbtFile(settings, projects, importDefs, manipulations, definitions)
} }
} }
/** move a project to be relative to this file after we've evaluated it. */ /** move a project to be relative to this file after we've evaluated it. */
@ -184,7 +188,7 @@ object EvaluateConfigurations {
* @return A method that given an sbt classloader, can return the actual Seq[Setting[_]] defined by * @return A method that given an sbt classloader, can return the actual Seq[Setting[_]] defined by
* the expression. * the expression.
*/ */
@deprecated("Build DSL now includes non-Setting[_] type settings.", "0.13.6") @deprecated("Build DSL now includes non-Setting[_] type settings.", "0.13.6") // Note: This method is used by the SET command, so we may want to evaluate that sucker a bit.
def evaluateSetting(eval: Eval, name: String, imports: Seq[(String, Int)], expression: String, range: LineRange): ClassLoader => Seq[Setting[_]] = def evaluateSetting(eval: Eval, name: String, imports: Seq[(String, Int)], expression: String, range: LineRange): ClassLoader => Seq[Setting[_]] =
{ {
evaluateDslEntry(eval, name, imports, expression, range) andThen { evaluateDslEntry(eval, name, imports, expression, range) andThen {
@ -231,11 +235,10 @@ object EvaluateConfigurations {
val trimmed = line.trim val trimmed = line.trim
DefinitionKeywords.exists(trimmed startsWith _) DefinitionKeywords.exists(trimmed startsWith _)
} }
private[this] def evaluateDefinitions(eval: Eval, name: String, imports: Seq[(String, Int)], definitions: Seq[(String, LineRange)]) = private[this] def evaluateDefinitions(eval: Eval, name: String, imports: Seq[(String, Int)], definitions: Seq[(String, LineRange)]): compiler.EvalDefinitions =
{ {
val convertedRanges = definitions.map { case (s, r) => (s, r.start to r.end) } val convertedRanges = definitions.map { case (s, r) => (s, r.start to r.end) }
val findTypes = (classOf[Project] :: /*classOf[Build] :: */ Nil).map(_.getName) eval.evalDefinitions(convertedRanges, new EvalImports(imports, name), name)
eval.evalDefinitions(convertedRanges, new EvalImports(imports, name), name, findTypes)
} }
} }
object Index { object Index {

View File

@ -426,9 +426,13 @@ object Load {
val defaultProjects = loadProjects(projectsFromBuild(b, normBase), false) val defaultProjects = loadProjects(projectsFromBuild(b, normBase), false)
(defaultProjects ++ loadedProjectsRaw, b) (defaultProjects ++ loadedProjectsRaw, b)
} }
val defs = if (defsScala.isEmpty) defaultBuildIfNone :: Nil else defsScala val defs = if (defsScala.isEmpty) defaultBuildIfNone :: Nil else defsScala
val loadedDefs = new sbt.LoadedDefinitions(defDir, Nil, plugs.loader, defs, loadedProjects, plugs.detected.builds.names) // HERE we pull out the defined vals from memoSettings and unify them all so
// we can use them later.
val valDefinitions = memoSettings.values.foldLeft(DefinedSbtValues.empty) { (prev, sbtFile) =>
prev.zip(sbtFile.definitions)
}
val loadedDefs = new sbt.LoadedDefinitions(defDir, Nil, plugs.loader, defs, loadedProjects, plugs.detected.builds.names, valDefinitions)
new sbt.BuildUnit(uri, normBase, loadedDefs, plugs) new sbt.BuildUnit(uri, normBase, loadedDefs, plugs)
} }
@ -479,6 +483,9 @@ object Load {
* @param context The plugin management context for autogenerated IDs. * @param context The plugin management context for autogenerated IDs.
* *
* @return The completely resolved/updated sequence of projects defined, with all settings expanded. * @return The completely resolved/updated sequence of projects defined, with all settings expanded.
*
* TODO - We want to attach the known (at this time) vals/lazy vals defined in each project's
* build.sbt to that project so we can later use this for the `set` command.
*/ */
private[this] def loadTransitive( private[this] def loadTransitive(
newProjects: Seq[Project], newProjects: Seq[Project],
@ -523,7 +530,6 @@ object Load {
loadTransitive(rest ++ discovered, buildBase, plugins, eval, injectSettings, acc :+ finished, memoSettings, log, false, buildUri, context) loadTransitive(rest ++ discovered, buildBase, plugins, eval, injectSettings, acc :+ finished, memoSettings, log, false, buildUri, context)
case Nil if makeOrDiscoverRoot => case Nil if makeOrDiscoverRoot =>
log.debug(s"[Loading] Scanning directory ${buildBase}") log.debug(s"[Loading] Scanning directory ${buildBase}")
// TODO - Here we want to fully discover everything and make a default build...
discover(AddSettings.defaultSbtFiles, buildBase) match { discover(AddSettings.defaultSbtFiles, buildBase) match {
case DiscoveredProjects(Some(root), discovered, files) => case DiscoveredProjects(Some(root), discovered, files) =>
log.debug(s"[Loading] Found root project ${root.id} w/ remaining ${discovered.map(_.id).mkString(",")}") log.debug(s"[Loading] Found root project ${root.id} w/ remaining ${discovered.map(_.id).mkString(",")}")
@ -651,6 +657,8 @@ object Load {
// Classloader of the build // Classloader of the build
val loader = loadedPlugins.loader val loader = loadedPlugins.loader
// How to load an individual file for use later. // How to load an individual file for use later.
// TODO - We should import vals defined in other sbt files here, if we wish to
// share. For now, build.sbt files have their own unique namespace.
def loadSettingsFile(src: File): LoadedSbtFile = def loadSettingsFile(src: File): LoadedSbtFile =
EvaluateConfigurations.evaluateSbtFile(eval(), src, IO.readLines(src), loadedPlugins.detected.imports, 0)(loader) EvaluateConfigurations.evaluateSbtFile(eval(), src, IO.readLines(src), loadedPlugins.detected.imports, 0)(loader)
// How to merge SbtFiles we read into one thing // How to merge SbtFiles we read into one thing

View File

@ -1,6 +1,7 @@
package sbt package sbt
import Def.Setting import Def.Setting
import java.io.File
/** /**
* Represents the exported contents of a .sbt file. Currently, that includes the list of settings, * Represents the exported contents of a .sbt file. Currently, that includes the list of settings,
@ -10,15 +11,69 @@ private[sbt] final class LoadedSbtFile(
val settings: Seq[Setting[_]], val settings: Seq[Setting[_]],
val projects: Seq[Project], val projects: Seq[Project],
val importedDefs: Seq[String], val importedDefs: Seq[String],
val manipulations: Seq[Project => Project]) { val manipulations: Seq[Project => Project],
// TODO - we may want to expose a simpler interface on top of here for the set command,
// rather than what we have now...
val definitions: DefinedSbtValues) {
@deprecated("LoadedSbtFiles are no longer directly merged.", "0.13.6") @deprecated("LoadedSbtFiles are no longer directly merged.", "0.13.6")
def merge(o: LoadedSbtFile): LoadedSbtFile = def merge(o: LoadedSbtFile): LoadedSbtFile =
new LoadedSbtFile(settings ++ o.settings, projects ++ o.projects, importedDefs ++ o.importedDefs, manipulations) new LoadedSbtFile(
settings ++ o.settings,
projects ++ o.projects,
importedDefs ++ o.importedDefs,
manipulations,
definitions zip o.definitions)
def clearProjects = new LoadedSbtFile(settings, Nil, importedDefs, manipulations) def clearProjects = new LoadedSbtFile(settings, Nil, importedDefs, manipulations, definitions)
} }
/**
* Represents the `val`/`lazy val` definitions defined within a build.sbt file
* which we can reference in other settings.
*/
private[sbt] final class DefinedSbtValues(val sbtFiles: Seq[compiler.EvalDefinitions]) {
def values(parent: ClassLoader): Seq[Any] =
sbtFiles flatMap (_ values parent)
def classloader(parent: ClassLoader): ClassLoader =
sbtFiles.foldLeft(parent) { (cl, e) => e.loader(cl) }
def imports: Seq[String] = {
// TODO - Sanity check duplicates and such, so users get a nice warning rather
// than explosion.
for {
file <- sbtFiles
m = file.enclosingModule
v <- file.valNames
} yield s"import ${m}.${v}"
}
def generated: Seq[File] =
sbtFiles flatMap (_.generated)
// Returns a classpath for the generated .sbt files.
def classpath: Seq[File] =
generated.map(_.getParentFile).distinct
/**
* Joins the defines of this build.sbt with another.
* TODO - we may want to figure out scoping rules, as this could lead to
* ambiguities.
*/
def zip(other: DefinedSbtValues): DefinedSbtValues =
new DefinedSbtValues(sbtFiles ++ other.sbtFiles)
}
private[sbt] object DefinedSbtValues {
/** Construct a DefinedSbtValues object directly from the underlying representation. */
def apply(eval: compiler.EvalDefinitions): DefinedSbtValues =
new DefinedSbtValues(Seq(eval))
/** Construct an empty value object. */
def empty = new DefinedSbtValues(Nil)
}
private[sbt] object LoadedSbtFile { private[sbt] object LoadedSbtFile {
/** Represents an empty .sbt file: no Projects, imports, or settings.*/ /** Represents an empty .sbt file: no Projects, imports, or settings.*/
def empty = new LoadedSbtFile(Nil, Nil, Nil, Nil) def empty = new LoadedSbtFile(Nil, Nil, Nil, Nil, DefinedSbtValues.empty)
} }

View File

@ -234,7 +234,19 @@ object BuiltinCommands {
case (s, (all, arg)) => case (s, (all, arg)) =>
val extracted = Project extract s val extracted = Project extract s
import extracted._ import extracted._
val settings = EvaluateConfigurations.evaluateSetting(session.currentEval(), "<set>", imports(extracted), arg, LineRange(0, 0))(currentLoader) val dslVals = extracted.currentUnit.unit.definitions.dslDefinitions
// TODO - This is horribly inefficient. We should try to only attach the
// classloader + imports NEEDED to compile the set command.
System.err.println(s"DSL imports: ${dslVals.imports mkString "\n"}")
val ims = (imports(extracted) ++ dslVals.imports.map(i => (i, -1)))
val cl = dslVals.classloader(currentLoader)
val settings = EvaluateConfigurations.evaluateSetting(
session.currentEval(),
"<set>",
ims,
arg,
LineRange(0, 0)
)(cl)
val setResult = if (all) SettingCompletions.setAll(extracted, settings) else SettingCompletions.setThis(s, extracted, settings, arg) val setResult = if (all) SettingCompletions.setAll(extracted, settings) else SettingCompletions.setThis(s, extracted, settings, arg)
s.log.info(setResult.quietSummary) s.log.info(setResult.quietSummary)
s.log.debug(setResult.verboseSummary) s.log.debug(setResult.verboseSummary)

View File

@ -0,0 +1,23 @@
TaskKey[Unit]("checkName", "") := {
assert(name.value == "hello-world", "Name is not hello-worled, failed to set!")
}
val notExistingThing = settingKey[Int]("Something new")
TaskKey[Unit]("checkBuildSbtDefined", "") := {
assert(notExistingThing.?.value == Some(5), "Failed to set a settingKey defined in build.sbt")
}
commands ++= Seq(
Command.command("helloWorldTest") { state: State =>
"""set name := "hello-world"""" ::
"checkName" ::
state
},
Command.command("buildSbtTest") { state: State =>
"""set notExistingThing := 5""" ::
"checkBuildSbtDefined" ::
state
}
)

View File

@ -0,0 +1,2 @@
> helloWorldTest
> buildSbtTest