Defining sbt task that invokes method from project code?
Asked Answered
S

3

22

I'm using SBT to build a scala project. I want to define a very simple task, that when I input generate in sbt:

sbt> generate

It will invoke my my.App.main(..) method to generate something.

There is a App.scala file in myproject/src/main/scala/my, and the simplified code is like this:

object App {
   def main(args: Array[String]) {
       val source = readContentOfFile("mysource.txt")
       val result = convert(source)
       writeToFile(result, "mytarget.txt");
   }
   // ignore some methods here
}

I tried to add following code into myproject/build.sbt:

lazy val generate = taskKey[Unit]("Generate my file")

generate := {
  my.App.main(Array())
}

But which doesn't compile since it can't find my.App.

Then I tried to add it to myproject/project/build.scala:

import sbt._
import my._

object HelloBuild extends Build {

  lazy val generate = taskKey[Unit]("Generate my file")

  generate := {
    App.main(Array())
  }

}

But it still can't be compiled, that it can't find package my.

How to define such a task in SBT?

Schnorkle answered 1/5, 2014 at 14:43 Comment(0)
I
15

In .sbt format, do:

lazy val generate = taskKey[Unit]("Generate my file")

fullRunTask(generate, Compile, "my.App")

This is documented at http://www.scala-sbt.org/0.13.2/docs/faq.html, “How can I create a custom run task, in addition to run?”

Another approach would be:

lazy val generate = taskKey[Unit]("Generate my file")

generate := (runMain in Compile).toTask(" my.App").value

which works fine in simple cases but isn't as customizable.

Update: Jacek's advice to use resourceGenerators or sourceGenerators instead is good, if it fits your use case — can't tell from your description whether it does.

Indemonstrable answered 1/5, 2014 at 16:12 Comment(3)
In .scala format, you can put the lazy val part anywhere, and the generate := ... part is an ordinary setting that you can put in the same part of your .scala file that has the rest of your settings.Indemonstrable
why is there a space before my ... shouldnt it be "my.App" instead of " my.App"????Gott
I forget what the space is for. But the toTask examples in scala-sbt.org/0.13/docs/Input-Tasks.html all have itIndemonstrable
S
10

The other answers fit the question very well, but I think the OP might benefit from mine, too :)

The OP asked about "I want to define a very simple task, that when I input generate in sbt will invoke my my.App.main(..) method to generate something." that might ultimately complicate the build.

Sbt already offers a way to generate files at build time - sourceGenerators and resourceGenerators - and I can't seem to notice a need to define a separate task for this from having read the question.

In Generating files (see the future version of the document in the commit) you can read:

sbt provides standard hooks for adding source or resource generation tasks.

With the knowledge one could think of the following solution:

sourceGenerators in Compile += Def.task {
  my.App.main(Array()) // it's not going to work without one change, though
  Seq[File]()          // a workaround before the above change is in effect
}.taskValue

To make that work you should return a Seq[File] that contains files generated (and not the empty Seq[File]()).

The main change for the code to work is to move the my.App class to project folder. It then becomes a part of the build definition. It also reflects what the class does as it's really a part of the build not the artifact that's the product of it. When the same code is a part of the build and the artifact itself you don't keep the different concerns separate. If the my.App class participates in a build, it should belong to it - hence the move to the project folder.

The project's layout would then be as follows:

$ tree
.
├── build.sbt
└── project
    ├── App.scala
    └── build.properties

Separation of concerns (aka @joescii in da haus)

There's a point in @joescii's answer (which I extend in the answer) - "to make it a separate project that other projects can use. To do this, you will need to put your App object into a separate project and include it as a dependency in project/project", i.e.

Let's assume you've got a separate project build-utils with App.scala under src/main/scala. It's a regular sbt configuration with just the Scala code.

jacek:~/sandbox/so/generate-project-code
$ tree build-utils/
build-utils/
└── src
    └── main
        └── scala
            └── App.scala

You could test it out as a regular Scala application without messing up with sbt. No additional setup's required (and frees your mind from sbt that might be beneficial at times - less setup is always of help).

In another project - project-code - that uses App.scala that is supposed to be a base for the build, build.sbt is as follows:

project-code/build.sbt

lazy val generate = taskKey[Unit]("Generate my file")

generate := {
  my.App.main(Array())
}

Now the most important part - the wiring between projects so the App code is visible for the build of project-code:

project-code/project/build.sbt

lazy val buildUtils = RootProject(
  uri("file:/Users/jacek/sandbox/so/generate-project-code/build-utils")
)

lazy val plugins = project in file(".") dependsOn buildUtils

With the build definition(s), executing generate gives you the following:

jacek:~/sandbox/so/generate-project-code/project-code
$ sbt
[info] Loading global plugins from /Users/jacek/.sbt/0.13/plugins
[info] Loading project definition from /Users/jacek/sandbox/so/generate-project-code/project-code/project
[info] Updating {file:/Users/jacek/sandbox/so/generate-project-code/build-utils/}build-utils...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Updating {file:/Users/jacek/sandbox/so/generate-project-code/project-code/project/}plugins...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Compiling 1 Scala source to /Users/jacek/sandbox/so/generate-project-code/build-utils/target/scala-2.10/classes...
[info] Set current project to project-code (in build file:/Users/jacek/sandbox/so/generate-project-code/project-code/)
> generate
Hello from App.main
[success] Total time: 0 s, completed May 2, 2014 2:54:29 PM

I've changed the code of App to be:

> eval "cat ../build-utils/src/main/scala/App.scala"!
package my

object App {
  def main(args: Array[String]) {
    println("Hello from App.main")
  }
}

The project structure is as follows:

jacek:~/sandbox/so/generate-project-code/project-code
$ tree
.
├── build.sbt
└── project
    ├── build.properties
    └── build.sbt

Other changes aka goodies

I'd also propose some other changes to the code of the source generator:

  • Move the code out of main method to a separate method that returns the files generated and have main call it. It'll make reusing the code in sourceGenerators easier (without unnecessary Array() to call it as well as explicitly returning the files).
  • Use filter or map functions for convert (to add a more functional flavour).
Sophomore answered 1/5, 2014 at 20:27 Comment(2)
Thank you for this solution, it's very useful if I want to create some utils for SBT. For this question, I can't choose it since I have to keep my App.scala in src directory to make others to see a standard scala project without much knowledge of sbtSchnorkle
Hi! How your solution will work in a multi module project? Child modules can't have their own project/build.sbtAndino
W
5

The solution that @SethTisue proposes will work. Another approach is to make it a separate project that other projects can use. To do this, you will need to put your App object into a separate project and include it as a dependency in project/project, OR package it as an sbt plugin ideally with this task definition included.

For an example of how to create a lib that is packaged as a plugin, take a look at snmp4s. The gen directory contains the code that does some code generation (analogous to your App code) and the sbt directory contains the sbt plugin wrapper for gen.

Widera answered 1/5, 2014 at 14:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.