From 6d2bbbe0c11e3afe2d915f49de50b52646de1ef8 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Wed, 24 Nov 2010 14:18:59 -0500 Subject: [PATCH] TaskGroups, Context in tasks, new tasks add syncTask task constructor and copy-resources/copy-test-resources instances add console-quick, test-console, console, test-run add IntegrationTest trait make Context available through 'context' task update 'last' and 'show' to use Context to retrieve task by name drop SingleProject (superseded by Project) add TaskGroup to be able to inject groups of named tasks fix watchPaths missing flat sources proper logging in a few more places, such as compile --- main/Compile.scala | 17 ++- main/DefaultProject.scala | 195 +++++++++++++++++++++++---------- main/MultiProject.scala | 17 ++- main/OutputTasks.scala | 23 ++-- main/SingleProject.scala | 70 +++++++----- main/Sync.scala | 92 ++++++++++++++++ tasks/standard/System.scala | 15 ++- util/collection/Relation.scala | 27 +++-- 8 files changed, 337 insertions(+), 119 deletions(-) create mode 100644 main/Sync.scala diff --git a/main/Compile.scala b/main/Compile.scala index c6fd53eae..c921d8031 100644 --- a/main/Compile.scala +++ b/main/Compile.scala @@ -22,25 +22,24 @@ object Compile case _ => Nil } - final class Inputs(val compilers: Compilers, val config: Options, val incSetup: IncSetup, val log: Logger) + final class Inputs(val compilers: Compilers, val config: Options, val incSetup: IncSetup) final class Options(val classpath: Seq[File], val sources: Seq[File], val classesDirectory: File, val options: Seq[String], val javacOptions: Seq[String], val maxErrors: Int) - final class IncSetup(val javaSrcBases: Seq[File], val analysisMap: Map[File, Analysis], val cacheDirectory: File) + final class IncSetup(val srcBases: Seq[File], val analysisMap: Map[File, Analysis], val cacheDirectory: File) final class Compilers(val scalac: AnalyzingCompiler, val javac: JavaCompiler) - def inputs(classpath: Seq[File], sources: Seq[File], outputDirectory: File, options: Seq[String], javacOptions: Seq[String], javaSrcBases: Seq[File], maxErrors: Int)(implicit compilers: Compilers, log: Logger): Inputs = + def inputs(classpath: Seq[File], sources: Seq[File], outputDirectory: File, options: Seq[String], javacOptions: Seq[String], srcBases: Seq[File], maxErrors: Int)(implicit compilers: Compilers, log: Logger): Inputs = { import Path._ val classesDirectory = outputDirectory / "classes" val cacheDirectory = outputDirectory / "cache" val augClasspath = classesDirectory.asFile +: classpath - inputs(augClasspath, sources, classesDirectory, options, javacOptions, javaSrcBases, Map.empty, cacheDirectory, maxErrors) + inputs(augClasspath, sources, classesDirectory, options, javacOptions, srcBases, Map.empty, cacheDirectory, maxErrors) } - def inputs(classpath: Seq[File], sources: Seq[File], classesDirectory: File, options: Seq[String], javacOptions: Seq[String], javaSrcBases: Seq[File], analysisMap: Map[File, Analysis], cacheDirectory: File, maxErrors: Int)(implicit compilers: Compilers, log: Logger): Inputs = + def inputs(classpath: Seq[File], sources: Seq[File], classesDirectory: File, options: Seq[String], javacOptions: Seq[String], srcBases: Seq[File], analysisMap: Map[File, Analysis], cacheDirectory: File, maxErrors: Int)(implicit compilers: Compilers, log: Logger): Inputs = new Inputs( compilers, new Options(classpath, sources, classesDirectory, options, javacOptions, maxErrors), - new IncSetup(javaSrcBases, analysisMap, cacheDirectory), - log + new IncSetup(srcBases, analysisMap, cacheDirectory) ) def compilers(implicit app: AppConfiguration, log: Logger): Compilers = @@ -77,13 +76,13 @@ object Compile new AnalyzingCompiler(instance, componentManager, cpOptions, log) } - def apply(in: Inputs): Analysis = + def apply(in: Inputs, log: Logger): Analysis = { import in.compilers._ import in.config._ import in.incSetup._ val agg = new build.AggressiveCompile(cacheDirectory) - agg(scalac, javac, sources, classpath, classesDirectory, javaSrcBases, options, javacOptions, analysisMap, maxErrors)(in.log) + agg(scalac, javac, sources, classpath, classesDirectory, srcBases, options, javacOptions, analysisMap, maxErrors)(log) } } \ No newline at end of file diff --git a/main/DefaultProject.scala b/main/DefaultProject.scala index 706419ddf..825d9f592 100644 --- a/main/DefaultProject.scala +++ b/main/DefaultProject.scala @@ -7,7 +7,8 @@ package sbt import compile.{Discovered,Discovery} import inc.Analysis import TaskExtra._ - import Configurations.{Compile => CompileConfig, Test => TestConfig, Runtime => RunConfig, Default => DefaultConfig} + import Path.fileToPath + import Configurations.{Compile => CompileConfig, Test => TestConfig, Runtime => RunConfig, Default => DefaultConfig, IntegrationTest => ITestConfig} import ClasspathProject._ import Types._ import xsbti.api.Definition @@ -19,101 +20,93 @@ class DefaultProject(val info: ProjectInfo) extends BasicProject { override def name = "test" } +trait IntegrationTest extends BasicProject +{ + override def productsTask(conf: Configuration): Task[Seq[Attributed[File]]] = + conf match { + case ITestConfig => makeProducts(integrationTestCompile.compile, integrationTestCompile.compileInputs, name, "it-") + case _ => super.productsTask(conf) + } + + override def configurations: Seq[Configuration] = super.configurations :+ Configurations.IntegrationTest + + lazy val integrationTestOptions: Task[Seq[TestOption]] = testOptions + lazy val integrationTest = testTasks(Some("it"), Configurations.IntegrationTest, integrationTestOptions, integrationTestCompile.compile, buildScalaInstance) + lazy val integrationTestCompile = compileTasks(Some("it"), Configurations.IntegrationTest, "src" / "it", Path.emptyPathFinder, buildScalaInstance) +} abstract class BasicProject extends TestProject with MultiClasspathProject with ReflectiveClasspathProject { // easier to demo for now override def organization = "org.example" override def version = "1.0" - override def watchPaths: PathFinder = descendents("src","*") + override def watchPaths: PathFinder = (info.projectDirectory: Path) * sourceFilter +++ descendents("src","*") def javacOptions: Seq[String] = Nil def scalacOptions: Seq[String] = Nil + def consoleOptions: Seq[String] = scalacOptions + def initialCommands: String = "" def outputDirectory = "target": Path def cacheDirectory = outputDirectory / "cache" + def mainResources = descendents("src" / "main" / "resources" ###, "*") + def testResources = descendents("src" / "test" / "resources" ###, "*") def classesDirectory(configuration: Configuration): File = configuration match { case CompileConfig => outputDirectory / "classes" - case x => outputDirectory / (x.toString + "-classes") + case c => outputDirectory / (c.name + "-classes") } lazy val products: Classpath = TaskMap(productsTask) - // TODO: it config, include resources, perhaps handle jars v. directories - def productsTask(conf: Configuration) = + // TODO: include resources, perhaps handle jars v. directories + def productsTask(conf: Configuration): Task[Seq[Attributed[File]]] = conf match { - case CompileConfig | DefaultConfig => makeProducts(compile, compileInputs, name, "") - case TestConfig => makeProducts(testCompile, testCompileInputs, name, "test-") + case CompileConfig | DefaultConfig => makeProducts(compile.compile, compile.compileInputs, name, "") + case TestConfig => makeProducts(testCompile.compile, testCompile.compileInputs, name, "test-") case x => task { Nil } } - lazy val buildScalaInstance: Task[ScalaInstance] = task { - val provider = info.app.scalaProvider - ScalaInstance(provider.version, provider) - } + lazy val buildScalaVersions: Task[String] = task { info.app.scalaProvider.version }//cross(MultiProject.ScalaVersion)(info.app.scalaProvider.version) + lazy val buildScalaInstance: Task[ScalaInstance] = + buildScalaVersions map { version => ScalaInstance(version, info.app.scalaProvider) } lazy val discoverMain: Task[Seq[(Definition,Discovered)]] = - compile map { analysis => Discovery.applications(Test.allDefs(analysis)) } + compile.compile map { analysis => Discovery.applications(Test.allDefs(analysis)) } lazy val discoveredMainClasses: Task[Seq[String]] = discoverMain map { _ collect { case (definition, discovered) if(discovered.hasMain) => definition.name } } - lazy val runMainClass: Task[Option[String]] = - discoveredMainClasses map { classes => SelectMainClass(Some(SimpleReader readLine _), classes) } - lazy val pkgMainClass: Task[Option[String]] = discoveredMainClasses map { classes => SelectMainClass(None, classes) } lazy val runner: Task[ScalaRun] = buildScalaInstance map { si => new Run(si) } - lazy val run = (input :^: fullClasspath(RunConfig) :^: runMainClass :^: streams :^: runner :^: KNil) map { case in :+: cp :+: main :+: s :+: r :+: HNil => - val mainClass = main getOrElse error("No main class detected.") - val classpath = cp.map(x => Path.fromFile(x.data)) - r.run(mainClass, classpath, in.splitArgs, s.log) foreach error - } + lazy val run = runTasks(None, RunConfig, discoveredMainClasses) + lazy val testRun = runTasks(Some("test"), TestConfig, test.testDiscoveredMainClasses) lazy val testFrameworks: Task[Seq[TestFramework]] = task { import TestFrameworks._ Seq(ScalaCheck, Specs, ScalaTest, ScalaCheckCompat, ScalaTestCompat, SpecsCompat, JUnit) } - lazy val testLoader: Task[ClassLoader] = - fullClasspath(TestConfig) :^: buildScalaInstance :^: KNil map { case classpath :+: instance :+: HNil => - TestFramework.createTestLoader(data(classpath), instance) - } - - lazy val loadedTestFrameworks: Task[Map[TestFramework,Framework]] = - testFrameworks :^: streams :^: testLoader :^: KNil map { case frameworks :+: s :+: loader :+: HNil => - frameworks.flatMap( f => f.create(loader, s.log).map( x => (f, x)).toIterable ).toMap - } - - lazy val discoverTest: Task[(Seq[TestDefinition], Set[String])] = - loadedTestFrameworks :^: testCompile :^: KNil map { case frameworkMap :+: analysis :+: HNil => - Test.discover(frameworkMap.values.toSeq, analysis) - } - lazy val definedTests: Task[Seq[TestDefinition]] = discoverTest.map(_._1) - lazy val testOptions: Task[Seq[TestOption]] = task { Nil } - lazy val test = (loadedTestFrameworks :^: testOptions :^: testLoader :^: definedTests :^: streams :^: KNil) flatMap { - case frameworkMap :+: options :+: loader :+: discovered :+: s :+: HNil => + lazy val test = testTasks(None, TestConfig, testOptions, testCompile.compile, buildScalaInstance) - val toTask = (testTask: NamedTestTask) => task { testTask.run() } named(testTask.name) - def dependsOn(on: Iterable[Task[_]]): Task[Unit] = task { () } dependsOn(on.toSeq : _*) + lazy val compile = compileTasks(None, CompileConfig, "src" / "main", info.projectDirectory : Path, buildScalaInstance) + lazy val testCompile = compileTasks(Some("test"), TestConfig, "src" / "test", Path.emptyPathFinder, buildScalaInstance) - val (begin, work, end) = Test(frameworkMap, loader, discovered, options, s.log) - val beginTasks = dependsOn( begin.map(toTask) ) // test setup tasks - val workTasks = work.map(w => toTask(w) dependsOn(beginTasks) ) // the actual tests - val endTasks = dependsOn( end.map(toTask) ) // tasks that perform test cleanup and are run regardless of success of tests - dependsOn( workTasks ) doFinally { endTasks } - } + lazy val consoleQuick = consoleTask(dependencyClasspath(CompileConfig), consoleOptions, initialCommands, compile.compileInputs) + lazy val console = consoleTask(CompileConfig, consoleOptions, initialCommands, compile.compileInputs) + lazy val testConsole = consoleTask(TestConfig, consoleOptions, initialCommands, testCompile.compileInputs) + + lazy val clean = task { IO.delete(outputDirectory) } + + // lazy val doc, test-only, test-quick, test-failed, publish(-local), deliver(-local), make-pom, package-*, javap, copy-resources - lazy val clean = task { - IO.delete(outputDirectory) - } lazy val set = input map { in => val Seq(name, value) = in.splitArgs.take(2) println(name + "=" + value) @@ -122,9 +115,10 @@ abstract class BasicProject extends TestProject with MultiClasspathProject with def sourceFilter: FileFilter = "*.java" | "*.scala" - def compileTask(inputs: Task[Compile.Inputs]): Task[Analysis] = inputs map Compile.apply + def compileTask(inputs: Task[Compile.Inputs]): Task[Analysis] = + inputs :^: streams :^: KNil map { case i :+: s :+: HNil => Compile(i, s.log) } - def compileInputsTask(configuration: Configuration, base: PathFinder, scalaInstance: Task[ScalaInstance]): Task[Compile.Inputs] = + def compileInputsTask(configuration: Configuration, bases: PathFinder, shallow: PathFinder, scalaInstance: Task[ScalaInstance]): Task[Compile.Inputs] = { val dep = dependencyClasspath(configuration) (dep, scalaInstance) map { case (cp :+: si :+: HNil) => @@ -134,18 +128,105 @@ abstract class BasicProject extends TestProject with MultiClasspathProject with val scalaSrc = base / "scala" val out = "target" / si.actualVersion import Path._ - val sources = descendents((javaSrc +++ scalaSrc), sourceFilter) +++ (if(configuration == CompileConfig) info.projectDirectory * (sourceFilter -- defaultExcludes) else Path.emptyPathFinder) + val deepBases = (bases / "java") +++ (bases / "scala") + val allBases = deepBases +++ shallow + val sources = descendents(deepBases, sourceFilter) +++ shallow * (sourceFilter -- defaultExcludes) val classes = classesDirectory(configuration) val classpath = classes +: data(cp) val analysis = analysisMap(cp) val cache = cacheDirectory / "compile" / configuration.toString - Compile.inputs(classpath, sources.getFiles.toSeq, classes, scalacOptions, javacOptions, javaSrc.getFiles.toSeq, analysis, cache, 100)(compilers, log) + Compile.inputs(classpath, sources.getFiles.toSeq, classes, scalacOptions, javacOptions, allBases.getFiles.toSeq, analysis, cache, 100)(compilers, log) } } - lazy val compileInputs: Task[Compile.Inputs] = compileInputsTask(Configurations.Compile, "src" / "main", buildScalaInstance) named(name + "/compile-inputs") - lazy val compile: Task[Analysis] = compileTask(compileInputs) named(name + "/compile") + def copyResourcesTask(resources: PathFinder, config: Configuration): Task[Relation[File,File]] = + streams map { s => + val target = classesDirectory(config) + val cacheFile = cacheDirectory / config.name / "copy-resources" + val mappings = resources.get.map(path => (path.asFile, new File(target, path.relativePath))) + s.log.debug("Copy resource (" + config.name + ") mappings: " + mappings.mkString("\n\t")) + Sync(cacheFile)( mappings ) + } - lazy val testCompileInputs: Task[Compile.Inputs] = compileInputsTask(Configurations.Test, "src" / "test", buildScalaInstance) named(name + "/test-inputs") - lazy val testCompile: Task[Analysis] = compileTask(testCompileInputs) named(name + "/test") + lazy val copyResources = copyResourcesTask(mainResources, CompileConfig) + lazy val copyTestResources = copyResourcesTask(testResources, TestConfig) + + def syncTask(cacheFile: File, mappings: Iterable[(File, File)]): Task[Relation[File,File]] = + task { Sync(cacheFile)(mappings) } + + def consoleTask(config: Configuration, options: Seq[String], initialCommands: String, compilers: Task[Compile.Inputs]): Task[Unit] = + consoleTask(fullClasspath(config), options, initialCommands, compilers) + def consoleTask(classpath: Task[Seq[Attributed[File]]], options: Seq[String], initialCommands: String, compilers: Task[Compile.Inputs]): Task[Unit] = + consoleTask(compilers.map(_.compilers), classpath, options, initialCommands) + def consoleTask(compilers: Task[Compile.Compilers], classpath: Task[Seq[Attributed[File]]], options: Seq[String], initialCommands: String): Task[Unit] = + compilers :^: classpath :^: streams :^: KNil map { case cs :+: cp :+: s :+: HNil => + (new Console(cs.scalac))(data(cp), options, initialCommands, s.log) + } + + def compileTasks(prefix: Option[String], config: Configuration, base: PathFinder, shallow: PathFinder, si: Task[ScalaInstance]): CompileTasks = + { + val confStr = if(config == Configurations.Compile) "" else config.name + "-" + val compileInputs: Task[Compile.Inputs] = compileInputsTask(config, base, shallow, buildScalaInstance) named(confStr + "compile-inputs") + val compile: Task[Analysis] = compileTask(compileInputs) named(confStr + "compile") + new CompileTasks(prefix, compile, compileInputs) + } + + def testTasks(prefix: Option[String], config: Configuration, options: Task[Seq[TestOption]], compile: Task[Analysis], instance: Task[ScalaInstance]): TestTasks = + new TestTasks(prefix, this, testFrameworks, config, options, compile, instance) + + def runTasks(prefix: Option[String], config: Configuration, discoveredMainClasses: Task[Seq[String]]): RunTasks = + new RunTasks(prefix, this, config, discoveredMainClasses, runner) + +} + +// TODO: move these to separate file. The main problem with this approach is modifying dependencies and otherwise overriding a task. +final class CompileTasks(val prefix: Option[String], val compile: Task[Analysis], val compileInputs: Task[Compile.Inputs]) extends TaskGroup + +class RunTasks(val prefix: Option[String], val project: ClasspathProject with Project, val config: Configuration, discoveredMainClasses: Task[Seq[String]], runner: Task[ScalaRun]) extends TaskGroup +{ + def selectRunMain(allMainClasses: Task[Seq[String]]): Task[Option[String]] = + allMainClasses map { classes => SelectMainClass(Some(SimpleReader readLine _), classes) } + + def runTask(fullcp: Task[Seq[Attributed[File]]], mainClass: Task[Option[String]]): Task[Unit] = + runTask(project.input, fullcp, mainClass, project.streams, runner) + + def runTask(input: Task[Input], fullcp: Task[Seq[Attributed[File]]], mainClass: Task[Option[String]], streams: Task[TaskStreams], runner: Task[ScalaRun]): Task[Unit] = + (input :^: fullcp :^: mainClass :^: streams :^: runner :^: KNil) map { case in :+: cp :+: main :+: s :+: r :+: HNil => run0(in.splitArgs, cp, main, s.log, r) } + + def run0(args: Seq[String], cp: Seq[Attributed[File]], main: Option[String], log: Logger, r: ScalaRun) + { + val mainClass = main getOrElse error("No main class detected.") + val classpath = cp.map(x => Path.fromFile(x.data)) + r.run(mainClass, classpath, args, log) foreach error + } + + lazy val runMainClass: Task[Option[String]] = selectRunMain(discoveredMainClasses) + lazy val run = runTask(project.fullClasspath(config), runMainClass) +} + +class TestTasks(val prefix: Option[String], val project: ClasspathProject with Project, testFrameworks: Task[Seq[TestFramework]], val config: Configuration, options: Task[Seq[TestOption]], compile: Task[Analysis], scalaInstance: Task[ScalaInstance]) extends TaskGroup +{ + lazy val testLoader: Task[ClassLoader] = + project.fullClasspath(config) :^: scalaInstance :^: KNil map { case classpath :+: instance :+: HNil => + TestFramework.createTestLoader(data(classpath), instance) + } + + lazy val loadedTestFrameworks: Task[Map[TestFramework,Framework]] = + testFrameworks :^: project.streams :^: testLoader :^: KNil map { case frameworks :+: s :+: loader :+: HNil => + frameworks.flatMap( f => f.create(loader, s.log).map( x => (f, x)).toIterable ).toMap + } + + lazy val discoverTest: Task[(Seq[TestDefinition], Set[String])] = + loadedTestFrameworks :^: compile :^: KNil map { case frameworkMap :+: analysis :+: HNil => + Test.discover(frameworkMap.values.toSeq, analysis) + } + lazy val definedTests: Task[Seq[TestDefinition]] = discoverTest.map(_._1) + lazy val testDiscoveredMainClasses: Task[Seq[String]] = discoverTest.map(_._2.toSeq) + + lazy val executeTests = (loadedTestFrameworks :^: options :^: testLoader :^: definedTests :^: project.streams :^: KNil) flatMap { + case frameworkMap :+: options :+: loader :+: discovered :+: s :+: HNil => + + Test(frameworkMap, loader, discovered, options, s.log) + } + lazy val test = (project.streams, executeTests) map { case s :+: results :+: HNil => Test.showResults(s.log, results) } } diff --git a/main/MultiProject.scala b/main/MultiProject.scala index 28ed2e076..5a117577f 100644 --- a/main/MultiProject.scala +++ b/main/MultiProject.scala @@ -19,6 +19,7 @@ import annotation.tailrec object MultiProject { val InitialProject = AttributeKey[Project]("initial-project") + val ScalaVersion = AttributeKey[String]("scala-version") val defaultExcludes: FileFilter = (".*" - ".") || HiddenFileFilter def descendents(base: PathFinder, select: FileFilter) = base.descendentsExcept(select, defaultExcludes) @@ -56,7 +57,7 @@ object MultiProject val target = crossPath(buildDir / "target", compilers.scalac.scalaInstance) val inputs = Compile.inputs(classpath, sources.getFiles.toSeq, target, Nil, Nil, javaSrcBase :: Nil, Compile.DefaultMaxErrors)(compilers, log) - val analysis = Compile(inputs) + val analysis = Compile(inputs, log) val info = ProjectInfo(None, base, projectDir, Nil, None)(configuration, analysis, inputs, load(configuration, log, externals), externals) val discovered = Build.discover(analysis, Some(false), Auto.Subclass, projectClassName) @@ -106,11 +107,11 @@ object MultiProject def makeContext(root: Project) = { - val contexts = topologicalSort(root) map { p => (p, ReflectiveContext(p, p.name)) } + val contexts = topologicalSort(root) map { p => (p, ReflectiveContext(p, p.name, root)) } val externals = root.info.externals def subs(f: Project => Seq[ProjectDependency]): Project => Seq[Project] = p => f(p) map( _.project match { case Left(path) => externals(path); case Right(proj) => proj } ) - MultiContext(contexts)(subs(_.aggregate), subs(_.dependencies) ) + MultiContext(contexts, root)(subs(_.aggregate), subs(_.dependencies) ) } def lefts[A,B](e: Seq[Either[A,B]]):Seq[A] = e collect { case Left(l) => l } @@ -132,8 +133,9 @@ object MultiContext for( (a,b) <- in) map(a) = b map } + def fun[A,B](map: collection.Map[A,B]): A => Option[B] = map get _ - def apply[Owner <: AnyRef](contexts: Iterable[(Owner, Context[Owner])])(agg: Owner => Iterable[Owner], deps: Owner => Iterable[Owner]): Context[Owner] = new Context[Owner] + def apply[Owner <: AnyRef](contexts: Iterable[(Owner, Context[Owner])], root: Owner)(agg: Owner => Iterable[Owner], deps: Owner => Iterable[Owner]): Context[Owner] = new Context[Owner] { val ownerReverse = (for( (owner, context) <- contexts; name <- context.ownerName(owner).toList) yield (name, owner) ).toMap val ownerMap = identityMap[Task[_],Owner]( for((owner, context) <- contexts; task <- context.allTasks(owner) ) yield (task, owner) ) @@ -142,6 +144,7 @@ object MultiContext val aggMap = subMap(agg) val depMap = subMap(deps) + def rootOwner: Owner = root def allTasks(owner: Owner): Iterable[Task[_]] = context(owner).allTasks(owner) def ownerForName(name: String): Option[Owner] = ownerReverse get name val staticName: Task[_] => Option[String] = t => owner(t) flatMap { o => context(o).staticName(t) } @@ -175,6 +178,7 @@ trait Project extends Tasked with HistoryEnabled with Member[Project] with Named implicit def streams = Dummy.Streams def input = Dummy.In def state = Dummy.State + def context = Dummy.Context def aggregate: Seq[ProjectDependency.Execution] = info.dependencies collect { case ex: ProjectDependency.Execution => ex } def dependencies: Seq[ProjectDependency.Classpath] = info.dependencies collect { case cp: ProjectDependency.Classpath => cp } @@ -184,10 +188,11 @@ trait Project extends Tasked with HistoryEnabled with Member[Project] with Named { import Dummy._ val context = MultiProject.makeContext(this) - val dummies = new Transform.Dummies(In, State, Streams) + val dummies = new Transform.Dummies(In, State, Streams, Context) def name(t: Task[_]): String = context.staticName(t.original) getOrElse std.Streams.name(t) val mklog = LogManager.construct(context, settings) - val actualStreams = std.Streams(t => context.owner(t.original).get.streamBase / name(t), mklog ) + def getOwner(t: Task[_]) = context.owner(t.original).getOrElse(error("No owner for " + name(t.original) + "\n\t" + t.original)) + val actualStreams = std.Streams(t => getOwner(t).streamBase / name(t), mklog ) val injected = new Transform.Injected( input, state, actualStreams ) context.static(this, transformName(input.name)) map { t => (t.merge.map(_ => state), Transform(dummies, injected, context) ) } } diff --git a/main/OutputTasks.scala b/main/OutputTasks.scala index 7a3ab0eb0..dedf316df 100644 --- a/main/OutputTasks.scala +++ b/main/OutputTasks.scala @@ -6,14 +6,17 @@ package sbt import std._ import Path._ import TaskExtra._ + import Types._ + import OutputUtil._ trait PrintTask { def input: Task[Input] - lazy val show = input flatMap { in => - val m = ReflectUtilities.allVals[Task[_]](this) + def context: Task[Transform.Context[Project]] + lazy val show = (input, context) flatMap { case in :+: ctx :+: HNil => val taskStrings = in.splitArgs map { name => - m(name).merge.map { + val selected = taskForName(ctx, name) + selected.merge.map { case Seq() => "No result for " + name case Seq( (conf, v) ) => name + ": " + v.toString case confs => confs map { case (conf, v) => conf + ": " + v } mkString(name + ":\n\t", "\n\t", "\n") @@ -27,20 +30,26 @@ trait LastOutput { def input: Task[Input] def streams: Task[TaskStreams] - lazy val last = (streams, input) flatMap { (s: TaskStreams, i: Input) => - val tasks = ReflectUtilities.allVals[Task[_]](this) - (s.readText( tasks(i.arguments) , update = false ) map { reader => + def context: Task[Transform.Context[Project]] + lazy val last = (streams, input, context) flatMap { case s :+: i :+: ctx :+: HNil => + val task = taskForName(ctx, i.arguments) + (s.readText( task, update = false ) map { reader => val out = s.text() def readL() { val line = reader.readLine() if(line ne null) { - readL() out.println(line) println(line) + readL() } } readL() }).merge } } +object OutputUtil +{ + def taskForName(ctx: Transform.Context[Project], name: String): Task[_] = + ctx.static(ctx.rootOwner, MultiProject.transformName(name)).getOrElse(error("No task '" + name + "'")) +} \ No newline at end of file diff --git a/main/SingleProject.scala b/main/SingleProject.scala index 3421c1158..893a2ba3d 100644 --- a/main/SingleProject.scala +++ b/main/SingleProject.scala @@ -10,50 +10,68 @@ package sbt import java.io.File -trait SingleProject extends Tasked with PrintTask with TaskExtra with Types -{ - def base = new File(".") - def streamBase = base / "streams" - - implicit def streams = Dummy.Streams - def input = Dummy.In - def state = Dummy.State - - type Task[T] = sbt.Task[T] - def act(input: Input, state: State): Option[(Task[State], Execute.NodeView[Task])] = - { - import Dummy._ - val context = ReflectiveContext(this, "project") - val dummies = new Transform.Dummies(In, State, Streams) - def name(t: Task[_]): String = context.staticName(t) getOrElse std.Streams.name(t) - val injected = new Transform.Injected( input, state, std.Streams(t => streamBase / name(t), (t, writer) => ConsoleLogger() ) ) - context.static(this, input.name) map { t => (t.merge.map(_ => state), Transform(dummies, injected, context) ) } - } - - def help: Seq[Help] = Nil -} object Dummy { val InName = "command-line-input" val StateName = "command-state" val StreamsName = "task-streams" + val ContextName = "task-context" def dummy[T](name: String): Task[T] = task( error("Dummy task '" + name + "' did not get converted to a full task.") ) named name val In = dummy[Input](InName) val State = dummy[State](StateName) val Streams = dummy[TaskStreams](StreamsName) + val Context = dummy[Transform.Context[Project]](ContextName) +} +/** A group of tasks, often related in some way. +* This trait should be used like: +* def testTasks(prefix: Option[String]) = new TestTasks(prefix) +* final class TestTasks(val prefix: Option[String]) extends TaskGroup { +* lazy val test = ... +* lazy val discover = ... +* } +* +* This allows a method to return multiple tasks that can still be referenced uniformly and directly: +* val test = testTasks(None) +* val it = testTasks(Some("integration")) +* +* > test +* ... run test.test ... +* > integration-test +* ... run it.test ... +*/ +trait TaskGroup +{ + def prefix: Option[String] + def tasks: immutable.SortedMap[String, Task[_]] = ReflectiveContext.deepTasks(this) } - object ReflectiveContext { - import Transform.Context - def apply[Owner <: AnyRef : Manifest](context: Owner, name: String): Context[Owner] = new Context[Owner] + def deepTasks(context: AnyRef): immutable.SortedMap[String, Task[_]] = { - private[sbt] lazy val tasks: immutable.SortedMap[String, Task[_]] = ReflectUtilities.allVals[Task[_]](context) + val direct = ReflectUtilities.allVals[Task[_]](context) + val groups = ReflectUtilities.allVals[TaskGroup](context) + val nested = groups.flatMap { case (name, group) => prefixAll(group.prefix, group.tasks) } + nested ++ direct // this order necessary so that direct shadows nested + } + def prefixAll(pre: Option[String], tasks: immutable.SortedMap[String, Task[_]]): immutable.SortedMap[String, Task[_]] = + pre match + { + case Some(p) if !p.isEmpty => prefix(p, tasks) + case _ => tasks + } + def prefix(p: String, tasks: Iterable[(String, Task[_])]): immutable.SortedMap[String, Task[_]] = + immutable.TreeMap[String, Task[_]]() ++ (tasks map { case (name, task) => (p + name.capitalize, task) }) + + import Transform.Context + def apply[Owner <: AnyRef : Manifest](context: Owner, name: String, root: Owner): Context[Owner] = new Context[Owner] + { + private[sbt] lazy val tasks: immutable.SortedMap[String, Task[_]] = deepTasks(context) private[sbt] lazy val reverseName: collection.Map[Task[_], String] = reverseMap(tasks) private[sbt] lazy val sub: Map[String, Owner] = ReflectUtilities.allVals[Owner](context) private[sbt] lazy val reverseSub: collection.Map[Owner, String] = reverseMap(sub) + def rootOwner = root val staticName: Task[_] => Option[String] = reverseName.get _ val ownerName = (o: Owner) => if(o eq context) Some(name) else None val owner = (t: Task[_]) => if(reverseName contains t) Some(context) else None diff --git a/main/Sync.scala b/main/Sync.scala new file mode 100644 index 000000000..5d9e13759 --- /dev/null +++ b/main/Sync.scala @@ -0,0 +1,92 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt + + import java.io.File + +/** +Maintains a set of mappings so that they are uptodate. +Specifically, 'apply' applies the mappings by creating target directories and copying source files to their destination. +For each mapping no longer present, the old target is removed. +Caution: Existing files are overwritten. +Caution: The removal of old targets assumes that nothing else has written to or modified those files. + It tries not to obliterate large amounts of data by only removing previously tracked files and empty directories. + That is, it won't remove a directory with unknown (untracked) files in it. +Warning: It is therefore inappropriate to use this with anything other than an automatically managed destination or a dedicated target directory. +Warning: Specifically, don't mix this with a directory containing manually created files, like sources. +It is safe to use for its intended purpose: copying resources to a class output directory. +*/ +object Sync +{ + def apply(cacheFile: File, inStyle: FileInfo.Style = FileInfo.lastModified, outStyle: FileInfo.Style = FileInfo.exists): Iterable[(File,File)] => Relation[File,File] = + mappings => + { + val relation = Relation.empty ++ mappings + noDuplicateTargets(relation) + val currentInfo = relation._1s.map(s => (s, inStyle(s)) ).toMap + + val (previousRelation, previousInfo) = readInfo(cacheFile)(inStyle.format) + val removeTargets = previousRelation._2s -- relation._2s + + def outofdate(source: File, target: File): Boolean = + !previousRelation.contains(source, target) || + (previousInfo get source) != (currentInfo get source) || + !target.exists || + target.isDirectory != source.isDirectory + + val updates = relation filter outofdate + + val (cleanDirs, cleanFiles) = (updates._2s ++ removeTargets).partition(_.isDirectory) + + IO.delete(cleanFiles) + IO.deleteIfEmpty(cleanDirs) + updates.all.foreach((copy _).tupled) + + writeInfo(cacheFile, relation, currentInfo)(inStyle.format) + relation + } + + def copy(source: File, target: File): Unit = + if(source.isFile) + IO.copyFile(source, target, true) + else if(!target.exists) // we don't want to update the last modified time of an existing directory + { + IO.createDirectory(target) + IO.copyLastModified(source, target) + } + + def noDuplicateTargets(relation: Relation[File, File]) + { + val dups = relation.reverseMap.collect { + case (target, srcs) if srcs.size >= 2 => + "\n\t" + target + "\nfrom\n\t" + srcs.mkString("\n\t") + } + if(!dups.isEmpty) + error("Duplicate mappings:" + dups.mkString) + } + + + import java.io.{File, IOException} + import sbinary._ + import Operations.{read, write} + import DefaultProtocol.{FileFormat => _, _} + import JavaIO._ + import inc.AnalysisFormats._ + + def writeInfo[F <: FileInfo](file: File, relation: Relation[File, File], info: Map[File, F])(implicit infoFormat: Format[F]): Unit = + IO.gzipFileOut(file) { out => + write(out, (relation, info) ) + } + + type RelationInfo[F] = (Relation[File,File], Map[File, F]) + + def readInfo[F <: FileInfo](file: File)(implicit infoFormat: Format[F]): RelationInfo[F] = + try { readUncaught(file)(infoFormat) } + catch { case e: IOException => (Relation.empty, Map.empty) } + + def readUncaught[F <: FileInfo](file: File)(implicit infoFormat: Format[F]): RelationInfo[F] = + IO.gzipFileIn(file) { in => + read[RelationInfo[F]](in) + } +} \ No newline at end of file diff --git a/tasks/standard/System.scala b/tasks/standard/System.scala index d07af40ff..9ea822a1b 100644 --- a/tasks/standard/System.scala +++ b/tasks/standard/System.scala @@ -15,13 +15,17 @@ object System implicit def to_~>| [K[_], V[_]](map: RMap[K,V]) : K ~>| V = new (K ~>| V) { def apply[T](k: K[T]): Option[V[T]] = map.get(k) } - def dummyMap[Input, State](dummyIn: Task[Input], dummyState: Task[State])(in: Input, state: State): Task ~>| Task = + def dummyMap[Input, State, Owner](dummyIn: Task[Input], dummyState: Task[State], dummyCtx: Task[Transform.Context[Owner]])( + in: Input, state: State, context: Transform.Context[Owner]): Task ~>| Task = { // helps ensure that the same Task[Nothing] can't be passed for dummyIn and dummyState - assert(dummyIn ne dummyState, "Dummy tasks for Input and State must be distinct.") + assert((dummyIn ne dummyState) + && (dummyState ne dummyCtx), "Dummy tasks for Input, State, and Context must be distinct.") + val pmap = new DelegatingPMap[Task, Task](new collection.mutable.ListMap) pmap(dummyIn) = fromDummyStrict(dummyIn, in) pmap(dummyState) = fromDummyStrict(dummyState, state) + pmap(dummyCtx) = fromDummyStrict(dummyCtx, context) pmap } @@ -103,10 +107,11 @@ object System } object Transform { - final class Dummies[Input, State](val dummyIn: Task[Input], val dummyState: Task[State], val dummyStreams: Task[TaskStreams]) + final class Dummies[Input, State, Owner](val dummyIn: Task[Input], val dummyState: Task[State], val dummyStreams: Task[TaskStreams], val dummyContext: Task[Context[Owner]]) final class Injected[Input, State](val in: Input, val state: State, val streams: Streams) trait Context[Owner] { + def rootOwner: Owner def staticName: Task[_] => Option[String] def owner: Task[_] => Option[Owner] def ownerName: Owner => Option[String] @@ -127,14 +132,14 @@ object Transform } } - def apply[Input, State, Owner](dummies: Dummies[Input, State], injected: Injected[Input, State], context: Context[Owner]) = + def apply[Input, State, Owner](dummies: Dummies[Input, State, Owner], injected: Injected[Input, State], context: Context[Owner]) = { import dummies._ import injected._ import context._ import System._ import Convert._ - val inputs = dummyMap(dummyIn, dummyState)(in, state) + val inputs = dummyMap(dummyIn, dummyState, dummyContext)(in, state, context) Convert.taskToNode ∙ setOriginal(streamed(streams, dummyStreams)) ∙ implied(owner, aggregate, static) ∙ setOriginal(name(staticName)) ∙ getOrId(inputs) } } diff --git a/util/collection/Relation.scala b/util/collection/Relation.scala index b305bb47c..c5195ffb7 100644 --- a/util/collection/Relation.scala +++ b/util/collection/Relation.scala @@ -29,7 +29,7 @@ object Relation private[sbt] def combine[X,Y](a: M[X,Y], b: M[X,Y]): M[X,Y] = (a /: b) { (map, mapping) => add(map, mapping._1, mapping._2) } - private[sbt] def add[X,Y](map: M[X,Y], from: X, to: Iterable[Y]): M[X,Y] = + private[sbt] def add[X,Y](map: M[X,Y], from: X, to: Traversable[Y]): M[X,Y] = map.updated(from, get(map, from) ++ to) private[sbt] def get[X,Y](map: M[X,Y], t: X): Set[Y] = map.getOrElse(t, Set.empty[Y]) @@ -49,15 +49,15 @@ trait Relation[A,B] /** Includes the relation (a, b). */ def +(a: A, b: B): Relation[A,B] /** Includes the relations (a, b) for all b in bs. */ - def +(a: A, bs: Iterable[B]): Relation[A,B] + def +(a: A, bs: Traversable[B]): Relation[A,B] /** Returns the union of the relation r with this relation. */ def ++(r: Relation[A,B]): Relation[A,B] /** Includes the given relations. */ - def ++(rs: Iterable[(A,B)]): Relation[A,B] + def ++(rs: Traversable[(A,B)]): Relation[A,B] /** Removes all relations (_1, _2) for all _1 in _1s. */ - def --(_1s: Iterable[A]): Relation[A,B] + def --(_1s: Traversable[A]): Relation[A,B] /** Removes all `pairs` from this relation. */ - def --(pairs: Traversable[(A,B)]): Relation[A,B] + def --(pairs: TraversableOnce[(A,B)]): Relation[A,B] /** Removes all pairs (_1, _2) from this relation. */ def -(_1: A): Relation[A,B] /** Removes `pair` from this relation. */ @@ -69,6 +69,11 @@ trait Relation[A,B] /** Returns the number of pairs in this relation */ def size: Int + /** Returns true iff (a,b) is in this relation*/ + def contains(a: A, b: B): Boolean + /** Returns a relation with only pairs (a,b) for which f(a,b) is true.*/ + def filter(f: (A,B) => Boolean): Relation[A,B] + /** Returns all pairs in this relation.*/ def all: Traversable[(A,B)] @@ -92,14 +97,14 @@ private final class MRelation[A,B](fwd: Map[A, Set[B]], rev: Map[B, Set[A]]) ext def +(pair: (A,B)) = this + (pair._1, Set(pair._2)) def +(from: A, to: B) = this + (from, to :: Nil) - def +(from: A, to: Iterable[B]) = + def +(from: A, to: Traversable[B]) = new MRelation( add(fwd, from, to), (rev /: to) { (map, t) => add(map, t, from :: Nil) }) - def ++(rs: Iterable[(A,B)]) = ((this: Relation[A,B]) /: rs) { _ + _ } + def ++(rs: Traversable[(A,B)]) = ((this: Relation[A,B]) /: rs) { _ + _ } def ++(other: Relation[A,B]) = new MRelation[A,B]( combine(fwd, other.forwardMap), combine(rev, other.reverseMap) ) - def --(ts: Iterable[A]): Relation[A,B] = ((this: Relation[A,B]) /: ts) { _ - _ } - def --(pairs: Traversable[(A,B)]): Relation[A,B] = ((this: Relation[A,B]) /: pairs) { _ - _ } + def --(ts: Traversable[A]): Relation[A,B] = ((this: Relation[A,B]) /: ts) { _ - _ } + def --(pairs: TraversableOnce[(A,B)]): Relation[A,B] = ((this: Relation[A,B]) /: pairs) { _ - _ } def -(pair: (A,B)): Relation[A,B] = new MRelation( remove(fwd, pair._1, pair._2), remove(rev, pair._2, pair._1) ) def -(t: A): Relation[A,B] = @@ -110,5 +115,9 @@ private final class MRelation[A,B](fwd: Map[A, Set[B]], rev: Map[B, Set[A]]) ext case None => this } + def filter(f: (A,B) => Boolean): Relation[A,B] = Relation.empty[A,B] ++ all.filter(f.tupled) + + def contains(a: A, b: B): Boolean = forward(a)(b) + override def toString = all.map { case (a,b) => a + " -> " + b }.mkString("Relation [", ", ", "]") } \ No newline at end of file