Defining sbt task that invokes method from project code?

后端 未结 3 1701
醉梦人生
醉梦人生 2020-12-08 11:18

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
相关标签:
3条回答
  • 2020-12-08 11:26

    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.

    0 讨论(0)
  • 2020-12-08 11:35

    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).
    0 讨论(0)
  • 2020-12-08 11:46

    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.

    0 讨论(0)
提交回复
热议问题