Pluggable build resolvers

This commit is contained in:
Mark Harrah 2011-05-07 22:02:06 -04:00
parent 0ad682f2c1
commit 40c6ca3b3d
6 changed files with 91 additions and 25 deletions

View File

@ -179,8 +179,7 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se
val DefaultStartLine = 0
private[this] def makeModuleName(hash: String): String = "$" + halve(hash)
private[this] def halve(s: String): String = if(s.length > 2) s.substring(0, s.length / 2) else s
private[this] def makeModuleName(hash: String): String = "$" + Hash.halve(hash)
private[this] def noImports = new EvalImports(Nil, "")
private[this] def mkUnit(srcName: String, firstLine: Int, s: String) = new CompilationUnit(new EvalSourceFile(srcName, firstLine, s))
private[this] def checkError(label: String) = if(reporter.hasErrors) throw new EvalException(label)

View File

@ -18,6 +18,7 @@ trait Build
{
def projects: Seq[Project]
def settings: Seq[Setting[_]] = Defaults.buildCore
def buildResolvers: Seq[BuildLoader.BuildResolver] = Nil
}
trait Plugin
{
@ -34,24 +35,28 @@ object Build
}
object RetrieveUnit
{
def apply(tempDir: File, base: URI): File =
def apply(tempDir: File, base: URI): Option[() => File] =
{
lazy val tmp = temporary(tempDir, base)
base.getScheme match
{
case "file" => val f = new File(base); if(f.isDirectory) f else error("Not a directory: '" + base + "'")
case "git" => gitClone(base, tmp); tmp
case "http" | "https" => downloadAndExtract(base, tmp); tmp
case _ => error("Unknown scheme in '" + base + "'")
case "git" => Some { () => gitClone(base, tmp); tmp }
case "http" | "https" => Some { () => downloadAndExtract(base, tmp); tmp }
case "file" =>
val f = new File(base)
if(f.isDirectory) Some(() => f) else None
case _ => None
}
}
def downloadAndExtract(base: URI, tempDir: File): Unit = if(!tempDir.exists) IO.unzipURL(base.toURL, tempDir)
def temporary(tempDir: File, uri: URI): File = new File(tempDir, hash(uri))
def temporary(tempDir: File, uri: URI): File = new File(tempDir, Hash.halve(hash(uri)))
def hash(uri: URI): String = Hash.toHex(Hash(uri.toASCIIString))
import Process._
def gitClone(base: URI, tempDir: File): Unit =
if(!tempDir.exists) ("git" :: "clone" :: base.toASCIIString :: tempDir.getAbsolutePath :: Nil) ! ;
if(!tempDir.exists) ("git" :: "clone" :: dropFragment(base).toASCIIString :: tempDir.getAbsolutePath :: branch(base)) ! ;
def branch(base: URI): List[String] = base.getFragment match { case null => Nil; case b => "-b" :: b :: Nil }
def dropFragment(base: URI): URI = if(base.getFragment eq null) base else new URI(base.getScheme, base.getSchemeSpecificPart, null)
}
object EvaluateConfigurations
{

51
main/BuildLoader.scala Normal file
View File

@ -0,0 +1,51 @@
/* sbt -- Simple Build Tool
* Copyright 2011 Mark Harrah
*/
package sbt
import java.io.File
import java.net.URI
import Load.{BuildUnit, LoadBuildConfiguration}
import BuildLoader._
final class ResolveInfo(val build: URI, val staging: File)
final class BuildLoader(val load: (URI, File) => BuildUnit, val builtIn: BuildResolver, val root: Option[BuildResolver], val nonRoots: List[(URI, BuildResolver)], val fail: URI => Nothing, val config: LoadBuildConfiguration)
{
import Alternatives._
import config.{log, stagingDirectory => dir}
def apply(uri: URI): BuildUnit = load(uri, resolve(new ResolveInfo(uri, dir)))
def resolve(info: ResolveInfo): File =
(baseLoader(info), applyNonRoots(info)) match
{
case (None, Nil) => fail(info.build)
case (None, xs @ (_, nr) :: ignored ) =>
if(!ignored.isEmpty) warn("Using first of multiple matching non-root build resolver for " + info.build, log, xs)
nr()
case (Some(b), xs) =>
if(!xs.isEmpty) warn("Ignoring shadowed non-root build resolver(s) for " + info.build, log, xs)
b()
}
def baseLoader: BuildResolver = root match { case Some(rl) => rl | builtIn; case None => builtIn }
def addNonRoot(uri: URI, loader: BuildResolver) = new BuildLoader(load, builtIn, root, (uri, loader) :: nonRoots, fail, config)
def setRoot(resolver: BuildResolver) = new BuildLoader(load, builtIn, Some(resolver), nonRoots, fail, config)
def applyNonRoots(info: ResolveInfo): List[(URI, () => File)] =
nonRoots flatMap { case (definingURI, loader) => loader(info) map { unit => (definingURI, unit) } }
private[this] def warn(baseMessage: String, log: Logger, matching: Seq[(URI, () => File)])
{
log.warn(baseMessage)
log.debug("Non-root build resolvers defined in:")
log.debug(matching.map(_._1).mkString("\n\t"))
}
}
object BuildLoader
{
/** in: Build URI and staging directory
* out: None if unhandled or Some containing the retrieve function, which returns the directory retrieved to (can be the same as the staging directory) */
type BuildResolver = ResolveInfo => Option[() => File]
def apply(load: (URI, File) => BuildUnit, builtIn: BuildResolver, fail: URI => Nothing, config: LoadBuildConfiguration): BuildLoader =
new BuildLoader(load, builtIn, None, Nil, fail, config)
}

View File

@ -167,15 +167,30 @@ object Load
if(srcs.isEmpty) Nil else EvaluateConfigurations(eval(), srcs, imports)
def load(file: File, s: State, config: LoadBuildConfiguration): PartBuild =
load(file, uri => loadUnit(uri, RetrieveUnit(config.stagingDirectory, uri), s, config) )
def load(file: File, loader: URI => BuildUnit): PartBuild = loadURI(IO.directoryURI(file), loader)
def loadURI(uri: URI, loader: URI => BuildUnit): PartBuild =
{
val loader = (uri: URI, local: File) => loadUnit(uri, local, s, config)
val fail = (uri: URI) => error("Invalid build URI: " + uri)
val builtinLoader = BuildLoader(loader, info => RetrieveUnit(info.staging, info.build), fail, config)
load(file, builtinLoader)
}
def load(file: File, loaders: BuildLoader): PartBuild = loadURI(IO.directoryURI(file), loaders)
def loadURI(uri: URI, loaders: BuildLoader): PartBuild =
{
IO.assertAbsolute(uri)
val (referenced, map) = loadAll(uri :: Nil, Map.empty, loader, Map.empty)
val (referenced, map) = loadAll(uri :: Nil, Map.empty, loaders, Map.empty)
checkAll(referenced, map)
new PartBuild(uri, map)
}
def addResolvers(unit: BuildUnit, isRoot: Boolean, loaders: BuildLoader): BuildLoader =
unit.definitions.builds.flatMap(_.buildResolvers) match
{
case Nil => loaders
case x :: xs =>
import Alternatives._
val resolver = (x /: xs){ _ | _ }
if(isRoot) loaders.setRoot(resolver) else loaders.addNonRoot(unit.uri, resolver)
}
def loaded(unit: BuildUnit): (PartBuildUnit, List[ProjectReference]) =
{
val defined = projects(unit)
@ -197,17 +212,18 @@ object Load
Project.transform(resolve, unit.definitions.builds.flatMap(_.settings))
}
@tailrec def loadAll(bases: List[URI], references: Map[URI, List[ProjectReference]], externalLoader: URI => BuildUnit, builds: Map[URI, PartBuildUnit]): (Map[URI, List[ProjectReference]], Map[URI, PartBuildUnit]) =
@tailrec def loadAll(bases: List[URI], references: Map[URI, List[ProjectReference]], loaders: BuildLoader, builds: Map[URI, PartBuildUnit]): (Map[URI, List[ProjectReference]], Map[URI, PartBuildUnit]) =
bases match
{
case b :: bs =>
if(builds contains b)
loadAll(bs, references, externalLoader, builds)
loadAll(bs, references, loaders, builds)
else
{
val (loadedBuild, refs) = loaded(externalLoader(b))
val (loadedBuild, refs) = loaded(loaders(b))
checkBuildBase(loadedBuild.unit.localBase)
loadAll(refs.flatMap(Reference.uri) reverse_::: bs, references.updated(b, refs), externalLoader, builds.updated(b, loadedBuild))
val newLoader = addResolvers(loadedBuild.unit, builds.isEmpty, loaders)
loadAll(refs.flatMap(Reference.uri) reverse_::: bs, references.updated(b, refs), newLoader, builds.updated(b, loadedBuild))
}
case Nil => (references, builds)
}
@ -456,10 +472,4 @@ object Load
final case class LoadBuildConfiguration(stagingDirectory: File, commonPluginClasspath: Seq[Attributed[File]], classpath: Seq[File], loader: ClassLoader, compilers: Compilers, evalPluginDef: (BuildStructure, State) => Seq[Attributed[File]], delegates: LoadedBuild => Scope => Seq[Scope], scopeLocal: ScopeLocal, injectSettings: Seq[Setting[_]], log: Logger)
// information that is not original, but can be reconstructed from the rest of BuildStructure
final class StructureIndex(val keyMap: Map[String, AttributeKey[_]], val taskToKey: Map[Task[_], ScopedKey[Task[_]]], val triggers: Triggers[Task], val keyIndex: KeyIndex)
private[this] def memo[A,B](f: A => B): A => B =
{
val dcache = new mutable.HashMap[A,B]
(a: A) => dcache.getOrElseUpdate(a, f(a))
}
}

View File

@ -15,7 +15,7 @@ object Script
Command.command(Name) { state =>
val scriptArg = state.remainingCommands.headOption getOrElse error("No script file specified")
val script = new File(scriptArg).getAbsoluteFile
val hash = halve(Hash.toHex(Hash(script.getAbsolutePath)))
val hash = Hash.halve(Hash.toHex(Hash(script.getAbsolutePath)))
val base = new File(CommandSupport.bootDirectory(state), hash)
IO.createDirectory(base)
@ -36,7 +36,6 @@ object Script
val newState = "run" :: state.copy(remainingCommands = state.remainingCommands.drop(1))
Project.setProject(session, newStructure, newState)
}
def halve(s: String): String = if(s.length > 3) s.substring(0, s.length / 2) else s
final case class Block(offset: Int, lines: Seq[String])
def blocks(file: File): Seq[Block] =

View File

@ -32,6 +32,8 @@ object Hash
}
array
}
def halve(s: String): String = if(s.length > 3) s.substring(0, s.length / 2) else s
/** Calculates the SHA-1 hash of the given String.*/
def apply(s: String): Array[Byte] = apply(s.getBytes("UTF-8"))
/** Calculates the SHA-1 hash of the given Array[Byte].*/