From 522b412d14cee5a887d0d5dc321728d6d426f53d Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Fri, 19 Feb 2010 18:59:33 -0500 Subject: [PATCH] processors --- src/main/scala/sbt/BuilderProject.scala | 10 ++ src/main/scala/sbt/Main.scala | 38 +++++- .../scala/sbt/processor/CommandRunner.scala | 28 ++++ src/main/scala/sbt/processor/Execute.scala | 24 ++++ src/main/scala/sbt/processor/Handler.scala | 34 +++++ src/main/scala/sbt/processor/Info.scala | 26 ++++ src/main/scala/sbt/processor/Loader.scala | 41 ++++++ src/main/scala/sbt/processor/Manager.scala | 84 ++++++++++++ src/main/scala/sbt/processor/Parser.scala | 50 +++++++ src/main/scala/sbt/processor/Persist.scala | 26 ++++ src/main/scala/sbt/processor/Processor.scala | 125 ++++++++++++++++++ src/main/scala/sbt/processor/Retrieve.scala | 30 +++++ 12 files changed, 512 insertions(+), 4 deletions(-) create mode 100644 src/main/scala/sbt/processor/CommandRunner.scala create mode 100644 src/main/scala/sbt/processor/Execute.scala create mode 100644 src/main/scala/sbt/processor/Handler.scala create mode 100644 src/main/scala/sbt/processor/Info.scala create mode 100644 src/main/scala/sbt/processor/Loader.scala create mode 100644 src/main/scala/sbt/processor/Manager.scala create mode 100644 src/main/scala/sbt/processor/Parser.scala create mode 100644 src/main/scala/sbt/processor/Persist.scala create mode 100644 src/main/scala/sbt/processor/Processor.scala create mode 100644 src/main/scala/sbt/processor/Retrieve.scala diff --git a/src/main/scala/sbt/BuilderProject.scala b/src/main/scala/sbt/BuilderProject.scala index 0697659e8..793c7c1b9 100644 --- a/src/main/scala/sbt/BuilderProject.scala +++ b/src/main/scala/sbt/BuilderProject.scala @@ -221,4 +221,14 @@ class PluginProject(info: ProjectInfo) extends DefaultProject(info) /* Some setup to make publishing quicker to configure. */ override def useMavenConfigurations = true override def managedStyle = ManagedStyle.Maven +} +class ProcessorProject(info: ProjectInfo) extends DefaultProject(info) +{ + /* Fix the version used to build to the version currently running sbt. */ + override def buildScalaVersion = defScalaVersion.value + /* Add sbt to the classpath */ + override def unmanagedClasspath = super.unmanagedClasspath +++ info.sbtClasspath + /* Some setup to make publishing quicker to configure. */ + override def useMavenConfigurations = true + override def managedStyle = ManagedStyle.Maven } \ No newline at end of file diff --git a/src/main/scala/sbt/Main.scala b/src/main/scala/sbt/Main.scala index 884a2147b..7ba273dce 100755 --- a/src/main/scala/sbt/Main.scala +++ b/src/main/scala/sbt/Main.scala @@ -1,5 +1,5 @@ /* sbt -- Simple Build Tool - * Copyright 2008, 2009 Steven Blundy, Mark Harrah, David MacIver, Mikko Peltonen + * Copyright 2008, 2009, 2010 Steven Blundy, Mark Harrah, David MacIver, Mikko Peltonen */ package sbt @@ -117,6 +117,7 @@ class xMain extends xsbti.AppMain def ExitOnFailure = None lazy val interactiveContinue = Some( InteractiveCommand ) def remoteContinue(port: Int) = Some( FileCommandsPrefix + "-" + port ) + lazy val PHandler = new processor.Handler(baseProject) // replace in 2.8 trait Trampoline @@ -134,6 +135,7 @@ class xMain extends xsbti.AppMain def rememberCurrent(newArgs: List[String]) = rememberProject(rememberFail(newArgs)) def rememberProject(newArgs: List[String]) = if(baseProject.name != project.name) (ProjectAction + " " + project.name) :: newArgs else newArgs def rememberFail(newArgs: List[String]) = failAction.map(f => (FailureHandlerPrefix + f)).toList ::: newArgs + def tryOrFail(action: => Trampoline) = try { action } catch { case e: Exception => logCommandError(project.log, e); failed(BuildErrorExitCode) } def failed(code: Int) = failAction match { @@ -204,7 +206,21 @@ class xMain extends xsbti.AppMain case action :: tail if action.startsWith(FailureHandlerPrefix) => val errorAction = action.substring(FailureHandlerPrefix.length).trim continue(project, tail, if(errorAction.isEmpty) None else Some(errorAction) ) - + + case action :: tail if action.startsWith(ProcessorPrefix) => + val processorCommand = action.substring(ProcessorPrefix.length).trim + val runner = processor.CommandRunner(PHandler.manager, PHandler.defParser, ProcessorPrefix, project.log) + tryOrFail { + runner(processorCommand) + continue(project, tail, failAction) + } + + case PHandler(processor, arguments) :: tail => + tryOrFail { + val result =processor(project, arguments) + continue(project, result.insertArguments ::: tail, failAction) + } + case action :: tail => val success = processAction(baseProject, project, action, failAction == interactiveContinue) if(success) continue(project, tail, failAction) @@ -356,6 +372,8 @@ class xMain extends xsbti.AppMain val FileCommandsPrefix = "<" /** The prefix used to identify the action to run after an error*/ val FailureHandlerPrefix = "!" + /** The prefix used to identify commands for managing processors.*/ + val ProcessorPrefix = "*" /** The number of seconds between polling by the continuous compile command.*/ val ContinuousCompilePollDelaySeconds = 1 @@ -387,7 +405,7 @@ class xMain extends xsbti.AppMain handleCommand(currentProject, action) } - private def printCmd(name:String, desc:String) = Console.println("\t" + name + " : " + desc) + private def printCmd(name:String, desc:String) = Console.println(" " + name + " : " + desc) val BatchHelpHeader = "You may execute any project action or method or one of the commands described below." val InteractiveHelpHeader = "You may execute any project action or one of the commands described below. Only one action " + "may be executed at a time in interactive mode and is entered by name, as it would be at the command line." + @@ -399,10 +417,12 @@ class xMain extends xsbti.AppMain printCmd("", "Executes the project specified action.") printCmd(" *", "Executes the project specified method.") + printCmd(" ", "Runs the specified processor.") printCmd(ContinuousExecutePrefix + " ", "Executes the project specified action or method whenever source files change.") printCmd(FileCommandsPrefix + " file", "Executes the commands in the given file. Each command should be on its own line. Empty lines and lines beginning with '#' are ignored") printCmd(CrossBuildPrefix + " ", "Executes the project specified action or method for all versions of Scala defined in crossScalaVersions.") printCmd(SpecificBuildPrefix + " ", "Changes the version of Scala building the project and executes the provided command. is optional.") + printCmd(ProcessorPrefix, "Prefix for commands for managing processors. Run '" + ProcessorPrefix + "help' for details.") printCmd(ShowActions, "Shows all available actions.") printCmd(RebootCommand, "Reloads sbt, picking up modifications to sbt.version or scala.version and recompiling modified project definitions.") printCmd(HelpAction, "Displays this help message.") @@ -504,7 +524,7 @@ class xMain extends xsbti.AppMain } private def printTraceEnabled(project: Project) { - def traceLevel(level: Int) = if(level == 0) " (no stack elements)" else if(level == MaxInt) "" else " (maximum " + level + " stack elements per exception)" + def traceLevel(level: Int) = if(level == 0) " (no sbt stack elements)" else if(level == MaxInt) "" else " (maximum " + level + " stack elements per exception)" Console.println("Stack traces are " + (if(project.log.traceEnabled) "enabled" + traceLevel(project.log.getTrace) else "disabled")) } /** Sets the logging level on the given project.*/ @@ -700,4 +720,14 @@ class xMain extends xsbti.AppMain private def setProjectError(log: Logger) = logError(log)("Invalid arguments for 'project': expected project name.") private def logError(log: Logger)(s: String) = { log.error(s); false } + private def logCommandError(log: Logger, e: Throwable) = + e match + { + case pe: processor.ProcessorException => + if(pe.getCause ne null) log.trace(pe.getCause) + log.error(e.getMessage) + case e => + log.trace(e) + log.error(e.toString) + } } diff --git a/src/main/scala/sbt/processor/CommandRunner.scala b/src/main/scala/sbt/processor/CommandRunner.scala new file mode 100644 index 000000000..3428036d9 --- /dev/null +++ b/src/main/scala/sbt/processor/CommandRunner.scala @@ -0,0 +1,28 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt.processor + +/** Parses and executes a command (connects a parser to a runner). */ +class CommandRunner(parser: CommandParsing, execute: Executing) +{ + def apply(processorCommand: String): Unit = + parser.parseCommand(processorCommand) match + { + case Left(err) => throw new ProcessorException(err) + case Right(command) => execute(command) + } +} +object CommandRunner +{ + /** Convenience method for constructing a CommandRunner with the minimal information required.*/ + def apply(manager: Manager, defParser: DefinitionParser, prefix: String, log: Logger): CommandRunner = + { + val parser = new CommandParser(defaultErrorMessage(prefix), defParser) + val info = new InfoImpl(manager, prefix, parser, System.out.println) + val execute = new Execute(manager, info, log) + new CommandRunner(parser, execute) + } + def defaultErrorMessage(prefix: String) = + "Invalid processor command. Run " + prefix + "help to see valid commands." +} \ No newline at end of file diff --git a/src/main/scala/sbt/processor/Execute.scala b/src/main/scala/sbt/processor/Execute.scala new file mode 100644 index 000000000..5b86b8564 --- /dev/null +++ b/src/main/scala/sbt/processor/Execute.scala @@ -0,0 +1,24 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt.processor + +/** Executes a parsed command. */ +class Execute(manager: Manager, info: Info, log: Logger) extends Executing +{ + def apply(command: Command): Unit = + command match + { + case dr: DefineRepository => + manager.defineRepository(dr.repo) + log.info("Defined new processor repository '" + dr.repo + "'") + case dp: DefineProcessor => + manager.defineProcessor(dp.pdef) + log.info("Defined new processor '" + dp.pdef + "'") + case rd: RemoveDefinition => + val removed = manager.removeDefinition(rd.label) + log.info("Removed '" + removed + "'") + case Help => info.help() + case Show => info.show() + } +} \ No newline at end of file diff --git a/src/main/scala/sbt/processor/Handler.scala b/src/main/scala/sbt/processor/Handler.scala new file mode 100644 index 000000000..9933f2e4c --- /dev/null +++ b/src/main/scala/sbt/processor/Handler.scala @@ -0,0 +1,34 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt.processor + +class Handler(baseProject: Project) extends NotNull +{ + def unapply(line: String): Option[(Processor, String)] = + line.split("""\s+""", 2) match + { + case Array(GetProcessor(processor), args @ _*) => Some( (processor, args.mkString) ) + case _ => None + } + private object GetProcessor + { + def unapply(name: String): Option[Processor] = + manager.processorDefinition(name).flatMap(manager.processor) + } + + def lock = baseProject.info.launcher.globalLock + + lazy val scalaVersion = baseProject.defScalaVersion.value + lazy val base = baseProject.info.bootPath / ("scala-" + scalaVersion) / "sbt-processors" + lazy val persistBase = Path.userHome / ".ivy2" / "sbt" + + def retrieveLockFile = base / lockName + def persistLockFile = persistBase / lockName + def lockName = "processors.lock" + def definitionsFile = persistBase / "processors.properties" + def files = new ManagerFiles(base.asFile, retrieveLockFile.asFile, definitionsFile.asFile) + + lazy val defParser = new DefinitionParser + lazy val manager = new ManagerImpl(files, scalaVersion, new Persist(lock, persistLockFile.asFile, defParser), baseProject.log) +} \ No newline at end of file diff --git a/src/main/scala/sbt/processor/Info.scala b/src/main/scala/sbt/processor/Info.scala new file mode 100644 index 000000000..cc84f6e33 --- /dev/null +++ b/src/main/scala/sbt/processor/Info.scala @@ -0,0 +1,26 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt.processor + +class InfoImpl(manager: Manager, prefix: String, parser: CommandParser, print: String => Unit) extends Info +{ + def show() + { + print("Processors:\n\t" + manager.processors.values.mkString("\n\t")) + print("\nProcessor repositories:\n\t" + manager.repositories.values.mkString("\n\t")) + } + def help() + { + import parser.{ShowCommand, HelpCommand, ProcessorCommand, RemoveCommand, RepositoryCommand} + val usage = + (HelpCommand -> "Display this help message") :: + (ShowCommand -> "Display defined processors and repositories") :: + (ProcessorCommand -> "Define 'label' to be the processor with the given ID") :: + (RepositoryCommand -> "Add a repository for searching for processors") :: + (RemoveCommand -> "Undefine the repository or processor with the given 'label'") :: + Nil + + print("Processor management commands:\n " + (usage.map{ case (c,d) => prefix + "" + c + " " + d}).mkString("\n ")) + } +} \ No newline at end of file diff --git a/src/main/scala/sbt/processor/Loader.scala b/src/main/scala/sbt/processor/Loader.scala new file mode 100644 index 000000000..46f6a9096 --- /dev/null +++ b/src/main/scala/sbt/processor/Loader.scala @@ -0,0 +1,41 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt.processor + +import java.io.File +import java.net.{URL, URLClassLoader} +import xsbt.FileUtilities.read +import xsbt.OpenResource.urlInputStream +import xsbt.Paths._ +import xsbt.GlobFilter._ + +import ProcessorException.error + +class Loader extends NotNull +{ + def classNameResource = "sbt.processor" + def getProcessor(directory: File): Either[Throwable, Processor] = getProcessor( getLoader(directory) ) + private def getProcessor(loader: ClassLoader): Either[Throwable, Processor] = + { + val resource = loader.getResource(classNameResource) + if(resource eq null) Left(new ProcessorException("Processor existed but did not contain '" + classNameResource + "' descriptor.")) + else loadProcessor(loader, resource) + } + private def loadProcessor(loader: ClassLoader, resource : URL): Either[Throwable, Processor] = + try { Right(loadProcessor(loader, className(resource))) } + catch { case e: Exception => Left(e) } + + private def loadProcessor(loader: ClassLoader, className: String): Processor = + { + val processor = Class.forName(className, true, loader).newInstance + classOf[Processor].cast(processor) + } + private def className(resource: URL): String = urlInputStream(resource) { in => read(in).trim } + private def getLoader(dir: File) = + { + val jars = dir ** "*.jar" + val jarURLs = jars.files.toArray[File].map(_.toURI.toURL) + new URLClassLoader(jarURLs, getClass.getClassLoader) + } +} \ No newline at end of file diff --git a/src/main/scala/sbt/processor/Manager.scala b/src/main/scala/sbt/processor/Manager.scala new file mode 100644 index 000000000..ac7254ad9 --- /dev/null +++ b/src/main/scala/sbt/processor/Manager.scala @@ -0,0 +1,84 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt.processor + +import java.io.File +import xsbt.Paths._ +import ProcessorException.error + +/** Files needed by ManagerImpl. +* `retrieveBaseDirectory` is the directory that processors are retrieved under. +* `retrieveLockFile` is used to synchronize access to that directory. +* `definitionsFile` is the file to save repository and processor definitions to. It is usually per-user instead of per-project.*/ +class ManagerFiles(val retrieveBaseDirectory: File, val retrieveLockFile: File, val definitionsFile: File) + +class ManagerImpl(files: ManagerFiles, scalaVersion: String, persist: Persist, log: Logger) extends Manager +{ + import files._ + + def processorDefinition(label: String): Option[ProcessorDefinition] = processors.get(label) + def processor(pdef: ProcessorDefinition): Option[Processor] = + { + def tryProcessor: Either[Throwable, Processor] = + (new Loader).getProcessor( retrieveDirectory(pdef) ) + + // try to load the processor. It will succeed here if the processor has already been retrieved + tryProcessor.left.flatMap { _ => + // if it hasn't been retrieved, retrieve the processor and its dependencies + retrieveProcessor(pdef) + // try to load the processor now that it has been retrieved + tryProcessor.left.map { // if that fails, log a warning + case p: ProcessorException => log.warn(p.getMessage) + case t => log.trace(t); log.warn(t.toString) + } + }.right.toOption + } + def defineProcessor(p: ProcessorDefinition) + { + checkExisting(p) + retrieveProcessor(p) + add(p) + } + def defineRepository(r: RepositoryDefinition) + { + checkExisting(r) + add(r) + } + def removeDefinition(label: String): Definition = + definitions.removeKey(label) match + { + case Some(removed) => + saveDefinitions() + removed + case None => error("Label '" + label + "' not defined.") + } + + private def retrieveProcessor(p: ProcessorDefinition): Unit = + { + val resolvers = repositories.values.toList.map(toResolver) + val module = p.toModuleID(scalaVersion) + ( new Retrieve(retrieveDirectory(p), module, persist.lock, retrieveLockFile, resolvers, log) ).retrieve() + } + private def add(d: Definition) + { + definitions(d.label) = d + saveDefinitions() + } + + private lazy val definitions = loadDefinitions(definitionsFile) + def repositories = Map() ++ partialMap(definitions) { case (label, d: RepositoryDefinition) => (label, d) } + def processors = Map() ++ partialMap(definitions) { case (label, p: ProcessorDefinition) => (label, p) } + + private def checkExisting(p: Definition): Unit = definitions.get(p.label) map { d => error ("Label '" + p.label + "' already in use: " + d) } + private def partialMap[T,S](i: Iterable[T])(f: PartialFunction[T,S]) = i.filter(f.isDefinedAt).map(f) + private def toResolver(repo: RepositoryDefinition): Resolver = new MavenRepository(repo.label, repo.url) + + def retrieveDirectory(p: ProcessorDefinition) = retrieveBaseDirectory / p.group / p.module / p.rev + + private def saveDefinitions(): Unit = saveDefinitions(definitionsFile) + private def saveDefinitions(file: File): Unit = persist.save(file)(definitions.values.toList) + private def loadDefinitions(file: File): scala.collection.mutable.Map[String, Definition] = + scala.collection.mutable.HashMap( (if(file.exists) rawLoad(file) else Nil) : _*) + private def rawLoad(file: File): Seq[(String, Definition)] = persist.load(definitionsFile).map { d => (d.label, d) } +} \ No newline at end of file diff --git a/src/main/scala/sbt/processor/Parser.scala b/src/main/scala/sbt/processor/Parser.scala new file mode 100644 index 000000000..4d379f522 --- /dev/null +++ b/src/main/scala/sbt/processor/Parser.scala @@ -0,0 +1,50 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt.processor + +/** Parses commands. `errorMessage` is the String used when a command is invalid. +* There is no detailed error reporting. +* Input Strings are assumed to be trimmed.*/ +class CommandParser(errorMessage: String, defParser: DefinitionParsing) extends CommandParsing +{ + def parseCommand(line: String): Either[String, Command] = + defParser.parseDefinition(line) match + { + case Some(p: ProcessorDefinition) => Right(new DefineProcessor(p)) + case Some(r: RepositoryDefinition) => Right(new DefineRepository(r)) + case None => parseOther(line) + } + + def parseOther(line: String) = + line match + { + case RemoveRegex(label) => Right(new RemoveDefinition(label)) + case HelpCommand | "" => Right(Help) + case ShowCommand => Right(Show) + case _ => Left(errorMessage) + } + + val ShowCommand = "show" + val HelpCommand = "help" + val ProcessorCommand = "