From a3a9263e104a506e7f8a45630d767ed12fe7fef1 Mon Sep 17 00:00:00 2001 From: James Earl Douglas Date: Sat, 28 Sep 2024 18:13:27 -0700 Subject: [PATCH] Misc cleanup for usability * Copy edit comments * Add non-compiled classpath resources to the scripted tests * Move `War / forkOptions` out of config to `warForkOptions` * Add some missing type annotations * Re-scope some settings helpers --- .../com/earldouglas/sbt/war/SbtWar.scala | 5 + .../sbt/war/WarPackagePlugin.scala | 3 +- .../sbt/war/WarPackageRunnerPlugin.scala | 111 +++++++++--------- .../sbt/war/WebappComponentsPlugin.scala | 53 ++++++--- .../war/WebappComponentsRunnerPlugin.scala | 81 +++++++------ .../sbt/war/WebappRunnerPlugin.scala | 3 + .../plugins/sbt-war/project/build.properties | 1 + src/sbt-test/plugins/war-package/sbt/test.sbt | 7 +- .../plugins/webapp-components/sbt/test.sbt | 7 +- src/template/build.sbt | 3 + src/template/src/main/resources/logback.xml | 10 ++ .../src/main/scala/04-runners/servlet.scala | 4 +- 12 files changed, 173 insertions(+), 115 deletions(-) create mode 100644 src/sbt-test/plugins/sbt-war/project/build.properties create mode 100644 src/template/src/main/resources/logback.xml diff --git a/src/main/scala/com/earldouglas/sbt/war/SbtWar.scala b/src/main/scala/com/earldouglas/sbt/war/SbtWar.scala index 021745f3..8bd73c39 100644 --- a/src/main/scala/com/earldouglas/sbt/war/SbtWar.scala +++ b/src/main/scala/com/earldouglas/sbt/war/SbtWar.scala @@ -3,6 +3,11 @@ package com.earldouglas.sbt.war import sbt.AutoPlugin import sbt.Plugins +/** The top-level plugin to be used by default. From the required + * plugins, this brings in all of the webapp components mappings, .war + * file packaging, and mechanisms for running both raw webapp + * components and a packaged .war file. + */ object SbtWar extends AutoPlugin { override val requires: Plugins = diff --git a/src/main/scala/com/earldouglas/sbt/war/WarPackagePlugin.scala b/src/main/scala/com/earldouglas/sbt/war/WarPackagePlugin.scala index bfbbf67a..b8d0e4f9 100644 --- a/src/main/scala/com/earldouglas/sbt/war/WarPackagePlugin.scala +++ b/src/main/scala/com/earldouglas/sbt/war/WarPackagePlugin.scala @@ -20,9 +20,10 @@ object WarPackagePlugin extends AutoPlugin { override lazy val projectSettings: Seq[Setting[_]] = { + // Flip webappContents around from (dst -> src) to (src -> dst) val packageContents: Initialize[Task[Seq[(java.io.File, String)]]] = WebappComponentsPlugin.webappContents - .map(_.toSeq.map({ case (k, v) => (v, k) })) + .map(_.map(_.swap).toSeq) val packageTaskSettings: Seq[Setting[_]] = Defaults.packageTaskSettings(pkg, packageContents) diff --git a/src/main/scala/com/earldouglas/sbt/war/WarPackageRunnerPlugin.scala b/src/main/scala/com/earldouglas/sbt/war/WarPackageRunnerPlugin.scala index 3307fdcd..40c39fa5 100644 --- a/src/main/scala/com/earldouglas/sbt/war/WarPackageRunnerPlugin.scala +++ b/src/main/scala/com/earldouglas/sbt/war/WarPackageRunnerPlugin.scala @@ -10,7 +10,7 @@ import java.util.concurrent.atomic.AtomicReference import scala.sys.process.{Process => ScalaProcess} /** Launches the .war file managed by WarPackagePlugin. Uses a forked - * JVM to run Tomcat via webapp-runner. + * JVM to run Tomcat via com.heroku:webapp-runner. */ object WarPackageRunnerPlugin extends AutoPlugin { @@ -20,6 +20,8 @@ object WarPackageRunnerPlugin extends AutoPlugin { lazy val warStart = taskKey[Unit]("start war container") lazy val warJoin = taskKey[Unit]("join war container") lazy val warStop = taskKey[Unit]("stop war container") + lazy val warForkOptions = + settingKey[ForkOptions]("war container fork options") } import autoImport._ @@ -28,76 +30,79 @@ object WarPackageRunnerPlugin extends AutoPlugin { override val requires: Plugins = WarPackagePlugin && WebappRunnerPlugin - override val projectConfigurations: Seq[Configuration] = Seq(War) + override val projectConfigurations: Seq[Configuration] = + Seq(War) private lazy val containerInstance = new AtomicReference[Option[ScalaProcess]](None) - private val startWar: Initialize[Task[Unit]] = - Def.task { - stopContainerInstance() - - val runners: Seq[File] = - Classpaths - .managedJars(War, classpathTypes.value, update.value) - .map(_.data) - .toList - - runners match { - case runner :: Nil => - streams.value.log.info("[sbt-war] Starting server") - val process: ScalaProcess = - Fork.java.fork( - (War / forkOptions).value, - Seq( - "-jar", - runner.file.getPath(), - "--port", - warPort.value.toString(), - pkg.value.getPath() + override val projectSettings: Seq[Setting[_]] = { + + def stopContainerInstance(): Unit = { + val oldProcess = containerInstance.getAndSet(None) + oldProcess.foreach(_.destroy()) + } + + val startWar: Initialize[Task[Unit]] = + Def.task { + stopContainerInstance() + + val runners: Seq[File] = + Classpaths + .managedJars(War, classpathTypes.value, update.value) + .map(_.data) + .toList + + runners match { + case runner :: Nil => + streams.value.log.info("[sbt-war] Starting server") + val process: ScalaProcess = + Fork.java.fork( + warForkOptions.value, + Seq( + "-jar", + runner.file.getPath(), + "--port", + warPort.value.toString(), + pkg.value.getPath() + ) ) + containerInstance.set(Some(process)) + case _ :: _ => + streams.value.log.error( + s"""[sbt-war] Expected one runner, but found ${runners.length}: ${runners + .mkString("\n * ", " * ", "")}""" ) - containerInstance.set(Some(process)) - case _ :: _ => - streams.value.log.error( - s"""[sbt-war] Expected one runner, but found ${runners.length}: ${runners - .mkString("\n * ", " * ", "")}""" - ) - case _ => - streams.value.log.error( - """[sbt-war] Expected one runner, but found none""" - ) + case _ => + streams.value.log.error( + """[sbt-war] Expected a runner, but found none""" + ) + } } - } - private val joinWar: Initialize[Task[Unit]] = - Def.task(containerInstance.get.map(_.exitValue)) + val joinWar: Initialize[Task[Unit]] = + Def.task(containerInstance.get.map(_.exitValue)) - private def stopContainerInstance(): Unit = { - val oldProcess = containerInstance.getAndSet(None) - oldProcess.foreach(_.destroy()) - } - - private val stopWar: Initialize[Task[Unit]] = - Def.task(stopContainerInstance()) + val stopWar: Initialize[Task[Unit]] = + Def.task(stopContainerInstance()) - private val onLoadSetting: Initialize[State => State] = - Def.setting { - (Global / onLoad).value - .compose { state: State => - state.addExitHook(stopContainerInstance()) - } - } + val onLoadSetting: Initialize[State => State] = + Def.setting { + (Global / onLoad).value + .compose { state: State => + state.addExitHook(stopContainerInstance()) + } + } - override lazy val projectSettings = Seq( warPort := 8080, warStart := startWar.value, warJoin := joinWar.value, warStop := stopWar.value, - War / forkOptions := ForkOptions(), + warForkOptions := ForkOptions(), Global / onLoad := onLoadSetting.value, libraryDependencies += ("com.heroku" % "webapp-runner" % webappRunnerVersion.value intransitive ()) % War ) + } } diff --git a/src/main/scala/com/earldouglas/sbt/war/WebappComponentsPlugin.scala b/src/main/scala/com/earldouglas/sbt/war/WebappComponentsPlugin.scala index 8f564f81..17e07119 100644 --- a/src/main/scala/com/earldouglas/sbt/war/WebappComponentsPlugin.scala +++ b/src/main/scala/com/earldouglas/sbt/war/WebappComponentsPlugin.scala @@ -8,18 +8,33 @@ import sbt._ /** Identifies the files that compose the webapp (resources, .class * files, and .jar files). This is used by user-facing plugins * (WarPlugin and WebappRunnerPlugin). + * + * Webapp components are managed as three sets of mappings: + * + * - webappResources: All the static HTML, CSS, JS, images, etc. + * files to be served by the application. Also, optionally, the + * WEB-INF/web.xml deployment descriptor. + * - webappClasses: All of the classes, etc. on the classpath to be + * copied into the WEB-INF/classes directory. + * - webappLib: All of the .jar files to be copied into the + * WEB-INF/lib directory. + * + * These mappings each have the type Map[String, File], where the key + * is the relative path within the .war file (e.g. + * WEB-INF/classes/Foo.class), and the value is the location of the + * file to be copied there (e.g. target/classes/Foo.class). */ object WebappComponentsPlugin extends AutoPlugin { object autoImport { - lazy val webappResources = + lazy val webappResources: TaskKey[Map[String, File]] = taskKey[Map[String, File]]("webapp resources") - lazy val webappClasses = + lazy val webappClasses: TaskKey[Map[String, File]] = taskKey[Map[String, File]]("webapp classes") - lazy val webappLib = + lazy val webappLib: TaskKey[Map[String, File]] = taskKey[Map[String, File]]("webapp lib") } @@ -27,22 +42,29 @@ object WebappComponentsPlugin extends AutoPlugin { override def requires = plugins.JvmPlugin - override def projectSettings: Seq[Setting[_]] = { + lazy val webappContents: Initialize[Task[Map[String, File]]] = + Def.task { + webappResources.value ++ + webappClasses.value ++ + webappLib.value + } - val webappResourcesDir: Initialize[File] = - Def.setting((Compile / sourceDirectory).value / "webapp") + override val projectSettings: Seq[Setting[_]] = { val webappResourcesTask: Initialize[Task[Map[String, File]]] = - Def.task(WebappComponents.getResources(webappResourcesDir.value)) - - val classpathFiles: Initialize[Task[Seq[File]]] = - Def.task((Runtime / fullClasspath).value.files) + (Compile / sourceDirectory) + .map(_ / "webapp") + .map(WebappComponents.getResources) val webappClassesTask: Initialize[Task[Map[String, File]]] = - Def.task(WebappComponents.getClasses(classpathFiles.value)) + (Runtime / fullClasspath) + .map(_.files) + .map(WebappComponents.getClasses) val webappLibTask: Initialize[Task[Map[String, File]]] = - Def.task(WebappComponents.getLib(classpathFiles.value)) + (Runtime / fullClasspath) + .map(_.files) + .map(WebappComponents.getLib) Seq( webappResources := webappResourcesTask.value, @@ -50,11 +72,4 @@ object WebappComponentsPlugin extends AutoPlugin { webappLib := webappLibTask.value ) } - - lazy val webappContents: Initialize[Task[Map[String, File]]] = - Def.task { - webappResources.value ++ - webappClasses.value ++ - webappLib.value - } } diff --git a/src/main/scala/com/earldouglas/sbt/war/WebappComponentsRunnerPlugin.scala b/src/main/scala/com/earldouglas/sbt/war/WebappComponentsRunnerPlugin.scala index 0a36b77c..050868fb 100644 --- a/src/main/scala/com/earldouglas/sbt/war/WebappComponentsRunnerPlugin.scala +++ b/src/main/scala/com/earldouglas/sbt/war/WebappComponentsRunnerPlugin.scala @@ -21,51 +21,53 @@ object WebappComponentsRunnerPlugin extends AutoPlugin { import autoImport._ - override val requires: Plugins = WebappComponentsPlugin + override val requires: Plugins = + WebappComponentsPlugin private lazy val containerInstance = new AtomicReference[Option[WebappComponentsRunner]](None) - private val startWebapp: Initialize[Task[Unit]] = - Def.task { - stopContainerInstance() - - val emptyDir: File = - WebappComponentsRunner.mkdir( - (Compile / target).value / "empty" - ) - - val runner: WebappComponentsRunner = - WebappComponentsRunner( - hostname = "localhost", // TODO this could be a settingKey - port = webappPort.value, - contextPath = "", // TODO this could be a settingKey - emptyWebappDir = emptyDir, - emptyClassesDir = emptyDir, - resourceMap = WebappComponentsPlugin.webappContents.value - ) - runner.start() - - containerInstance.set(Some(runner)) - } - - private val joinWebapp: Initialize[Task[Unit]] = - Def.task(containerInstance.get.foreach(_.join())) - - private def stopContainerInstance(): Unit = - containerInstance.getAndSet(None).foreach(_.stop()) - - private val stopWebapp: Initialize[Task[Unit]] = - Def.task(stopContainerInstance()) - - private val onLoadSetting: Initialize[State => State] = - Def.setting { - (Global / onLoad).value compose { state: State => - state.addExitHook(stopContainerInstance()) + override val projectSettings: Seq[Setting[_]] = { + + def stopContainerInstance(): Unit = + containerInstance.getAndSet(None).foreach(_.stop()) + + val startWebapp: Initialize[Task[Unit]] = + Def.task { + stopContainerInstance() + + val emptyDir: File = + WebappComponentsRunner.mkdir( + (Compile / target).value / "empty" + ) + + val runner: WebappComponentsRunner = + WebappComponentsRunner( + hostname = "localhost", // TODO this could be a settingKey + port = webappPort.value, + contextPath = "", // TODO this could be a settingKey + emptyWebappDir = emptyDir, + emptyClassesDir = emptyDir, + resourceMap = WebappComponentsPlugin.webappContents.value + ) + runner.start() + + containerInstance.set(Some(runner)) + } + + val joinWebapp: Initialize[Task[Unit]] = + Def.task(containerInstance.get.foreach(_.join())) + + val stopWebapp: Initialize[Task[Unit]] = + Def.task(stopContainerInstance()) + + val onLoadSetting: Initialize[State => State] = + Def.setting { + (Global / onLoad).value compose { state: State => + state.addExitHook(stopContainerInstance()) + } } - } - override lazy val projectSettings: Seq[Setting[_]] = Seq( webappPort := 8080, webappStart := startWebapp.value, @@ -73,4 +75,5 @@ object WebappComponentsRunnerPlugin extends AutoPlugin { webappStop := stopWebapp.value, Global / onLoad := onLoadSetting.value ) + } } diff --git a/src/main/scala/com/earldouglas/sbt/war/WebappRunnerPlugin.scala b/src/main/scala/com/earldouglas/sbt/war/WebappRunnerPlugin.scala index f4c180c8..4568d89d 100644 --- a/src/main/scala/com/earldouglas/sbt/war/WebappRunnerPlugin.scala +++ b/src/main/scala/com/earldouglas/sbt/war/WebappRunnerPlugin.scala @@ -2,6 +2,9 @@ package com.earldouglas.sbt.war import sbt.Def.settingKey import sbt._ +/** Launches a webapp composed of in-place resources, classes, and + * libraries. + */ object WebappRunnerPlugin extends AutoPlugin { object autoImport { diff --git a/src/sbt-test/plugins/sbt-war/project/build.properties b/src/sbt-test/plugins/sbt-war/project/build.properties new file mode 100644 index 00000000..ee4c672c --- /dev/null +++ b/src/sbt-test/plugins/sbt-war/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.1 diff --git a/src/sbt-test/plugins/war-package/sbt/test.sbt b/src/sbt-test/plugins/war-package/sbt/test.sbt index eb5db8e8..09d91737 100644 --- a/src/sbt-test/plugins/war-package/sbt/test.sbt +++ b/src/sbt-test/plugins/war-package/sbt/test.sbt @@ -67,6 +67,7 @@ lazy val checkWar: Def.Initialize[Task[Unit]] = "WEB-INF/classes/drivers/mem/mem$package$.class", "WEB-INF/classes/drivers/mem/mem$package.class", "WEB-INF/classes/drivers/mem/mem$package.tasty", + "WEB-INF/classes/logback.xml", "WEB-INF/classes/runners/", "WEB-INF/classes/runners/CountServlet.class", "WEB-INF/classes/runners/CountServlet.tasty", @@ -86,13 +87,17 @@ lazy val checkWar: Def.Initialize[Task[Unit]] = "WEB-INF/lib/cats-effect_3-3.5.4.jar", "WEB-INF/lib/cats-kernel_3-2.9.0.jar", "WEB-INF/lib/h2-2.2.224.jar", + "WEB-INF/lib/logback-classic-1.5.8.jar", + "WEB-INF/lib/logback-core-1.5.8.jar", "WEB-INF/lib/scala-library-2.13.14.jar", + "WEB-INF/lib/scala-logging_3-3.9.5.jar", "WEB-INF/lib/scala3-library_3-3.5.0.jar", + "WEB-INF/lib/slf4j-api-2.0.15.jar", "WEB-INF/web.xml", "favicon.ico", "index.html", "styles/", - "styles/theme.css" + "styles/theme.css", ) val warFile: File = pkg.value diff --git a/src/sbt-test/plugins/webapp-components/sbt/test.sbt b/src/sbt-test/plugins/webapp-components/sbt/test.sbt index 940edead..13f70ac1 100644 --- a/src/sbt-test/plugins/webapp-components/sbt/test.sbt +++ b/src/sbt-test/plugins/webapp-components/sbt/test.sbt @@ -54,6 +54,7 @@ val checkClasses: Def.Initialize[Task[Unit]] = "drivers/mem/mem$package$.class", "drivers/mem/mem$package.class", "drivers/mem/mem$package.tasty", + "logback.xml", "runners/CountServlet.class", "runners/CountServlet.tasty", "runners/HelloServlet.class", @@ -116,8 +117,12 @@ val checkLib: Def.Initialize[Task[Unit]] = "cats-effect_3-3.5.4.jar", "cats-kernel_3-2.9.0.jar", "h2-2.2.224.jar", + "logback-classic-1.5.8.jar", + "logback-core-1.5.8.jar", "scala-library-2.13.14.jar", - "scala3-library_3-3.5.0.jar" + "scala-logging_3-3.9.5.jar", + "scala3-library_3-3.5.0.jar", + "slf4j-api-2.0.15.jar", ) assertContains( diff --git a/src/template/build.sbt b/src/template/build.sbt index fd7976fa..54b6c084 100644 --- a/src/template/build.sbt +++ b/src/template/build.sbt @@ -4,6 +4,9 @@ libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % "test" libraryDependencies += "com.h2database" % "h2" % "2.2.224" libraryDependencies += "jakarta.servlet" % "jakarta.servlet-api" % "6.0.0" % "provided" +libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5" +libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.5.8" + scalaVersion := "3.5.0" semanticdbEnabled := true semanticdbVersion := scalafixSemanticdb.revision diff --git a/src/template/src/main/resources/logback.xml b/src/template/src/main/resources/logback.xml new file mode 100644 index 00000000..3b19e206 --- /dev/null +++ b/src/template/src/main/resources/logback.xml @@ -0,0 +1,10 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n + + + + + + diff --git a/src/template/src/main/scala/04-runners/servlet.scala b/src/template/src/main/scala/04-runners/servlet.scala index 29bafe85..4a388dce 100644 --- a/src/template/src/main/scala/04-runners/servlet.scala +++ b/src/template/src/main/scala/04-runners/servlet.scala @@ -1,5 +1,6 @@ package runners +import com.typesafe.scalalogging.LazyLogging import jakarta.servlet.annotation.WebServlet import jakarta.servlet.http.HttpServlet import jakarta.servlet.http.HttpServletRequest @@ -31,12 +32,13 @@ class CountServlet extends HttpServlet with Main: res.getWriter().write(unsafeIncrementAndGetAsJson()) @WebServlet(urlPatterns = Array("/hello")) -class HelloServlet extends HttpServlet: +class HelloServlet extends HttpServlet with LazyLogging: override def doGet( request: HttpServletRequest, response: HttpServletResponse ): Unit = + logger.info("doGet") response.setCharacterEncoding("UTF-8") response.setContentType("text/html") response.getWriter.write("""

Hello, world!

""")