Skip to content

Commit

Permalink
Initial work on using JDK's Ant task for generating packages.
Browse files Browse the repository at this point in the history
Provides more features and better configurability.

History:
* Fixed scripted property checking messages.
* Reworked settings to use `.value` over `<<=` + `map` (and related constructs).
* Initial attempt at resourcing mappings.
* Updating tests for JDKPackagerPlugin.
* Added call to Ant library to build package.
* Added explicit icon to Ant build.xml.
* Removed command-line mechanism for building package. Now completely Ant based.
* Added ability to specify JVM user args and application args.
* Added specification of runtime properties.
* Added ability to define file associations.
* Updated documentation.
* Fixed expected output type.
* Modularized Ant DOM building to appease static code checker.
* Moved support case classes into autoImport.
* Updated documentation in general, and specifically on specifying location of `ant-javafx.jar`.
* Added debug logging messages during search for `ant-javafx.jar`.
  • Loading branch information
Simeon H.K. Fitch committed May 20, 2015
1 parent 1b3bdec commit 6a40111
Show file tree
Hide file tree
Showing 25 changed files with 796 additions and 280 deletions.
7 changes: 4 additions & 3 deletions src/main/scala/com/typesafe/sbt/packager/FileUtil.scala
@@ -1,11 +1,12 @@
package com.typesafe.sbt
package packager

import scala.util.Try
import java.io.{ File, IOException }
import java.nio.file.{ Paths, Files }
import java.io.File
import java.nio.file.Files
import java.nio.file.attribute.{ PosixFilePermission, PosixFilePermissions }

import scala.util.Try

/**
* Setting the file permissions
*/
Expand Down
@@ -0,0 +1,266 @@
package com.typesafe.sbt.packager.jdkpackager

import com.typesafe.sbt.packager.jdkpackager.JDKPackagerPlugin.autoImport._
import org.apache.tools.ant.{ BuildEvent, BuildListener, ProjectHelper }
import sbt.Keys._
import sbt._

