Skip to content

Commit cee8a07

Browse files
dwickernmuuki88
authored andcommitted
Windows batch script improvements (#1042)
* bat: refactor argument loop into functions * bat: remove unused goto labels * bat: refactor out config file parse function * bat: use APP_HOME instead of dynamic environment variable * bat: add application.ini support * bat: escape arguments with special characters * bat: add test for javaOptions * bat: add test for extra jvm arg with APP_HOME expansion * bat: allow <APP_NAME>_HOME to be used in batScriptExtraDefines fixes backwards compat for this snippet from the docs: // add jvm parameter for typesafe config batScriptExtraDefines += """set _JAVA_OPTS=%_JAVA_OPTS% -Dconfig.file=%EXAMPLE_CLI_HOME%\\conf\\app.config""" * bat: document add_java/add_app methods * bat: remove note about <APP_NAME>_config.txt file * add javadoc comment to ApplicationIniGenerator
1 parent 42750a7 commit cee8a07

File tree

18 files changed

+259
-95
lines changed

18 files changed

+259
-95
lines changed

src/main/resources/com/typesafe/sbt/packager/archetypes/scripts/bat-template

+90-58
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,26 @@
99

1010
@echo off
1111

12-
if "%@@APP_ENV_NAME@@_HOME%"=="" set "@@APP_ENV_NAME@@_HOME=%~dp0\\.."
1312

14-
set "APP_LIB_DIR=%@@APP_ENV_NAME@@_HOME%\lib\"
13+
if "%@@APP_ENV_NAME@@_HOME%"=="" (
14+
set "APP_HOME=%~dp0\\.."
15+
16+
rem Also set the old env name for backwards compatibility
17+
set "@@APP_ENV_NAME@@_HOME=%~dp0\\.."
18+
) else (
19+
set "APP_HOME=%@@APP_ENV_NAME@@_HOME%"
20+
)
21+
22+
set "APP_LIB_DIR=%APP_HOME%\lib\"
1523

1624
rem Detect if we were double clicked, although theoretically A user could
1725
rem manually run cmd /c
1826
for %%x in (!cmdcmdline!) do if %%~x==/c set DOUBLECLICKED=1
1927

2028
rem FIRST we load the config file of extra options.
21-
set "CFG_FILE=%@@APP_ENV_NAME@@_HOME%\@@APP_ENV_NAME@@_config.txt"
29+
set "CFG_FILE=%APP_HOME%\@@APP_ENV_NAME@@_config.txt"
2230
set CFG_OPTS=
23-
if exist "%CFG_FILE%" (
24-
FOR /F "tokens=* eol=# usebackq delims=" %%i IN ("%CFG_FILE%") DO (
25-
set DO_NOT_REUSE_ME=%%i
26-
rem ZOMG (Part #2) WE use !! here to delay the expansion of
27-
rem CFG_OPTS, otherwise it remains "" for this loop.
28-
set CFG_OPTS=!CFG_OPTS! !DO_NOT_REUSE_ME!
29-
)
30-
)
31+
call :parse_config "%CFG_FILE%" CFG_OPTS
3132

3233
rem We use the value of the JAVACMD environment variable if defined
3334
set _JAVACMD=%JAVACMD%
@@ -79,55 +80,14 @@ rem "-J" is stripped, "-D" is left as is, and everything is appended to JAVA_OPT
7980
set _JAVA_PARAMS=
8081
set _APP_ARGS=
8182

82-
:param_loop
83-
call set _PARAM1=%%1
84-
set "_TEST_PARAM=%~1"
85-
86-
if ["!_PARAM1!"]==[""] goto param_afterloop
87-
83+
@@APP_DEFINES@@
8884

89-
rem ignore arguments that do not start with '-'
90-
if "%_TEST_PARAM:~0,1%"=="-" goto param_java_check
91-
set _APP_ARGS=!_APP_ARGS! !_PARAM1!
92-
shift
93-
goto param_loop
85+
rem if configuration files exist, prepend their contents to the script arguments so it can be processed by this runner
86+
call :parse_config "%SCRIPT_CONF_FILE%" SCRIPT_CONF_ARGS
9487

95-
:param_java_check
96-
if "!_TEST_PARAM:~0,2!"=="-J" (
97-
rem strip -J prefix
98-
set _JAVA_PARAMS=!_JAVA_PARAMS! !_TEST_PARAM:~2!
99-
shift
100-
goto param_loop
101-
)
102-
103-
if "!_TEST_PARAM:~0,2!"=="-D" (
104-
rem test if this was double-quoted property "-Dprop=42"
105-
for /F "delims== tokens=1,*" %%G in ("!_TEST_PARAM!") DO (
106-
if not ["%%H"] == [""] (
107-
set _JAVA_PARAMS=!_JAVA_PARAMS! !_PARAM1!
108-
) else if [%2] neq [] (
109-
rem it was a normal property: -Dprop=42 or -Drop="42"
110-
call set _PARAM1=%%1=%%2
111-
set _JAVA_PARAMS=!_JAVA_PARAMS! !_PARAM1!
112-
shift
113-
)
114-
)
115-
) else (
116-
if "!_TEST_PARAM!"=="-main" (
117-
call set CUSTOM_MAIN_CLASS=%%2
118-
shift
119-
) else (
120-
set _APP_ARGS=!_APP_ARGS! !_PARAM1!
121-
)
122-
)
123-
shift
124-
goto param_loop
125-
:param_afterloop
88+
call :process_args %SCRIPT_CONF_ARGS% %%*
12689

12790
set _JAVA_OPTS=!_JAVA_OPTS! !_JAVA_PARAMS!
128-
:run
129-
130-
@@APP_DEFINES@@
13191

13292
if defined CUSTOM_MAIN_CLASS (
13393
set MAIN_CLASS=!CUSTOM_MAIN_CLASS!
@@ -140,7 +100,79 @@ rem Call the application and pass all arguments unchanged.
140100

141101
@endlocal
142102

103+
exit /B %ERRORLEVEL%
143104

144-
:end
145105

146-
exit /B %ERRORLEVEL%
106+
rem Loads a configuration file full of default command line options for this script.
107+
rem First argument is the path to the config file.
108+
rem Second argument is the name of the environment variable to write to.
109+
:parse_config
110+
set _PARSE_FILE=%~1
111+
set _PARSE_OUT=
112+
if exist "%_PARSE_FILE%" (
113+
FOR /F "tokens=* eol=# usebackq delims=" %%i IN ("%_PARSE_FILE%") DO (
114+
set _PARSE_OUT=!_PARSE_OUT! %%i
115+
)
116+
)
117+
set %2=!_PARSE_OUT!
118+
exit /B 0
119+
120+
121+
:add_java
122+
set _JAVA_PARAMS=!_JAVA_PARAMS! %*
123+
exit /B 0
124+
125+
126+
:add_app
127+
set _APP_ARGS=!_APP_ARGS! %*
128+
exit /B 0
129+
130+
131+
rem Processes incoming arguments and places them in appropriate global variables
132+
:process_args
133+
:param_loop
134+
call set _PARAM1=%%1
135+
set "_TEST_PARAM=%~1"
136+
137+
if ["!_PARAM1!"]==[""] goto param_afterloop
138+
139+
140+
rem ignore arguments that do not start with '-'
141+
if "%_TEST_PARAM:~0,1%"=="-" goto param_java_check
142+
set _APP_ARGS=!_APP_ARGS! !_PARAM1!
143+
shift
144+
goto param_loop
145+
146+
:param_java_check
147+
if "!_TEST_PARAM:~0,2!"=="-J" (
148+
rem strip -J prefix
149+
set _JAVA_PARAMS=!_JAVA_PARAMS! !_TEST_PARAM:~2!
150+
shift
151+
goto param_loop
152+
)
153+
154+
if "!_TEST_PARAM:~0,2!"=="-D" (
155+
rem test if this was double-quoted property "-Dprop=42"
156+
for /F "delims== tokens=1,*" %%G in ("!_TEST_PARAM!") DO (
157+
if not ["%%H"] == [""] (
158+
set _JAVA_PARAMS=!_JAVA_PARAMS! !_PARAM1!
159+
) else if [%2] neq [] (
160+
rem it was a normal property: -Dprop=42 or -Drop="42"
161+
call set _PARAM1=%%1=%%2
162+
set _JAVA_PARAMS=!_JAVA_PARAMS! !_PARAM1!
163+
shift
164+
)
165+
)
166+
) else (
167+
if "!_TEST_PARAM!"=="-main" (
168+
call set CUSTOM_MAIN_CLASS=%%2
169+
shift
170+
) else (
171+
set _APP_ARGS=!_APP_ARGS! !_PARAM1!
172+
)
173+
)
174+
shift
175+
goto param_loop
176+
:param_afterloop
177+
178+
exit /B 0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.typesafe.sbt.packager.archetypes.scripts
2+
3+
import java.io.File
4+
5+
import sbt._
6+
7+
trait ApplicationIniGenerator {
8+
/**
9+
* @return the existing mappings plus a generated application.ini
10+
* if custom javaOptions are specified
11+
*/
12+
def generateApplicationIni(universalMappings: Seq[(File, String)],
13+
javaOptions: Seq[String],
14+
bashScriptConfigLocation: Option[String],
15+
tmpDir: File,
16+
log: Logger): Seq[(File, String)] =
17+
bashScriptConfigLocation
18+
.collect {
19+
case location if javaOptions.nonEmpty =>
20+
val configFile = tmpDir / "tmp" / "conf" / "application.ini"
21+
val pathMapping = cleanApplicationIniPath(location)
22+
//Do not use writeLines here because of issue #637
23+
IO.write(configFile, ("# options from build" +: javaOptions).mkString("\n"))
24+
val hasConflict = universalMappings.exists {
25+
case (file, path) => file != configFile && path == pathMapping
26+
}
27+
// Warn the user if he tries to specify options
28+
if (hasConflict) {
29+
log.warn("--------!!! JVM Options are defined twice !!!-----------")
30+
log.warn(
31+
"application.ini is already present in output package. Will be overridden by 'javaOptions in Universal'"
32+
)
33+
}
34+
(configFile -> pathMapping) +: universalMappings
35+
36+
}
37+
.getOrElse(universalMappings)
38+
39+
/**
40+
* @param path that could be relative to app_home
41+
* @return path relative to app_home
42+
*/
43+
protected def cleanApplicationIniPath(path: String): String
44+
}

src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BashStartScriptPlugin.scala

+2-29
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import sbt._
1616
* [[com.typesafe.sbt.packager.archetypes.JavaAppPackaging]].
1717
*
1818
*/
19-
object BashStartScriptPlugin extends AutoPlugin {
19+
object BashStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator {
2020

2121
/**
2222
* Name of the bash template if user wants to provide custom one
@@ -85,32 +85,6 @@ object BashStartScriptPlugin extends AutoPlugin {
8585
Seq("template_declares" -> defineString)
8686
}
8787

88-
private[this] def generateApplicationIni(universalMappings: Seq[(File, String)],
89-
javaOptions: Seq[String],
90-
bashScriptConfigLocation: Option[String],
91-
tmpDir: File,
92-
log: Logger): Seq[(File, String)] =
93-
bashScriptConfigLocation
94-
.collect {
95-
case location if javaOptions.nonEmpty =>
96-
val configFile = tmpDir / "tmp" / "conf" / "application.ini"
97-
//Do not use writeLines here because of issue #637
98-
IO.write(configFile, ("# options from build" +: javaOptions).mkString("\n"))
99-
val filteredMappings = universalMappings.filter {
100-
case (file, path) => path != appIniLocation
101-
}
102-
// Warn the user if he tries to specify options
103-
if (filteredMappings.size < universalMappings.size) {
104-
log.warn("--------!!! JVM Options are defined twice !!!-----------")
105-
log.warn(
106-
"application.ini is already present in output package. Will be overridden by 'javaOptions in Universal'"
107-
)
108-
}
109-
(configFile -> cleanApplicationIniPath(location)) +: filteredMappings
110-
111-
}
112-
.getOrElse(universalMappings)
113-
11488
private[this] def generateStartScripts(config: BashScriptConfig,
11589
mainClass: Option[String],
11690
discoveredMainClasses: Seq[String],
@@ -153,8 +127,7 @@ object BashStartScriptPlugin extends AutoPlugin {
153127
* @param path that could be relative to app_home
154128
* @return path relative to app_home
155129
*/
156-
private[this] def cleanApplicationIniPath(path: String): String =
157-
path.replaceFirst("\\$\\{app_home\\}/../", "")
130+
protected def cleanApplicationIniPath(path: String): String = path.stripPrefix("${app_home}/../")
158131

159132
/**
160133
* Bash defines

src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BatStartScriptKeys.scala

+4
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@ trait BatStartScriptKeys {
2525
"A list of extra definitions that should be written to the bat file template."
2626
)
2727

28+
val batScriptConfigLocation = TaskKey[Option[String]](
29+
"batScriptConfigLocation",
30+
"The location where the bat script will load default argument configuration from."
31+
)
2832
}

src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BatStartScriptPlugin.scala

+27-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import sbt._
1717
* [[com.typesafe.sbt.packager.archetypes.JavaAppPackaging]].
1818
*
1919
*/
20-
object BatStartScriptPlugin extends AutoPlugin {
20+
object BatStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator {
2121

2222
/**
2323
* Name of the bat template if user wants to provide custom one
@@ -34,6 +34,11 @@ object BatStartScriptPlugin extends AutoPlugin {
3434
*/
3535
val scriptTargetFolder = "bin"
3636

37+
/**
38+
* Location for the application.ini file used by the bat script to load initialization parameters for jvm and app
39+
*/
40+
val appIniLocation = "%APP_HOME%\\conf\\application.ini"
41+
3742
override val requires = JavaAppPackaging
3843
override val trigger = AllRequirements
3944

@@ -42,19 +47,29 @@ object BatStartScriptPlugin extends AutoPlugin {
4247

4348
private[this] case class BatScriptConfig(executableScriptName: String,
4449
scriptClasspath: Seq[String],
50+
configLocation: Option[String],
4551
extraDefines: Seq[String],
4652
replacements: Seq[(String, String)],
4753
batScriptTemplateLocation: File)
4854

4955
override def projectSettings: Seq[Setting[_]] = Seq(
5056
batScriptTemplateLocation := (sourceDirectory.value / "templates" / batTemplate),
57+
batScriptConfigLocation := (batScriptConfigLocation ?? Some(appIniLocation)).value,
5158
batScriptExtraDefines := Nil,
5259
batScriptReplacements := Replacements(executableScriptName.value),
5360
// Generating the application configuration
61+
mappings in Universal := generateApplicationIni(
62+
(mappings in Universal).value,
63+
(javaOptions in Universal).value,
64+
batScriptConfigLocation.value,
65+
(target in Universal).value,
66+
streams.value.log
67+
),
5468
makeBatScripts := generateStartScripts(
5569
BatScriptConfig(
5670
executableScriptName = s"${executableScriptName.value}.bat",
5771
scriptClasspath = (scriptClasspath in batScriptReplacements).value,
72+
configLocation = batScriptConfigLocation.value,
5873
extraDefines = batScriptExtraDefines.value,
5974
replacements = batScriptReplacements.value,
6075
batScriptTemplateLocation = batScriptTemplateLocation.value
@@ -104,6 +119,13 @@ object BatStartScriptPlugin extends AutoPlugin {
104119
clazz(0).toLower + lowerCased + ".bat"
105120
}
106121

122+
/**
123+
* @param path that could be relative to APP_HOME
124+
* @return path relative to APP_HOME
125+
*/
126+
protected def cleanApplicationIniPath(path: String): String =
127+
path.stripPrefix("%APP_HOME%\\").stripPrefix("/").replace('\\', '/')
128+
107129
/**
108130
* Bat script replacements
109131
*/
@@ -113,7 +135,9 @@ object BatStartScriptPlugin extends AutoPlugin {
113135
Seq("APP_NAME" -> name, "APP_ENV_NAME" -> NameHelper.makeEnvFriendlyName(name))
114136

115137
def appDefines(mainClass: String, config: BatScriptConfig, replacements: Seq[(String, String)]): (String, String) = {
116-
val defines = Seq(makeWindowsRelativeClasspathDefine(config.scriptClasspath), Defines.mainClass(mainClass)) ++ config.extraDefines
138+
val defines = Seq(makeWindowsRelativeClasspathDefine(config.scriptClasspath), Defines.mainClass(mainClass)) ++
139+
config.configLocation.map(Defines.configFileDefine) ++
140+
config.extraDefines
117141
"APP_DEFINES" -> Defines(defines, replacements)
118142
}
119143

@@ -140,6 +164,7 @@ object BatStartScriptPlugin extends AutoPlugin {
140164
defines.map(replace(_, replacements)).mkString("\r\n")
141165

142166
def mainClass(mainClass: String): String = s"""set "APP_MAIN_CLASS=$mainClass""""
167+
def configFileDefine(configFile: String): String = s"""set "SCRIPT_CONF_FILE=$configFile""""
143168

144169
// TODO - use more of the template writer for this...
145170
private[this] def replace(line: String, replacements: Seq[(String, String)]): String =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import com.typesafe.sbt.packager.Compat._
2+
3+
enablePlugins(JavaAppPackaging)
4+
5+
name := "simple-app"
6+
7+
version := "0.1.0"
8+
9+
batScriptExtraDefines += """call :add_java "-Dconfig.file=%APP_HOME%\conf\production.conf""""
10+
11+
TaskKey[Unit]("runCheck") := {
12+
val cwd = (stagingDirectory in Universal).value
13+
val cmd = Seq((cwd / "bin" / s"${packageName.value}.bat").getAbsolutePath)
14+
val configFile = (sys.process.Process(cmd, cwd).!!).replaceAll("\r\n", "")
15+
assert(configFile.contains("""stage\bin\\\..\conf\production.conf"""), "Output didn't contain config file path: " + configFile)
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
object MainApp extends App {
2+
val config = sys.props("config.file") ensuring (_ ne null, "didn't pick up -Dconfig.file argument")
3+
print(config)
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Run the staging and check the script.
2+
> stage
3+
> runCheck

0 commit comments

Comments
 (0)