diff --git a/src/main/scala/com/typesafe/sbt/packager/archetypes/AshScriptPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/archetypes/AshScriptPlugin.scala new file mode 100644 index 000000000..1fa9502db --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/archetypes/AshScriptPlugin.scala @@ -0,0 +1,113 @@ +package com.typesafe.sbt +package packager +package archetypes + +import sbt._ +import sbt.Keys.{ mappings, target, name, mainClass, sourceDirectory, javaOptions, streams } +import packager.Keys.{ packageName, executableScriptName } +import linux.{ LinuxFileMetaData, LinuxPackageMapping } +import linux.LinuxPlugin.autoImport.{ linuxPackageMappings, defaultLinuxInstallLocation } +import SbtNativePackager.{ Universal, Debian } + +/** + * == Java Application == + * + * This class is an alternate to JavaAppPackaging designed to support the ash shell. JavaAppPackaging + * generates bash-specific code that is not compatible with ash, a very stripped-down, lightweight shell + * used by popular micro base Docker images like BusyBox. The AshScriptPlugin will generate simple + * ash-compatible output. + * + * Just like with JavaAppPackaging you can override the bash-template file by creating a src/templates + * directory and adding your own bash-template file. Actually this isn't a bad idea as the default + * bash-template file inherited from JavaAppPackaging has a lot of stuff you probably don't want/need + * in a highly-constrained environment like ash+BusyBox. Something much simpler will do, for example: + * + * #!/usr/bin/env sh + * + * realpath () { + * ( + * TARGET_FILE="$1" + * + * cd "$(dirname "$TARGET_FILE")" + * TARGET_FILE=$(basename "$TARGET_FILE") + * + * COUNT=0 + * while [ -L "$TARGET_FILE" -a $COUNT -lt 100 ] + * do + * TARGET_FILE=$(readlink "$TARGET_FILE") + * cd "$(dirname "$TARGET_FILE")" + * TARGET_FILE=$(basename "$TARGET_FILE") + * COUNT=$(($COUNT + 1)) + * done + * + * if [ "$TARGET_FILE" == "." -o "$TARGET_FILE" == ".." ]; then + * cd "$TARGET_FILE" + * TARGET_FILEPATH= + * else + * TARGET_FILEPATH=/$TARGET_FILE + * fi + * + * echo "$(pwd -P)/$TARGET_FILE" + * ) + * } + * + * real_script_path="$(realpath "$0")" + * app_home="$(realpath "$(dirname "$real_script_path")")" + * lib_dir="$(realpath "${app_home}/../lib")" + * + * ${{template_declares}} + * + * java -classpath $app_classpath $app_mainclass $@ + * + * + * == Configuration == + * + * This plugin adds new settings to configure your packaged application. + * The keys are defined in [[com.typesafe.sbt.packager.archetypes.JavaAppKeys]] + * + * @example Enable this plugin in your `build.sbt` with + * + * {{{ + * enablePlugins(AshScriptPlugin) + * }}} + */ +object AshScriptPlugin extends AutoPlugin { + + val bashTemplate = "bash-template" + + override def requires = JavaAppPackaging + + //object autoImport extends JavaAppKeys + + import JavaAppPackaging.autoImport._ + + override def projectSettings = Seq( + makeBashScript <<= (bashScriptTemplateLocation, bashScriptDefines, target in Universal, executableScriptName, sourceDirectory) map makeUniversalAshScript, + bashScriptDefines <<= (Keys.mainClass in (Compile, bashScriptDefines), scriptClasspath in bashScriptDefines, bashScriptExtraDefines, bashScriptConfigLocation) map { (mainClass, cp, extras, config) => + val hasMain = + for { + cn <- mainClass + } yield JavaAppAshScript.makeDefines(cn, appClasspath = cp, extras = extras, configFile = config) + hasMain getOrElse Nil + } + ) + + def makeUniversalAshScript(defaultTemplateLocation: File, defines: Seq[String], tmpDir: File, name: String, sourceDir: File): Option[File] = + if (defines.isEmpty) None + else { + val template = resolveTemplate(defaultTemplateLocation) + val scriptBits = JavaAppAshScript.generateScript(defines, template) + val script = tmpDir / "tmp" / "bin" / name + IO.write(script, scriptBits) + // TODO - Better control over this! + script.setExecutable(true) + Some(script) + } + + private def resolveTemplate(defaultTemplateLocation: File): URL = { + if (defaultTemplateLocation.exists) + defaultTemplateLocation.toURI.toURL + else + getClass.getResource(defaultTemplateLocation.getName) + } +} diff --git a/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaAppAshScript.scala b/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaAppAshScript.scala new file mode 100644 index 000000000..853372b52 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaAppAshScript.scala @@ -0,0 +1,58 @@ +package com.typesafe.sbt.packager.archetypes + +import java.net.URL + +/** + * Constructs a bash script for running a java application. + * + * Makes use of the associated bash-template, with a few hooks + * + */ +object JavaAppAshScript { + + private[this] def bashTemplateSource = + getClass.getResource("bash-template") + + /** + * Creates the block of defines for a script. + * + * @param mainClass The required "main" method class we use to run the program. + * @param appClasspath A sequence of relative-locations (to the lib/ folder) of jars + * to include on the classpath. + * @param configFile An (optional) filename from which the script will read arguments. + * @param extras Any additional defines/commands that should be run in this script. + */ + def makeDefines( + mainClass: String, + appClasspath: Seq[String] = Seq("*"), + configFile: Option[String] = None, + extras: Seq[String] = Nil): Seq[String] = + Seq(mainClassDefine(mainClass)) ++ + (configFile map configFileDefine).toSeq ++ + Seq(makeClasspathDefine(appClasspath)) ++ + extras + + private def makeClasspathDefine(cp: Seq[String]): String = { + val fullString = cp map (n => "$lib_dir/" + n) mkString ":" + "app_classpath=\"" + fullString + "\"\n" + } + def generateScript(defines: Seq[String], template: URL = bashTemplateSource): String = { + val defineString = defines mkString "\n" + val replacements = Seq("template_declares" -> defineString) + TemplateWriter.generateScript(template, replacements) + } + + def configFileDefine(configFile: String) = + "script_conf_file=\"%s\"" format (configFile) + + def mainClassDefine(mainClass: String) = { + val jarPrefixed = """^\-jar (.*)""".r + val args = mainClass match { + case jarPrefixed(jarName) => Seq("-jar", jarName) + case className => Seq(className) + } + val quotedArgsSpaceSeparated = args.map(s => "\"" + s + "\"").mkString(" ") + "app_mainclass=%s\n" format (quotedArgsSpaceSeparated) + } + +} diff --git a/src/sbt-test/ash/override-templates/build.sbt b/src/sbt-test/ash/override-templates/build.sbt new file mode 100644 index 000000000..c6b4980c7 --- /dev/null +++ b/src/sbt-test/ash/override-templates/build.sbt @@ -0,0 +1,18 @@ +import scala.io.Source + +enablePlugins(AshScriptPlugin) + +name := "override-templates" + +version := "0.1.0" + +bashScriptTemplateLocation := baseDirectory.value / "custom-templates" / "custom-ash-template" + +TaskKey[Unit]("run-check-ash") := { + val cwd = (stagingDirectory in Universal).value + val source = scala.io.Source.fromFile((cwd / "bin" / packageName.value).getAbsolutePath) + val contents = try source.getLines mkString "\n" finally source.close() + assert(contents contains "this is the custom bash template", "Bash template didn't contain the right text: \n" + contents) + assert(contents contains "app_mainclass=","Template didn't contain the right text: \n"+contents) + assert( !(contents contains "declare"),"Template didn't contain the right text: \n"+contents) +} diff --git a/src/sbt-test/ash/override-templates/custom-templates/custom-ash-template b/src/sbt-test/ash/override-templates/custom-templates/custom-ash-template new file mode 100644 index 000000000..d4fce6467 --- /dev/null +++ b/src/sbt-test/ash/override-templates/custom-templates/custom-ash-template @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +# this is the custom bash template + +${{template_declares}} \ No newline at end of file diff --git a/src/sbt-test/ash/override-templates/project/plugins.sbt b/src/sbt-test/ash/override-templates/project/plugins.sbt new file mode 100644 index 000000000..b53de154c --- /dev/null +++ b/src/sbt-test/ash/override-templates/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/ash/override-templates/src/main/scala/MainApp.scala b/src/sbt-test/ash/override-templates/src/main/scala/MainApp.scala new file mode 100644 index 000000000..0325f9cb5 --- /dev/null +++ b/src/sbt-test/ash/override-templates/src/main/scala/MainApp.scala @@ -0,0 +1,3 @@ +object MainApp extends App { + println("SUCCESS!") +} diff --git a/src/sbt-test/ash/override-templates/test b/src/sbt-test/ash/override-templates/test new file mode 100644 index 000000000..aa3b5ad81 --- /dev/null +++ b/src/sbt-test/ash/override-templates/test @@ -0,0 +1,3 @@ +# Run the staging and check the script. +> stage +> run-check-ash diff --git a/src/sbt-test/ash/simple-app/build.sbt b/src/sbt-test/ash/simple-app/build.sbt new file mode 100644 index 000000000..277d38d7d --- /dev/null +++ b/src/sbt-test/ash/simple-app/build.sbt @@ -0,0 +1,12 @@ +enablePlugins(AshScriptPlugin) + +name := "simple-app" + +version := "0.1.0" + +TaskKey[Unit]("run-check") := { + val cwd = (stagingDirectory in Universal).value + val cmd = Seq((cwd / "bin" / packageName.value).getAbsolutePath) + val output = Process(cmd, cwd).!! + assert(output contains "SUCCESS!", "Output didn't contain success: " + output) +} diff --git a/src/sbt-test/ash/simple-app/project/plugins.sbt b/src/sbt-test/ash/simple-app/project/plugins.sbt new file mode 100644 index 000000000..b53de154c --- /dev/null +++ b/src/sbt-test/ash/simple-app/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/ash/simple-app/src/main/scala/MainApp.scala b/src/sbt-test/ash/simple-app/src/main/scala/MainApp.scala new file mode 100644 index 000000000..0325f9cb5 --- /dev/null +++ b/src/sbt-test/ash/simple-app/src/main/scala/MainApp.scala @@ -0,0 +1,3 @@ +object MainApp extends App { + println("SUCCESS!") +} diff --git a/src/sbt-test/ash/simple-app/test b/src/sbt-test/ash/simple-app/test new file mode 100644 index 000000000..877989c06 --- /dev/null +++ b/src/sbt-test/ash/simple-app/test @@ -0,0 +1,3 @@ +# Run the staging and check the script. +> stage +> run-check \ No newline at end of file