diff --git a/build.sbt b/build.sbt index 195866ddc..10938a5ac 100644 --- a/build.sbt +++ b/build.sbt @@ -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"), diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 709332434..9fc7a93ab 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -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) diff --git a/main/src/main/scala/sbt/nio/Keys.scala b/main/src/main/scala/sbt/nio/Keys.scala index 9e528e6f2..c8212ecca 100644 --- a/main/src/main/scala/sbt/nio/Keys.scala +++ b/main/src/main/scala/sbt/nio/Keys.scala @@ -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) diff --git a/main/src/main/scala/sbt/nio/Settings.scala b/main/src/main/scala/sbt/nio/Settings.scala index dd59ef092..fe13b8fab 100644 --- a/main/src/main/scala/sbt/nio/Settings.scala +++ b/main/src/main/scala/sbt/nio/Settings.scala @@ -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)) }) /** diff --git a/sbt/src/main/scala/sbt/Import.scala b/sbt/src/main/scala/sbt/Import.scala index b1f0adfb4..b7addad35 100644 --- a/sbt/src/main/scala/sbt/Import.scala +++ b/sbt/src/main/scala/sbt/Import.scala @@ -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 diff --git a/sbt/src/sbt-test/nio/clean/build.sbt b/sbt/src/sbt-test/nio/clean/build.sbt index 99f2e58ae..1c98f43ae 100644 --- a/sbt/src/sbt-test/nio/clean/build.sbt +++ b/sbt/src/sbt-test/nio/clean/build.sbt @@ -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) } diff --git a/sbt/src/sbt-test/nio/clean/test b/sbt/src/sbt-test/nio/clean/test index 9bae66dc5..0d587dea6 100644 --- a/sbt/src/sbt-test/nio/clean/test +++ b/sbt/src/sbt-test/nio/clean/test @@ -51,3 +51,9 @@ $ copy-file changes/Foo.txt base/Foo.txt > checkOutDirectoryIsEmpty > checkCount 0 + +> copyFile / allOutputFiles + +> checkCount 1 + +> checkOutDirectoryHasFile diff --git a/sbt/src/sbt-test/nio/diff/build.sbt b/sbt/src/sbt-test/nio/diff/build.sbt index b346825e1..69a0927f7 100644 --- a/sbt/src/sbt-test/nio/diff/build.sbt +++ b/sbt/src/sbt-test/nio/diff/build.sbt @@ -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 diff --git a/sbt/src/sbt-test/nio/diff/test b/sbt/src/sbt-test/nio/diff/test index cb61825c9..1a1fd1c11 100644 --- a/sbt/src/sbt-test/nio/diff/test +++ b/sbt/src/sbt-test/nio/diff/test @@ -1,5 +1,3 @@ -> fileInputTask - -> fileInputTask $ copy-file changes/Bar.md base/Bar.md diff --git a/sbt/src/sbt-test/nio/file-hashes/build.sbt b/sbt/src/sbt-test/nio/file-hashes/build.sbt index f232bda77..4b4e093eb 100644 --- a/sbt/src/sbt-test/nio/file-hashes/build.sbt +++ b/sbt/src/sbt-test/nio/file-hashes/build.sbt @@ -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" diff --git a/sbt/src/sbt-test/nio/glob-dsl/build.sbt b/sbt/src/sbt-test/nio/glob-dsl/build.sbt index 8dcaba164..a0bdb6c29 100644 --- a/sbt/src/sbt-test/nio/glob-dsl/build.sbt +++ b/sbt/src/sbt-test/nio/glob-dsl/build.sbt @@ -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") diff --git a/sbt/src/sbt-test/nio/last-modified/build.sbt b/sbt/src/sbt-test/nio/last-modified/build.sbt index 200deb1c4..58678dcf0 100644 --- a/sbt/src/sbt-test/nio/last-modified/build.sbt +++ b/sbt/src/sbt-test/nio/last-modified/build.sbt @@ -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") diff --git a/sbt/src/sbt-test/nio/last-modified/test b/sbt/src/sbt-test/nio/last-modified/test index a1ac20587..15dab9326 100644 --- a/sbt/src/sbt-test/nio/last-modified/test +++ b/sbt/src/sbt-test/nio/last-modified/test @@ -1,5 +1,3 @@ -> fileInputTask - -> fileInputTask $ touch base/Bar.md diff --git a/sbt/src/sbt-test/nio/make-clone/build.sbt b/sbt/src/sbt-test/nio/make-clone/build.sbt index 0727d3b65..f10df2f02 100644 --- a/sbt/src/sbt-test/nio/make-clone/build.sbt +++ b/sbt/src/sbt-test/nio/make-clone/build.sbt @@ -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("").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")) + () +} diff --git a/sbt/src/sbt-test/nio/make-clone/changes/lib.c b/sbt/src/sbt-test/nio/make-clone/changes/lib.c new file mode 100644 index 000000000..d811899fb --- /dev/null +++ b/sbt/src/sbt-test/nio/make-clone/changes/lib.c @@ -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)" "); +} diff --git a/sbt/src/sbt-test/nio/make-clone/project/RunBinary.scala b/sbt/src/sbt-test/nio/make-clone/project/RunBinary.scala new file mode 100644 index 000000000..008ceecdc --- /dev/null +++ b/sbt/src/sbt-test/nio/make-clone/project/RunBinary.scala @@ -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 + } +} diff --git a/sbt/src/sbt-test/nio/make-clone/test b/sbt/src/sbt-test/nio/make-clone/test index 708e7696a..11c715993 100644 --- a/sbt/src/sbt-test/nio/make-clone/test +++ b/sbt/src/sbt-test/nio/make-clone/test @@ -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 diff --git a/sbt/src/sbt-test/nio/make-clone/tests.sbt b/sbt/src/sbt-test/nio/make-clone/tests.sbt new file mode 100644 index 000000000..37baa0b06 --- /dev/null +++ b/sbt/src/sbt-test/nio/make-clone/tests.sbt @@ -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)))) + } +}