From cb298935ed331638ed84911f44346984eccef5bb Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Thu, 11 Apr 2024 18:44:42 +0200 Subject: [PATCH] fix support for top-level `main` methods in Scala 3 (#1592) * correctly escape app_mainclass variable * add scripted test * add ash test * use sbt 1.9.9 for top-level-main test * actually run command-line-settings test * don't `eval` everything but only `$residual_args` in ash-template * correct escaping of scary shell meta characters in ash-template * check more scary metacharacters, factor escaping out into a function * test more crazy edge cases * avoid some backslashes * more shell nasty! * improve `checkComplexResidual` test * fix an expansion that doesn't work in `ash` --- .../sbt/packager/archetypes/scripts/ash-template | 11 ++++++++--- .../scripts/BashStartScriptPlugin.scala | 8 +++++++- src/sbt-test/ash/command-line-settings/build.sbt | 9 +++++---- src/sbt-test/ash/command-line-settings/test | 1 + src/sbt-test/ash/top-level-main/build.sbt | 16 ++++++++++++++++ .../ash/top-level-main/project/build.properties | 1 + .../ash/top-level-main/project/plugins.sbt | 1 + .../top-level-main/src/main/scala/MainApp.scala | 2 ++ src/sbt-test/ash/top-level-main/test | 3 +++ src/sbt-test/bash/top-level-main/build.sbt | 16 ++++++++++++++++ .../bash/top-level-main/project/build.properties | 1 + .../bash/top-level-main/project/plugins.sbt | 1 + .../top-level-main/src/main/scala/MainApp.scala | 2 ++ src/sbt-test/bash/top-level-main/test | 3 +++ 14 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 src/sbt-test/ash/top-level-main/build.sbt create mode 100644 src/sbt-test/ash/top-level-main/project/build.properties create mode 100644 src/sbt-test/ash/top-level-main/project/plugins.sbt create mode 100644 src/sbt-test/ash/top-level-main/src/main/scala/MainApp.scala create mode 100644 src/sbt-test/ash/top-level-main/test create mode 100644 src/sbt-test/bash/top-level-main/build.sbt create mode 100644 src/sbt-test/bash/top-level-main/project/build.properties create mode 100644 src/sbt-test/bash/top-level-main/project/plugins.sbt create mode 100644 src/sbt-test/bash/top-level-main/src/main/scala/MainApp.scala create mode 100644 src/sbt-test/bash/top-level-main/test diff --git a/src/main/resources/com/typesafe/sbt/packager/archetypes/scripts/ash-template b/src/main/resources/com/typesafe/sbt/packager/archetypes/scripts/ash-template index 24d602305..bf8866ecc 100644 --- a/src/main/resources/com/typesafe/sbt/packager/archetypes/scripts/ash-template +++ b/src/main/resources/com/typesafe/sbt/packager/archetypes/scripts/ash-template @@ -43,8 +43,12 @@ addApp () { app_commands="$app_commands $1" } +shellEscape () { + printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/")" +} + addResidual () { - residual_args="$residual_args \"$1\"" + residual_args="$residual_args $(shellEscape "$1")" } # Allow user to specify java options. These get listed first per bash-template. @@ -90,7 +94,7 @@ process_args () { -java-home) require_arg path "$1" "$2" && jre=`eval echo $2` && java_cmd="$jre/bin/java" && shift 2 ;; -D*|-agentlib*|-agentpath*|-javaagent*|-XX*) addJava "$1" && shift ;; - -J*) addJava "${1:2}" && shift ;; + -J*) addJava "$(printf %s "$1" | sed s/^..//)" && shift ;; *) addResidual "$1" && shift ;; esac done @@ -129,4 +133,5 @@ java_cmd="$(get_java_cmd)" # If a configuration file exist, read the contents to $opts [ -f "$script_conf_file" ] && opts=$(loadConfigFile "$script_conf_file") -eval "exec $java_cmd $java_opts -classpath $app_classpath $opts $app_mainclass $app_commands $residual_args" +eval "set -- $residual_args" +exec $java_cmd $java_opts -classpath $app_classpath $opts $app_mainclass $app_commands "$@" diff --git a/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BashStartScriptPlugin.scala b/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BashStartScriptPlugin.scala index 799244062..b54c8eb29 100644 --- a/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BashStartScriptPlugin.scala +++ b/src/main/scala/com/typesafe/sbt/packager/archetypes/scripts/BashStartScriptPlugin.scala @@ -143,13 +143,19 @@ object BashStartScriptPlugin extends AutoPlugin with ApplicationIniGenerator wit else "" + private[this] def shellEscape(s: String): String = + if (s.startsWith("-jar ")) + s + else + s"'${s.replace("'", "'\\''")}'" + override protected[this] def createReplacementsForMainScript( mainClass: String, mainClasses: Seq[String], config: SpecializedScriptConfig ): Seq[(String, String)] = Seq( - "app_mainclass" -> mainClass, + "app_mainclass" -> shellEscape(mainClass), "available_main_classes" -> usageMainClassReplacement(mainClasses) ) ++ config.replacements } diff --git a/src/sbt-test/ash/command-line-settings/build.sbt b/src/sbt-test/ash/command-line-settings/build.sbt index e1b5caaa5..4ed99facd 100644 --- a/src/sbt-test/ash/command-line-settings/build.sbt +++ b/src/sbt-test/ash/command-line-settings/build.sbt @@ -23,10 +23,11 @@ TaskKey[Unit]("checkResidual") := { } TaskKey[Unit]("checkComplexResidual") := { - val args = """arg1 "arg 2" 'arg "3"'""" + val args = Seq("-J-Dfoo=bar", "arg1", "--", "-J-Dfoo=bar", "arg 2", "--", "\"", "$foo", "'", "%s", "-y", "bla", "\\'", "\\\"") val cwd = (stagingDirectory in Universal).value - val cmd = Seq((cwd / "bin" / packageName.value).getAbsolutePath, args) + val cmd = Seq((cwd / "bin" / packageName.value).getAbsolutePath) ++ args + val expected = """arg1|-J-Dfoo=bar|arg 2|--|"|$foo|'|%s|-y|bla|\'|\"""" - val output = (sys.process.Process(cmd, cwd).!!).replaceAll("\n", "") - assert(output.contains(args), s"Application did not receive residual args '$args'") + val output = (sys.process.Process(cmd, cwd).!!).split("\n").last + assert(output == expected, s"Application did not receive residual args '$expected' (got '$output')") } diff --git a/src/sbt-test/ash/command-line-settings/test b/src/sbt-test/ash/command-line-settings/test index 373b6fabb..9d8ebd734 100644 --- a/src/sbt-test/ash/command-line-settings/test +++ b/src/sbt-test/ash/command-line-settings/test @@ -3,3 +3,4 @@ $ exists target/universal/stage/bin/command-line-app > checkSystemProperty > checkResidual +> checkComplexResidual diff --git a/src/sbt-test/ash/top-level-main/build.sbt b/src/sbt-test/ash/top-level-main/build.sbt new file mode 100644 index 000000000..1d584b608 --- /dev/null +++ b/src/sbt-test/ash/top-level-main/build.sbt @@ -0,0 +1,16 @@ +import com.typesafe.sbt.packager.Compat._ + +enablePlugins(AshScriptPlugin) + +name := "top-level-main" + +version := "0.1.0" + +scalaVersion := "3.3.3" + +TaskKey[Unit]("runCheck") := { + val cwd = (stagingDirectory in Universal).value + val cmd = Seq((cwd / "bin" / packageName.value).getAbsolutePath) + val output = sys.process.Process(cmd, cwd).!! + assert(output contains "SUCCESS!", "Output didn't contain success: " + output) +} diff --git a/src/sbt-test/ash/top-level-main/project/build.properties b/src/sbt-test/ash/top-level-main/project/build.properties new file mode 100644 index 000000000..04267b14a --- /dev/null +++ b/src/sbt-test/ash/top-level-main/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.9 diff --git a/src/sbt-test/ash/top-level-main/project/plugins.sbt b/src/sbt-test/ash/top-level-main/project/plugins.sbt new file mode 100644 index 000000000..218f1a27d --- /dev/null +++ b/src/sbt-test/ash/top-level-main/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.github.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/ash/top-level-main/src/main/scala/MainApp.scala b/src/sbt-test/ash/top-level-main/src/main/scala/MainApp.scala new file mode 100644 index 000000000..a545e076a --- /dev/null +++ b/src/sbt-test/ash/top-level-main/src/main/scala/MainApp.scala @@ -0,0 +1,2 @@ +def main(args: Array[String]): Unit = + println("SUCCESS!") diff --git a/src/sbt-test/ash/top-level-main/test b/src/sbt-test/ash/top-level-main/test new file mode 100644 index 000000000..61dad9b64 --- /dev/null +++ b/src/sbt-test/ash/top-level-main/test @@ -0,0 +1,3 @@ +# Run the staging and check the script. +> stage +> runCheck \ No newline at end of file diff --git a/src/sbt-test/bash/top-level-main/build.sbt b/src/sbt-test/bash/top-level-main/build.sbt new file mode 100644 index 000000000..990031d4a --- /dev/null +++ b/src/sbt-test/bash/top-level-main/build.sbt @@ -0,0 +1,16 @@ +import com.typesafe.sbt.packager.Compat._ + +enablePlugins(JavaAppPackaging) + +name := "top-level-main" + +version := "0.1.0" + +scalaVersion := "3.3.3" + +TaskKey[Unit]("runCheck") := { + val cwd = (stagingDirectory in Universal).value + val cmd = Seq((cwd / "bin" / packageName.value).getAbsolutePath) + val output = sys.process.Process(cmd, cwd).!! + assert(output contains "SUCCESS!", "Output didn't contain success: " + output) +} diff --git a/src/sbt-test/bash/top-level-main/project/build.properties b/src/sbt-test/bash/top-level-main/project/build.properties new file mode 100644 index 000000000..04267b14a --- /dev/null +++ b/src/sbt-test/bash/top-level-main/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.9 diff --git a/src/sbt-test/bash/top-level-main/project/plugins.sbt b/src/sbt-test/bash/top-level-main/project/plugins.sbt new file mode 100644 index 000000000..218f1a27d --- /dev/null +++ b/src/sbt-test/bash/top-level-main/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.github.sbt" % "sbt-native-packager" % sys.props("project.version")) diff --git a/src/sbt-test/bash/top-level-main/src/main/scala/MainApp.scala b/src/sbt-test/bash/top-level-main/src/main/scala/MainApp.scala new file mode 100644 index 000000000..49549d796 --- /dev/null +++ b/src/sbt-test/bash/top-level-main/src/main/scala/MainApp.scala @@ -0,0 +1,2 @@ +def main(args: Array[String]) = + println("SUCCESS!") diff --git a/src/sbt-test/bash/top-level-main/test b/src/sbt-test/bash/top-level-main/test new file mode 100644 index 000000000..61dad9b64 --- /dev/null +++ b/src/sbt-test/bash/top-level-main/test @@ -0,0 +1,3 @@ +# Run the staging and check the script. +> stage +> runCheck \ No newline at end of file