diff --git a/src/main/resources/com/typesafe/sbt/packager/archetypes/bash-template b/src/main/resources/com/typesafe/sbt/packager/archetypes/bash-template new file mode 100644 index 000000000..fea67e291 --- /dev/null +++ b/src/main/resources/com/typesafe/sbt/packager/archetypes/bash-template @@ -0,0 +1,280 @@ +#!/bin/bash + +### ------------------------------- ### +### Helper methods for BASH scripts ### +### ------------------------------- ### + +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 + + # make sure we grab the actual windows path, instead of cygwin's path. + echo $(cygwinpath "$(pwd -P)/$TARGET_FILE") +) +} + +# TODO - Do we need to detect msys? + +# Uses uname to detect if we're in the odd cygwin environment. +is_cygwin() { + local os=$(uname -s) + case "$os" in + CYGWIN*) return 0 ;; + *) return 1 ;; + esac +} + +# This can fix cygwin style /cygdrive paths so we get the +# windows style paths. +cygwinpath() { + local file="$1" + if is_cygwin; then + echo $(cygpath -w $file) + else + echo $file + fi +} + +# Make something URI friendly +make_url() { + url="$1" + local nospaces=${url// /%20} + if is_cygwin; then + echo "/${nospaces//\\//}" + else + echo "$nospaces" + fi +} + +# Detect if we should use JAVA_HOME or just try PATH. +get_java_cmd() { + if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then + echo "$JAVA_HOME/bin/java" + else + echo "java" + fi +} + +echoerr () { + echo 1>&2 "$@" +} +vlog () { + [[ $verbose || $debug ]] && echoerr "$@" +} +dlog () { + [[ $debug ]] && echoerr "$@" +} +execRunner () { + # print the arguments one to a line, quoting any containing spaces + [[ $verbose || $debug ]] && echo "# Executing command line:" && { + for arg; do + if printf "%s\n" "$arg" | grep -q ' '; then + printf "\"%s\"\n" "$arg" + else + printf "%s\n" "$arg" + fi + done + echo "" + } + + exec "$@" +} +addJava () { + dlog "[addJava] arg = '$1'" + java_args=( "${java_args[@]}" "$1" ) +} +addApp () { + dlog "[addApp] arg = '$1'" + sbt_commands=( "${app_commands[@]}" "$1" ) +} +addResidual () { + dlog "[residual] arg = '$1'" + residual_args=( "${residual_args[@]}" "$1" ) +} +addDebugger () { + addJava "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1" +} +# a ham-fisted attempt to move some memory settings in concert +# so they need not be messed around with individually. +get_mem_opts () { + local mem=${1:-1024} + local perm=$(( $mem / 4 )) + (( $perm > 256 )) || perm=256 + (( $perm < 1024 )) || perm=1024 + local codecache=$(( $perm / 2 )) + + echo "-Xms${mem}m -Xmx${mem}m -XX:MaxPermSize=${perm}m -XX:ReservedCodeCacheSize=${codecache}m" +} +require_arg () { + local type="$1" + local opt="$2" + local arg="$3" + if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then + die "$opt requires <$type> argument" + fi +} +require_arg () { + local type="$1" + local opt="$2" + local arg="$3" + if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then + die "$opt requires <$type> argument" + fi +} +is_function_defined() { + declare -f "$1" > /dev/null +} + +# Attempt to detect if the script is running via a GUI or not +# TODO - Determine where/how we use this generically +detect_terminal_for_ui() { + [[ ! -t 0 ]] && [[ "${#residual_args}" == "0" ]] && { + echo "true" + } + # SPECIAL TEST FOR MAC + [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]] && [[ "${#residual_args}" == "0" ]] && { + echo "true" + } +} + +# Processes incoming arguments and places them in appropriate global variables. called by the run method. +process_args () { + while [[ $# -gt 0 ]]; do + case "$1" in + -h|-help) usage; exit 1 ;; + -v|-verbose) verbose=1 && shift ;; + -d|-debug) debug=1 && shift ;; + + -mem) require_arg integer "$1" "$2" && app_mem="$2" && shift 2 ;; + -jvm-debug) require_arg port "$1" "$2" && addDebugger $2 && shift 2 ;; + + -java-home) require_arg path "$1" "$2" && java_cmd="$2/bin/java" && shift 2 ;; + + -D*) addJava "$1" && shift ;; + -J*) addJava "${1:2}" && shift ;; + *) addResidual "$1" && shift ;; + esac + done + + is_function_defined process_my_args && { + myargs=("${residual_args[@]}") + residual_args=() + process_my_args "${myargs[@]}" + } +} + +# Actually runs the script. +run() { + # TODO - check for sane environment + + # process the combined args, then reset "$@" to the residuals + process_args "$@" + set -- "${residual_args[@]}" + argumentCount=$# + + #check for jline terminal fixes on cygwin + if is_cygwin; then + stty -icanon min 1 -echo > /dev/null 2>&1 + addJava "-Djline.terminal=jline.UnixTerminal" + addJava "-Dsbt.cygwin=true" + fi + # run sbt + execRunner "$java_cmd" \ + $(get_mem_opts $app_mem) \ + ${java_opts} \ + ${java_args[@]} \ + -cp "$app_classpath" \ + $app_mainclass \ + "${app_commands[@]}" \ + "${residual_args[@]}" + + local exit_code=$? + if is_cygwin; then + stty icanon echo > /dev/null 2>&1 + fi + exit $exit_code +} + +# Loads a configuration file full of default command line options for this script. +loadConfigFile() { + cat "$1" | sed '/^\#/d' +} + +### ------------------------------- ### +### Start of customized settings ### +### ------------------------------- ### +usage() { + cat < set memory options (default: $sbt_mem, which is $(get_mem_opts $sbt_mem)) + -jvm-debug Turn on JVM debugging, open at the given port. + + # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) + -java-home alternate JAVA_HOME + + # jvm options and output control + JAVA_OPTS environment variable, if unset uses "$java_opts" + -Dkey=val pass -Dkey=val directly to the java runtime + -J-X pass option -X directly to the java runtime + (-J is stripped) + +In the case of duplicated or conflicting options, the order above +shows precedence: JAVA_OPTS lowest, command line options highest. +EOM +} + +### ------------------------------- ### +### Main script ### +### ------------------------------- ### + +declare -a residual_args +declare -a java_args +declare -a app_commands +declare -r app_home="$(realpath "$(dirname "$0")")" +# TODO - Check whether this is ok in cygwin... +declare -r lib_dir="${app_home}/../lib" +${{template_declares}} +declare -r java_cmd=$(get_java_cmd) + +# Now check to see if it's a good enough version +# TODO - Check to see if we have a configured default java version, otherwise use 1.6 +declare -r java_version=$("$java_cmd" -version 2>&1 | awk -F '"' '/version/ {print $2}') +if [[ "$java_version" == "" ]]; then + echo + echo No java installations was detected. + echo Please go to http://www.java.com/getjava/ and download + echo + exit 1 +elif [[ ! "$java_version" > "1.6" ]]; then + echo + echo The java installation you have is not up to date + echo $app_name requires at least version 1.6+, you have + echo version $java_version + echo + echo Please go to http://www.java.com/getjava/ and download + echo a valid Java Runtime and install before running $app_name. + echo + exit 1 +fi + + +# if configuration files exist, prepend their contents to $@ so it can be processed by this runner +[[ -f "$script_conf_file" ]] && set -- $(loadConfigFile "$script_conf_file") "$@" + +run "$@" \ No newline at end of file diff --git a/src/main/resources/com/typesafe/sbt/packager/archetypes/bat-template b/src/main/resources/com/typesafe/sbt/packager/archetypes/bat-template new file mode 100644 index 000000000..11e398181 --- /dev/null +++ b/src/main/resources/com/typesafe/sbt/packager/archetypes/bat-template @@ -0,0 +1,104 @@ +@REM @@APP_NAME@@ launcher script +@REM +@REM Envioronment: +@REM JAVA_HOME - location of a JDK home dir (optional if java on path) +@REM CFG_OPTS - JVM options (optional) +@REM Configuration: +@REM @@APP_ENV_NAME@@_config.txt found in the @@APP_ENV_NAME@@_HOME. +@setlocal enabledelayedexpansion + +@echo off +if "%@@APP_ENV_NAME@@_HOME%"=="" set "@@APP_ENV_NAME@@_HOME=%~dp0\\.." +set ERROR_CODE=0 + +set "APP_LIB_DIR=%@@APP_ENV_NAME@@_HOME%\lib\" + +rem Detect if we were double clicked, although theoretically A user could +rem manually run cmd /c +for %%x in (%cmdcmdline%) do if %%~x==/c set DOUBLECLICKED=1 + +rem FIRST we load the config file of extra options. +set "CFG_FILE=%@@APP_ENV_NAME@@_HOME%\@@APP_ENV_NAME@@_config.txt" +set CFG_OPTS= +if exist %CFG_FILE% ( + FOR /F "tokens=* eol=# usebackq delims=" %%i IN ("%CFG_FILE%") DO ( + set DO_NOT_REUSE_ME=%%i + rem ZOMG (Part #2) WE use !! here to delay the expansion of + rem CFG_OPTS, otherwise it remains "" for this loop. + set CFG_OPTS=!CFG_OPTS! !DO_NOT_REUSE_ME! + ) +) + +rem We use the value of the JAVACMD environment variable if defined +set _JAVACMD=%JAVACMD% + +if "%_JAVACMD%"=="" ( + if not "%JAVA_HOME%"=="" ( + if exist "%JAVA_HOME%\bin\java.exe" set "_JAVACMD=%JAVA_HOME%\bin\java.exe" + ) +) + +if "%_JAVACMD%"=="" set _JAVACMD=java + +rem Detect if this java is ok to use. +for /F %%j in ('"%_JAVACMD%" -version 2^>^&1') do ( + if %%~j==Java set JAVAINSTALLED=1 +) + +rem Detect the same thing about javac +if "%_JAVACCMD%"=="" ( + if not "%JAVA_HOME%"=="" ( + if exist "%JAVA_HOME%\bin\javac.exe" set "_JAVACCMD=%JAVA_HOME%\bin\javac.exe" + ) +) +if "%_JAVACCMD%"=="" set _JAVACCMD=javac +for /F %%j in ('"%_JAVACCMD%" -version 2^>^&1') do ( + if %%~j==javac set JAVACINSTALLED=1 +) + +rem BAT has no logical or, so we do it OLD SCHOOL! Oppan Redmond Style +set JAVAOK=true +if not defined JAVAINSTALLED set JAVAOK=false +rem TODO - JAVAC is an optional requirement. +if not defined JAVACINSTALLED set JAVAOK=false + +if "%JAVAOK%"=="false" ( + echo. + echo A Java JDK is not installed or can't be found. + if not "%JAVA_HOME%"=="" ( + echo JAVA_HOME = "%JAVA_HOME%" + ) + echo. + echo Please go to + echo http://www.oracle.com/technetwork/java/javase/downloads/index.html + echo and download a valid Java JDK and install before running @@APP_NAME@@. + echo. + echo If you think this message is in error, please check + echo your environment variables to see if "java.exe" and "javac.exe" are + echo available via JAVA_HOME or PATH. + echo. + if defined DOUBLECLICKED pause + exit /B 1 +) + + +rem We use the value of the JAVA_OPTS environment variable if defined, rather than the config. +set _JAVA_OPTS=%JAVA_OPTS% +if "%_JAVA_OPTS%"=="" set _JAVA_OPTS=%CFG_OPTS% + +:run + +set "APP_CLASSPATH=@@APP_CLASSPATH@@" +rem TODO - figure out how to pass arguments.... +"%_JAVACMD%" %_JAVA_OPTS% %@@APP_ENV_NAME@@_OPTS% -cp "%APP_CLASSPATH%" @@APP_MAIN_CLASS@@ %CMDS% +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end + +@endlocal + +exit /B %ERROR_CODE% \ No newline at end of file diff --git a/src/main/scala/com/typesafe/sbt/PackagerPlugin.scala b/src/main/scala/com/typesafe/sbt/PackagerPlugin.scala index 317d739d7..ef13e10a8 100644 --- a/src/main/scala/com/typesafe/sbt/PackagerPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/PackagerPlugin.scala @@ -41,8 +41,9 @@ object SbtNativePackager extends Plugin addPackage(UniversalDocs, packageXzTarball in UniversalDocs, "txz") object packageArchetype { + private[this] def genericMappingSettings: Seq[Setting[_]] = packagerSettings ++ mapGenericFilesToLinux ++ mapGenericFilesToWindows def java_application: Seq[Setting[_]] = - packagerSettings ++ archetypes.JavaAppPackaging.settings + genericMappingSettings ++ archetypes.JavaAppPackaging.settings } // TODO - Add a few targets that detect the current OS and build a package for that OS. diff --git a/src/main/scala/com/typesafe/sbt/packager/GenericPackageSettings.scala b/src/main/scala/com/typesafe/sbt/packager/GenericPackageSettings.scala index 9ae441730..651ed8b23 100644 --- a/src/main/scala/com/typesafe/sbt/packager/GenericPackageSettings.scala +++ b/src/main/scala/com/typesafe/sbt/packager/GenericPackageSettings.scala @@ -113,14 +113,13 @@ trait GenericPackageSettings } yield ComponentFile(name, editable = (name startsWith "conf")) val corePackage = WindowsFeature( - id=name+"Core", + id=WixHelper.cleanStringForId(name+"_core"), title=name, desc="All core files.", absent="disallow", components = files ) // TODO - Detect bat files to add paths... - val homeEnvVar = name.toUpperCase +"_HOME" val addBinToPath = // TODO - we may have issues here... WindowsFeature( diff --git a/src/main/scala/com/typesafe/sbt/packager/Keys.scala b/src/main/scala/com/typesafe/sbt/packager/Keys.scala index ab76a811d..06c08994c 100644 --- a/src/main/scala/com/typesafe/sbt/packager/Keys.scala +++ b/src/main/scala/com/typesafe/sbt/packager/Keys.scala @@ -1,8 +1,28 @@ package com.typesafe.sbt package packager +import sbt._ + object Keys extends linux.Keys with debian.DebianKeys with rpm.RpmKeys with windows.WindowsKeys - with universal.UniversalKeys {} \ No newline at end of file + with universal.UniversalKeys { + + // TODO - Do these keys belong here? + + // These keys are used by the JavaApp archetype. + val makeBashScript = TaskKey[Option[File]]("makeBashScript", "Creates or discovers the bash script used by this project.") + val bashScriptDefines = TaskKey[Seq[String]]("bashScriptDefines", "A list of definitions that should be written to the bash file template.") + val bashScriptExtraDefines = TaskKey[Seq[String]]("bashScriptExtraDefines", "A list of extra definitions that should be written to the bash file template.") + val scriptClasspathOrdering = TaskKey[Seq[(File, String)]]("scriptClasspathOrdering", "The order of the classpath used at runtime for the bat/bash scripts.") + val scriptClasspath = TaskKey[Seq[String]]("scriptClasspath", "A list of relative filenames (to the lib/ folder in the distribution) of what to include on the classpath.") + val makeBatScript = TaskKey[Option[File]]("makeBatScript", "Creates or discovers the bat script used by this project.") + val batScriptReplacements = TaskKey[Seq[(String,String)]]("batScriptReplacements", + """|Replacements of template parameters used in the windows bat script. + | Default supported templates: + | APP_ENV_NAME - the name of the application for defining _HOME variables + | APP_CLASSPATH - the string to use for teh classpath of java. + | """.stripMargin) + +} \ No newline at end of file diff --git a/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaApp.scala b/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaApp.scala index 34998b2cd..728f27085 100644 --- a/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaApp.scala +++ b/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaApp.scala @@ -4,7 +4,7 @@ package archetypes import Keys._ import sbt._ -import sbt.Keys.{mappings, target, name, mainClass} +import sbt.Keys.{mappings, target, name, mainClass, normalizedName} import linux.LinuxPackageMapping import SbtNativePackager._ @@ -18,61 +18,92 @@ import SbtNativePackager._ */ object JavaAppPackaging { - def settings = - defaultUniversalSettings ++ - defaultLinuxSettings - - /// Universal packaging defaults. - def defaultUniversalSettings: Seq[Setting[_]] = Seq( - mappings in Universal <++= (Keys.managedClasspath in Compile) map universalDepMappings, - mappings in Universal <+= (Keys.packageBin in Compile) map { jar => - jar -> ("lib/" + jar.getName) + def settings: Seq[Setting[_]] = Seq( + // Here we record the classpath as it's added to the mappings separately, so + // we can use its order to generate the bash/bat scripts. + scriptClasspathOrdering := Nil, + scriptClasspathOrdering <+= (Keys.packageBin in Compile) map { jar => + jar -> ("lib/" + jar.getName) + }, + scriptClasspathOrdering <++= (Keys.dependencyClasspath in Runtime) map universalDepMappings, + mappings in Universal <++= scriptClasspathOrdering, + scriptClasspath <<= scriptClasspathOrdering map makeRelativeClasspathNames, + bashScriptExtraDefines := Nil, + bashScriptDefines <<= (Keys.mainClass in Compile, scriptClasspath, bashScriptExtraDefines) map { (mainClass, cp, extras) => + val hasMain = + for { + cn <- mainClass + } yield JavaAppBashScript.makeDefines(cn, appClasspath = cp, extras = extras) + hasMain getOrElse Nil }, - mappings in Universal <++= (Keys.mainClass in Compile, target in Universal, name in Universal) map makeUniversalBinScript + makeBashScript <<= (bashScriptDefines, target in Universal, normalizedName) map makeUniversalBinScript, + batScriptReplacements <<= (normalizedName, Keys.mainClass in Compile, scriptClasspath) map { (name, mainClass, cp) => + mainClass map { mc => + JavaAppBatScript.makeReplacements(name = name, mainClass = mc, appClasspath = cp) + } getOrElse Nil + + }, + makeBatScript <<= (batScriptReplacements, target in Universal, normalizedName) map makeUniversalBatScript, + mappings in Universal <++= (makeBashScript, normalizedName) map { (script, name) => + for { + s <- script.toSeq + } yield s -> ("bin/" + name) + }, + mappings in Universal <++= (makeBatScript, normalizedName) map { (script, name) => + for { + s <- script.toSeq + } yield s -> ("bin/" + name + ".bat") + } ) - def makeUniversalBinScript(mainClass: Option[String], tmpDir: File, name: String): Seq[(File, String)] = - for(mc <- mainClass.toSeq) yield { - val scriptBits = JavaAppBashScript.generateScript(mc) + def makeRelativeClasspathNames(mappings: Seq[(File, String)]): Seq[String] = + for { + (file, name) <- mappings + } yield { + // Here we want the name relative to the lib/ folder... + // For now we just cheat... + if(name startsWith "lib/") name drop 4 + else "../" + name + } + + def makeUniversalBinScript(defines: Seq[String], tmpDir: File, name: String): Option[File] = + if(defines.isEmpty) None + else { + val scriptBits = JavaAppBashScript.generateScript(defines) val script = tmpDir / "tmp" / "bin" / name IO.write(script, scriptBits) - script -> ("bin/" + name) + // TODO - Better control over this! + script.setExecutable(true) + Some(script) } + def makeUniversalBatScript(replacements: Seq[(String, String)], tmpDir: File, name: String): Option[File] = + if(replacements.isEmpty) None + else { + val scriptBits = JavaAppBatScript.generateScript(replacements) + val script = tmpDir / "tmp" / "bin" / (name + ".bat") + IO.write(script, scriptBits) + Some(script) + } + // Converts a managed classpath into a set of lib mappings. def universalDepMappings(deps: Seq[Attributed[File]]): Seq[(File,String)] = for { dep <- deps file = dep.data - // TODO - Figure out what to do with jar files. if file.isFile - } yield dep.data -> ("lib/" + dep.data.getName) - - - // Default linux settings are driven off of the universal settings. - def defaultLinuxSettings: Seq[Setting[_]] = Seq( - linuxPackageMappings <+= (mappings in Universal, name in Linux) map filterLibs, - linuxPackageMappings <++= (mainClass in Compile, name in Linux, target in Linux) map makeLinuxBinScrit - ) - - def filterLibs(mappings: Seq[(File, String)], name: String): LinuxPackageMapping = { - val libs = for { - (file, location) <- mappings - if location startsWith "lib/" - } yield file -> ("/usr/share/"+name+"/" + location) - packageMapping(libs:_*) - } - - - def makeLinuxBinScrit(mainClass: Option[String], name: String, tmpDir: File): Seq[LinuxPackageMapping] = - for(mc <- mainClass.toSeq) yield { - val scriptBits = JavaAppBashScript.generateScript( - mainClass = mc, - libDir = "/usr/share/" + name + "/lib") - val script = tmpDir / "tmp" / "bin" / name - IO.write(script, scriptBits) - val scriptMapping = script -> ("/usr/bin/" + name) - - packageMapping(scriptMapping).withPerms("0755") + // TODO - Figure out what to do with jar files. + } yield { + val filename: Option[String] = for { + module <- dep.metadata.get(AttributeKey[ModuleID]("module-id")) + artifact <- dep.metadata.get(AttributeKey[Artifact]("artifact")) + } yield { + module.organization + "." + + module.name + "-" + + Option(artifact.name.replace(module.name, "")).filterNot(_.isEmpty).map(_ + "-").getOrElse("") + + module.revision + ".jar" + } + + dep.data -> ("lib/" + filename.getOrElse(file.getName)) } } \ No newline at end of file diff --git a/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaAppBashScript.scala b/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaAppBashScript.scala index 344ad2254..c3fd4e412 100644 --- a/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaAppBashScript.scala +++ b/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaAppBashScript.scala @@ -1,225 +1,57 @@ package com.typesafe.sbt.packager.archetypes +/** + * Constructs a bash script for running a java application. + * + * Makes use of the associated bash-template, with a few hooks + * + */ object JavaAppBashScript { - def generateScript(mainClass: String, - libDir: String = "$(dirname $(realpath $0))/../lib", - configFile: Option[String] = None): String = { - val sb = new StringBuffer - sb append template_header - sb append mainClassDefine(mainClass) - sb append defaultUsage - configFile foreach (f => sb append configFileDefine(f)) - sb append defineLibLocation(libDir) - sb append template_footer + private[this] def bashTemplateSource = + getClass.getResource("bash-template") + private[this] def charset = + java.nio.charset.Charset.forName("UTF-8") + + /** 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 ":" + "declare -r app_classpath=\""+fullString+"\"\n" + } + def generateScript(defines: Seq[String]): String = { + val sb = new StringBuffer + for(line <- sbt.IO.readLinesURL(bashTemplateSource, charset)) { + if(line contains """${{template_declares}}""") { + sb append (defines mkString "\n") + } else { + sb append line + sb append "\n" + } + } sb.toString } - def defineLibLocation(libDir: String) = - "declare -r lib_dir=%s" format (libDir) - def configFileDefine(configFile: String) = "declare -r script_conf_file=\"%s\"" format (configFile) def mainClassDefine(mainClass: String) = "declare -r app_mainclass=\"%s\"\n" format (mainClass) - - val defaultUsage = """usage() { - cat < set memory options (default: $sbt_mem, which is $(get_mem_opts $sbt_mem)) - -jvm-debug Turn on JVM debugging, open at the given port. - - # java version (default: java from PATH, currently $(java -version 2>&1 | grep version)) - -java-home alternate JAVA_HOME - - # jvm options and output control - JAVA_OPTS environment variable, if unset uses "$java_opts" - -Dkey=val pass -Dkey=val directly to the java runtime - -J-X pass option -X directly to the java runtime - (-J is stripped) - -In the case of duplicated or conflicting options, the order above -shows precedence: JAVA_OPTS lowest, command line options highest. -EOM -} -""" - - - - val template_header = """#!/bin/bash - -### ------------------------------- ### -### Helper methods for BASH scripts ### -### ------------------------------- ### - -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 - - echo $(pwd -P)/$TARGET_FILE -) -} -echoerr () { - echo 1>&2 "$@" -} -vlog () { - [[ $verbose || $debug ]] && echoerr "$@" -} -dlog () { - [[ $debug ]] && echoerr "$@" -} -execRunner () { - # print the arguments one to a line, quoting any containing spaces - [[ $verbose || $debug ]] && echo "# Executing command line:" && { - for arg; do - if printf "%s\n" "$arg" | grep -q ' '; then - printf "\"%s\"\n" "$arg" - else - printf "%s\n" "$arg" - fi - done - echo "" - } - - exec "$@" -} -addJava () { - dlog "[addJava] arg = '$1'" - java_args=( "${java_args[@]}" "$1" ) -} -addApp () { - dlog "[addApp] arg = '$1'" - sbt_commands=( "${app_commands[@]}" "$1" ) -} -addResidual () { - dlog "[residual] arg = '$1'" - residual_args=( "${residual_args[@]}" "$1" ) -} -addDebugger () { - addJava "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=$1" -} -# a ham-fisted attempt to move some memory settings in concert -# so they need not be dicked around with individually. -get_mem_opts () { - local mem=${1:-1536} - local perm=$(( $mem / 4 )) - (( $perm > 256 )) || perm=256 - (( $perm < 1024 )) || perm=1024 - local codecache=$(( $perm / 2 )) - - echo "-Xms${mem}m -Xmx${mem}m -XX:MaxPermSize=${perm}m -XX:ReservedCodeCacheSize=${codecache}m" -} -require_arg () { - local type="$1" - local opt="$2" - local arg="$3" - if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then - die "$opt requires <$type> argument" - fi -} -require_arg () { - local type="$1" - local opt="$2" - local arg="$3" - if [[ -z "$arg" ]] || [[ "${arg:0:1}" == "-" ]]; then - die "$opt requires <$type> argument" - fi -} -is_function_defined() { - declare -f "$1" > /dev/null -} - -# Processes incoming arguments and places them in appropriate global variables. called by the run method. -process_args () { - while [[ $# -gt 0 ]]; do - case "$1" in - -h|-help) usage; exit 1 ;; - -v|-verbose) verbose=1 && shift ;; - -d|-debug) debug=1 && shift ;; - - -mem) require_arg integer "$1" "$2" && app_mem="$2" && shift 2 ;; - -jvm-debug) require_arg port "$1" "$2" && addDebugger $2 && shift 2 ;; - - -java-home) require_arg path "$1" "$2" && java_cmd="$2/bin/java" && shift 2 ;; - - -D*) addJava "$1" && shift ;; - -J*) addJava "${1:2}" && shift ;; - *) addResidual "$1" && shift ;; - esac - done - - is_function_defined process_my_args && { - myargs=("${residual_args[@]}") - residual_args=() - process_my_args "${myargs[@]}" - } -} - -# Actually runs the script. -run() { - # TODO - check for sane environment - - # process the combined args, then reset "$@" to the residuals - process_args "$@" - set -- "${residual_args[@]}" - argumentCount=$# - - # run sbt - execRunner "$java_cmd" \ - $(get_mem_opts $app_mem) \ - ${java_opts} \ - ${java_args[@]} \ - -cp "$app_classpath" \ - $app_mainclass \ - "${app_commands[@]}" \ - "${residual_args[@]}" -} - -# Loads a configuration file full of default command line options for this script. -loadConfigFile() { - cat "$1" | sed '/^\#/d' -} - -### ------------------------------- ### -### Start of customized settings ### -### ------------------------------- ### -""" - - - val template_footer = """ - -### ------------------------------- ### -### Main script ### -### ------------------------------- ### - -declare -a residual_args -declare -a java_args -declare -a app_commands -declare java_cmd=java -declare -r app_classpath="$lib_dir/*" - - -# if configuration files exist, prepend their contents to $@ so it can be processed by this runner -[[ -f "$script_conf_file" ]] && set -- $(loadConfigFile "$script_conf_file") "$@" - -run "$@" -""" } \ No newline at end of file diff --git a/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaAppBatScript.scala b/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaAppBatScript.scala new file mode 100644 index 000000000..94b57e466 --- /dev/null +++ b/src/main/scala/com/typesafe/sbt/packager/archetypes/JavaAppBatScript.scala @@ -0,0 +1,46 @@ +package com.typesafe.sbt.packager.archetypes + +object JavaAppBatScript { + private[this] def bashTemplateSource = + getClass.getResource("bat-template") + private[this] def charset = + java.nio.charset.Charset.forName("UTF-8") + + def makeEnvFriendlyName(name: String): String = + name.toUpperCase.replaceAll("\\W", "_") + + def makeWindowsRelativeClasspath(cp: Seq[String]): String = { + def cleanPath(path: String): String = path.replaceAll("/", "\\") + def makeRelativePath(path: String): String = + "%APP_LIB_DIR%\\" + cleanPath(path) + cp map makeRelativePath mkString ":" + } + // TODO - Allow recursive replacements.... + def makeReplacements( + name: String, + mainClass: String, + appClasspath: Seq[String] = Seq("*"), + extras: Seq[(String,String)] = Nil): Seq[(String, String)] = { + Seq( + "APP_NAME" -> name, + "APP_MAIN_CLASS" -> mainClass, + "APP_ENV_NAME" -> makeEnvFriendlyName(name), + "APP_CLASSPATH" -> makeWindowsRelativeClasspath(appClasspath) + ) ++ extras + } + + def generateScript( + replacements: Seq[(String,String)]): String = { + val sb = new StringBuffer + for(line <- sbt.IO.readLinesURL(bashTemplateSource, charset)) { + val fixed = + replacements.foldLeft(line) { + case (line, (key, value)) => + line.replaceAll("@@"+key+"@@", java.util.regex.Matcher.quoteReplacement(value)) + } + sb append fixed + sb append "\r\n" + } + sb.toString + } +} \ No newline at end of file diff --git a/src/main/scala/com/typesafe/sbt/packager/universal/Keys.scala b/src/main/scala/com/typesafe/sbt/packager/universal/Keys.scala index 9209edf1d..8f3a07382 100644 --- a/src/main/scala/com/typesafe/sbt/packager/universal/Keys.scala +++ b/src/main/scala/com/typesafe/sbt/packager/universal/Keys.scala @@ -8,9 +8,9 @@ trait UniversalKeys { val packageZipTarball = TaskKey[File]("package-zip-tarball", "Creates a tgz package.") val packageXzTarball = TaskKey[File]("package-xz-tarball", "Creates a txz package.") val packageOsxDmg = TaskKey[File]("package-osx-dmg", "Creates a dmg package for OSX (only on osx).") - val stagingDirectory = SettingKey[File]("stagingDirectory", "The location where a staged distribution will be generated.") val stage = TaskKey[Unit]("stage", "Create a local directory with all the files laid out as they would be in the final distribution.") val dist = TaskKey[File]("dist", "Creates the distribution packages.") + val stagingDirectory = SettingKey[File]("stagingDirectory", "Directory where we stage distributions/releases.") } object Keys extends UniversalKeys { @@ -21,4 +21,6 @@ object Keys extends UniversalKeys { def name = sbt.Keys.name def target = sbt.Keys.target def sourceDirectory = sbt.Keys.sourceDirectory + def streams = sbt.Keys.streams + def version = sbt.Keys.version } \ No newline at end of file diff --git a/src/main/scala/com/typesafe/sbt/packager/universal/UniversalPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/universal/UniversalPlugin.scala index ec667db4e..625a75bd7 100644 --- a/src/main/scala/com/typesafe/sbt/packager/universal/UniversalPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/universal/UniversalPlugin.scala @@ -5,6 +5,7 @@ package universal import sbt._ import Keys._ import Archives._ +import sbt.Keys.TaskStreams /** Defines behavior to construct a 'universal' zip for installation. */ trait UniversalPlugin extends Plugin { @@ -14,6 +15,13 @@ trait UniversalPlugin extends Plugin { /** The basic settings for the various packaging types. */ def universalSettings: Seq[Setting[_]] = + Seq[Setting[_]]( + // For now, we provide delegates from dist/stage to universal... + dist <<= dist in Universal, + stage <<= stage in Universal, + // TODO - New default to naming, is this right? + name in Universal <<= (name, version) apply (_ + "-" + _) + ) ++ makePackageSettingsForConfig(Universal) ++ makePackageSettingsForConfig(UniversalDocs) ++ makePackageSettingsForConfig(UniversalSrc) @@ -26,7 +34,7 @@ trait UniversalPlugin extends Plugin { makePackageSettings(packageXzTarball, config)(makeTxz) ++ inConfig(config)(Seq( mappings <<= sourceDirectory map findSources, - dist <<= packageBin, + dist <<= (packageBin, streams) map printDist, stagingDirectory <<= target apply (_ / "stage"), stage <<= (stagingDirectory, mappings) map stageFiles )) ++ Seq( @@ -39,7 +47,12 @@ trait UniversalPlugin extends Plugin { makePackageSettings(packageBin, UniversalDocs)(makeNativeZip) ++ makePackageSettings(packageBin, UniversalSrc)(makeNativeZip) - + private[this] def printDist(dist: File, streams: TaskStreams): File = { + streams.log.info("") + streams.log.info("Your package is ready in " + dist.getCanonicalPath) + streams.log.info("") + dist + } private[this] def stageFiles(to: File, mappings: Seq[(File, String)]): Unit = { val copies = mappings collect { case (f, p) => f -> (to / p) } diff --git a/src/main/scala/com/typesafe/sbt/packager/windows/WixHelper.scala b/src/main/scala/com/typesafe/sbt/packager/windows/WixHelper.scala index 5759e1904..d18bf3dec 100644 --- a/src/main/scala/com/typesafe/sbt/packager/windows/WixHelper.scala +++ b/src/main/scala/com/typesafe/sbt/packager/windows/WixHelper.scala @@ -84,7 +84,7 @@ object WixHelper { case w: WindowsFeature => sys.error("Nested windows features currently unsupported!") case AddDirectoryToPath(dir) => val dirRef = if(dir.isEmpty) "INSTALLDIR" else cleanStringForId(dir) - val homeEnvVar = name.toUpperCase + "_HOME" + val homeEnvVar = archetypes.JavaAppBatScript.makeEnvFriendlyName(name) +"_HOME" val pathAddition = if(dir.isEmpty) "%"+homeEnvVar+"%" else "[INSTALLDIR]\\"+dir.replaceAll("\\/", "\\\\") diff --git a/src/sbt-test/debian/java-app-archetype/build.sbt b/src/sbt-test/debian/java-app-archetype/build.sbt new file mode 100644 index 000000000..35f3ce61a --- /dev/null +++ b/src/sbt-test/debian/java-app-archetype/build.sbt @@ -0,0 +1,32 @@ +import NativePackagerKeys._ + +packageArchetype.java_application + +name := "debian-test" + +version := "0.1.0" + +maintainer := "Josh Suereth " + +packageSummary := "Test debian package" + +packageDescription := """A fun package description of our software, + with multiple lines.""" + +debianPackageDependencies in Debian ++= Seq("java2-runtime", "bash (>= 2.05a-11)") + +debianPackageRecommends in Debian += "git" + + +TaskKey[Unit]("check-script") <<= (stagingDirectory in Universal, name, streams) map { (dir, name, streams) => + val script = dir / "bin" / name + val cmd = "bash " + script.getAbsolutePath + val result = + Process(cmd) ! streams.log match { + case 0 => () + case n => sys.error("Failed to run script: " + script.getAbsolutePath + " error code: " + n) + } + val output = Process("bash " + script.getAbsolutePath).!! + val expected = "SUCCESS!" + assert(output contains expected, "Failed to correctly run the main script!. Found ["+output+"] wanted ["+expected+"]") +} diff --git a/src/sbt-test/debian/java-app-archetype/project/plugins.sbt b/src/sbt-test/debian/java-app-archetype/project/plugins.sbt new file mode 100644 index 000000000..b53de154c --- /dev/null +++ b/src/sbt-test/debian/java-app-archetype/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/debian/java-app-archetype/src/main/scala/test/Test.scala b/src/sbt-test/debian/java-app-archetype/src/main/scala/test/Test.scala new file mode 100644 index 000000000..242dc2381 --- /dev/null +++ b/src/sbt-test/debian/java-app-archetype/src/main/scala/test/Test.scala @@ -0,0 +1,5 @@ +package test + +object Test extends App { + println("SUCCESS!") +} diff --git a/src/sbt-test/debian/java-app-archetype/test b/src/sbt-test/debian/java-app-archetype/test new file mode 100644 index 000000000..09a72e41b --- /dev/null +++ b/src/sbt-test/debian/java-app-archetype/test @@ -0,0 +1,6 @@ +# Run the debian packaging. +> debian:package-bin +$ exists target/debian-test-0.1.0.deb +> stage +$ exists target/universal/stage/bin/debian-test +> check-script diff --git a/src/sbt-test/debian/test-mapping/build.sbt b/src/sbt-test/debian/test-mapping/build.sbt new file mode 100644 index 000000000..a856482cd --- /dev/null +++ b/src/sbt-test/debian/test-mapping/build.sbt @@ -0,0 +1,20 @@ +import NativePackagerKeys._ + +packagerSettings + +mapGenericFilesToLinux + +name := "debian-test" + +version := "0.1.0" + +maintainer := "Josh Suereth " + +packageSummary := "Test debian package" + +packageDescription := """A fun package description of our software, + with multiple lines.""" + +debianPackageDependencies in Debian ++= Seq("java2-runtime", "bash (>= 2.05a-11)") + +debianPackageRecommends in Debian += "git" diff --git a/src/sbt-test/debian/test-mapping/project/plugins.sbt b/src/sbt-test/debian/test-mapping/project/plugins.sbt new file mode 100644 index 000000000..b53de154c --- /dev/null +++ b/src/sbt-test/debian/test-mapping/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/debian/test-mapping/src/debian/changelog b/src/sbt-test/debian/test-mapping/src/debian/changelog new file mode 100644 index 000000000..ec30582fa --- /dev/null +++ b/src/sbt-test/debian/test-mapping/src/debian/changelog @@ -0,0 +1,12 @@ + +sbt (0.12.0-build-0100) + + * No need for different launcher jar files now + + -- Joshua Suereth 2012-07-2012 + +sbt (0.11.2-build-0100) + + * First debian package release + + -- Joshua Suereth 2011-11-29 diff --git a/src/sbt-test/debian/test-mapping/src/linux/usr/share/man/man1/test.1 b/src/sbt-test/debian/test-mapping/src/linux/usr/share/man/man1/test.1 new file mode 100644 index 000000000..0eb351c37 --- /dev/null +++ b/src/sbt-test/debian/test-mapping/src/linux/usr/share/man/man1/test.1 @@ -0,0 +1,6 @@ +.\" Process this file with +.\" groff -man -Tascii sbt.1 +.\" +.TH TEST 1 "NOVEMBER 2013" Linux "User Manuals" +.SH NAME +test \- TEST diff --git a/src/sbt-test/debian/test-mapping/src/universal/bin/test b/src/sbt-test/debian/test-mapping/src/universal/bin/test new file mode 100755 index 000000000..e69de29bb diff --git a/src/sbt-test/debian/test-mapping/test b/src/sbt-test/debian/test-mapping/test new file mode 100644 index 000000000..edb38a825 --- /dev/null +++ b/src/sbt-test/debian/test-mapping/test @@ -0,0 +1,6 @@ +# Run the debian packaging. +> debian:package-bin +$ exists target/debian-test-0.1.0.deb + + +# TODO - Test that the generic mapping did the right thing. diff --git a/src/sbt-test/universal/dist/build.sbt b/src/sbt-test/universal/dist/build.sbt new file mode 100644 index 000000000..8c687dea6 --- /dev/null +++ b/src/sbt-test/universal/dist/build.sbt @@ -0,0 +1,7 @@ +import NativePackagerKeys._ + +packagerSettings + +name := "simple-test" + +version := "0.1.0" diff --git a/src/sbt-test/universal/dist/project/plugins.sbt b/src/sbt-test/universal/dist/project/plugins.sbt new file mode 100644 index 000000000..b53de154c --- /dev/null +++ b/src/sbt-test/universal/dist/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/universal/dist/src/universal/conf/test b/src/sbt-test/universal/dist/src/universal/conf/test new file mode 100644 index 000000000..ab1006ffd --- /dev/null +++ b/src/sbt-test/universal/dist/src/universal/conf/test @@ -0,0 +1 @@ +# Test configuration to include in zips. diff --git a/src/sbt-test/universal/dist/test b/src/sbt-test/universal/dist/test new file mode 100644 index 000000000..09179aca1 --- /dev/null +++ b/src/sbt-test/universal/dist/test @@ -0,0 +1,3 @@ +# Create the distribution and ensure files show up. +> dist +$ exists target/universal/simple-test-0.1.0.zip diff --git a/src/sbt-test/universal/test-native-zip/test b/src/sbt-test/universal/test-native-zip/test index c2fda0ebe..d3f226f48 100644 --- a/src/sbt-test/universal/test-native-zip/test +++ b/src/sbt-test/universal/test-native-zip/test @@ -1,5 +1,5 @@ # Run the zip packaging. > show universal:package-bin -$ exists target/universal/simple-test.zip +$ exists target/universal/simple-test-0.1.0.zip # TODO - Check contents of zips. Ensure file permissions are preserved. diff --git a/src/sbt-test/universal/test-zips/test b/src/sbt-test/universal/test-zips/test index fdfa4346f..72fdb39de 100644 --- a/src/sbt-test/universal/test-zips/test +++ b/src/sbt-test/universal/test-zips/test @@ -1,14 +1,14 @@ # Run the zip packaging. > show universal:package-bin -$ exists target/universal/simple-test.zip +$ exists target/universal/simple-test-0.1.0.zip # Run the tgz packaging. > universal:package-zip-tarball -$ exists target/universal/simple-test.tgz +$ exists target/universal/simple-test-0.1.0.tgz # Run the txz packaging. > universal:package-xz-tarball -$ exists target/universal/simple-test.txz +$ exists target/universal/simple-test-0.1.0.txz # TODO - Check contents of zips diff --git a/src/sphinx/archetypes.rst b/src/sphinx/archetypes.rst index 3b627ddd7..c12298180 100644 --- a/src/sphinx/archetypes.rst +++ b/src/sphinx/archetypes.rst @@ -17,9 +17,6 @@ Curently, in the nativepackager these archetypes are available: * Java Command Line Application (Experimental) - - - Java Command Line Application ----------------------------- @@ -30,17 +27,13 @@ this archetype in your build, do the following in your ``build.sbt``: archetypes.java_application - mapGenericFilesToLinux - - mapGenericFilesToWindows - name := "A-package-friendly-name" packageSummary in Linux := "The name you want displayed in package summaries" packageSummary in Windows := "The name you want displayed in Add/Remove Programs" - packageDescription := " A descriptioin of your project" + packageDescription := " A description of your project" maintainer in Windows := "Company" @@ -49,3 +42,17 @@ this archetype in your build, do the following in your ``build.sbt``: wixProductId := "ce07be71-510d-414a-92d4-dff47631848a" wixProductUpgradeId := "4552fb0e-e257-4dbd-9ecb-dba9dbacf424" + + +This archetype will use the ``mainClass`` setting of sbt (automatically discovers your main class) to generate ``bat`` and ``bin`` scripts for your project. It +produces a universal layout that looks like the following: + + + bin/ + <- BASH script + .bat <- cmd.exe script + lib/ + + + +You can add additional files to the project by placing things in ``src/windows``, ``src/universal`` or ``src/linux`` as needed.