Search code examples
sbtsbt-native-packager

Additional resource generator with sbt native packager


I have a submodule, that is compiled by invoking external command. I would like to include generated file into jar. So I wrote a task: ```

myTask := {
  import sys.process.stringSeqToProcess
  Seq("my", "command") !
}
unmanagedResourceDirectories in Compile += baseDirectory.value / "dist"
cleanFiles <+= baseDirectory { base => base / "dist" }

Keys.`package` <<= (Keys.`package` in Compile) dependsOn npmBuildTask.toTask

and when I invoke mySubmodule/package task it works well. But when I invoke stage task from sbt-native-packager my task is ignored(is not executed).


Solution

  • There are a couple of options to solve this issue. I assume you want to add the dist folder to your resulting application jar.

    Your configuration doesn't work because stage doesn't depend on package. This results npmBuildTask not being called.

    1. Add dependency to stage

    The easiest way to fix this is by simply adding the npmBuildTask as a dependency to stage

    stage <<= stage dependsOn npmBuildTask.toTask
    

    I wouldn't recommend this approach.

    2. Resource generators

    SBTs Resoure Generators are exactly defined for this purpose. An inline version could look like this

    resourceGenerators in Compile += Def.task {
      streams.value.log.info("running npm generator")
      val base = (resourceManaged in Compile).value / "dist"
    
      // A resource generator returns a Seq[File]. This is just an example
      List("index.js", "test.js").map { file =>
        IO.writeLines(base / file, List("var x = 1"))
        base / file
      }
    }.taskValue
    

    Or you could extract this in an AutoPlugin to separate the "what" and "how.

    3. AutoPlugin and resource generators

    Create project/NpmPlugin.scala and add the following content

    import sbt._
    import sbt.Keys._
    import sbt.plugins.JvmPlugin
    
    object NpmPlugin extends AutoPlugin {
    
      override val requires = JvmPlugin
      override val trigger = AllRequirements
    
      object autoImport {
        val npmBuildTask = TaskKey[Seq[File]]("npm-build-task", "Runs npm and builds the application")
      }
    
      import autoImport._
    
      override def projectSettings: Seq[Setting[_]] = Seq(
        // define a custom target directory for npm
        target in npmBuildTask := target.value / "npm",
        // the actual build task
        npmBuildTask := {
          val npmSource = (target in npmBuildTask).value
          val npmTarget = (resourceManaged in Compile).value / "dist"
          // run npm here, which generates the necessary values
          streams.value.log.info("running npm generator")
          // move generated sources to target folder
          IO.copyDirectory(npmSource, npmTarget)
          // recursively get all files in the npmTarget
          (npmTarget ***).get
        },
        resourceGenerators in Compile += npmBuildTask.taskValue
      )
    
    }
    

    The build.sbt will then look like this

    name := "resource-gen-test"
    version := "1.0"
    
    enablePlugins(JavaAppPackaging)
    

    Pretty clean :)

    4. Use mappings

    Last but not least you could use mappings. They are the low level detail that drives a lot of the package-generation in sbt. The main idea of this solution is to

    • Create a task that returns a mapping definition ( Seq[(File, String)] )
    • Append this to the appropriate mappings

    The advantage of this approach is that you are more flexible where you want to put your mappings.

    import sbt._
    import sbt.Keys._
    import sbt.plugins.JvmPlugin
    import com.typesafe.sbt.SbtNativePackager.Universal
    import com.typesafe.sbt.SbtNativePackager.autoImport.NativePackagerHelper._
    
    object NpmMappingsPlugin extends AutoPlugin {
    
      override val requires = JvmPlugin
      override val trigger = AllRequirements
    
      object autoImport {
        val npmBuildTask = TaskKey[Seq[(File, String)]]("npm-build-task", "Runs npm and builds the application")
      }
    
      import autoImport._
    
      override def projectSettings: Seq[Setting[_]] = Seq(
        // define a custom target directory for npm
        target in npmBuildTask := target.value / "npm" / "dist",
        // the actual build task
        npmBuildTask := {
          val npmTarget = (target in npmBuildTask).value
          // run npm here, which generates the necessary values
          streams.value.log.info("running npm generator")   
          // recursively get all files in the npmTarget
          // contentOf(npmTarget) would skip the top-level-directory
          directory(npmTarget)
        },
        // add npm resources to the generated jar
        mappings in (Compile, packageBin) ++= npmBuildTask.value,
    
        // add npm resources to resulting package
        mappings in Universal ++= npmBuildTask.value
      )
    
    }
    

    As you can see in this approach we can easily add the resulting files to different mappings.

    However I only recommend this approach if you need this kind of flexibility as it requires a bit more knowledge of native-packager.