diff options
author | Christopher Vogt <oss.nsp@cvogt.org> | 2016-11-08 00:58:30 -0500 |
---|---|---|
committer | Christopher Vogt <oss.nsp@cvogt.org> | 2016-11-08 00:58:30 -0500 |
commit | 47a93993a84c572b4a2cd4562b52ec552f36879a (patch) | |
tree | b21c25f2f4b593101d47dd7ddf178b2624fef10b | |
parent | 8ccefab7f8a09579087626fe75911115e8f6f483 (diff) | |
download | cbt-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.md | 37 | ||||
-rw-r--r-- | examples/dynamic-overrides-example/build/build.scala | 31 | ||||
-rw-r--r-- | libraries/eval/build/build/build.scala | 3 | ||||
-rw-r--r-- | plugins/essentials/CommandLineOverrides.scala | 25 | ||||
-rw-r--r-- | plugins/essentials/DynamicOverrides.scala | 66 | ||||
-rw-r--r-- | plugins/essentials/Readme.md | 3 | ||||
-rw-r--r-- | plugins/essentials/build/build.scala | 6 | ||||
-rw-r--r-- | stage1/logger.scala | 2 | ||||
-rw-r--r-- | stage2/BasicBuild.scala | 4 | ||||
-rw-r--r-- | stage2/BuildBuild.scala | 17 | ||||
-rw-r--r-- | stage2/Lib.scala | 14 | ||||
-rw-r--r-- | test/test.scala | 75 |
12 files changed, 273 insertions, 10 deletions
@@ -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) } { |