diff --git a/main/CommandSupport.scala b/main/CommandSupport.scala index c3d05a32d..a2308834c 100644 --- a/main/CommandSupport.scala +++ b/main/CommandSupport.scala @@ -166,6 +166,10 @@ CompileSyntax + """ def LoadCommand = "load-commands" def LoadCommandLabel = "commands" + def LoadProject = "loadp" + def LoadProjectBrief = (LoadProject, LoadProjectDetailed) + def LoadProjectDetailed = "Loads the project in the current directory" + def Shell = "shell" def ShellBrief = (Shell, ShellDetailed) def ShellDetailed = "Provides an interactive prompt from which commands can be run." diff --git a/main/Compile.scala b/main/Compile.scala index 4e4322eb6..018257fc3 100644 --- a/main/Compile.scala +++ b/main/Compile.scala @@ -10,6 +10,8 @@ package sbt object Compile { + val DefaultMaxErrors = 100 + final class Inputs(val compilers: Compilers, val config: Options, val incSetup: IncSetup, val log: Logger) 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 cacheDirectory: File) @@ -30,7 +32,13 @@ object Compile new IncSetup(javaSrcBases, cacheDirectory), log ) - + + def compilers(implicit app: AppConfiguration, log: Logger): Compilers = + { + val scalaProvider = app.provider.scalaProvider + compilers(ScalaInstance(scalaProvider.version, scalaProvider.launcher)) + } + def compilers(instance: ScalaInstance)(implicit app: AppConfiguration, log: Logger): Compilers = compilers(instance, ClasspathOptions.auto) diff --git a/main/Main.scala b/main/Main.scala index af1dae7b9..1a3e60f7a 100644 --- a/main/Main.scala +++ b/main/Main.scala @@ -228,6 +228,13 @@ object Commands case Left(e) => handleException(e, s, false) } } + + def loadProject = Command.simple(LoadProject, LoadProjectBrief, LoadProjectDetailed) { (in, s) => + val base = s.configuration.baseDirectory + val p = MultiProject.load(s.configuration, ConsoleLogger() /*TODO*/)(base) + val exts = p match { case pc: ProjectContainer => MultiProject.loadExternals(pc :: Nil, p.info.construct); case _ => Map.empty[File, Project] } + s.copy(project = p)().put(MultiProject.ExternalProjects, exts.updated(base, p)) + } def handleException(e: Throwable, s: State, trace: Boolean = true): State = { // TODO: log instead of print diff --git a/main/MultiProject.scala b/main/MultiProject.scala new file mode 100644 index 000000000..5a190f241 --- /dev/null +++ b/main/MultiProject.scala @@ -0,0 +1,173 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt + +import std._ +import Path._ +import TaskExtra._ +import GlobFilter._ +import inc.Analysis +import build.{Auto, Build} +import xsbti.AppConfiguration + +import java.io.File + +object MultiProject +{ + val ExternalProjects = AttributeKey[Map[File, Project]]("external-projects") + + val defaultExcludes: FileFilter = (".*" - ".") || HiddenFileFilter + def descendents(base: PathFinder, select: FileFilter) = base.descendentsExcept(select, defaultExcludes) + def children(base: PathFinder, select: FileFilter) = base * (select -- defaultExcludes) + def projectClassName = classOf[Project].getName + + def projectStandard(base: Path) = base / "project" + def projectHidden(base: Path) = base / ".sbt" + def selectProjectDir(base: Path) = + { + val a = projectHidden(base) + val b = projectStandard(base) + if(a.exists) a else b + } + + def crossPath(base: Path, instance: ScalaInstance) = base / ("scala_" + instance.actualVersion) + + def construct[T <: AnyRef](arg: T)(implicit mf: Manifest[T]): Class[_] => Any = clazz => + Build.constructor(clazz, mf.erasure) match { + case Some(c) => c.newInstance(arg) + case None => error("Couldn't find constructor with one argument of type " + mf.toString) + } + + def load(configuration: AppConfiguration, log: Logger)(base: File): Project = + { + val projectDir = selectProjectDir(base) + val buildDir = projectDir / "build" + val srcMain = buildDir / "src" / "main" + val javaSrcBase = srcMain / "java" + val sources = children(buildDir +++ projectDir, "*.scala") +++ descendents(buildDir / "scala" +++ buildDir / "java", "*.scala" | "*.java") + val classpath = configuration.provider.mainClasspath.toSeq + val compilers = Compile.compilers(configuration, log) + 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 info = ProjectInfo(None, base, projectDir, Nil, None)(configuration, analysis, inputs, load(configuration, log)) + + val discovered = Build.check(Build.discover(analysis, Some(false), Auto.Subclass, projectClassName), false) + Build.binaries(inputs.config.classpath, discovered, getClass.getClassLoader)(construct(info)).head.asInstanceOf[Project] + } + + def loadExternals[P](from: Iterable[ProjectContainer], loadImpl: File => P): Map[File, P] = + { + def load(loaded: Map[File, P], file: File): Map[File, P] = + (loaded get file) match + { + case None => doLoad(loaded, file) + case Some(p) => loaded + } + def doLoad(loaded: Map[File, P], file: File): Map[File, P] = + { + val loadedProject = loadImpl(file) + val newMap = loaded.updated(file, loadedProject) + loadedProject match + { + case container: ProjectContainer => loadAll( externals(container :: Nil), loaded ) + case _ => newMap + } + } + def loadAll(files: Set[File], loaded: Map[File, P]): Map[File, P] = (loaded /: files)(load) + + loadAll( externals(from) , Map.empty) + } + + def externals(containers: Iterable[ProjectContainer]): Set[File] = + { + def exts(containers: Iterable[ProjectContainer]): Iterable[File] = + containers flatMap { container => container.externalProjects ++ exts(container.internalProjects) } + exts(containers).toSet + } + + def makeContext(root: Project, state: State) = + { + val allProjects = ProjectContainer.topologicalSort(root, state) + val names = allProjects.map { p: Project => (p, p.name) }.toMap + //val tasks = allProjects map { p => + ReflectiveContext(root, names.get _) + } +} + +/*trait MultiContext[Owner] extends Context[Owner] +{ + def ownerForName(name: String): Option[Owner] + def allTasks(owner: Owner): Iterable[Task[_]] + def externalProject(base: File): Project +}*/ + +trait ProjectContainer +{ + /** All projects that are loaded from their base directory instead of being defined in this container's compilation set. + * This is for any contained projects, including execution and classpath dependencies. */ + def externalProjects: Iterable[File] + /** All projects directly contained in this that are defined in this container's compilation set. + * This is for any contained projects, including execution and classpath dependencies, but not external projects. */ + def internalProjects: Iterable[Project] +} + +object ProjectContainer +{ + def internalTopologicalSort(root: Project): Seq[Project] = + Dag.topologicalSort(root) { _.internalProjects } + + def topologicalSort(root: Project, state: State): Seq[Project] = topologicalSort(root)(state get MultiProject.ExternalProjects getOrElse Map.empty) + def topologicalSort(root: Project)(implicit resolveExternal: File => Project): Seq[Project] = + Dag.topologicalSort(root) { p => + (p.externalProjects map resolveExternal) ++ p.internalProjects + } +} + +trait Project extends Tasked with ProjectContainer +{ + val info: ProjectInfo + def name: String = info.name.get + + def base = info.projectDirectory + def outputRootPath = base / "target" + def streamBase = outputRootPath / "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 = MultiProject.makeContext(this, state) + 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.forName(input.name) map { t => (t.merge.map(_ => state), Transform(dummies, injected, context) ) } + } + + def help: Seq[Help] = Nil +} + +trait ProjectConstructors +{ + val info: ProjectInfo + def project(base: Path, name: String, deps: ProjectDependency*): Project + def project[P <: Project](path: Path, name: String, builderClass: Class[P], deps: ProjectDependency*): P + def project[P <: Project](path: Path, name: String, construct: ProjectInfo => P, deps: ProjectDependency*): P + def project(base: Path): ExternalProject = new ExternalProject(base) + + implicit def defaultProjectDependency(p: Project): ProjectDependency = new ProjectDependency(p, None) + implicit def dependencyConstructor(p: Project): ProjectDependencyConstructor = new ProjectDependencyConstructor { + def %(conf: String) = new ProjectDependency(p, Some(conf)) + } +} +final class ProjectDependency(val project: Project, val configuration: Option[String]) +sealed trait ProjectDependencyConstructor { + def %(conf: String): ProjectDependency +} +final class ExternalProject(val path: Path) \ No newline at end of file diff --git a/main/ProjectInfo.scala b/main/ProjectInfo.scala new file mode 100644 index 000000000..29cec563f --- /dev/null +++ b/main/ProjectInfo.scala @@ -0,0 +1,31 @@ +/* sbt -- Simple Build Tool + * Copyright 2008, 2010 Mark Harrah + */ +package sbt + +import java.io.File +import xsbti.{AppConfiguration, AppProvider, ScalaProvider} +import inc.Analysis + +/** Represents the minimal information necessary to construct a Project. +* +* `projectDirectory` is the base directory for the project (not the root project directory) +* `builderPath` is the base directory for the project (not the root project directory) +* `dependencies` are the Projects that this Project depends on. +* `parent` is the parent Project, or None if this is the root project. +* `buildScalaVersion` contains the explicitly requested Scala version to use for building (as when using `+` or `++`) or None if the normal version should be used. +*/ +final case class ProjectInfo(name: Option[String], projectDirectory: File, builderDir: File, dependencies: Iterable[Project], parent: Option[Project])( + val configuration: AppConfiguration, val analysis: Analysis, val compileInputs: Compile.Inputs, val construct: File => Project) +{ + def app = configuration.provider + /** The version of Scala running sbt.*/ + def definitionScalaVersion = app.scalaProvider.version + /** The launcher instance that booted sbt.*/ + def launcher = app.scalaProvider.launcher +} + +object ProjectInfo +{ + val MetadataDirectoryName = "project" +} \ No newline at end of file