I would like to create an sbt plugin for my project before I open source it.
The project attaches a Java agent to the start of running an application, to instrument it for various types of profiling. The agent writes out text files for later processing.
I would like to be able to write an sbt plugin that can
run
called runWithProfiling
which launches a new java process, with the agent added to the argument list, and passing all the users commands.I know roughly how to create the new command, but I don't know how to best implement an alternative to run
... I don't want to re-invent the wheel by copying all the code that run
does. Is there a way I can invoke run
but ensure that my parameters are passed (one time) and that it is definitely a new java process?
Also, being able to do the same for the tests would be great.
UPDATE: this is the code that I currently have, but it suffers from several problems, marked up as TODO
s
import sbt._
import Keys._
import sbt.Attributed.data
object LionPlugin extends Plugin {
val lion = TaskKey[Unit]("lion", "Run a main class with lions-share profiling.")
override val projectSettings = Seq(
fork := true,
javaOptions ++= Seq(
"-Xloggc:gc.log", "-XX:+PrintGCDetails", "-XX:+PrintGCDateStamps",
"-XX:+PrintTenuringDistribution", "-XX:+PrintHeapAtGC"
// TODO: need to get hold of the local jar file for a particular artifact
// IMPL: pass the jar as the agent
),
lion <<= (
runner,
fullClasspath in Runtime,
mainClass in Runtime,
streams in Runtime
) map runLion
)
// TODO: update to a task that can take parameters (e.g. number of repeats, profiling settings)
def runLion(runner: ScalaRun, cp: Classpath, main: Option[String], streams: TaskStreams): Unit = {
assert(runner.isInstanceOf[ForkRun], "didn't get a forked runner... SBT is b0rk3d")
println("RUNNING with " + runner.getClass)
// TODO: ask user if main is None, like 'run' does
val m = main.getOrElse("Scratch")
// TODO: get the user's arguments
val args = Nil
runner.run(m, data(cp), args, streams.log)
// IMPL: post-process and produce the report
println("FINISHED")
}
}
A plugin can define a sequence of sbt settings that are automatically added to all projects or that are explicitly declared for selected projects. For example, a plugin might add a proguard task and associated (overridable) settings. Finally, a plugin can define new commands (via the commands setting).
Plugins can be installed for all your projects at once by declaring them in $HOME/. sbt/1.0/plugins/ .
Plugin authors need to abide by an unwritten Hippocratic Oath, which is "first, do no harm." Your implementation currently forces itself to every subproject and mutates fork
and javaOptions
of the default behavior, which I think is dangerous. I think you need to duplicate the run
parameters scoped to your task so the default settings are unharmed.
// TODO: update to a task that can take parameters (e.g. number of repeats, profiling settings)
See Plugins Best Practices and existing plugins like sbt-appengine for an example.
In sbt-appengine devServer
is an input task that you can set a bunch of parameters.
gae.devServer := {
val args = startArgsParser.parsed
val x = (products in Compile).value
AppEngine.restartDevServer(streams.value, (gae.reLogTag in gae.devServer).value,
thisProjectRef.value, (gae.reForkOptions in gae.devServer).value,
(mainClass in gae.devServer).value, (fullClasspath in gae.devServer).value,
(gae.reStartArgs in gae.devServer).value, args,
packageWar.value,
(gae.onStartHooks in gae.devServer).value, (gae.onStopHooks in gae.devServer).value)
}
As you see the gut of the code is actually implemented in a method under AppEngine
object, so someone else can reuse your stuff potentially.
Many of the parameters into the method (in this case restartDevServer
) are scoped to gae.devServer
task like (mainClass in gae.devServer)
.
How do you intend the plugin to be set up by the build users? Are they going to enable it once as a global plugin and use the same settings everywhere, or are they going to enable it for each build in project/lion.sbt
? Plugins Best Practices's recommendation is to provide baseLionSettings
and lionSettings
so the build user can pick and chose which subproject would have the lion
task enabled.
As per actual running goes, you might want to take a look at reusing the code from sbt-revolver similar to what I did in sbt-appengine.
See “How can I create a custom run task, in addition to run?” at http://www.scala-sbt.org/0.13.0/docs/faq.html . In your custom task, you'll want to set fork
to true in order to launch a fresh JVM.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With