import scala.util.Try
import scala.xml.Elem
/**
* Helpers for working with Ant build definitions
*
* @author <a href="mailto:fitch@datamininglab.com">Simeon H.K. Fitch</a>
* @since 5/7/15
*/
object JDKPackagerAntHelper {

/** Attempts to compute the path to the `javapackager` tool. */
private[jdkpackager] def locateAntTasks(javaHome: Option[File], logger: Logger): Option[File] = {
val jarname = "ant-javafx.jar"

// This approach to getting JDK bits is borrowed from: http://stackoverflow.com/a/25163628/296509
// Starting with an ordered list of possible java directory sources, create derivative and
// then test for the tool. It's nasty looking because there's no canonical way of finding the
// JDK from the JRE, and JDK_HOME isn't always defined.
val searchPoints = Seq(
// Build-defined
javaHome,
// Environment override
sys.env.get("JDK_HOME").map(file),
sys.env.get("JAVA_HOME").map(file),
// MacOS X
Try("/usr/libexec/java_home".!!.trim).toOption.map(file),
// From system properties
sys.props.get("java.home").map(file)
)

// Unlift searchPoint `Option`-s, and for each base directory, add the parent variant to cover nested JREs on Unix.
val entryPoints = searchPoints.flatten.flatMap(f Seq(f, f.getAbsoluteFile))

// On Windows we're often running in the JRE and not the JDK. If JDK is installed,
// it's likely to be in a parallel directory, with the "jre" prefix changed to "jdk"
val entryPointsSpecialCaseWindows = entryPoints.flatMap { f
if (f.getName.startsWith("jre")) Seq(f, f.getParentFile / ("jdk" + f.getName.drop(3)))
else Seq(f)
}

// Now search for the tool
entryPointsSpecialCaseWindows
.map(_ / "lib" / jarname)
.find { f logger.debug(s"Looking for '$jarname' in '${f.getParent}'"); f.exists() }
.map { f logger.debug(s"Found '$f'!"); f }
}

type PlatformDOM = Elem
/** Creates the `<fx:platform>` definition. */
private[jdkpackager] def platformDOM(jvmArgs: Seq[String], properties: Map[String, String]): PlatformDOM =
// format: OFF
<fx:platform id="platform" javafx="8+" j2se="8+">
{
for {
arg <- jvmArgs
} yield <fx:jvmarg value={arg}/>
}
{
for {
(key, value) <- properties
} yield <fx:property name={key} value={value}/>
}
</fx:platform>
// format: ON

type ApplicationDOM = Elem
/** Create the `<fx:application>` definition. */
private[jdkpackager] def applicationDOM(
name: String,
version: String,
mainClass: Option[String],
toolkit: JDKPackagerToolkit,
appArgs: Seq[String]): ApplicationDOM =
// format: OFF
<fx:application id="app"
name={name}
version={version}
mainClass={mainClass.map(xml.Text.apply)}
toolkit={toolkit.arg}>
{
for {
arg <- appArgs
} yield <fx:argument>{arg}</fx:argument>
}
</fx:application>
// format: ON

type InfoDOM = Elem
/** Create the `<fx:info>` definition. */
private[jdkpackager] def infoDOM(
name: String,
description: String,
maintainer: String,
iconPath: Option[File],
associations: Seq[FileAssociation]): InfoDOM =
// format: OFF
<fx:info id="info" title={name} description={description} vendor={maintainer}>
{
if (iconPath.nonEmpty) <fx:icon href={iconPath.get.getAbsolutePath} kind="default"/>
}
{
for {
fa <- associations
} yield <fx:association extension={fa.extension} mimetype={fa.mimetype}
description={fa.description}
icon={fa.icon.map(_.getAbsolutePath).map(xml.Text.apply)}/>
}
</fx:info>
// format: ON

type DeployDOM = Elem
/** Create the `<fx:deploy>` definition. */
private[jdkpackager] def deployDOM(
basename: String,
packageType: String,
mainJar: File,
outputDir: File,
infoDOM: InfoDOM): DeployDOM =
// format: OFF
<fx:deploy outdir={outputDir.getAbsolutePath}
outfile={basename}
nativeBundles={packageType}
verbose="true">

<fx:preferences install="true" menu="true" shortcut="true"/>


<fx:application refid="app"/>

<fx:platform refid="platform"/>

{infoDOM}

<fx:resources>
<fx:fileset refid="jar.files"/>
<fx:fileset refid="data.files"/>
</fx:resources>

<fx:bundleArgument arg="mainJar" value={"lib/" + mainJar.getName} />

</fx:deploy>
// format: ON

type BuildDOM = xml.Elem
/**
* Create Ant project DOM for building packages, using ant-javafx.jar tasks.
*
* see: https://docs.oracle.com/javase/8/docs/technotes/guides/deploy/javafx_ant_task_reference.html
*/
private[jdkpackager] def makeAntBuild(
antTaskLib: Option[File],
name: String,
sourceDir: File,
mappings: Seq[(File, String)],
platformDOM: PlatformDOM,
applicationDOM: ApplicationDOM,
deployDOM: DeployDOM): BuildDOM = {

if (antTaskLib.isEmpty) {
sys.error(
"Please set key `antPackagerTasks in JDKPackager` to `ant-javafx.jar` path, " +
"which should be find in the `lib` directory of the Oracle JDK 8 installation. For example (Windows):\n" +
"""(antPackagerTasks in JDKPackager) := Some("C:\\Program Files\\Java\\jdk1.8.0_45\\lib\\ant-javafx.jar")""")
}

val taskClassPath = Seq(sourceDir.getAbsolutePath, antTaskLib.get, ".")

val (jarFiles, supportFiles) = mappings.partition(_._2.endsWith(".jar"))

// format: OFF
<project name={name} default="default" basedir="." xmlns:fx="javafx:com.sun.javafx.tools.ant">
<target name="default">

<property name="plugin.classpath" value={taskClassPath.mkString(":")}/>

<taskdef resource="com/sun/javafx/tools/ant/antlib.xml"
uri="javafx:com.sun.javafx.tools.ant" classpath="${plugin.classpath}"/>

{platformDOM}
{applicationDOM}

<fx:fileset id="jar.files" dir={sourceDir.getAbsolutePath} type="jar">
{jarFiles.map(_._2).map(f => <include name={f}/> )}
</fx:fileset>

<fx:fileset id="data.files" dir={sourceDir.getAbsolutePath} type="data">
{supportFiles.map(_._2).map(f => <include name={f}/> )}
</fx:fileset>

{deployDOM}

</target>
</project>
// format: ON
}

/**
* Locate the generated packge.
* TODO: replace with something significantly more intelligent.
* @param output output directory
* @return generated file location
*/
private[jdkpackager] def findResult(output: File, s: TaskStreams): Option[File] = {
// Oooof. Need to do better than this to determine what was generated.
val globs = Seq("*.dmg", "*.pkg", "*.app", "*.msi", "*.exe", "*.deb", "*.rpm")
val finder = globs.foldLeft(PathFinder.empty)(_ +++ output ** _)
val result = finder.getPaths.headOption
result.foreach(f s.log.info("Wrote " + f))
result.map(file)
}

/** Serialize the Ant DOM to `build.xml`. */
private[jdkpackager] def writeAntFile(outdir: File, dom: xml.Node, s: TaskStreams) = {
if (!outdir.exists()) IO.createDirectory(outdir)
val out = outdir / "build.xml"
scala.xml.XML.save(out.getAbsolutePath, dom, "UTF-8", xmlDecl = true)
s.log.info("Wrote " + out)
out
}

/** Build package via Ant build.xml definition. */
private[jdkpackager] def buildPackageWithAnt(
buildXML: File, target: File, s: TaskStreams): File = {
import org.apache.tools.ant.{ Project AntProject }

val ap = new AntProject
ap.setUserProperty("ant.file", buildXML.getAbsolutePath)
val adapter = new AntLogAdapter(s)
ap.addBuildListener(adapter)
ap.init()

val antHelper = ProjectHelper.getProjectHelper
antHelper.parse(ap, buildXML)

ap.executeTarget(ap.getDefaultTarget)
ap.removeBuildListener(adapter)

// Not sure what to do when we can't find the result
findResult(target, s).getOrElse(target)
}

/** For piping Ant messages to sbt logger. */
private class AntLogAdapter(s: TaskStreams) extends BuildListener {
import org.apache.tools.ant.{ Project AntProject }
def buildFinished(event: BuildEvent): Unit = ()
def buildStarted(event: BuildEvent): Unit = ()
def targetStarted(event: BuildEvent): Unit = ()
def taskFinished(event: BuildEvent): Unit = ()
def targetFinished(event: BuildEvent): Unit = ()
def taskStarted(event: BuildEvent): Unit = ()
def messageLogged(event: BuildEvent): Unit = event.getPriority match {
case AntProject.MSG_ERR s.log.error(event.getMessage)
case AntProject.MSG_WARN s.log.warn(event.getMessage)
case AntProject.MSG_INFO s.log.info(event.getMessage)
case AntProject.MSG_VERBOSE s.log.verbose(event.getMessage)
case _ s.log.debug(event.getMessage)
}
}
}

0 comments on commit 6a40111

Please sign in to comment.