aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristopher Vogt <oss.nsp@cvogt.org>2016-11-08 00:58:30 -0500
committerChristopher Vogt <oss.nsp@cvogt.org>2016-11-08 00:58:30 -0500
commit47a93993a84c572b4a2cd4562b52ec552f36879a (patch)
treeb21c25f2f4b593101d47dd7ddf178b2624fef10b
parent8ccefab7f8a09579087626fe75911115e8f6f483 (diff)
downloadcbt-47a93993a84c572b4a2cd4562b52ec552f36879a.tar.gz
cbt-47a93993a84c572b4a2cd4562b52ec552f36879a.tar.bz2
cbt-47a93993a84c572b4a2cd4562b52ec552f36879a.zip
Add support for dynamic re-configuration.
The exact precedence rule of override code vs original code may still need to be tweaked as we go along.
-rw-r--r--README.md37
-rw-r--r--examples/dynamic-overrides-example/build/build.scala31
-rw-r--r--libraries/eval/build/build/build.scala3
-rw-r--r--plugins/essentials/CommandLineOverrides.scala25
-rw-r--r--plugins/essentials/DynamicOverrides.scala66
-rw-r--r--plugins/essentials/Readme.md3
-rw-r--r--plugins/essentials/build/build.scala6
-rw-r--r--stage1/logger.scala2
-rw-r--r--stage2/BasicBuild.scala4
-rw-r--r--stage2/BuildBuild.scala17
-rw-r--r--stage2/Lib.scala14
-rw-r--r--test/test.scala75
12 files changed, 273 insertions, 10 deletions
diff --git a/README.md b/README.md
index 554ccc5..14277ea 100644
--- a/README.md
+++ b/README.md
@@ -267,6 +267,39 @@ Make sure you deleted your main projects class Main when running your tests.
Congratulations, you successfully created a dependent project and ran your tests.
+### Dynamic overrides and eval
+
+By making your Build extend trait `CommandLineOverrides` you get access to the `eval` and `with` commands.
+
+`eval` allows you to evaluate scala expressions in the scope of your build and show the result.
+You can use this to inspect your build.
+
+```
+$ cbt eval '1 + 1'
+2
+
+$ cbt eval scalaVersion
+2.11.8
+
+$ cbt eval 'sources.strings.mkString(":")'
+/a/b/c:/d/e/f
+```
+
+`with` allows you to inject code into your build (or rather a dynamically generated subclass).
+Follow the code with another task name and arguments if needed to run tasks of your modified build.
+You can use this to override build settings dynamically.
+
+```
+$ cbt with 'def hello = "Hello"' hello
+Hello
+
+$ cbt with 'def hello = "Hello"; def world = "World"; def helloWorld = hello ++ " " ++ world' helloWorld
+Hello World
+
+$ cbt with 'def version = super.version ++ "-SNAPSHOT"' package
+/a/b/c/e-SNAPSHOT.jar
+```
+
### Multi-projects Builds
A single build only handles a single project in CBT. So there isn't exactly
@@ -302,7 +335,7 @@ will not realize that and keep using the old version).
### Using CBT like a boss
Do you own your Build Tool or does your Build Tool own you? CBT makes it easy for YOU
-to be in control. We try to work on solid documentation, but even good
+to be in control. We try to work on solid documentation, but even good
documentation never tells the whole truth. Documentation can tell how to use
something and why things are happening, but only the code can tell all the
details of what exactly is happening. Reading the code can be intimidating for
@@ -319,11 +352,9 @@ to use it the next time. This means any changes you make are instantly reflected
This and the simple code make it super easy to fix bugs or add features yourself
and feed them back into main line CBT.
-
When debugging things, it can help to enable CBT's debug logging by passing
`-Dlog=all` to CBT (or a logger name instead of `all`).
-
Other design decisions
--------------------
diff --git a/examples/dynamic-overrides-example/build/build.scala b/examples/dynamic-overrides-example/build/build.scala
new file mode 100644
index 0000000..6cab975
--- /dev/null
+++ b/examples/dynamic-overrides-example/build/build.scala
@@ -0,0 +1,31 @@
+import cbt._
+class Build(val context: Context) extends DynamicOverrides with CommandLineOverrides{
+ def foo2 = "Build"
+ def bar2: String =
+ newBuild[Build]{"""
+ override def foo2 = "Bar2: "+Option(getClass.getName)
+ """}.foo2
+
+ def baz2: String =
+ newBuild[Build]{"""
+ override def foo2 = "Baz2: "+Option(getClass.getName)
+ override def baz2 = bar2
+ """}.baz2
+ def foo = "Build"
+
+ def bar: String = newBuild[Bar].bar
+ def baz: String = newBuild[Baz].baz
+ def bam: String = newBuild[Bam].baz
+}
+trait Bar extends Build{
+ override def bar: String = foo
+ override def foo = "Bar: "+getClass.getName
+}
+trait Baz extends Build{
+ override def foo = "Baz: "+getClass.getName
+ override def baz = bar
+}
+trait Bam extends Bar{
+ override def foo = "Baz: "+getClass.getName
+ override def baz = bar
+}
diff --git a/libraries/eval/build/build/build.scala b/libraries/eval/build/build/build.scala
index 22349c7..b93554e 100644
--- a/libraries/eval/build/build/build.scala
+++ b/libraries/eval/build/build/build.scala
@@ -1,6 +1,7 @@
import cbt._
-class Build(val context: Context) extends BuildBuild{
+class Build(val context: Context) extends BuildBuildWithoutEssentials{
+ // this is used by the essentials plugin, so we can't depend on it
override def dependencies = super.dependencies ++ Seq(
plugins.scalaTest
)
diff --git a/plugins/essentials/CommandLineOverrides.scala b/plugins/essentials/CommandLineOverrides.scala
new file mode 100644
index 0000000..32b8403
--- /dev/null
+++ b/plugins/essentials/CommandLineOverrides.scala
@@ -0,0 +1,25 @@
+package cbt
+trait CommandLineOverrides extends DynamicOverrides{
+ def `with`: Any = {
+ new lib.ReflectObject(
+ newBuild[DynamicOverrides](
+ context.copy(
+ args = context.args.drop(2)
+ )
+ )( s"""
+ ${context.args.lift(0).getOrElse("")}
+ """ )
+ ){
+ def usage = ""
+ }.callNullary(context.args.lift(1) orElse Some("void"))
+ }
+ def eval = {
+ new lib.ReflectObject(
+ newBuild[CommandLineOverrides](
+ context.copy(
+ args = ( context.args.lift(0).map("println{ "+_+" }") ).toSeq
+ )
+ ){""}
+ ){def usage = ""}.callNullary(Some("with"))
+ }
+}
diff --git a/plugins/essentials/DynamicOverrides.scala b/plugins/essentials/DynamicOverrides.scala
new file mode 100644
index 0000000..0826f12
--- /dev/null
+++ b/plugins/essentials/DynamicOverrides.scala
@@ -0,0 +1,66 @@
+package cbt
+import cbt.eval.Eval
+trait DynamicOverrides extends BaseBuild{
+ private val twitterEval = cached("eval"){
+ new Eval{
+ override lazy val impliedClassPath: List[String] = context.parentBuild.get.classpath.strings.toList//new ScalaCompilerDependency( context.cbtHasChanged, context.paths.mavenCache, scalaVersion ).classpath.strings.toList
+ override def classLoader = DynamicOverrides.this.getClass.getClassLoader
+ }
+ }
+
+ protected [cbt] def overrides: String = ""
+
+ // TODO: add support for Build inner classes
+ def newBuild[T <: DynamicOverrides:scala.reflect.ClassTag]: DynamicOverrides with T = newBuild[T](context)("")
+ def newBuild[T <: DynamicOverrides:scala.reflect.ClassTag](body: String): DynamicOverrides with T = newBuild[T](context)(body)
+ def newBuild[T <: DynamicOverrides:scala.reflect.ClassTag](context: Context)(body: String): DynamicOverrides with T = {
+ val mixinClass = scala.reflect.classTag[T].runtimeClass
+ assert(mixinClass.getTypeParameters.size == 0)
+ val mixin = if(
+ mixinClass == classOf[Nothing]
+ || mixinClass.getSimpleName == "Build"
+ ) "" else " with "+mixinClass.getName
+ import scala.collection.JavaConverters._
+ val parent = Option(
+ if(this.getClass.getName.startsWith("Evaluator__"))
+ this.getClass.getName.dropWhile(_ != '$').drop(1).stripSuffix("$1")
+ else
+ this.getClass.getName
+ ).getOrElse(
+ throw new Exception( "You cannot have more than one newBuild call on the Stack right now." )
+ )
+ val overrides = "" // currently disables, but can be used to force overrides everywhere
+ val name = if(mixin == "" && overrides == "" && body == ""){
+ "Build"
+ } else if(overrides == ""){
+ val name = "DynamicBuild" + System.currentTimeMillis
+ val code = s"""
+ class $name(context: _root_.cbt.Context)
+ extends $parent(context)$mixin{
+ $body
+ }
+ """
+ logger.dynamic("Dynamically generated code:\n" ++ code)
+ twitterEval.compile(code)
+ name
+ } else {
+ val name = "DynamicBuild" + System.currentTimeMillis
+ val code = s"""
+ class $name(context: _root_.cbt.Context)
+ extends $parent(context)$mixin{
+ $body
+ }
+ class ${name}Overrides(context: _root_.cbt.Context)
+ extends $name(context){
+ $overrides
+ }
+ """
+ logger.dynamic("Dynamically generated code:\n" ++ code)
+ twitterEval.compile(code)
+ name+"Overrides"
+ }
+
+ val createBuild = twitterEval.apply[Context => T](s"new $name(_: _root_.cbt.Context)",false)
+ createBuild( context ).asInstanceOf[DynamicOverrides with T]
+ }
+}
diff --git a/plugins/essentials/Readme.md b/plugins/essentials/Readme.md
new file mode 100644
index 0000000..76d3756
--- /dev/null
+++ b/plugins/essentials/Readme.md
@@ -0,0 +1,3 @@
+Essential CBT plugins
+
+Not part of CBT's core to keep it slim and so they can have dependencies if needed.
diff --git a/plugins/essentials/build/build.scala b/plugins/essentials/build/build.scala
new file mode 100644
index 0000000..1288367
--- /dev/null
+++ b/plugins/essentials/build/build.scala
@@ -0,0 +1,6 @@
+import cbt._
+class Build(val context: Context) extends Plugin{
+ override def dependencies =
+ super.dependencies :+ // don't forget super.dependencies here for scala-library, etc.
+ DirectoryDependency( context.cbtHome ++ "/libraries/eval" )
+}
diff --git a/stage1/logger.scala b/stage1/logger.scala
index 57f0cfa..effdc35 100644
--- a/stage1/logger.scala
+++ b/stage1/logger.scala
@@ -40,6 +40,7 @@ case class Logger(enabledLoggers: Set[String], start: Long) {
final def test(msg: => String) = log(names.test, msg)
final def git(msg: => String) = log(names.git, msg)
final def pom(msg: => String) = log(names.pom, msg)
+ final def dynamic(msg: => String) = log(names.dynamic, msg)
private object names{
val stage1 = "stage1"
@@ -52,6 +53,7 @@ case class Logger(enabledLoggers: Set[String], start: Long) {
val test = "test"
val pom = "pom"
val git = "git"
+ val dynamic = "dynamic"
}
private def logUnguarded(name: String, msg: => String) = {
diff --git a/stage2/BasicBuild.scala b/stage2/BasicBuild.scala
index ef4757e..752e0d2 100644
--- a/stage2/BasicBuild.scala
+++ b/stage2/BasicBuild.scala
@@ -246,6 +246,7 @@ trait BaseBuild extends BuildInterface with DependencyImplementation with Trigge
override def show = this.getClass.getSimpleName ++ "(" ++ projectDirectory.string ++ ")"
// TODO: allow people not provide the method name, maybe via macro
+ // TODO: pull this out into lib
/**
caches given value in context keyed with given key and projectDirectory
the context is fresh on every complete run of cbt
@@ -261,4 +262,7 @@ trait BaseBuild extends BuildInterface with DependencyImplementation with Trigge
value
}
}
+
+ // a method that can be called only to trigger any side-effects
+ final def `void` = ()
}
diff --git a/stage2/BuildBuild.scala b/stage2/BuildBuild.scala
index 68603da..5eb7622 100644
--- a/stage2/BuildBuild.scala
+++ b/stage2/BuildBuild.scala
@@ -1,14 +1,18 @@
package cbt
import java.nio.file._
-trait BuildBuild extends BaseBuild{
- private final val managedContext = context.copy(
+trait BuildBuild extends BuildBuildWithoutEssentials{
+ override def dependencies =
+ super.dependencies :+ plugins.essentials
+}
+trait BuildBuildWithoutEssentials extends BaseBuild{
+ protected final val managedContext = context.copy(
projectDirectory = managedBuildDirectory,
parentBuild=Some(this)
)
object plugins{
- // TODO: maybe move this out of the OO?
+ // TODO: move this out of the OO
final lazy val scalaTest = DirectoryDependency( context.cbtHome ++ "/plugins/scalatest" )
final lazy val sbtLayout = DirectoryDependency( context.cbtHome ++ "/plugins/sbt_layout" )
final lazy val scalaJs = DirectoryDependency( context.cbtHome ++ "/plugins/scalajs" )
@@ -17,6 +21,7 @@ trait BuildBuild extends BaseBuild{
final lazy val wartremover = DirectoryDependency( context.cbtHome ++ "/plugins/wartremover" )
final lazy val uberJar = DirectoryDependency( context.cbtHome ++ "/plugins/uber-jar" )
final lazy val sonatypeRelease = DirectoryDependency( context.cbtHome ++ "/plugins/sonatype-release" )
+ final lazy val essentials = DirectoryDependency( context.cbtHome ++ "/plugins/essentials" )
}
override def dependencies =
@@ -64,6 +69,12 @@ trait BuildBuild extends BaseBuild{
throw new Exception(
"No file build.scala (lower case) found in " ++ projectDirectory.getPath
)
+ /*
+ // is this not needed?
+ } else if( projectDirectory.getParentFile.getName == "build" && projectDirectory.getParentFile.getParentFile.getName == "build" ){
+ // can't use essentiasy, when building essentials themselves
+ new BasicBuild( managedContext ) with BuildBuildWithoutEssentials
+ */
} else if( projectDirectory.getParentFile.getName == "build" ){
new BasicBuild( managedContext ) with BuildBuild
} else {
diff --git a/stage2/Lib.scala b/stage2/Lib.scala
index 25183a3..3f6242f 100644
--- a/stage2/Lib.scala
+++ b/stage2/Lib.scala
@@ -48,7 +48,19 @@ final class Lib(logger: Logger) extends Stage1Lib(logger) with Scaffold{
val rootBuildClassName = if( useBasicBuildBuild ) buildBuildClassName else buildClassName
try{
- if(useBasicBuildBuild) default( context ) else new cbt.BasicBuild( context.copy( projectDirectory = start ) ) with BuildBuild
+ if(useBasicBuildBuild)
+ default( context )
+ else if(
+ // essentials depends on eval, which has a build that depends on scalatest
+ // this means in these we can't depend on essentials
+ // hopefully we find a better way that this pretty hacky exclusion rule
+ context.projectDirectory == (context.cbtHome ++ "/plugins/essentials")
+ || context.projectDirectory == (context.cbtHome ++ "/libraries/eval")
+ || context.projectDirectory == (context.cbtHome ++ "/plugins/scalatest")
+ )
+ new cbt.BasicBuild( context.copy( projectDirectory = start ) ) with BuildBuildWithoutEssentials
+ else
+ new cbt.BasicBuild( context.copy( projectDirectory = start ) ) with BuildBuild
} catch {
case e:ClassNotFoundException if e.getMessage == rootBuildClassName =>
throw new Exception(s"no class $rootBuildClassName found in " ++ start.string)
diff --git a/test/test.scala b/test/test.scala
index 72360d0..60f3c90 100644
--- a/test/test.scala
+++ b/test/test.scala
@@ -257,8 +257,79 @@ object Main{
{
val res = runCbt("../examples/wartremover-example", Seq("compile"))
assert(!res.exit0)
- assert(res.err.contains("var is disabled"), res.out)
- assert(res.err.contains("null is disabled"), res.out)
+ assert(res.err.contains("var is disabled"), res.err)
+ assert(res.err.contains("null is disabled"), res.err)
+ }
+
+ {
+ val res = runCbt("../examples/dynamic-overrides-example", Seq("with","""def dummy = "1.2.3" """, "dummy"))
+ assert(res.exit0)
+ assert(res.out == "1.2.3\n", res.out)
+ assert(res.err.isEmpty)
+ }
+
+ {
+ val res = runCbt("../examples/dynamic-overrides-example", Seq("with","""def dummy = "1.2.3" """, "dummy"))
+ assert(res.exit0)
+ assert(res.out == "1.2.3\n", res.out)
+ assert(res.err.isEmpty)
+ }
+
+ {
+ val res = runCbt("../examples/dynamic-overrides-example", Seq("eval",""" scalaVersion; 1 + 1 """))
+ assert(res.exit0)
+ assert(res.out == "2\n", res.out)
+ assert(res.err.isEmpty)
+ }
+
+ {
+ val res = runCbt("../examples/dynamic-overrides-example", Seq("foo"))
+ assert(res.exit0)
+ assert(res.out == "Build\n", res.out)
+ assert(res.err.isEmpty)
+ }
+
+ {
+ val res = runCbt("../examples/dynamic-overrides-example", Seq("bar"))
+ assert(res.exit0)
+ assert(res.out startsWith "Bar: DynamicBuild", res.out)
+ assert(res.out startsWith "", res.out)
+ assert(res.err.isEmpty)
+ }
+
+ {
+ val res = runCbt("../examples/dynamic-overrides-example", Seq("baz"))
+ assert(res.exit0)
+ assert(res.out startsWith "Bar: DynamicBuild", res.out)
+ assert(res.err.isEmpty)
+ }
+
+ {
+ val res = runCbt("../examples/dynamic-overrides-example", Seq("bam"))
+ assert(res.exit0)
+ assert(res.out startsWith "Baz: DynamicBuild", res.out)
+ assert(res.err.isEmpty)
+ }
+
+ {
+ val res = runCbt("../examples/dynamic-overrides-example", Seq("foo2"))
+ assert(res.exit0)
+ assert(res.out == "Build\n", res.out)
+ assert(res.err.isEmpty)
+ }
+
+ {
+ val res = runCbt("../examples/dynamic-overrides-example", Seq("bar2"))
+ assert(res.exit0)
+ assert(res.out startsWith "Bar2: Some(DynamicBuild", res.out)
+ assert(res.err.isEmpty)
+ }
+
+ {
+ val res = runCbt("../examples/dynamic-overrides-example", Seq("baz2"))
+ assert(res.exit0)
+ assert(res.out startsWith "Bar2: Some(DynamicBuild", res.out)
+ assert(res.err.isEmpty)
}
{