Natures - Experimental high level abstraction for builds

Added by Tobias Roeser over 1 year ago

Today, I will announce some very exciting development in SBuild land...

Natures

For now, I have only a prototype, but looking onto what I have already, I am very excited to release it in a near future.

Imagine, you could write a SBuild build file like so:

 1class SBuild(implicit project: Project) {
 2
 3  // Easy access to Maven Central repo
 4  SchemeHandler("mvn", new MvnSchemeHandler())
 5
 6  // A compile classpath
 7  val compileCp = 
 8    "mvn:org.scala-lang:scala-library:2.9.2" ~
 9    "mvn:org.slf4j:slf4j-api:1.7.2" 
10
11  // Define an artifact with some good default, that of course can be overridden
12  val myArtifact =
13    // implicitly adds a "clean" target
14    new CleanNature
15    // implicitly adds a "compileScala" target
16    with CompileScalaNature
17    // implicitly adds a "jar" target
18    with JarNature
19    {
20      // here, we define the bare minimum, to get this example to work
21      // a name, a version and the compile dependencies 
22      override def artifact_name = "org.example.myartifact" 
23      override def artifact_version = "1.0.0" 
24      override def compileScala_dependsOn = compileCp
25    }
26
27  // this will effectively create all targets implied by the configuration in this project
28  myArtifact.createTargets
29
30}

What looks like a whole lot of magic, is far from magic at all. Each nature is a trait extending a Nature trait, potentially already providing some good defaults. With a bit of naming policy, this is a very clean concept. All the work does the compiler. And, of course at no point you sacrify the control. You can always write targets directly. And, as you get the targets which you created with Nature.createTargets as a return value, you can even extend or manipulate them further, e.g. by adding additional dependencies.

The output of 'sbuild -l' of the above defined project:

% sbuild -l
clean 
compileScala
target/org.example.myartifact-1.0.0.jar

The above buildscript is al that is needed to have a working compile, jar and clean target in a project.

Want have a look at the current internals?

Here are my current drafts for three of the obove used natures.

Nature trait

 1/**
 2 * To ensure, that it is always obvious where a def/val comes from, some naming policy is required.
 3 * Without, users would very fast have the feeling of magic and uncertainty.
 4 *
 5 * Also, as a best practice, when configuring your mixed natures,
 6 * you should always add the optional "overwrite" keyword when you intend to overwrite something.
 7 * That way, the compiler will understand your intend and can give a meaningful error message,
 8 * if for some reason there is no such def to override.
 9 *
10 * Naming policy: Each nature should only define new methods in its own namespace.
11 * Example: The Nature "MyOwnNature" should create all def's with the prefix "myOwn_", the "Nature" suffix should not be part of it.
12 * Of course, "myOwn" would be also ok as a name, if only one def is needed and the name is already self describing,
13 * like e.g. "OutputDirNature" and the def "outputDir".
14 *
15 */
16trait Nature {
17
18  /**
19   * Create target(s) in the scope of the given (implicit) project.
20   * Any implementation has to take care of calling super.createTargets.
21   */
22  def createTargets(implicit project: Project): Seq[Target] = Seq()
23
24}

OutputDirNature trait

 1/**
 2 * Basic nature, configuring an output directory.
 3 */
 4trait OutputDirNature extends Nature {
 5  /**
 6   * The primary output directory.
 7   */
 8  def outputDir: String = "target" 
 9}

CleanNature trait

 1/**
 2 * Nature adding a "clean" target which removes an output directory.
 3 */
 4trait CleanNature extends OutputDirNature {
 5  /**
 6   * The target name.
 7   */
 8  def clean_targetName: String = "clean" 
 9
10  // add a new target "phony:clean" which utilizes Ant delete task to remove the dir defined in OutputDirNature.outputDir
11  abstract override def createTargets(implicit project: Project) = super.createTargets(project) ++
12    Seq(Target("phony:" + clean_targetName) exec {
13      AntDelete(dir = Path(outputDir))
14    })
15}
16

As you can see, no magic at all. The createTargets methods create normal targets. Pure Scala mixins on top of an already powerful buildsystem.

Stay tuned,

Tobias


Comments