Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cli/src/main/scala/mdoc/internal/cli/Settings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ case class Settings(
@Description("Start a file watcher and incrementally re-generate the site on file save.")
@ExtraName("w")
watch: Boolean = false,
@Description("Sets the file watcher to run in the background and not ask for user input in order to stop.")
@ExtraName("b")
background: Boolean = false,
@Description(
"Instead of generating a new site, report an error if generating the site would produce a diff " +
"against an existing site. Useful for asserting in CI that a site is up-to-date."
Expand Down
7 changes: 7 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ performance.
> docs/mdoc --watch
```

You can run mdoc in the background so that you can issue other commands to sbt.

```scala
> docs/mdocBgStart
> docs/mdocBgStop
```

See [`--help`](#help) to learn more how to use the command-line interface.

```scala
Expand Down
67 changes: 65 additions & 2 deletions mdoc-sbt/src/main/scala/mdoc/MdocPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package mdoc

import java.io.File
import sbt.Keys._
import sbt._
import sbt.{taskKey, _}

import scala.collection.mutable.ListBuffer

object MdocPlugin extends AutoPlugin {
Expand Down Expand Up @@ -47,6 +48,15 @@ object MdocPlugin extends AutoPlugin {
"If false, do not add mdoc as a library dependency this project. " +
"Default value is true."
)
val mdocBgStart =
inputKey[JobHandle](
"Run mdoc in the background. " +
"By default it runs with arguments --watch and --background (via `mdocBgStart/mdocExtraArguments`)."
)
val mdocBgStop =
taskKey[Unit](
"Stops mdoc that is running in the background."
)
}
val mdocInternalVariables =
settingKey[List[(String, String)]](
Expand All @@ -71,19 +81,23 @@ object MdocPlugin extends AutoPlugin {
"if not provided, the classpath will be formed by resolving the worker dependency"
)

// The macro is overzealous and prevents us using this.
private val showKey = Def.showFullKey.show _

override def projectSettings: Seq[Def.Setting[_]] =
List(
mdocIn := baseDirectory.in(ThisBuild).value / "docs",
mdocOut := target.in(Compile).value / "mdoc",
mdocVariables := Map.empty,
mdocExtraArguments := Nil,
mdocExtraArguments.in(mdocBgStart) := Vector("--watch", "--background"),
mdocJS := None,
mdocJSLibraries := Nil,
mdocJSWorkerClasspath := None,
mdocAutoDependency := true,
mdocInternalVariables := Nil,
mdoc := Def.inputTaskDyn {
validateSettings.value
val _ = validateSettings.value
val parsed = sbt.complete.DefaultParsers.spaceDelimited("<arg>").parsed
val args = Iterator(
mdocExtraArguments.value,
Expand All @@ -93,6 +107,55 @@ object MdocPlugin extends AutoPlugin {
runMain.in(Compile).toTask(s" mdoc.SbtMain $args")
}
}.evaluated,
// Workaround for https://github.com/sbt/sbt/issues/3572.
mdocBgStart := InputTask
.createDyn[Seq[String], JobHandle] {
InputTask.initParserAsInput(Def.setting {
sbt.complete.DefaultParsers.spaceDelimited("<arg>")
})
} {
Def.task { parsed =>
Def.taskDyn {
val _ = validateSettings.value
val args = Iterator(
mdocExtraArguments.in(mdocBgStart).value,
parsed
).flatten.mkString(" ")
val service = bgJobService.value
val spawningTask = resolvedScoped.value
val s = state.value
service.jobs.find(_.spawningTask == spawningTask) match {
case Some(jobHandle) =>
Def.task {
s.log.info(s"mdoc is already running in the background")
jobHandle
}
case None =>
// Use this rather than bgRunMain so that the spawningTask is set correctly.
Defaults.bgRunMainTask(
exportedProductJars.in(Compile),
fullClasspathAsJars.in(Compile),
bgCopyClasspath.in(Compile, bgRunMain),
runner.in(run)
).toTask(s" mdoc.SbtMain $args")
}
}
}
}.evaluated,
mdocBgStop := Def.task {
val service = bgJobService.value
val spawningTask = resolvedScoped.value.copy(key = mdocBgStart.key)
val s = state.value
s.log.debug(s"looking for background job that was spawned by ${showKey(spawningTask)}")
service.jobs.find(_.spawningTask == spawningTask) match {
case Some(jobHandle) =>
s.log.info(s"stopping mdoc")
service.stop(jobHandle)
service.waitFor(jobHandle)
case None =>
s.log.info(s"mdoc is not running in the background")
}
}.value,
dependencyOverrides ++= List(
"org.scala-lang" %% "scala3-library" % scalaVersion.value,
"org.scala-lang" %% "scala3-compiler" % scalaVersion.value,
Expand Down
14 changes: 14 additions & 0 deletions mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ThisBuild / scalaVersion := "2.12.17"

enablePlugins(MdocPlugin)

TaskKey[Unit]("check") := {
SbtTest.test(
TestCommand("mdocBgStart", "Waiting for file changes (press enter to interrupt)"),
TestCommand("show version", "[info] 0.1.0-SNAPSHOT"),
TestCommand("mdocBgStart", "mdoc is already running in the background"),
TestCommand("mdocBgStop", "stopping mdoc"),
TestCommand("mdocBgStop", "mdoc is not running in the background"),
TestCommand("exit")
)
}
3 changes: 3 additions & 0 deletions mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/docs/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```scala mdoc
println(example.Example.greeting)
```
77 changes: 77 additions & 0 deletions mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/SbtTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import java.nio.charset.StandardCharsets
import java.util.concurrent.LinkedBlockingQueue
import scala.collection.mutable
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.sys.process._

object SbtTest {

def test(commands: TestCommand*) = {
val commandsToSend = new LinkedBlockingQueue[String]()
def sendInput(output: java.io.OutputStream): Unit = {
val newLine = "\n".getBytes(StandardCharsets.UTF_8)
try {
while (true) {
val command = commandsToSend.take()
output.write(command.getBytes(StandardCharsets.UTF_8))
output.write(newLine)
output.flush()
}
} catch {
case _: InterruptedException => // Ignore
} finally {
output.close()
}
}

val commandQueue: mutable.Queue[TestCommand] = mutable.Queue(commands: _*)
var expectedOutput: Option[String] = Some("[info] started sbt server")
def processOut(out: String): Unit = {
if (expectedOutput.forall(out.endsWith)) {
if (commandQueue.nonEmpty) {
val command = commandQueue.dequeue()
Thread.sleep(command.delay.toMillis)
commandsToSend.put(command.command)
expectedOutput = command.expectedOutput
}
}
println(s"[SbtTest] $out")
}

val error = new StringBuilder()
def processError(err: String): Unit = {
println(s"[SbtTest error] $err")
error.append(err)
}

// TODO: Do we need the -Xmx setting and any other future options?
val command = Seq(
"sbt",
s"-Dplugin.version=${sys.props("plugin.version")}",
"--no-colors",
"--supershell=never"
)
val logger = ProcessLogger(processOut, processError)
val basicIO = BasicIO(withIn = false, logger)
val io = new ProcessIO(sendInput, basicIO.processOutput, basicIO.processError)
val p = command.run(io)

val deadline = 30.seconds.fromNow
Future {
while (p.isAlive()) {
if (deadline.isOverdue()) {
p.destroy()
}
}
}

val code = p.exitValue()

expectedOutput.foreach { expected =>
throw new AssertionError(s"Expected to find output: $expected")
}
assert(code == 0, s"Expected exit code 0 but got $code")
}
}
22 changes: 22 additions & 0 deletions mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/project/TestCommand.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import scala.concurrent.duration._

/**
* @param command the command to send
* @param expectedOutput expected output of the command
* @param delay time to wait before sending the command
*/
final case class TestCommand(command: String, expectedOutput: Option[String], delay: FiniteDuration)

object TestCommand {
def apply(command: String, expectedOutput: String, delay: FiniteDuration): TestCommand =
TestCommand(command, Some(expectedOutput), delay)

def apply(command: String, expectedOutput: String): TestCommand =
TestCommand(command, Some(expectedOutput), Duration.Zero)

def apply(command: String, delay: FiniteDuration): TestCommand =
TestCommand(command, None, delay)

def apply(command: String): TestCommand =
TestCommand(command, None, Duration.Zero)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version = 1.8.2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("org.scalameta" % "sbt-mdoc" % sys.props("plugin.version"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package example

object Example {
def greeting = "Hello world!"
}
3 changes: 3 additions & 0 deletions mdoc-sbt/src/sbt-test/sbt-mdoc/bg-tasks/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Not sure why we need to do this. If we don't it fails with java.nio.file.NoSuchFileException.
$ mkdir target/mdoc
> check
21 changes: 21 additions & 0 deletions mdoc-sbt/src/sbt-test/sbt-mdoc/run-in-background/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import scala.concurrent.duration._
import MdocPlugin._

ThisBuild / scalaVersion := "2.12.17"

enablePlugins(MdocPlugin)

InputKey[Unit]("mdocBg") := Def.inputTaskDyn {
validateSettings.value
val parsed = sbt.complete.DefaultParsers.spaceDelimited("<arg>").parsed
val args = (mdocExtraArguments.value ++ parsed).mkString(" ")
(Compile / bgRunMain).toTask(s" mdoc.SbtMain $args")
}.evaluated

TaskKey[Unit]("check") := {
SbtTest.test(
TestCommand("mdocBg --watch --background", "Waiting for file changes (press enter to interrupt)"),
TestCommand("show version", "[info] 0.1.0-SNAPSHOT", 3.seconds),
TestCommand("exit")
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```scala mdoc
println(example.Example.greeting)
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import java.nio.charset.StandardCharsets
import java.util.concurrent.LinkedBlockingQueue
import scala.collection.mutable
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.sys.process._

object SbtTest {

def test(commands: TestCommand*) = {
val commandsToSend = new LinkedBlockingQueue[String]()
def sendInput(output: java.io.OutputStream): Unit = {
val newLine = "\n".getBytes(StandardCharsets.UTF_8)
try {
while (true) {
val command = commandsToSend.take()
output.write(command.getBytes(StandardCharsets.UTF_8))
output.write(newLine)
output.flush()
}
} catch {
case _: InterruptedException => // Ignore
} finally {
output.close()
}
}

val commandQueue: mutable.Queue[TestCommand] = mutable.Queue(commands: _*)
var expectedOutput: Option[String] = Some("[info] started sbt server")
def processOut(out: String): Unit = {
if (expectedOutput.forall(out.endsWith)) {
if (commandQueue.nonEmpty) {
val command = commandQueue.dequeue()
Thread.sleep(command.delay.toMillis)
commandsToSend.put(command.command)
expectedOutput = command.expectedOutput
}
}
println(s"[SbtTest] $out")
}

val error = new StringBuilder()
def processError(err: String): Unit = {
println(s"[SbtTest error] $err")
error.append(err)
}

// TODO: Do we need the -Xmx setting and any other future options?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TODO: Do we need the -Xmx setting and any other future options?

I don't think we need any other options for the test, should we remove the comment?

val command = Seq(
"sbt",
s"-Dplugin.version=${sys.props("plugin.version")}",
"--no-colors",
"--supershell=never"
)
val logger = ProcessLogger(processOut, processError)
val basicIO = BasicIO(withIn = false, logger)
val io = new ProcessIO(sendInput, basicIO.processOutput, basicIO.processError)
val p = command.run(io)

val deadline = 30.seconds.fromNow
Future {
while (p.isAlive()) {
if (deadline.isOverdue()) {
p.destroy()
}
}
}

val code = p.exitValue()

expectedOutput.foreach { expected =>
throw new AssertionError(s"Expected to find output: $expected")
}
assert(code == 0, s"Expected exit code 0 but got $code")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import scala.concurrent.duration._

/**
* @param command the command to send
* @param expectedOutput expected output of the command
* @param delay time to wait before sending the command
*/
final case class TestCommand(command: String, expectedOutput: Option[String], delay: FiniteDuration)

object TestCommand {
def apply(command: String, expectedOutput: String, delay: FiniteDuration): TestCommand =
TestCommand(command, Some(expectedOutput), delay)

def apply(command: String, expectedOutput: String): TestCommand =
TestCommand(command, Some(expectedOutput), Duration.Zero)

def apply(command: String, delay: FiniteDuration): TestCommand =
TestCommand(command, None, delay)

def apply(command: String): TestCommand =
TestCommand(command, None, Duration.Zero)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version = 1.8.2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("org.scalameta" % "sbt-mdoc" % sys.props("plugin.version"))
Loading