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 @@
+