From 507346f3f68281a06b9fd3daf2bdf97426b1192d Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Tue, 30 Apr 2019 12:02:28 -0700 Subject: [PATCH] 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. --- build.sbt | 2 + main/src/main/scala/sbt/Defaults.scala | 2 +- main/src/main/scala/sbt/nio/Keys.scala | 39 ++---- main/src/main/scala/sbt/nio/Settings.scala | 118 ++++++------------ sbt/src/main/scala/sbt/Import.scala | 2 + sbt/src/sbt-test/nio/clean/build.sbt | 16 ++- sbt/src/sbt-test/nio/clean/test | 6 + sbt/src/sbt-test/nio/diff/build.sbt | 3 +- sbt/src/sbt-test/nio/diff/test | 2 - sbt/src/sbt-test/nio/file-hashes/build.sbt | 14 +-- sbt/src/sbt-test/nio/glob-dsl/build.sbt | 4 +- sbt/src/sbt-test/nio/last-modified/build.sbt | 6 +- sbt/src/sbt-test/nio/last-modified/test | 2 - sbt/src/sbt-test/nio/make-clone/build.sbt | 46 ++++--- sbt/src/sbt-test/nio/make-clone/changes/lib.c | 16 +++ .../nio/make-clone/project/RunBinary.scala | 15 +++ sbt/src/sbt-test/nio/make-clone/test | 24 ++-- sbt/src/sbt-test/nio/make-clone/tests.sbt | 19 +++ 18 files changed, 176 insertions(+), 160 deletions(-) create mode 100644 sbt/src/sbt-test/nio/make-clone/changes/lib.c create mode 100644 sbt/src/sbt-test/nio/make-clone/project/RunBinary.scala create mode 100644 sbt/src/sbt-test/nio/make-clone/tests.sbt 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)))) + } +}