Simplify file management settings

I decided that there were too many settings related to the file
management that did similar things and had similar names but did
slightly different things. To improve this, I introduce the ChangedFiles
class to sbt.nio.file and switch to having just two task for file input
and output retrieval: all(Input|Output)Files and
changed(Input|Output)Files. If, for example, changedInputFiles returns
None that means that either the task has not yet been run or there were
no changes. If there have been any changes, then it will return
Some(changes) and the user can extract the relevant changes that they
are interested in.

The code may be slightly more verbose in a few places, but I think it's
worth it for the conceptual clarity.
This commit is contained in:
Ethan Atkins 2019-04-30 12:02:28 -07:00
parent 3319423369
commit 507346f3f6
18 changed files with 176 additions and 160 deletions

View File

@ -721,12 +721,14 @@ lazy val sbtIgnoredProblems = {
exclude[ReversedMissingMethodProblem]("sbt.Import.AnyPath"),
exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$**_="),
exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$*_="),
exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$ChangedFiles_="),
exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$AnyPath_="),
exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$Glob_="),
exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$RecursiveGlob_="),
exclude[ReversedMissingMethodProblem]("sbt.Import.sbt$Import$_setter_$RelativeGlob_="),
exclude[ReversedMissingMethodProblem]("sbt.Import.*"),
exclude[ReversedMissingMethodProblem]("sbt.Import.**"),
exclude[ReversedMissingMethodProblem]("sbt.Import.ChangedFiles"),
exclude[ReversedMissingMethodProblem]("sbt.Import.RecursiveGlob"),
exclude[ReversedMissingMethodProblem]("sbt.Import.Glob"),
exclude[ReversedMissingMethodProblem]("sbt.Import.RelativeGlob"),

View File

@ -452,7 +452,7 @@ object Defaults extends BuildCommon {
}
unmanagedResourceDirectories.value.map(_ ** filter)
},
unmanagedResources := (unmanagedResources / allInputPaths).value.map(_.toFile),
unmanagedResources := (unmanagedResources / allInputFiles).value.map(_.toFile),
resourceGenerators :== Nil,
resourceGenerators += Def.task {
PluginDiscovery.writeDescriptors(discoveredSbtPlugins.value, resourceManaged.value)

View File

@ -16,7 +16,7 @@ import sbt.internal.DynamicInput
import sbt.internal.nio.FileTreeRepository
import sbt.internal.util.AttributeKey
import sbt.internal.util.complete.Parser
import sbt.nio.file.{ FileAttributes, FileTreeView, Glob }
import sbt.nio.file.{ ChangedFiles, FileAttributes, FileTreeView, Glob }
import sbt.{ Def, InputKey, State, StateTransform }
import scala.concurrent.duration.FiniteDuration
@ -24,44 +24,27 @@ import scala.concurrent.duration.FiniteDuration
object Keys {
val allInputFiles =
taskKey[Seq[Path]]("All of the file inputs for a task excluding directories and hidden files.")
val allInputPaths = taskKey[Seq[Path]](
"All of the file inputs for a task with no filters applied. Regular files and directories are included. Excludes hidden files"
)
val changedInputFiles =
taskKey[Seq[Path]](
"All of the file inputs for a task that have changed since the last run. Includes new and modified files but excludes deleted files."
)
val modifiedInputFiles =
taskKey[Seq[Path]](
"All of the file inputs for a task that have changed since the last run. Excludes new files. Files are considered modified based on either the last modified time or the file stamp for the file."
)
val removedInputFiles =
taskKey[Seq[Path]]("All of the file inputs for a task that have changed since the last run.")
val changedInputFiles = taskKey[Option[ChangedFiles]]("The changed files for a task")
val fileInputs = settingKey[Seq[Glob]](
"The file globs that are used by a task. This setting will generally be scoped per task. It will also be used to determine the sources to watch during continuous execution."
)
val fileOutputs = settingKey[Seq[Glob]]("Describes the output files of a task.")
val allOutputPaths =
taskKey[Seq[Path]]("All of the file output for a task with no filters applied.")
val changedOutputPaths =
taskKey[Seq[Path]]("All of the task file outputs that have changed since the last run.")
val modifiedOutputPaths =
taskKey[Seq[Path]](
"All of the task file outputs that have been modified since the last run. Excludes new files."
)
val removedOutputPaths =
taskKey[Seq[Path]](
"All of the output paths that have been removed since the last run."
)
val inputFileStamper = settingKey[FileStamper](
"Toggles the file stamping implementation used to determine whether or not a file has been modified."
)
val fileOutputs = settingKey[Seq[Glob]]("Describes the output files of a task.")
val allOutputFiles =
taskKey[Seq[Path]]("All of the file output for a task excluding directories and hidden files.")
val changedOutputFiles =
taskKey[Option[ChangedFiles]]("The files that have changed since the last task run.")
val outputFileStamper = settingKey[FileStamper](
"Toggles the file stamping implementation used to determine whether or not a file has been modified."
)
val fileTreeView =
taskKey[FileTreeView.Nio[FileAttributes]]("A view of the local file system tree")
// watch related settings
val watchAntiEntropyRetentionPeriod = settingKey[FiniteDuration](
"Wall clock Duration for which a FileEventMonitor will store anti-entropy events. This prevents spurious triggers when a task takes a long time to run. Higher values will consume more memory but make spurious triggers less likely."
).withRank(BMinusSetting)

View File

@ -18,11 +18,13 @@ import sbt.internal.{ Clean, Continuous, DynamicInput, SettingsGraph }
import sbt.nio.FileStamp.{ fileStampJsonFormatter, pathJsonFormatter, _ }
import sbt.nio.FileStamper.{ Hash, LastModified }
import sbt.nio.Keys._
import sbt.nio.file.ChangedFiles
import sbt.std.TaskExtra._
import sjsonnew.JsonFormat
import scala.collection.JavaConverters._
import scala.collection.mutable
import scala.collection.immutable.VectorBuilder
private[sbt] object Settings {
private[sbt] def inject(transformed: Seq[Def.Setting[_]]): Seq[Def.Setting[_]] = {
@ -142,15 +144,8 @@ private[sbt] object Settings {
(transitiveClasspathDependency in scopedKey.scope := { () }) :: Nil
case allInputFiles.key => allFilesImpl(scopedKey) :: Nil
case changedInputFiles.key => changedInputFilesImpl(scopedKey)
case changedOutputPaths.key =>
changedFilesImpl(scopedKey, changedOutputPaths, outputFileStamps)
case modifiedInputFiles.key => modifiedInputFilesImpl(scopedKey)
case modifiedOutputPaths.key =>
modifiedFilesImpl(scopedKey, modifiedOutputPaths, outputFileStamps)
case removedInputFiles.key =>
removedFilesImpl(scopedKey, removedInputFiles, allInputPaths) :: Nil
case removedOutputPaths.key =>
removedFilesImpl(scopedKey, removedOutputPaths, allOutputPaths) :: Nil
case changedOutputFiles.key =>
changedFilesImpl(scopedKey, changedOutputFiles, outputFileStamps)
case pathToFileStamp.key => stamper(scopedKey) :: Nil
case _ => Nil
}
@ -211,7 +206,7 @@ private[sbt] object Settings {
}
dynamicInputs.foreach(_ ++= inputs.map(g => DynamicInput(g, stamper, forceTrigger)))
view.list(inputs)
}) :: fileStamps(scopedKey) :: allPathsImpl(scopedKey) :: Nil
}) :: fileStamps(scopedKey) :: allFilesImpl(scopedKey) :: Nil
}
private[this] val taskClass = classOf[Task[_]]
@ -219,19 +214,6 @@ private[sbt] object Settings {
private[this] val fileClass = classOf[java.io.File]
private[this] val pathClass = classOf[java.nio.file.Path]
/**
* Returns all of the paths described by a glob with no additional filtering.
* No additional filtering is performed.
*
* @param scopedKey the key whose file inputs we are seeking
* @return a task definition that retrieves the input files and their attributes scoped to a
* particular task.
*/
private[this] def allPathsImpl(scopedKey: Def.ScopedKey[_]): Def.Setting[_] =
addTaskDefinition(Keys.allInputPaths in scopedKey.scope := {
(Keys.allInputPathsAndAttributes in scopedKey.scope).value.map(_._1)
})
/**
* Returns all of the paths for the regular files described by a glob. Directories and hidden
* files are excluded.
@ -265,14 +247,39 @@ private[sbt] object Settings {
}) :: Nil
private[this] def changedFilesImpl(
scopedKey: Def.ScopedKey[_],
changeKey: TaskKey[Seq[Path]],
changeKey: TaskKey[Option[ChangedFiles]],
stampKey: TaskKey[Seq[(Path, FileStamp)]]
): Def.Setting[_] =
addTaskDefinition(changeKey in scopedKey.scope := {
val current = (stampKey in scopedKey.scope).value
(stampKey in scopedKey.scope).previous match {
case Some(previous) => (current diff previous).map(_._1)
case None => current.map(_._1)
case Some(previous) =>
val createdBuilder = new VectorBuilder[Path]
val deletedBuilder = new VectorBuilder[Path]
val updatedBuilder = new VectorBuilder[Path]
val currentMap = current.toMap
val prevMap = previous.toMap
current.foreach {
case (path, currentStamp) =>
prevMap.get(path) match {
case Some(oldStamp) => if (oldStamp != currentStamp) updatedBuilder += path
case None => createdBuilder += path
}
}
previous.foreach {
case (path, _) =>
if (currentMap.get(path).isEmpty) deletedBuilder += path
}
val created = createdBuilder.result()
val deleted = deletedBuilder.result()
val updated = updatedBuilder.result()
if (created.isEmpty && deleted.isEmpty && updated.isEmpty) {
None
} else {
val cf = ChangedFiles(created = created, deleted = deleted, updated = updated)
Some(cf)
}
case None => None
}
})
@ -326,7 +333,7 @@ private[sbt] object Settings {
Vector(allOutputPathsImpl(scope), outputFileStampsImpl(scope)) ++ cleanImpl(taskKey)
}
private[this] def allOutputPathsImpl(scope: Scope): Def.Setting[_] =
addTaskDefinition(allOutputPaths in scope := {
addTaskDefinition(allOutputFiles in scope := {
val fileOutputGlobs = (fileOutputs in scope).value
val allFileOutputs = fileTreeView.value.list(fileOutputGlobs).map(_._1)
val dynamicOutputs = (dynamicFileOutputs in scope).value
@ -338,62 +345,7 @@ private[sbt] object Settings {
case LastModified => FileStamp.lastModified
case Hash => FileStamp.hash
}
(allOutputPaths in scope).value.map(p => p -> stamper(p))
})
/**
* Returns all of the regular files whose stamp has changed since the last time the
* task was evaluated. The result includes modified files but neither new nor deleted
* files nor files whose stamp has not changed since the previous run. Directories and
* hidden files are excluded.
*
* @param scopedKey the key whose modified files we are seeking
* @return a task definition that retrieves the changed input files scoped to the key.
*/
private[this] def modifiedInputFilesImpl(scopedKey: Def.ScopedKey[_]): Seq[Def.Setting[_]] =
modifiedFilesImpl(scopedKey, modifiedInputFiles, inputFileStamps) ::
(watchForceTriggerOnAnyChange in scopedKey.scope := {
(watchForceTriggerOnAnyChange in scopedKey.scope).?.value match {
case Some(t) => t
case None => false
}
}) :: Nil
private[this] def modifiedFilesImpl(
scopedKey: Def.ScopedKey[_],
modifiedKey: TaskKey[Seq[Path]],
stampKey: TaskKey[Seq[(Path, FileStamp)]]
): Def.Setting[_] =
addTaskDefinition(modifiedKey in scopedKey.scope := {
val current = (stampKey in scopedKey.scope).value
(stampKey in scopedKey.scope).previous match {
case Some(previous) =>
val previousPathSet = previous.view.map(_._1).toSet
(current diff previous).collect { case (p, _) if previousPathSet(p) => p }
case None => current.map(_._1)
}
})
/**
* Returns all of the files that have been removed since the previous run.
* task was evaluated. The result includes modified files but neither new nor deleted
* files nor files whose stamp has not changed since the previous run. Directories and
* hidden files are excluded
*
* @param scopedKey the key whose removed files we are seeking
* @return a task definition that retrieves the changed input files scoped to the key.
*/
private[this] def removedFilesImpl(
scopedKey: Def.ScopedKey[_],
removeKey: TaskKey[Seq[Path]],
allKey: TaskKey[Seq[Path]]
): Def.Setting[_] =
addTaskDefinition(removeKey in scopedKey.scope := {
val current = (allKey in scopedKey.scope).value
(allKey in scopedKey.scope).previous match {
case Some(previous) => previous diff current
case None => Nil
}
(allOutputFiles in scope).value.map(p => p -> stamper(p))
})
/**

View File

@ -64,6 +64,8 @@ trait Import {
val ** = sbt.nio.file.**
val * = sbt.nio.file.*
val AnyPath = sbt.nio.file.AnyPath
type ChangedFiles = sbt.nio.file.ChangedFiles
val ChangedFiles = sbt.nio.file.ChangedFiles
type Glob = sbt.nio.file.Glob
val Glob = sbt.nio.file.Glob
type RelativeGlob = sbt.nio.file.RelativeGlob

View File

@ -1,18 +1,24 @@
import java.nio.file.Path
import sjsonnew.BasicJsonProtocol._
val copyFile = taskKey[Int]("dummy task")
copyFile / fileInputs += baseDirectory.value.toGlob / "base" / "*.txt"
copyFile / fileOutputs += baseDirectory.value.toGlob / "out" / "*.txt"
copyFile / target := baseDirectory.value / "out"
copyFile := Def.task {
val prev = copyFile.previous
val changes: Option[Seq[Path]] = (copyFile / changedInputFiles).value.map {
case ChangedFiles(c, _, u) => c ++ u
}
prev match {
case Some(v: Int) if (copyFile / changedInputFiles).value.isEmpty => v
case Some(v: Int) if changes.isEmpty => v
case _ =>
(copyFile / changedInputFiles).value.foreach { p =>
val outdir = baseDirectory.value / "out"
IO.createDirectory(baseDirectory.value / "out")
IO.copyFile(p.toFile, outdir / p.getFileName.toString)
changes.getOrElse((copyFile / allInputFiles).value).foreach { p =>
val outDir = baseDirectory.value / "out"
IO.createDirectory(outDir)
IO.copyFile(p.toFile, outDir / p.getFileName.toString)
}
prev.map(_ + 1).getOrElse(1)
}

View File

@ -51,3 +51,9 @@ $ copy-file changes/Foo.txt base/Foo.txt
> checkOutDirectoryIsEmpty
> checkCount 0
> copyFile / allOutputFiles
> checkCount 1
> checkOutDirectoryHasFile

View File

@ -5,6 +5,7 @@ val fileInputTask = taskKey[Unit]("task with file inputs")
fileInputTask / fileInputs += Glob(baseDirectory.value / "base", "*.md")
fileInputTask := Def.taskDyn {
if ((fileInputTask / changedInputFiles).value.nonEmpty) Def.task(assert(true))
if ((fileInputTask / changedInputFiles).value.fold(false)(_.updated.nonEmpty))
Def.task(assert(true))
else Def.task(assert(false))
}.value

View File

@ -1,5 +1,3 @@
> fileInputTask
-> fileInputTask
$ copy-file changes/Bar.md base/Bar.md

View File

@ -8,19 +8,19 @@ foo / fileInputs := Seq(
val checkModified = taskKey[Unit]("check that modified files are returned")
checkModified := Def.taskDyn {
val changed = (foo / changedInputFiles).value
val modified = (foo / modifiedInputFiles).value
if (modified.sameElements(changed)) Def.task(assert(true))
val modified = (foo / changedInputFiles).value.map(_.updated).getOrElse(Nil)
val allFiles = (foo / allInputFiles).value
if (modified.isEmpty) Def.task(assert(true))
else Def.task {
assert(modified != changed)
assert(modified != allFiles)
assert(modified == Seq((baseDirectory.value / "base" / "Bar.md").toPath))
}
}.value
val checkRemoved = taskKey[Unit]("check that modified files are returned")
val checkRemoved = taskKey[Unit]("check that removed files are returned")
checkRemoved := Def.taskDyn {
val files = (foo / allInputFiles).value
val removed = (foo / removedInputFiles).value
val removed = (foo / changedInputFiles).value.map(_.deleted).getOrElse(Nil)
if (removed.isEmpty) Def.task(assert(true))
else Def.task {
assert(files == Seq((baseDirectory.value / "base" / "Foo.txt").toPath))
@ -31,7 +31,7 @@ checkRemoved := Def.taskDyn {
val checkAdded = taskKey[Unit]("check that modified files are returned")
checkAdded := Def.taskDyn {
val files = (foo / allInputFiles).value
val added = (foo / modifiedInputFiles).value
val added = (foo / changedInputFiles).value.map(_.created).getOrElse(Nil)
if (added.isEmpty || files.sameElements(added)) Def.task(assert(true))
else Def.task {
val base = baseDirectory.value / "base"

View File

@ -5,7 +5,7 @@ val foo = taskKey[Seq[File]]("Retrieve Foo.txt")
foo / fileInputs += baseDirectory.value ** "*.txt"
foo := (foo / allInputPaths).value.map(_.toFile)
foo := (foo / allInputFiles).value.map(_.toFile)
val checkFoo = taskKey[Unit]("Check that the Foo.txt file is retrieved")
@ -16,7 +16,7 @@ val bar = taskKey[Seq[File]]("Retrieve Bar.md")
bar / fileInputs += baseDirectory.value / "base/subdir/nested-subdir" * "*.md"
bar := (bar / allInputPaths).value.map(_.toFile)
bar := (bar / allInputFiles).value.map(_.toFile)
val checkBar = taskKey[Unit]("Check that the Bar.md file is retrieved")

View File

@ -7,8 +7,10 @@ fileInputTask / fileInputs += (baseDirectory.value / "base").toGlob / "*.md"
fileInputTask / inputFileStamper := sbt.nio.FileStamper.LastModified
fileInputTask := Def.taskDyn {
if ((fileInputTask / changedInputFiles).value.nonEmpty) Def.task(assert(true))
else Def.task(assert(false))
(fileInputTask / changedInputFiles).value match {
case Some(ChangedFiles(_, _, u)) if u.nonEmpty => Def.task(assert(true))
case None => Def.task(assert(false))
}
}.value
val setLastModified = taskKey[Unit]("Reset the last modified time")

View File

@ -1,5 +1,3 @@
> fileInputTask
-> fileInputTask
$ touch base/Bar.md

View File

@ -7,9 +7,13 @@ compileLib / fileInputs := {
val base: Glob = (compileLib / sourceDirectory).value.toGlob
base / ** / "*.c" :: base / "include" / "*.h" :: Nil
}
compileLib / target := baseDirectory.value / "out" / "lib"
compileLib / target := baseDirectory.value / "out" / "objects"
compileLib := {
val inputs: Seq[Path] = (compileLib / changedInputFiles).value
val allFiles: Seq[Path] = (compileLib / allInputFiles).value
val changedFiles: Option[Seq[Path]] = (compileLib / changedInputFiles).value match {
case Some(ChangedFiles(c, _, u)) => Some(c ++ u)
case None => None
}
val include = (compileLib / sourceDirectory).value / "include"
val objectDir: Path = (compileLib / target).value.toPath / "objects"
val logger = streams.value.log
@ -18,16 +22,15 @@ compileLib := {
name.substring(0, name.lastIndexOf('.')) + ".o"
}
compileLib.previous match {
case Some(outputs: Seq[Path]) if inputs.isEmpty =>
case Some(outputs: Seq[Path]) if changedFiles.isEmpty =>
logger.info("Not compiling libfoo: no inputs have changed.")
outputs
case _ =>
Files.createDirectories(objectDir)
def extensionFilter(ext: String): Path => Boolean = _.getFileName.toString.endsWith(s".$ext")
val allInputs = (compileLib / allInputFiles).value
val cFiles: Seq[Path] =
if (inputs.exists(extensionFilter("h"))) allInputs.filter(extensionFilter("c"))
else inputs.filter(extensionFilter("c"))
if (changedFiles.fold(false)(_.exists(extensionFilter("h")))) allFiles.filter(extensionFilter("c"))
else changedFiles.getOrElse(allFiles).filter(extensionFilter("c"))
cFiles.map { file =>
val outFile = objectDir.resolve(objectFileName(file))
logger.info(s"Compiling $file to $outFile")
@ -38,13 +41,14 @@ compileLib := {
}
val linkLib = taskKey[Path]("")
linkLib / target := baseDirectory.value / "out" / "lib"
linkLib := {
val objects = (compileLib / changedOutputPaths).value
val outPath = (compileLib / target).value.toPath
val allObjects = (compileLib / allOutputPaths).value.map(_.toString)
val changedObjects = (compileLib / changedOutputFiles).value
val outPath = (linkLib / target).value.toPath
val allObjects = (compileLib / allOutputFiles).value.map(_.toString)
val logger = streams.value.log
linkLib.previous match {
case Some(p: Path) if objects.isEmpty =>
case Some(p: Path) if changedObjects.isEmpty =>
logger.info("Not running linker: no outputs have changed.")
p
case _ =>
@ -53,9 +57,10 @@ linkLib := {
(Seq("-dynamiclib", "-o", path.toString), path)
} else {
val path = outPath.resolve("libfoo.so")
(Seq("-shared", "-o", path.toString), path)
(Seq("-shared", "-fPIC", "-o", path.toString), path)
}
logger.info(s"Linking $libraryPath")
Files.createDirectories(outPath)
("gcc" +: (linkOptions ++ allObjects)).!!
libraryPath
}
@ -67,13 +72,14 @@ compileMain / fileInputs := (compileMain / sourceDirectory).value.toGlob / "main
compileMain / target := baseDirectory.value / "out" / "main"
compileMain := {
val library = linkLib.value
val changed = (compileMain / changedInputFiles).value ++ (linkLib / changedOutputPaths).value
val changed: Boolean = (compileMain / changedInputFiles).value.nonEmpty ||
(linkLib / changedOutputFiles).value.nonEmpty
val include = (compileLib / sourceDirectory).value / "include"
val logger = streams.value.log
val outDir = (compileMain / target).value.toPath
val outPath = outDir.resolve("main.out")
compileMain.previous match {
case Some(p: Path) if changed.isEmpty =>
case Some(p: Path) if changed =>
logger.info(s"Not building $outPath: no dependencies have changed")
p
case _ =>
@ -100,17 +106,27 @@ compileMain := {
val executeMain = inputKey[Unit]("run the main method")
executeMain := {
val args = Def.spaceDelimited("<arguments>").parsed
val binary = (compileMain / allOutputPaths).value
val binary: Seq[Path] = (compileMain / allOutputFiles).value
val logger = streams.value.log
binary match {
case Seq(b) =>
val argString =
if (args.nonEmpty) s" with arguments: ${args.mkString("'", "', '", "'")}" else ""
logger.info(s"Running $b$argString")
logger.info((b.toString +: args).!!)
logger.info(RunBinary(b, args, linkLib.value).mkString("\n"))
case b =>
throw new IllegalArgumentException(
s"compileMain generated multiple binaries: ${b.mkString(", ")}"
)
}
}
val checkOutput = inputKey[Unit]("check the output value")
checkOutput := {
val args @ Seq(arg, res) = Def.spaceDelimited("").parsed
val binary: Path = (compileMain / allOutputFiles).value.head
val output = RunBinary(binary, args, linkLib.value)
assert(output.contains(s"f($arg) = $res"))
()
}

View File

@ -0,0 +1,16 @@
#include "lib.h"
#define __STR(x) #x
#define STR(x) __STR(x)
#define BODY(x, op) x op x
#define OP *
#define ARG x
const int func(const int x) {
return BODY(ARG, OP);
}
const char* func_str() {
return BODY(STR(ARG), " "STR(OP)" ");
}

View File

@ -0,0 +1,15 @@
import java.nio.file.Path
import java.util.concurrent.TimeUnit
object RunBinary {
def apply(binary: Path, args: Seq[String], libraryPath: Path): Seq[String] = {
val builder = new java.lang.ProcessBuilder(binary.toString +: args :_*)
if (scala.util.Properties.isLinux) {
builder.environment.put("LD_LIBRARY_PATH", libraryPath.getParent.toString)
}
val process = builder.start()
process.waitFor(5, TimeUnit.SECONDS)
scala.io.Source.fromInputStream(process.getInputStream).getLines.toVector ++
scala.io.Source.fromInputStream(process.getErrorStream).getLines
}
}

View File

@ -1,25 +1,25 @@
> executeMain 1
#> executeMain 1
> checkDirectoryContents out/main main.out
#> compileLib / clean
> compileMain / clean
#> linkLib / clean
> checkDirectoryContents out/main empty
#> executeMain 1 2 3
> checkDirectoryContents out/lib libfoo*
#> compileLib / clean
> linkLib / clean
#> executeMain 2 3 4
> checkDirectoryContents out/lib empty
#> compileMain / clean
> executeMain 1
#> executeMain 4 5 6
> checkDirectoryContents out/main main.out
#> clean
> checkDirectoryContents out/lib libfoo*
#> executeMain 4
> checkOutput 2 8
#> compileLib / clean
$ copy-file changes/lib.c src/lib/lib.c
#> executeMain 3
> checkOutput 2 4

View File

@ -0,0 +1,19 @@
import java.nio.file.Path
val checkDirectoryContents = inputKey[Unit]("Validates that a directory has the expected files")
checkDirectoryContents := {
val arguments = Def.spaceDelimited("").parsed
val directory = (baseDirectory.value / arguments.head).toPath
val view = fileTreeView.value
val expected = arguments.tail
expected match {
case s if s.isEmpty => assert(view.list(directory.toGlob / **).isEmpty)
case Seq("empty") => assert(view.list(directory.toGlob / **).isEmpty)
case globStrings =>
val globs = globStrings.map(Glob.apply)
val actual: Seq[Path] = view.list(directory.toGlob / **).map {
case (p, _) => directory.relativize(p)
}
assert(actual.forall(f => globs.exists(_.matches(f))))
}
}