Using SBT to create an RPM (detour including Vagrant)

Okay, so I need to build proper RPMs rather than using FPM to create them. Our build toolchain uses SBT to build everything, and utilises sbt-assembly to generate a single executable JAR. Which we then wrap up in an RPM via FPM as I’ve mentioned. Great – except I’m on a Mac, which doesn’t have RPM tooling. So it’s Vagrant, Puppet and sbt-native-packager to the rescue!


Install VirtualBox and Vagrant on your Mac.

Create a new directory

1
2
mkdir vagrant
cd vagrant

Initialize Vagrant with a CentOS 6.4 box

1
vagrant init sbtrpm  http://developer.nrel.gov/downloads/vagrant-boxes/CentOS-6.4-x86_64-v20130731.box

My Vagrantfile looks something like this (note the config.vm.provision line which I’ve added)

1
2
3
4
5
6
7
8
9
10
11
# -*- mode: ruby -*-
# vi: set ft=ruby :
 
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
 
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "sbtrpm"
  config.vm.box_url = " http://developer.nrel.gov/downloads/vagrant-boxes/CentOS-6.4-x86_64-v20130731.box"
  config.vm.provision :puppet
end

# Vagrantfile API/syntax version. Don’t touch unless you know what you’re doing!
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "sbtrpm"
config.vm.box_url = " http://developer.nrel.gov/downloads/vagrant-boxes/CentOS-6.4-x86_64-v20130731.box"
config.vm.provision :puppet
end

to enable Puppet configuration.

Create the Puppet manifest

1
2
mkdir manifests
touch manifests/default.pp

Edit the manifests/default.pp file to be

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
node 'default' {
    # Ensure Java is present (needed for SBT)
    package {
        "java-1.7.0-openjdk.x86_64":
            ensure  =>  present,
    }
 
    # Ensure rpm-build tooling is installed so we can create RPMs
    package {
        "rpm-build":
            ensure  =>  present,
    }
 
    # Ensure rpmlint is available (only needed if you want to rpmlint the produced RPM)
    package {
        "rpmlint":
            ensure  =>  present,
    }
 
    # SBT isn't available via yum or rpm, so manually fetch it and set correct permissions
    exec {
        "/usr/bin/wget http://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/0.13.1/sbt-launch.jar -O /usr/local/bin/sbt-launch.jar && chmod 0755 /usr/local/bin/sbt-launch.jar":
            creates  =>  "/usr/local/bin/sbt-launch.jar",
    }
 
    # Create a launch script for SBT
    file {
        '/usr/local/bin/sbt':
            ensure  => file,
            content => "SBT_OPTS="-Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M"njava $SBT_OPTS -jar `dirname $0`/sbt-launch.jar "$@"n",
            mode => 0755,
    }
 
  # Force a system update
  #exec { "yum update -y":
  #  path => "/usr/bin",
  #}
}

# Ensure rpm-build tooling is installed so we can create RPMs
package {
"rpm-build":
ensure => present,
}

# Ensure rpmlint is available (only needed if you want to rpmlint the produced RPM)
package {
"rpmlint":
ensure => present,
}

# SBT isn’t available via yum or rpm, so manually fetch it and set correct permissions
exec {
"/usr/bin/wget http://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/0.13.1/sbt-launch.jar -O /usr/local/bin/sbt-launch.jar && chmod 0755 /usr/local/bin/sbt-launch.jar":
creates => "/usr/local/bin/sbt-launch.jar",
}

# Create a launch script for SBT
file {
‘/usr/local/bin/sbt’:
ensure => file,
content => "SBT_OPTS="-Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256M"njava $SBT_OPTS -jar `dirname $0`/sbt-launch.jar "$@"n",
mode => 0755,
}

# Force a system update
#exec { "yum update -y":
# path => "/usr/bin",
#}
}

