5

we need to run some code after the compile step. Making things happen after the compile step seems easy:

compile in Compile <<= (compile in Compile) map{x=>
    // post-compile work
    doFoo()
    x
}

but how do you run something in the freshly compiled code?

More info on the scenario: we are using less for css in a lift project. We wanted lift to compile less into css on the fly (if needed) to help dev, but produce less using the same code, during the build, before tests etc run. less-sbt may help but we are interested in how to solve this generally.

Channing Walton
  • 3,977
  • 1
  • 30
  • 59

2 Answers2

4

You can use the triggeredBy method like this:

yourTask <<= (fullClasspath in Runtime) map {classpath =>
  val loader: ClassLoader = ClasspathUtilities.toLoader(classpath.map(_.data).map(_.getAbsoluteFile))
  loader.loadClass("your.class.Here").newInstance()
} triggeredBy(compile in Compile)

This will instantiate your class that has just been compiled, using the runtime classpath for your application, after any compile.

Raymond Barlow
  • 489
  • 1
  • 3
  • 9
2

It would probably help if you explained your use scenario for this, since there are some different possible solution paths here and choosing between them might involve considerations that you haven't told us.

You won't be able to just write down an ordinary method call into the compiled code. That would be impossible since at the time your build definition is compiled, sbt hasn't looked at your project code yet.

Warning: rambling and thinking out loud ahead.

One trick I can suggest is to access testLoader in Test to get a classloader in which your compiled classes are loaded, and then use reflection to call methods there. For example, in my own build I have:

val netlogoVersion = taskKey[String]("...")

netlogoVersion := {
  (testLoader in Test).value
    .loadClass("org.nlogo.api.Version")
    .getMethod("version")
    .invoke(null).asInstanceOf[String]
}

I'm not sure whether accessing testLoader in Test will actually work in your case because testLoader loads your test classes as well as your regular classes, so you might get a circular dependency between compile in Compile and compile in Test.

If you want to try to make a classloader that just has your regular classes loaded, well, hmm. You could look in the sbt source code at the implementation of createTestLoader and use it for inspiration, modifying the arguments that are passed to ClasspathUtilities.makeLoader. (You might also look at the similar code in Run.run0. It calls makeLoader as part of the implementation of the run task.)

A different path you might consider is to reuse the machinery behind the run task to run your code. You won't be able to call an arbitrary method in your compiled code this way, only a main method, but perhaps you can live with that, if you don't need a return value back.

The fullRunTask method exists for creating entire run-like tasks. See "How can I create a custom run task, in addition to run?" from http://www.scala-sbt.org/0.13.1/docs/faq.html . fullRunTask makes it very easy to create a separate task that runs something in your compiled code, but by itself it won't get you all the way to a solution because you need a way of attaching that task to the existing compile in Compile task. If you go this route, I'd suggest asking it that last piece as a separate question.

Consider bypassing fullRunTask and just assembling your own call to Run.run. They use the same machinery. In my own build, I currently use fullRunTask, but back before fullRunTask was added by sbt, here was what my equivalent Run.run-based code looked like:

    (..., fullClasspath in Compile, runner, streams, ...) map {
      (..., cp, runner, s, ...) =>
        Run.run("name.of.my.MainClass",
                cp.map(_.data), Seq(), s.log)(runner)
    }

Pardon the sbt 0.12, pre-macro syntax; this would look nicer if redone with the 0.13 macros.

Anyway, hopefully something in this brain dump proves useful.

Seth Tisue
  • 29,985
  • 11
  • 82
  • 149