Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental support for formats generated by the JDK 8 javapackager tool. #492

Merged
merged 6 commits into from
Feb 15, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,44 @@ implementation in your `build.sbt`
packageBin in Debian <<= debianJDebPackaging in Debian
```


## Experimental Native Packages via `javapackager`

JDK 8 from Oracle includes the tool `javapackager` (née `javafxpackager`) to generate application
launchers and native installers for MacOS X, Windows, and Linux. This plugin complements the existing
`sbt-native-packager` formats by taking the staged output from `JavaAppPackaging` and `Universal`
settings and passing them through `javapackager`.

This plugin's most significant complement to the core capabilities is the generation of
MacOS X App bundles and derived `.dmg` and `.pkg` packages. It can also generate Linux `.deb` and `.rpm`
packages, and Windows `.exe` and `.msi` installers, provided the requisite tools are available on the
build platform.

For details on the capabilities of `javapackager`, see the [windows](http://docs.oracle.com/javase/8/docs/technotes/tools/windows/javapackager.html) and [Unix](http://docs.oracle.com/javase/8/docs/technotes/tools/unix/javapackager.html) references. (Note: only a few of the possible
settings are exposed through this plugin. Please submit a github issue or pull request if something
specific is desired.)

Using this plugin requires running SBT under JDK 8, or setting `jdkPackagerTool` to the location
of the tool.

To use, first get your application working per `JavaAppPackaging` instructions (including `mainClass`),
then add

```scala
enablePlugins(JDKPackagerPlugin)
```

to your build file. Then run `sbt jdkPackager:packageBin`.

By default, the plugin makes the installer type that is native to the current build platform in
the directory `target/jdkpackager`. The key `jdkPackageType` can be used to modify this behavior.
Run `help jdkPackageType` in sbt for details. The most popular setting is likely to be `jdkAppIcon`. See
[the example build file](test-project-jdkpackager/build.sbt) on how to use it.

To take it for a test spin, run `sbt jdkPackager:packageBin` in the `test-project-jdkpackager` directory
and then look in the `target/jdkpackager/bundles` directory for the result. See [Oracle documentation](http://docs.oracle.com/javase/8/docs/technotes/guides/deploy/self-contained-packaging.html) for Windows and
Linux build prerequisites.

## Documentation ##

There's a complete "getting started" guide and more detailed topics available at [the sbt-native-packager site](http://scala-sbt.org/sbt-native-packager).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.typesafe.sbt.packager.jdkpackager

import com.typesafe.sbt.packager.chmod
import sbt._

/**
* Support/helper functions for interacting with the `javapackager` tool.
* @author <a href="mailto:[email protected]">Simeon H.K. Fitch</a>
* @since 2/11/15
*/
object JDKPackagerHelper {

// Try to compute determine a default path for the java packager tool.
def locateJDKPackagerTool(): Option[File] = {
val jdkHome = sys.props.get("java.home").map(p ⇒ file(p))

// TODO: How to get version of javaHome target?
val javaVersion = VersionNumber(sys.props("java.specification.version"))
val toolname = javaVersion.numbers.take(2) match {
case Seq(1, m) if m >= 8 ⇒ "javapackager"
case _ ⇒ sys.error("Need at least JDK version 1.8 to run JDKPackager plugin")
}
jdkHome
.map(f ⇒ if (f.getName == "jre") f / ".." else f)
.map(f ⇒ f / "bin" / toolname)
.filter(_.exists())
}

def mkProcess(
tool: File,
mode: String,
argMap: Map[String, String],
log: Logger) = {

val invocation = Seq(tool.getAbsolutePath, mode, "-v")

val argSeq = argMap.map(p ⇒ Seq(p._1, p._2)).flatten[String].filter(_.length > 0)
val args = invocation ++ argSeq

val argString = args.map {
case s if s.contains(" ") ⇒ s""""$s""""
case s ⇒ s
}.mkString(" ")
log.debug(s"Package command: $argString")

// To help debug arguments
val script = file(argMap("-outdir")) / "jdkpackager.sh"
IO.write(script, s"#!/bin/bash\n$argString\n")
chmod.safe(script, "766")

Process(args)
}

def mkArgMap(
name: String,
version: String,
description: String,
maintainer: String,
packageType: String,
mainJar: String,
mainClass: Option[String],
basename: String,
iconPath: Option[File],
outputDir: File,
sourceDir: File) = {

def iconArg = iconPath
.map(_.getAbsolutePath)
.map(p ⇒ Map(s"-Bicon=$p" -> ""))
.getOrElse(Map.empty)

def mainClassArg = mainClass
.map(c ⇒ Map("-appclass" -> c))
.getOrElse(Map.empty)

// val cpSep = sys.props("path.separator")
// val cp = classpath.map(p ⇒ "lib/" + p)
// val cpStr = cp.mkString(cpSep)

// See http://docs.oracle.com/javase/8/docs/technotes/tools/unix/javapackager.html and
// http://docs.oracle.com/javase/8/docs/technotes/tools/windows/javapackager.html for
// command line options. NB: Built-in `-help` is incomplete.
Map(
"-name" -> name,
"-srcdir" -> sourceDir.getAbsolutePath,
"-native" -> packageType,
"-outdir" -> outputDir.getAbsolutePath,
"-outfile" -> basename,
"-description" -> description,
"-vendor" -> maintainer,
s"-BappVersion=$version" -> "",
s"-BmainJar=lib/$mainJar" -> ""
) ++ mainClassArg ++ iconArg

// TODO:
// * copyright/license
// * JVM options
// * application arguments
// * environment variables?
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.typesafe.sbt.packager.jdkpackager

import sbt._

/**
* Keys specific to deployment via the `javapackger` too.
*/
trait JDKPackagerKeys {
val jdkPackagerTool = SettingKey[Option[File]]("jdkPackagerTool",
"Path to `javapackager` or `javafxpackager` tool in JDK")
val packagerArgMap = TaskKey[Map[String, String]]("packagerArgMap",
"An intermediate task for computing the command line argument key/value pairs passed to " +
"`javapackager -deploy`")
val jdkPackagerBasename = SettingKey[String]("jdkPackagerOutputBasename",
"Filename sans extension for generated installer package.")
val jdkPackageType = SettingKey[String]("jdkPackageType",
"""Value passed as the `-native` argument to `javapackager -deploy`.
| Per `javapackager` documentation, this may be one of the following:
|
| * `all`: Runs all of the installers for the platform on which it is running,
| and creates a disk image for the application. This value is used if type is not specified.
| * `installer`: Runs all of the installers for the platform on which it is running.
| * `image`: Creates a disk image for the application. On OS X, the image is
| the .app file. On Linux, the image is the directory that gets installed.
| * `dmg`: Generates a DMG file for OS X.
| * `pkg`: Generates a .pkg package for OS X.
| * `mac.appStore`: Generates a package for the Mac App Store.
| * `rpm`: Generates an RPM package for Linux.
| * `deb`: Generates a Debian package for Linux.
| * `exe`: Generates a Windows .exe package.
| * `msi`: Generates a Windows Installer package.
|
| (NB: Your mileage may vary.)
""".stripMargin)

val jdkAppIcon = SettingKey[Option[File]]("jdkAppIcon",
"""Path to platform-specific application icon:
| * `icns`: MacOS
| * `ico`: Windows
| * `png`: Linux
""".stripMargin)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.typesafe.sbt.packager.jdkpackager

import com.typesafe.sbt.SbtNativePackager
import com.typesafe.sbt.packager.Keys._
import com.typesafe.sbt.packager.SettingsHelper
import com.typesafe.sbt.packager.archetypes.JavaAppPackaging
import com.typesafe.sbt.packager.archetypes.jar.ClasspathJarPlugin
import sbt.Keys._
import sbt._
import SbtNativePackager.Universal
/**
* Package format via Oracle's packaging tool bundled with JDK 8.
* @author <a href="mailto:[email protected]">Simeon H.K. Fitch</a>
* @since 2/11/15
*/
object JDKPackagerPlugin extends AutoPlugin {

object autoImport extends JDKPackagerKeys {
val JDKPackager = config("jdkPackager") extend Universal
}
import autoImport._
override def requires = JavaAppPackaging && ClasspathJarPlugin
override lazy val projectSettings = javaPackagerSettings

private val dirname = JDKPackager.name.toLowerCase

def javaPackagerSettings: Seq[Setting[_]] = Seq(
jdkPackagerTool := JDKPackagerHelper.locateJDKPackagerTool(),
jdkAppIcon := None,
jdkPackageType := "image",
jdkPackagerBasename <<= packageName apply (_ + "-pkg"),
mappings in JDKPackager <<= (mappings in Universal) map (_.filter(!_._2.startsWith("bin/")))
) ++ inConfig(JDKPackager)(
Seq(
sourceDirectory <<= sourceDirectory apply (_ / dirname),
target <<= target apply (_ / dirname),
mainClass <<= mainClass in Runtime,
name <<= name,
packageName <<= packageName,
maintainer <<= maintainer,
packageSummary <<= packageSummary,
packageDescription <<= packageDescription,
packagerArgMap <<= (
name,
version,
packageDescription,
maintainer,
jdkPackageType,
ClasspathJarPlugin.autoImport.classspathJarName,
mainClass,
jdkPackagerBasename,
jdkAppIcon,
target,
stage in Universal) map JDKPackagerHelper.mkArgMap
) ++ makePackageBuilder
)

private def checkTool(maybePath: Option[File]) = maybePath.getOrElse(
sys.error("Please set key `jdkPackagerTool` to `javapackager` path")
)

private def makePackageBuilder = Seq(
packageBin <<= (jdkPackagerTool, jdkPackageType, packagerArgMap, target, streams) map { (pkgTool, pkg, args, target, s) ⇒

val tool = checkTool(pkgTool)
val proc = JDKPackagerHelper.mkProcess(tool, "-deploy", args, s.log)

(proc ! s.log) match {
case 0 ⇒ ()
case x ⇒ sys.error(s"Error running '$tool', exit status: $x")
}

// Oooof. Need to do better than this to determine what was generated.
val root = file(args("-outdir"))
val globs = Seq("*.dmg", "*.pkg", "*.app", "*.msi", "*.exe", "*.deb", "*.rpm")
val finder = globs.foldLeft(PathFinder.empty)(_ +++ root ** _)
val result = finder.getPaths.headOption
result.foreach(f ⇒ s.log.info("Wrote " + f))
// Not sure what to do when we can't find the result
result.map(file).getOrElse(root)
}
)
}

object JDKPackagerDeployPlugin extends AutoPlugin {
import JDKPackagerPlugin.autoImport._
override def requires = JDKPackagerPlugin

override def projectSettings =
SettingsHelper.makeDeploymentSettings(JDKPackager, packageBin in JDKPackager, "jdkPackager")
}
32 changes: 32 additions & 0 deletions test-project-jdkpackager/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

name := "JDKPackagerPlugin Example"

version := "0.1.0"

organization := "com.foo.bar"

libraryDependencies ++= Seq(
"com.typesafe" % "config" % "1.2.1"
)

mainClass in Compile := Some("ExampleApp")

enablePlugins(JDKPackagerPlugin)

maintainer := "Simeon H.K Fitch <[email protected]>"

packageSummary := "JDKPackagerPlugin example package thingy"

packageDescription := "A test package using Oracle's JDK bundled javapackager tool."

lazy val iconGlob = sys.props("os.name").toLowerCase match {
case os if os.contains("mac") ⇒ "*.icns"
case os if os.contains("win") ⇒ "*.ico"
case _ ⇒ "*.png"
}

jdkAppIcon := (sourceDirectory.value ** iconGlob).getPaths.headOption.map(file)

jdkPackageType := "installer"

fork := true
1 change: 1 addition & 0 deletions test-project-jdkpackager/project/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=0.13.6
3 changes: 3 additions & 0 deletions test-project-jdkpackager/project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
lazy val root = Project("plugins", file(".")) dependsOn (packager)

lazy val packager = file("..").getAbsoluteFile.toURI
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
36 changes: 36 additions & 0 deletions test-project-jdkpackager/src/main/scala/ExampleApp.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.effect.InnerShadow
import javafx.scene.layout.Pane
import javafx.scene.paint.Color
import javafx.scene.text.{Font, FontWeight, Text}
import javafx.stage.Stage


/** Silly GUI app launcher. */
object ExampleApp {
def main(args: Array[String]): Unit = {
Application.launch(classOf[ExampleApp], args: _*)
}
}

/** Silly GUI app. */
class ExampleApp extends Application {
def start(primaryStage: Stage): Unit = {
primaryStage.setTitle("Scala on the Desktop!")
val stuff = new Text(90, 100, "Hello World")
stuff.setFont(Font.font(null, FontWeight.BOLD, 36))
stuff.setFill(Color.PEACHPUFF)
val is = new InnerShadow()
is.setOffsetX(4.0f)
is.setOffsetY(4.0f)
stuff.setEffect(is)
val root = new Pane(stuff)
root.setPrefWidth(400)
root.setPrefHeight(200)
primaryStage.setScene(new Scene(root))
primaryStage.sizeToScene()
primaryStage.show()
}
}