Create a custom zip file with sbt

Recently I wanted to create a custom zip file with a few artifacts gathered from other modules of the project. The zip file should have a specific layout, like

/etc
/bin
/webapp

The bin folder should contain the executable jar file (and maybe some scripts later), while the war-file should go in the webapp folder.

I'm used to go with maven, but this time I'm using sbt. Usually with maven, I would go to the assembly plugin documentation pages and start by creating a assembly descriptor and then continueing with some trial-error-reading cycles.

Now with sbt, it is quite as simple (actually, it is simpler imho). The documentation I opened was the source code of the IO utility class (find it here on github). Then I started with creating a new TaskKey[File] that I called serverDist. The task for this setting key is to create the zip file mentioned previously. The task obviously depends on some other task that must run before, like creating the war file and the executable jar file. In sbt this is modelled naturally using function arguments. The task is invoked if all arguments are available, which means they must be evaluated first.

So in short, this is what I did:

serverDist <<= (AssemblyKeys.assembly,
                PluginKeys.packageWar.in(War.module).in(Compile),
                Keys.target,
                Keys.name,
                Keys.version) map { (server:File, war:File, target:File, name:String, version:String) =>
  val distdir = target / (name +"-"+ version)
  val zipFile = target / (name +"-"+ version +".zip")
  IO.delete(zipFile)
  IO.delete(distdir)

  val etc = distdir / "etc"
  val bin = distdir / "bin"
  val webapp = distdir / "webapp"
  IO.createDirectories(Seq(distdir, etc, bin, webapp))

  IO.unzip(war, webapp)

  val serverFile = bin / server.getName
  IO.copyFile(server, serverFile)

  def entries(f: File):List[File] = f :: (if (f.isDirectory) IO.listFiles(f).toList.flatMap(entries(_)) else Nil)
  IO.zip(entries(distdir).map(d => (d, d.getAbsolutePath.substring(distdir.getParent.length +1))), zipFile)
  zipFile
}

The War.module is the project definition of the war sub module. Sbt is great: simply define all you need as arguments and sbt takes care of the dependency resolution. Now, if I type server-dist inside the project the war file is built as well as the executable jar and then fed into my new function.

Here is the complete build definition of the server submodule:

object Server extends Build {
  import sbtassembly.Plugin._
  import com.github.siasia._

  val serverDist = TaskKey[File]("server-dist", "Creates a distributable zip file containing the publet standalone server.")

  lazy val module = Project(
    id = "server",
    base = file("server"),
    settings = buildProperties
  )

  val buildProperties = Project.defaultSettings ++ assemblySettings ++ Seq[Project.Setting[_]](
    name := "publet-server",
    serverDist <<= (AssemblyKeys.assembly, PluginKeys.packageWar.in(War.module).in(Compile), Keys.target, Keys.name, Keys.version) map { (server:File, war:File, target:File, name:String, version:String) =>
      val distdir = target / (name +"-"+ version)
      val zipFile = target / (name +"-"+ version +".zip")

      IO.delete(zipFile)
      IO.delete(distdir)

      val etc = distdir / "etc"
      val bin = distdir / "bin"
      val webapp = distdir / "webapp"
      IO.createDirectories(Seq(distdir, etc, bin, webapp))

      val warFile = webapp / war.getName
      IO.copyFile(war, warFile)

      val serverFile = bin / server.getName
      IO.copyFile(server, serverFile)

      def entries(f: File):List[File] = f :: (if (f.isDirectory) IO.listFiles(f).toList.flatMap(entries(_)) else Nil)
      IO.zip(entries(distdir).map(d => (d, d.getAbsolutePath.substring(distdir.getParent.length))), zipFile)
      zipFile
    },
    libraryDependencies ++= deps
  )

  val deps = Seq(grizzledSlf4j, servletApi, jettyServer, logbackClassic)
}

Date: [2013-06-06 Do]

Created: 2015-05-25 Mo 21:35

Validate