Create your Scala project underneath this Vagrant directory (so it’s automatically mounted in your guest machine), and make some other directories which the native packager uses

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mkdir sbtproj
mkdir sbtproj/src
mkdir sbtproj/src/main
mkdir sbtproj/src/main/scala
touch sbtproj/src/main/scala/Main.scala
mkdir sbtproj/project
touch sbtproj/project/plugins.sbt
touch sbtproj/build.sbt
touch sbtproj/assembly.sbt
mkdir sbtproj/src/universal
mkdir sbtproj/src/universal/bin
mkdir sbtproj/src/universal/conf
mkdir sbtproj/src/universal/doc
mkdir sbtproj/src/universal/lib
echo Config > sbtproj/src/universal/conf/config.txt
echo Doc > sbtproj/src/universal/doc/documentation.txt

and files placed into these directories will end up correctly symlink’d when they are installed by RPM.

Edit your sbtproj/project/plugins.sbt file

1
2
3
4
5
logLevel := Level.Warn
 
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "0.6.3")
 
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.10.2")

addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "0.6.3")

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.10.2")

Edit your sbtproj/build.sbt file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import NativePackagerKeys._
 
packagerSettings
 
mapGenericFilesToLinux
 
name := "sbt-rpm"
 
version := "1.0"
 
maintainer := "Andrew <[email protected]>"
 
packageSummary := "Test rpm package"
 
packageDescription := """SBT built RPM."""
 
rpmRelease := "1"
 
rpmVendor := "devsoup"
 
rpmUrl := Some("http://www.devsoup.co.uk")
 
rpmLicense := Some("BSD")
 
rpmPre := Some("""echo "pre-install"""")
 
rpmPost := Some("""echo "post-install"""")
 
rpmPretrans := Some("""echo "pretrans"""")
 
rpmPosttrans := Some("""echo "posttrans"""")
 
rpmPreun := Some("""echo "pre-uninstall"""")
 
rpmPostun := Some("""echo "post-uninstall"""")
 
rpmRequirements := Seq("java-1.7.0-openjdk")

packagerSettings

mapGenericFilesToLinux

name := "sbt-rpm"

version := "1.0"

maintainer := "Andrew <[email protected]>"

packageSummary := "Test rpm package"

packageDescription := """SBT built RPM."""

rpmRelease := "1"

rpmVendor := "devsoup"

rpmUrl := Some("http://www.devsoup.co.uk")

rpmLicense := Some("BSD")

rpmPre := Some("""echo "pre-install"""")

rpmPost := Some("""echo "post-install"""")

rpmPretrans := Some("""echo "pretrans"""")

rpmPosttrans := Some("""echo "posttrans"""")

rpmPreun := Some("""echo "pre-uninstall"""")

rpmPostun := Some("""echo "post-uninstall"""")

rpmRequirements := Seq("java-1.7.0-openjdk")

Change your sbtproj/src/main/scala/Main.scala file

1
2
3
4
5
package uk.co.devsoup.sbt.rpm
 
object Hi {
  def main(args: Array[String]) = println("Hello world!")
}

object Hi {
def main(args: Array[String]) = println("Hello world!")
}

Edit your sbtproj/assembly.sbt file

1
2
3
4
5
6
7
import AssemblyKeys._
 
assemblySettings
 
mainClass in assembly := Some("uk.co.devsoup.sbt.rpm.Hi")
 
outputPath in assembly := new java.io.File(s"src/universal/lib/${name.value}-${version.value}.jar")

assemblySettings

mainClass in assembly := Some("uk.co.devsoup.sbt.rpm.Hi")

outputPath in assembly := new java.io.File(s"src/universal/lib/${name.value}-${version.value}.jar")

You can then do

1
2
3
4
5
6
7
vagrant up
vagrant ssh
cd /vagrant/sbtproj
sbt compile
sbt package
sbt assembly
sbt rpm:package-bin

This produces a single JAR in src/universal/lib, which the sbt-assembly plugin can then package up into an RPM which is installable.

1
2
3
4
sudo yum install -y target/rpm/RPMS/noarch/sbt-rpm-1.0-1.noarch.rpm 
ls -al /etc/sbt-rpm
java -jar /usr/share/sbt-rpm/lib/sbt-rpm-1.0.jar
sudo yum remove -y sbt-rpm

When you’re done with the Linux box

1
2
exit
vagrant destroy -f

This is my personal blog - all views are my own.

Tagged with: , , , , , ,