first shot at general command/definition model

This commit is contained in:
Mark Harrah 2010-07-17 12:07:41 -04:00
parent d0fa1eb461
commit d7b66458f2
19 changed files with 469 additions and 39 deletions

View File

@ -0,0 +1,11 @@
/* sbt -- Simple Build Tool
* Copyright 2010 Mark Harrah
*/
package sbt
package inc
final case class Discovered(baseClasses: Set[String], annotations: Set[String], hasMain: Boolean, isModule: Boolean)
object Discovered
{
def empty = new Discovered(Set.empty, Set.empty, false, false)
}

View File

@ -86,9 +86,4 @@ object Discovery
}
def isUnit(t: Type): Boolean = named(t, "scala.Unit")
}
final case class Discovered(baseClasses: Set[String], annotations: Set[String], hasMain: Boolean, isModule: Boolean)
object Discovered
{
def empty = new Discovered(Set.empty, Set.empty, false, false)
}

View File

@ -11,6 +11,7 @@ import java.io.File
object Incremental
{
def println(s: String) = ()
def compile(sources: Set[File], previous: Analysis, current: ReadStamps, externalAPI: String => Source, doCompile: Set[File] => Analysis)(implicit equivS: Equiv[Stamp]): Analysis =
{
val initialChanges = changedInitial(sources, previous.stamps, previous.apis, current, externalAPI)

View File

@ -206,6 +206,8 @@ final class API(val global: Global, val callback: xsbti.AnalysisCallback) extend
private def processType(t: Type): xsbti.api.Type =
{
class TypeCompat { def dealias = t } // 2.7.7 compatibility: don't bother dealiasing
implicit def compat(t: Type): TypeCompat = new TypeCompat
t.dealias match
{
case NoPrefix => Constants.emptyType

View File

@ -1,5 +1,5 @@
[scala]
version: 2.7.7
version: 2.8.0
#classifiers: sources, javadoc
[app]
@ -33,8 +33,8 @@
project.name: quick=set(test), new=prompt(Name), fill=prompt(Name)
project.organization: new=prompt(Organization)
project.version: quick=set(1.0), new=prompt(Version)[1.0], fill=prompt(Version)[1.0]
build.scala.versions: quick=set(2.7.7), new=prompt(Scala version)[2.7.7], fill=prompt(Scala version)[2.7.7]
sbt.version: quick=set(0.7.4), new=prompt(sbt version)[0.7.4], fill=prompt(sbt version)[0.7.4]
build.scala.versions: quick=set(2.8.0), new=prompt(Scala version)[2.8.0], fill=prompt(Scala version)[2.8.0]
sbt.version: quick=set(0.9.0-SNAPSHOT), new=prompt(sbt version)[0.9.0-SNAPSHOT], fill=prompt(sbt version)[0.9.0-SNAPSHOT]
project.scratch: quick=set(true)
project.initialize: quick=set(true), new=set(true)

View File

@ -4,6 +4,7 @@
package sbt
import sbt.compile.{AnalyzingCompiler, JavaCompiler}
import sbt.build.AggressiveCompile
import java.io.File
import System.{currentTimeMillis => now}
import Path._

46
main/Command.scala Normal file
View File

@ -0,0 +1,46 @@
/* sbt -- Simple Build Tool
* Copyright 2009, 2010 Mark Harrah
*/
package sbt
sealed trait Command
{
def applies: PartialFunction[State, Apply]
}
trait Apply
{
def help: Seq[(String,String)]
def run: PartialFunction[Input, State]
}
object Command
{
def apply(f: PartialFunction[State, Apply]): Command =
new Command { def applies = f }
def simple(name: String, help: (String, String)*)(f: State => State): Command =
apply { case s => Apply(help) { case in if in.line == name => f(s) }}
}
object Apply
{
def apply(h: Seq[(String,String)])(r: PartialFunction[Input, State]): Apply =
new Apply { def help = h; def run = r }
}
trait Logged
{
def log: Logger
}
trait HistoryEnabled
{
def historyPath: Option[Path]
}
final case class Input(line: String)
{
def name: String = error("TODO")
def arguments: String = error("TODO")
}
object Next extends Enumeration {
val Reload, Fail, Done, Continue = Value
}

100
main/Main.scala Normal file
View File

@ -0,0 +1,100 @@
/* sbt -- Simple Build Tool
* Copyright 2008, 2009, 2010 Mark Harrah
*/
package sbt
import complete.HistoryCommands
import HistoryCommands.{Start => HistoryPrefix}
import sbt.build.{AggressiveCompile, Build, BuildException, Parse, ParseException}
import scala.annotation.tailrec
/** This class is the entry point for sbt.*/
class xMain extends xsbti.AppMain
{
final def run(configuration: xsbti.AppConfiguration): xsbti.MainResult =
{
import Commands._
val initialCommands = Seq(help, history, exit, load)
val state = State( () )( configuration, initialCommands, Set.empty, None, configuration.arguments.map(_.trim).toList, Next.Continue )
run(state)
}
@tailrec final def run(state: State): xsbti.MainResult =
{
import Next._
state.next match
{
case Continue => run(next(state))
case Fail => Exit(1)
case Done => Exit(0)
case Reload =>
val app = state.configuration.provider
new Reboot(app.scalaProvider.version, state.commands, app.id, state.configuration.baseDirectory)
}
}
def next(state: State): State = state.process(process)
def process(command: String, state: State): State =
{
val in = Input(command)
Commands.applicable(state).flatMap( _.run.lift(in) ).headOption.getOrElse {
System.err.println("Unknown command '" + command + "'")
state.fail
}
}
}
object Commands
{
def applicable(state: State): Stream[Apply] =
state.processors.toStream.flatMap(_.applies.lift(state) )
def help = Command.simple("help", ("help", "Displays this help message.")) { s =>
val message = applicable(s).flatMap(_.help).map { case (a,b) => a + " : " + b }.mkString("\n")
System.out.println(message)
s
}
def history = Command { case s @ State(p: HistoryEnabled with Logged) =>
Apply(HistoryCommands.descriptions) {
case in if in.line startsWith("!") =>
HistoryCommands(in.line.substring(HistoryPrefix.length).trim, p.historyPath, 500/*JLine.MaxHistorySize*/, p.log.error _) match
{
case Some(commands) =>
commands.foreach(println) //better to print it than to log it
(commands ::: s).continue
case None => s.fail
}
}
}
def helpExit = (TerminateActions.mkString(", "), "Terminates the build.")
def exit = Command { case s => Apply(helpExit :: Nil) {
case Input(line) if TerminateActions contains line =>
s.exit(true)
}
}
def load = Command { case s => Apply(Nil) {
case Input(line) if line.startsWith("load") =>
loadCommand(line, s.configuration) match
{
case Right(newValue) =>
ExitHooks.runExitHooks(s.exitHooks.toSeq)
s.copy(project = newValue)(exitHooks = Set.empty)
case Left(e) => e.printStackTrace; System.err.println(e.toString); s.fail // TODO: log instead of print
}
}
}
def loadCommand(line: String, configuration: xsbti.AppConfiguration): Either[Throwable, Any] =
try { Right( Build( Parse(line)(configuration.baseDirectory), configuration ) ) }
catch { case e @ (_: ParseException | _: BuildException | _: xsbti.CompileFailed) => Left(e) }
val Exit = "exit"
val Quit = "quit"
/** The list of lowercase command names that may be used to terminate the program.*/
val TerminateActions: Seq[String] = Seq(Exit, Quit)
}

View File

@ -9,7 +9,7 @@ private case class Exit(code: Int) extends xsbti.Exit
{
require(code >= 0)
}
private class Reboot(val scalaVersion: String, argsList: List[String], val app: xsbti.ApplicationID, val baseDirectory: File) extends xsbti.Reboot
private class Reboot(val scalaVersion: String, argsList: Seq[String], val app: xsbti.ApplicationID, val baseDirectory: File) extends xsbti.Reboot
{
def arguments = argsList.toArray
}
@ -24,8 +24,4 @@ private class ApplicationID(delegate: xsbti.ApplicationID, newVersion: String) e
def crossVersioned = delegate.crossVersioned
def classpathExtra = delegate.classpathExtra
}
private final class ReloadException(val remainingArguments: List[String], val buildScalaVersion: Option[String]) extends RuntimeException
{
override def fillInStackTrace = this
}

46
main/State.scala Normal file
View File

@ -0,0 +1,46 @@
/* sbt -- Simple Build Tool
* Copyright 2008, 2009, 2010 Mark Harrah
*/
package sbt
case class State(project: Any)(
val configuration: xsbti.AppConfiguration,
val processors: Seq[Command],
val exitHooks: Set[ExitHook],
val onFailure: Option[String],
val commands: Seq[String],
val next: Next.Value
)
trait StateOps {
def process(f: (String, State) => State): State
def ::: (commands: Seq[String]): State
def :: (command: String): State
def continue: State
def reload: State
def exit(ok: Boolean): State
def fail: State
}
object State
{
implicit def stateOps(s: State): StateOps = new StateOps {
def process(f: (String, State) => State): State =
s.commands match {
case x :: xs => f(x, s.copy()(commands = xs))
case Nil => exit(true)
}
s.copy()(commands = s.commands.drop(1))
def ::: (newCommands: Seq[String]): State = s.copy()(commands = newCommands ++ s.commands)
def :: (command: String): State = s.copy()(commands = command +: s.commands)
def setNext(n: Next.Value) = s.copy()(next = n)
def continue = setNext(Next.Continue)
def reload = setNext(Next.Reload)
def exit(ok: Boolean) = setNext(if(ok) Next.Fail else Next.Done)
def fail =
s.onFailure match
{
case Some(c) => s.copy()(commands = c :: Nil, onFailure = None)
case None => exit(ok = false)
}
}
}

View File

@ -2,6 +2,7 @@
* Copyright 2010 Mark Harrah
*/
package sbt
package build
import inc._

98
main/build/Build.scala Normal file
View File

@ -0,0 +1,98 @@
/* sbt -- Simple Build Tool
* Copyright 2010 Mark Harrah
*/
package sbt
package build
import java.io.File
import classpath.ClasspathUtilities.toLoader
import ModuleUtilities.getObject
import compile.{AnalyzingCompiler, JavaCompiler}
import Path._
final class BuildException(msg: String) extends RuntimeException(msg)
object Build
{
def loader(configuration: xsbti.AppConfiguration): ClassLoader =
configuration.provider.mainClass.getClassLoader
def apply(command: LoadCommand, configuration: xsbti.AppConfiguration): Any =
command match
{
case BinaryLoad(classpath, module, name) =>
binary(classpath, module, name, loader(configuration))
case SourceLoad(classpath, sourcepath, output, module, auto, name) =>
source(classpath, sourcepath, output, module, auto, name, configuration)
case _ => error("Not implemented yet")
}
def binary(classpath: Seq[File], module: Boolean, name: String, parent: ClassLoader): Any =
{
if(name.isEmpty)
error("Class name required to load binary project.")
else
{
val loader = toLoader(classpath, parent)
if(module)
getObject(name, loader)
else
{
val clazz = Class.forName(name, true, loader)
clazz.newInstance
}
}
}
def source(classpath: Seq[File], sources: Seq[File], output: Option[File], module: Boolean, auto: Auto.Value, name: String, configuration: xsbti.AppConfiguration): Any =
{
// TODO: accept Logger as an argument
val log = new ConsoleLogger with Logger with sbt.IvyLogger
val scalaProvider = configuration.provider.scalaProvider
val launcher = scalaProvider.launcher
val instance = ScalaInstance(scalaProvider.version, launcher)
val out = output.getOrElse(configuration.baseDirectory / "target" asFile)
val target = out / ("scala_" + instance.actualVersion)
val outputDirectory = target / "classes"
val cacheDirectory = target / "cache"
val projectClasspath = outputDirectory.asFile +: classpath
val compileClasspath = projectClasspath ++ configuration.provider.mainClasspath.toSeq
val componentManager = new ComponentManager(launcher.globalLock, configuration.provider.components, log)
val compiler = new AnalyzingCompiler(instance, componentManager, log)
val javac = JavaCompiler.directOrFork(compiler.cp, compiler.scalaInstance)( (args: Seq[String], log: Logger) => Process("javac", args) ! log )
val agg = new AggressiveCompile(cacheDirectory)
val analysis = agg(compiler, javac, sources, compileClasspath, outputDirectory, Nil, Nil)(log)
val discovered = discover(analysis, module, auto, name)
load(discovered)(x => binary(projectClasspath, module, x, loader(configuration)) )
}
def discover(analysis: inc.Analysis, module: Boolean, auto: Auto.Value, name: String): Seq[String] =
{
import Auto.{Annotation, Explicit, Subclass}
auto match {
case Explicit => if(name.isEmpty) error("No name specified to load explicitly.") else Seq(name)
case Subclass => discover(analysis, module, new inc.Discovery(Set(name), Set.empty))
case Annotation => discover(analysis, module, new inc.Discovery(Set.empty, Set(name)))
}
}
def discover(analysis: inc.Analysis, module: Boolean, discovery: inc.Discovery): Seq[String] =
{
for(src <- analysis.apis.internal.values.toSeq;
(df, found) <- discovery(src.definitions) if found.isModule == module)
yield
df.name
}
def load(discovered: Seq[String])(doLoad: String => Any): Any =
discovered match
{
case Seq() => error("No project found")
case Seq(x) => doLoad(x)
case xs => error("Multiple projects found: " + discovered.mkString(", "))
}
def error(msg: String) = throw new BuildException(msg)
}

View File

@ -0,0 +1,18 @@
/* sbt -- Simple Build Tool
* Copyright 2010 Mark Harrah
*/
package sbt
package build
import java.io.File
sealed trait LoadCommand
final case class BinaryLoad(classpath: Seq[File], module: Boolean, name: String) extends LoadCommand
final case class SourceLoad(classpath: Seq[File], sourcepath: Seq[File], output: Option[File], module: Boolean, auto: Auto.Value, name: String) extends LoadCommand
final case class ProjectLoad(base: File, name: String) extends LoadCommand
object Auto extends Enumeration
{
val Subclass, Annotation, Explicit = Value
}

104
main/build/Parse.scala Normal file
View File

@ -0,0 +1,104 @@
/* sbt -- Simple Build Tool
* Copyright 2010 Mark Harrah
*/
package sbt
package build
import java.io.File
final class ParseException(msg: String) extends RuntimeException(msg)
/** Parses a load command.
*
* load ::= 'load' (binary | source | project)
*
* binary ::= classpath module name
* source ::= classpath '-src' paths ('-d' dir)? ('-auto' ('sub' | 'annot'))? module name
* project ::= ('-project' path)? name?
*
* name ::= '-name' nameString
* module ::= ('-module' ('true'|'false') )?
* classpath ::= '-cp' paths
* path ::= pathChar+
* paths ::= path (pathSep path)*
*/
object Parse
{
import File.{pathSeparatorChar => sep}
def error(msg: String) = throw new ParseException(msg)
def apply(commandString: String)(implicit base: File): LoadCommand =
{
val tokens = commandString.split("""\s+""").toSeq
if(tokens.isEmpty) error("Empty command")
else if(tokens.head != "load") error("Not a load command")
else
{
val args = tokens.drop(1)
val srcs = sourcepath(args)
val nme = name(args)
lazy val cp = classpath(args)
lazy val mod = module(args)
lazy val proj = project(args).getOrElse(base)
if(!srcs.isEmpty)
SourceLoad(cp, srcs, output(args), mod, auto(args), nme)
else if(!cp.isEmpty)
BinaryLoad(cp, mod, nme)
else
ProjectLoad(proj, nme)
}
}
def auto(args: Seq[String]): Auto.Value =
getArg(args, "auto") match {
case None => Auto.Explicit
case Some("sub") => Auto.Subclass
case Some("annot") => Auto.Annotation
case Some(x) => error("Illegal auto argument '" + x + "'")
}
def module(args: Seq[String]): Boolean =
getArg(args, "module") match {
case None | Some("false") => false
case Some("true") => true
case Some(x) => error("Expected boolean, got '" + x + "'")
}
def name(args: Seq[String]): String =
getArg(args, "name") getOrElse("")
def output(args: Seq[String])(implicit base: File): Option[File] =
getArg(args, "d") map file(base)
def project(args: Seq[String])(implicit base: File): Option[File] =
getArg(args, "project") map file(base)
def pathArg(args: Seq[String], name: String)(implicit base: File): Seq[File] =
getArg(args, name).toSeq flatMap paths
def classpath(args: Seq[String])(implicit base: File): Seq[File] = pathArg(args, "cp")
def sourcepath(args: Seq[String])(implicit base: File): Seq[File] = pathArg(args, "src")
def getArg(args: Seq[String], name: String): Option[String] =
{
val opt = "-" + name
val found = args.dropWhile(_ != opt)
if(found.isEmpty)
None
else
found.drop(1).headOption match
{
case x @ Some(arg) if !arg.startsWith("-") => x
case _ => error("No argument provided for -" + name)
}
}
def paths(implicit base: File): String => Seq[File] =
_ split sep map file(base)
def file(base: File) = (path: String) => Path.fromString(base, path).asFile
}

View File

@ -2,4 +2,4 @@ project.organization=org.scala-tools.sbt
project.name=xsbt
sbt.version=0.7.4
project.version=0.9.0-SNAPSHOT
build.scala.versions=2.8.0.RC6
build.scala.versions=2.8.0

View File

@ -40,11 +40,14 @@ class XSbt(info: ProjectInfo) extends ParentProject(info) with NoCrossPaths
val compilerSub = project(compilePath, "Compile", new CompileProject(_),
launchInterfaceSub, interfaceSub, ivySub, ioSub, classpathSub, compileInterfaceSub, logSub)
val altCompilerSub = baseProject("main", "Alternate Compiler Test",
classfileSub, compileIncrementalSub, compilerSub, ioSub, logSub, discoverySub, compilePersistSub, processSub)
val buildSub = baseProject("main" / "build", "Project Builder",
classfileSub, classpathSub, compilePersistSub, compilerSub, compileIncrementalSub, interfaceSub, ivySub, launchInterfaceSub, logSub, discoverySub, processSub)
val altCompilerSub = project("main", "Alternate Compiler Test", (i: ProjectInfo) => new Base(i) { override def normalizedName = "sbt" }, // temporary
buildSub, compileIncrementalSub, compilerSub, completeSub, discoverySub, ioSub, logSub, processSub)
/** following modules are not updated for 2.8 or 0.9 */
val testSub = project("scripted", "Test", new TestProject(_), ioSub)
/*val testSub = project("scripted", "Test", new TestProject(_), ioSub)
val trackingSub = baseProject(cachePath / "tracking", "Tracking", cacheSub)
@ -53,7 +56,7 @@ class XSbt(info: ProjectInfo) extends ParentProject(info) with NoCrossPaths
val installerSub = project(sbtPath / "install", "Installer", new InstallerProject(_) {}, sbtSub)
lazy val dist = task { None } dependsOn(launchSub.proguard, sbtSub.publishLocal, installerSub.publishLocal)
lazy val dist = task { None } dependsOn(launchSub.proguard, sbtSub.publishLocal, installerSub.publishLocal)*/
def baseProject(path: Path, name: String, deps: Project*) = project(path, name, new Base(_), deps : _*)
def testedBase(path: Path, name: String, deps: Project*) = project(path, name, new TestedBase(_), deps : _*)
@ -131,8 +134,7 @@ class XSbt(info: ProjectInfo) extends ParentProject(info) with NoCrossPaths
trait TestDependencies extends Project
{
val sc = "org.scala-tools.testing" %% "scalacheck" % "1.7" % "test"
val sp = "org.scala-tools.testing" %% "specs" % "1.6.5-SNAPSHOT" % "test"
val snaps = ScalaToolsSnapshots
val sp = "org.scala-tools.testing" %% "specs" % "1.6.5" % "test"
}
class LogProject(info: ProjectInfo) extends Base(info) with TestDependencies
{
@ -144,7 +146,7 @@ class XSbt(info: ProjectInfo) extends ParentProject(info) with NoCrossPaths
{
// these compilation options are useful for debugging caches and task composition
//override def compileOptions = super.compileOptions ++ List(Unchecked,ExplainTypes, CompileOption("-Xlog-implicits"))
val sbinary = "org.scala-tools.sbinary" %% "sbinary" % "0.3.1-SNAPSHOT"
val sbinary = "org.scala-tools.sbinary" %% "sbinary" % "0.3.1"
}
class Base(info: ProjectInfo) extends DefaultProject(info) with ManagedBase with Component with Licensed
{
@ -246,7 +248,8 @@ class XSbt(info: ProjectInfo) extends ParentProject(info) with NoCrossPaths
// sub projects for each version of Scala to precompile against other than the one sbt is built against
// each sub project here will add ~100k to the download
lazy val precompiled28 = precompiledSub("2.8.0.RC6")
//lazy val precompiled28 = precompiledSub("2.8.0")
lazy val precompiled27 = precompiledSub("2.7.7")
def precompiledSub(v: String) =
project(info.projectPath, "Precompiled " + v, new Precompiled(v)(_), cip.info.dependencies.toSeq : _* /*doesn't include subprojects of cip*/ )
@ -261,6 +264,7 @@ class XSbt(info: ProjectInfo) extends ParentProject(info) with NoCrossPaths
* subproject does not depend on any Scala subprojects, so mixing versions is not a problem. */
override def compileClasspath = cip.compileClasspath --- cip.mainUnmanagedClasspath +++ mainUnmanagedClasspath
override def compileOptions = Nil
// these ensure that the classes compiled against other versions of Scala are not exported (for compilation/testing/...)
override def projectClasspath(config: Configuration) = Path.emptyPathFinder
}

20
run/Console.scala Normal file
View File

@ -0,0 +1,20 @@
/* sbt -- Simple Build Tool
* Copyright 2008, 2009 Mark Harrah
*/
/*package sbt
import java.io.File
import compile.AnalyzingCompiler
final class Console(compiler: AnalyzingCompiler) extends NotNull
{
/** Starts an interactive scala interpreter session with the given classpath.*/
def apply(classpath: Iterable[File], log: Logger): Option[String] =
apply(classpath, Nil, "", log)
def apply(classpath: Iterable[File], options: Seq[String], initialCommands: String, log: Logger): Option[String] =
{
def console0 = compiler.console(Path.getFiles(classpath), options, initialCommands, log)
JLine.withJLine( Run.executeTrapExit(console0, log) )
}
}*/

View File

@ -12,21 +12,11 @@ trait ExitHook extends NotNull
def runBeforeExiting(): Unit
}
trait ExitHookRegistry
object ExitHooks
{
def register(hook: ExitHook): Unit
def unregister(hook: ExitHook): Unit
}
class ExitHooks extends ExitHookRegistry
{
private val exitHooks = new scala.collection.mutable.HashSet[ExitHook]
def register(hook: ExitHook) { exitHooks += hook }
def unregister(hook: ExitHook) { exitHooks -= hook }
/** Calls each registered exit hook, trapping any exceptions so that each hook is given a chance to run. */
def runExitHooks(debug: String => Unit): List[Throwable] =
exitHooks.toList.flatMap( hook =>
def runExitHooks(exitHooks: Seq[ExitHook]): Seq[Throwable] =
exitHooks.flatMap( hook =>
ErrorHandling.wideConvert( hook.runBeforeExiting() ).left.toOption
)
}

View File

@ -13,11 +13,8 @@ object JettyRunner
val DefaultPort = 8080
val DefaultScanInterval = 3
}
//TODO: don't pass registry, just handle it in client
class JettyRunner(configuration: JettyConfiguration, registry: ExitHookRegistry) extends ExitHook
class JettyRunner(configuration: JettyConfiguration) extends ExitHook
{
registry.register(this)
def name = "jetty-shutdown"
def runBeforeExiting() { stop() }
private var running: Option[Stoppable] = None