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))
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.")
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)
}
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) }
atPhase(run.typerPhase.next) { (new ValExtractor()).getVals(unit.body) }
}
def read(file: File) = IO.readLines(file)
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`.*/
private[this] final class ValExtractor(types: Seq[Type]) extends Traverser {
private[this] final class ValExtractor() 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 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
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 object Eval {
private[sbt] object Eval {
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 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.
* 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.
* 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. */
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.
* @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. */
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 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")
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) {
val extracted = Project extract state
val cpImports = new Imports(extracted, state)
val bindings = ("currentState" -> state) :: ("extracted" -> extracted) :: ("cpHelpers" -> cpImports) :: Nil
val unit = extracted.currentUnit
val compiler = Compiler.compilers(ClasspathOptions.repl)(state.configuration, log).scalac
val imports = BuildUtil.getImports(unit.unit) ++ BuildUtil.importAll(bindings.map(_._1))
val importString = imports.mkString("", ";\n", ";\n\n")
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. */
final class Imports private[sbt] (extracted: Extracted, state: State) {

View File

@ -99,12 +99,13 @@ object EvaluateConfigurations {
// detection for which project project manipulations should be applied.
val name = file.getPath
val parsed = parseConfiguration(lines, imports, offset)
val (importDefs, projects) = if (parsed.definitions.isEmpty) (Nil, (l: ClassLoader) => Nil) else {
val definitions = evaluateDefinitions(eval, name, parsed.imports, parsed.definitions)
val imp = BuildUtil.importAllRoot(definitions.enclosingModule :: Nil)
val projs = (loader: ClassLoader) => definitions.values(loader).map(p => resolveBase(file.getParentFile, p.asInstanceOf[Project]))
(imp, projs)
}
val (importDefs, definitions) =
if (parsed.definitions.isEmpty) (Nil, DefinedSbtValues.empty) else {
val definitions = evaluateDefinitions(eval, name, parsed.imports, parsed.definitions)
val imp = BuildUtil.importAllRoot(definitions.enclosingModule :: Nil)
val projs = (loader: ClassLoader) => definitions.values(loader).map(p => resolveBase(file.getParentFile, p.asInstanceOf[Project]))
(imp, DefinedSbtValues(definitions))
}
val allImports = importDefs.map(s => (s, -1)) ++ parsed.imports
val dslEntries = parsed.settings map {
case (dslExpression, range) =>
@ -112,6 +113,10 @@ object EvaluateConfigurations {
}
eval.unlinkDeferred()
loader => {
val projects =
definitions.values(loader).collect {
case p: Project => resolveBase(file.getParentFile, p)
}
val (settingsRaw, manipulationsRaw) =
dslEntries map (_ apply loader) partition {
case internals.ProjectSettings(_) => true
@ -124,9 +129,8 @@ object EvaluateConfigurations {
val manipulations = manipulationsRaw map {
case internals.ProjectManipulation(f) => f
}
val ps = projects(loader)
// 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. */
@ -184,7 +188,7 @@ object EvaluateConfigurations {
* @return A method that given an sbt classloader, can return the actual Seq[Setting[_]] defined by
* 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[_]] =
{
evaluateDslEntry(eval, name, imports, expression, range) andThen {
@ -231,11 +235,10 @@ 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)]) =
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 findTypes = (classOf[Project] :: /*classOf[Build] :: */ Nil).map(_.getName)
eval.evalDefinitions(convertedRanges, new EvalImports(imports, name), name, findTypes)
eval.evalDefinitions(convertedRanges, new EvalImports(imports, name), name)
}
}
object Index {

View File

@ -426,9 +426,13 @@ object Load {
val defaultProjects = loadProjects(projectsFromBuild(b, normBase), false)
(defaultProjects ++ loadedProjectsRaw, b)
}
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)
}
@ -479,6 +483,9 @@ object Load {
* @param context The plugin management context for autogenerated IDs.
*
* @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(
newProjects: Seq[Project],
@ -518,12 +525,11 @@ object Load {
// Continue loading if we find any more.
newProjects match {
case Seq(next, rest @ _*) =>
log.debug(s"[Loading] Loading project ${next.id} @ ${next.base}")
log.debug(s"[Loading] Loading project ${next.id} @ ${next.base}")
val (finished, discovered) = discoverAndLoad(next)
loadTransitive(rest ++ discovered, buildBase, plugins, eval, injectSettings, acc :+ finished, memoSettings, log, false, buildUri, context)
case Nil if makeOrDiscoverRoot =>
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 {
case DiscoveredProjects(Some(root), discovered, files) =>
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
val loader = loadedPlugins.loader
// 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 =
EvaluateConfigurations.evaluateSbtFile(eval(), src, IO.readLines(src), loadedPlugins.detected.imports, 0)(loader)
// How to merge SbtFiles we read into one thing

View File

@ -1,6 +1,7 @@
package sbt
import Def.Setting
import java.io.File
/**
* 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 projects: Seq[Project],
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")
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 {
/** 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)) =>
val extracted = Project extract s
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)
s.log.info(setResult.quietSummary)
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