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