david.perez
david.perez

Reputation: 7022

try/finally with SBT tasks

I have an existing task called myTask, whose implementation I don't control.

I want to redefine it in this way: myTask := { val x = prepare() try myTask.value finally cleanup(x) }

As you probably know, this code wouldn't work, as we don't control when myTask.value is executed.

prepare can be called with Def.sequential(), and cleanup with the andFinally construct. The only problem is how cleanup can get the return value of prepare().
Def.sequential{ Def.task{ prepare() }, myTask }.andFinally(cleanup(???))

One workaround is to use global variables, but this is a dirty hack.

Any ideas?

Related doc

Upvotes: 1

Views: 199

Answers (2)

skelantros
skelantros

Reputation: 16

I've come to the following solution that doesn't require to use any kind of the global state.

val initializer = taskKey[Int]("an initializer that makes something and returns some data")
val tryFinallyTask = taskKey[Unit]("a task that requires initializing and finalizer based on init results")
val myTask = taskKey[Unit]("your main task that can fail")

myTask := {
  if(scala.util.Random.nextBoolean()) throw new Exception()
}

initializer := { scala.util.Random.nextInt() }

// the task you would like to execute in a try-finally fashion
myTask := {
  if(scala.util.Random.nextBoolean) throw new Exception()
}

initializer := {
  // some logic here
  scala.util.Random.nextInt()
}

def finalizer(initValue: Int): Unit = {
  println(s"init result is $initValue")
}

tryFinallyTask := Def.taskDyn {
  Def.sequential(
    initializer,
    myTask.andFinally {
      val initRes = initializer.value
      finalizer(initRes)
    }
  )
}.value

tryFinallyTask executes initializer and then myTask. In case if initializer succeeds, i.e. myTask starts execution, finalizer logic will be executed in any case.

Some notes to the code:

  • Def.taskDyn expects a Def.Initializer of SBT task. This is the returning type of Def.sequential as well as andFinally.
  • You have to get initializer.value inside of Def.taskDyn and pass it into the finalizer function. Accessing the value in finalizer will not work, since you can't use .value outside of SBT macros.
  • You are free to use values of other tasks in andFinally clause, although the execution order of these tasks is not guaranteed.

Upvotes: 0

david.perez
david.perez

Reputation: 7022

I've tried to use global variables, and it works ok, even though it isn't the most elegant way to implement it.

I have:

  • project/MyTasks.scala
  • build.sbt

snippet in MyTasks.scala:

object MyTasks {
   var x = Option.empty[String]

   def prepare(): String = ???
   def cleanup(x: String): Unit = ???
}

snippet in build.sbt:

myTask := Def.sequential{ 
  Def.task{ 
     MyTasks.x = Some(MyTasks.prepare())
  }, 
  myTask 
}.andFinally {
     MyTasks.cleanup(MyTasks.x.get)
     MyTasks.x = None
}.value

In this way, we can get the state from prepare, and bypass SBT limitations.

Upvotes: 1

Related Questions