improve commands, proper build/project base resolution

finish alias support
better project printing in 'projects'
completion support for 'help'
resolve URIs in ProjectRef against base URI of defining build in keys and project relations
resolve base directories and record build URI in BuildUnit
preserve relative paths in File to URI conversion for later resolution
This commit is contained in:
Mark Harrah 2011-01-23 22:34:17 -05:00
parent 72261548ef
commit 2687836ca5
6 changed files with 132 additions and 47 deletions

View File

@ -181,8 +181,8 @@ object Load
def defaultLoad(state: State, log: Logger): BuildStructure =
{
val stagingDirectory = defaultStaging // TODO: properly configurable
val base = state.configuration.baseDirectory
val stagingDirectory = defaultStaging.getCanonicalFile // TODO: properly configurable
val base = state.configuration.baseDirectory.getCanonicalFile
val loader = getClass.getClassLoader
val provider = state.configuration.provider
val classpath = provider.mainClasspath ++ provider.scalaProvider.jars
@ -266,21 +266,25 @@ object Load
def configurations(srcs: Seq[File], eval: Eval): Seq[Setting[_]] =
if(srcs.isEmpty) Nil else EvaluateConfigurations(eval, srcs)
def load(file: File, config: LoadBuildConfiguration): LoadedBuild = load(file, uri => loadUnit(RetrieveUnit(config.stagingDirectory, uri), config) )
def load(file: File, loader: URI => BuildUnit): LoadedBuild = loadURI(file.getAbsoluteFile.toURI, loader)
def load(file: File, config: LoadBuildConfiguration): LoadedBuild = load(file, uri => loadUnit(uri, RetrieveUnit(config.stagingDirectory, uri), config) )
def load(file: File, loader: URI => BuildUnit): LoadedBuild = loadURI(IO.directoryURI(file), loader)
def loadURI(uri: URI, loader: URI => BuildUnit): LoadedBuild =
{
IO.assertAbsolute(uri)
val (referenced, map) = loadAll(uri :: Nil, Map.empty, loader, Map.empty)
checkAll(referenced, map)
new LoadedBuild(uri, map)
}
def loaded(unit: BuildUnit): (LoadedBuildUnit, List[ProjectRef]) =
{
val baseURI = unit.base.toURI.normalize
def isRoot(p: Project) = p.base.toURI.normalize == baseURI
// since everything should be resolved at this point, we can compare Files instead of converting to URIs
def isRoot(p: Project) = p.base == unit.localBase
val defined = projects(unit)
if(defined.isEmpty) error("No projects defined in build unit " + unit)
val externals = referenced(defined).toList
val rootProjects = defined.filter(isRoot).map(_.id)
val projectsInRoot = defined.filter(isRoot).map(_.id)
val rootProjects = if(projectsInRoot.isEmpty) defined.head.id :: Nil else projectsInRoot
(new LoadedBuildUnit(unit, defined.map(d => (d.id, d)).toMap, rootProjects), externals)
}
@ -293,10 +297,22 @@ object Load
else
{
val (loadedBuild, refs) = loaded(externalLoader(b))
checkBuildBase(loadedBuild.localBase)
loadAll(refs.flatMap(_.uri) reverse_::: bs, references.updated(b, refs), externalLoader, builds.updated(b, loadedBuild))
}
case Nil => (references, builds)
}
def checkProjectBase(buildBase: File, projectBase: File)
{
checkDirectory(projectBase)
assert(buildBase == projectBase || IO.relativize(buildBase, projectBase).isDefined, "Directory " + projectBase + " is not contained in build root " + buildBase)
}
def checkBuildBase(base: File) = checkDirectory(base)
def checkDirectory(base: File)
{
assert(base.isDirectory, "Not an existing directory: " + base)
assert(base.isAbsolute, "Not absolute: " + base)
}
def checkAll(referenced: Map[URI, List[ProjectRef]], builds: Map[URI, LoadedBuildUnit])
{
val rootProject = getRootProject(builds)
@ -310,12 +326,27 @@ object Load
}
}
def resolveBase(against: File): Project => Project =
def resolveBase(origin: URI, against: File): Project => Project =
{
val uri = against.getAbsoluteFile.toURI.normalize
p => p.copy(base = new File(uri.resolve(p.base.toURI).normalize))
assert(origin.isAbsolute, "Origin not absolute: " + origin)
def resolveRefs(prs: Seq[ProjectRef]) = prs map resolveRef
def resolveRef(pr: ProjectRef) = pr match { case ProjectRef(Some(uri), id) => ProjectRef(Some(resolveURI(uri)), id); case _ => pr }
def resolveDeps(ds: Seq[Project.ClasspathDependency]) = ds map resolveDep
def resolveDep(d: Project.ClasspathDependency) = d.copy(project = resolveRef(d.project))
def resolveURI(u: URI): URI = IO.directoryURI(origin resolve u)
def resolve(f: File) =
{
val fResolved = new File(IO.directoryURI(IO.resolve(against, f)))
checkProjectBase(against, fResolved)
fResolved
}
p => p.copy(base = resolve(p.base), aggregate = resolveRefs(p.aggregate), dependencies = resolveDeps(p.dependencies), inherits = resolveRefs(p.inherits))
}
def projects(unit: BuildUnit): Seq[Project] =
{
val resolve = resolveBase(unit.uri, unit.localBase)
unit.definitions.builds.flatMap(_.projects map resolve)
}
def projects(unit: BuildUnit): Seq[Project] = unit.definitions.builds.flatMap(_.projects map resolveBase(unit.base))
def getRootProject(map: Map[URI, LoadedBuildUnit]): URI => String =
uri => getBuild(map, uri).rootProjects.headOption getOrElse emptyBuild(uri)
def getConfiguration(map: Map[URI, LoadedBuildUnit], uri: URI, id: String, conf: ConfigKey): Configuration =
@ -331,20 +362,21 @@ object Load
def noProject(uri: URI, id: String) = error("No project '" + id + "' defined in '" + uri + "'.")
def noConfiguration(uri: URI, id: String, conf: String) = error("No configuration '" + conf + "' defined in project '" + id + "' in '" + uri +"'")
def loadUnit(base: File, config: LoadBuildConfiguration): BuildUnit =
def loadUnit(uri: URI, localBase: File, config: LoadBuildConfiguration): BuildUnit =
{
val defDir = selectProjectDir(base)
val normBase = localBase.getCanonicalFile
val defDir = selectProjectDir(normBase)
val pluginDir = pluginDirectory(defDir)
val plugs = plugins(pluginDir, config)
val defs = definitionSources(defDir)
val loadedDefs =
if(defs.isEmpty)
new LoadedDefinitions(defDir, outputDirectory(defDir), plugs.loader, Build.default(base) :: Nil)
new LoadedDefinitions(defDir, outputDirectory(defDir), plugs.loader, Build.default(normBase) :: Nil)
else
definitions(defDir, defs, plugs, config.compilers, config.log)
new BuildUnit(base, loadedDefs, plugs)
new BuildUnit(uri, normBase, loadedDefs, plugs)
}
def plugins(dir: File, config: LoadBuildConfiguration): LoadedPlugins = if(dir.exists) buildPlugins(dir, config) else noPlugins(config)
@ -411,15 +443,19 @@ object Load
object LoadedPlugins {
def empty(loader: ClassLoader) = new LoadedPlugins(Nil, loader, Nil)
}
final class BuildUnit(val base: File, val definitions: LoadedDefinitions, val plugins: LoadedPlugins)
final class BuildUnit(val uri: URI, val localBase: File, val definitions: LoadedDefinitions, val plugins: LoadedPlugins)
{
override def toString = if(uri.getScheme == "file") localBase.toString else (uri + " (locally: " + localBase +")")
}
final class LoadedBuild(val root: URI, val units: Map[URI, LoadedBuildUnit])
final class LoadedBuildUnit(val unit: BuildUnit, val defined: Map[String, Project], val rootProjects: Seq[String])
{
assert(!rootProjects.isEmpty, "No root projects defined for build unit '" + unit.base + "'")
def base = unit.base
assert(!rootProjects.isEmpty, "No root projects defined for build unit " + unit)
def localBase = unit.localBase
def classpath = unit.definitions.target +: unit.plugins.classpath
def loader = unit.definitions.loader
def loade = unit.definitions.loader
override def toString = unit.toString
}
// these are unresolved references
@ -473,7 +509,7 @@ object BuildStreams
def projectPath(units: Map[URI, LoadedBuildUnit], root: URI, scoped: ScopedKey[_]): (File, Seq[String]) =
scoped.scope.project match
{
case Global => (units(root).base, GlobalPath :: Nil)
case Global => (units(root).localBase, GlobalPath :: Nil)
case Select(ProjectRef(Some(uri), Some(id))) => (units(uri).defined(id).base, Nil)
case Select(pr) => error("Unresolved project reference (" + pr + ") in " + display(scoped))
case This => error("Unresolved project reference (This) in " + display(scoped))

View File

@ -14,6 +14,7 @@ package sbt
import scala.annotation.tailrec
import scala.collection.JavaConversions._
import Function.tupled
import java.net.URI
import Path._
import java.io.File
@ -66,10 +67,18 @@ object Commands
def detail(selected: Iterable[String])(h: Help): Option[String] =
h.detail match { case (commands, value) => if( selected exists commands ) Some(value) else None }
// TODO: tab complete on command names
def help = Command.args(HelpCommand, helpBrief, helpDetailed, "<command>") { (s, args) =>
def help = Command(HelpCommand, helpBrief, helpDetailed)(helpParser)
def helpParser(s: State) =
{
val h = s.processors.flatMap(_.help)
val helpCommands = h.flatMap(_.detail._1)
val args = (token(Space) ~> token( OpOrID.examples(helpCommands : _*) )).*
applyEffect(args)(runHelp(s, h))
}
def runHelp(s: State, h: Seq[Help])(args: Seq[String]): State =
{
val message =
if(args.isEmpty)
h.map( _.brief match { case (a,b) => a + " : " + b } ).mkString("\n", "\n", "\n")
@ -81,16 +90,20 @@ object Commands
def alias = Command(AliasCommand, AliasBrief, AliasDetailed) { s =>
val name = token(OpOrID.examples( aliasNames(s) : _*) )
val assign = token(Space ~ '=' ~ Space) ~> matched(Command.combine(s.processors)(s), partial = true)
val base = (OptSpace ~> (name ~ assign.?).?)
val assign = token(Space ~ '=' ~ OptSpace)
val sfree = removeAliases(s)
val to = matched(Command.combine(sfree.processors)(sfree), partial = true) | any.+.string
val base = (OptSpace ~> (name ~ (assign ~> to.?).?).?)
applyEffect(base)(t => runAlias(s, t) )
}
def runAlias(s: State, args: Option[(String, Option[String])]): State =
def runAlias(s: State, args: Option[(String, Option[Option[String]])]): State =
args match
{
case Some((name, Some(value))) => addAlias(s, name.trim, value.trim)
case Some((x, None)) if !x.isEmpty=> printAlias(s, x.trim); s
case None => printAliases(s); s
case Some(x ~ None) if !x.isEmpty => printAlias(s, x.trim); s
case Some(name ~ Some(None)) => removeAlias(s, name.trim)
case Some(name ~ Some(Some(value))) => addAlias(s, name.trim, value.trim)
}
def shell = Command.command(Shell, ShellBrief, ShellDetailed) { s =>
@ -199,8 +212,12 @@ object Commands
}
}*/
def indent(withStar: Boolean) = if(withStar) "\t*" else "\t "
def listProject(name: String, current: Boolean, log: Logger) = log.info( indent(current) + name )
def listBuild(uri: URI, build: Load.LoadedBuildUnit, current: Boolean, currentID: String, log: Logger) =
{
log.info("In " + uri)
def prefix(id: String) = if(currentID != id) " " else if(current) " * " else "(*)"
for(id <- build.defined.keys) log.info("\t" + prefix(id) + id)
}
def act = error("TODO")
def projects = Command.command(ProjectsCommand, projectsBrief, projectsDetailed ) { s =>
@ -208,11 +225,8 @@ object Commands
val session = Project.session(s)
val structure = Project.structure(s)
val (curi, cid) = session.current
for( (uri, build) <- structure.units)
{
log.info("In " + uri)
for(id <- build.defined.keys) listProject(id, cid == id, log)
}
listBuild(curi, structure.units(curi), true, cid, log)
for( (uri, build) <- structure.units if curi != uri) listBuild(uri, build, false, cid, log)
s
}
def withAttribute[T](s: State, key: AttributeKey[T], ifMissing: String)(f: T => State): State =
@ -317,7 +331,6 @@ object Commands
}
def addAlias(s: State, name: String, value: String): State =
{
if(Command validID name) {
val removed = removeAlias(s, name)
if(value.isEmpty) removed else removed.copy(processors = newAlias(name, value) +: removed.processors)
@ -325,10 +338,12 @@ object Commands
System.err.println("Invalid alias name '" + name + "'.")
s.fail
}
}
def removeAliases(s: State): State = s.copy(processors = removeAliases(s.processors))
def removeAliases(as: Seq[Command]): Seq[Command] = as.filter(c => ! (c.tags contains CommandAliasKey))
def removeAlias(s: State, name: String): State = s.copy(processors = s.processors.filter(c => !isAliasNamed(name, c)) )
def isAliasNamed(name: String, c: Command): Boolean = isNamed(name, getAlias(c))
def isNamed(name: String, alias: Option[(String,String)]): Boolean = alias match { case None => false; case Some((alias,_)) => name != alias }
def isNamed(name: String, alias: Option[(String,String)]): Boolean = alias match { case None => false; case Some((n,_)) => name == n }
def getAlias(c: Command): Option[(String,String)] = c.tags get CommandAliasKey
def printAlias(s: State, name: String): Unit = printAliases(aliases(s,(n,v) => n == name) )
@ -343,9 +358,9 @@ object Commands
s.processors.flatMap(c => getAlias(c).filter(tupled(pred)))
def newAlias(name: String, value: String): Command =
Command(name, (name, "<alias>"), "Alias of '" + value + "'")(aliasBody(name, value)).tag(CommandAliasKey, (name, value))
Command(name, (name, "'" + value + "'"), "Alias of '" + value + "'")(aliasBody(name, value)).tag(CommandAliasKey, (name, value))
def aliasBody(name: String, value: String)(state: State): Parser[() => State] =
Parser(Command.combine(removeAlias(state,name).processors)(state))(value)
Parser(Command.combine(state.processors)(state))(value)
val CommandAliasKey = AttributeKey[(String,String)]("is-command-alias")
}

View File

@ -71,7 +71,7 @@ object Project extends Init[Scope]
}
def transform(g: Scope => Scope, ss: Seq[Setting[_]]): Seq[Setting[_]] = {
val f = mapScope(g)
ss.map(_ mapKey f)
ss.map(_ mapKey f mapReferenced f)
}
val SessionKey = AttributeKey[SessionSettings]("session-settings")
@ -95,18 +95,18 @@ trait ProjectConstructors
implicit def configDependencyConstructor[T <% ProjectRef](p: T): Project.Constructor = new Project.Constructor(p)
implicit def classpathDependency[T <% ProjectRef](p: T): Project.ClasspathDependency = new Project.ClasspathDependency(p, None)
}
// the URI must be resolved and normalized before it is definitive
final case class ProjectRef(uri: Option[URI], id: Option[String])
object ProjectRef
{
def apply(base: URI, id: String): ProjectRef = ProjectRef(Some(base), Some(id))
/** Reference to the project with 'id' in the current build unit.*/
def apply(id: String): ProjectRef = ProjectRef(None, Some(id))
def apply(base: File, id: String): ProjectRef = ProjectRef(Some(base.toURI), Some(id))
def apply(base: File, id: String): ProjectRef = ProjectRef(Some(IO.toURI(base)), Some(id))
/** Reference to the root project at 'base'.*/
def apply(base: URI): ProjectRef = ProjectRef(Some(base), None)
/** Reference to the root project at 'base'.*/
def apply(base: File): ProjectRef = ProjectRef(Some(base.toURI), None)
def apply(base: File): ProjectRef = ProjectRef(Some(IO.toURI(base)), None)
/** Reference to the root project in the current build unit.*/
def root = ProjectRef(None, None)

View File

@ -42,7 +42,8 @@ object Scope
}
def resolveRef(current: URI, rootProject: URI => String, ref: ProjectRef): (URI, String) =
{
val uri = ref.uri getOrElse current
val unURI = ref.uri match { case Some(u) => current resolve u; case None => current }
val uri = unURI.normalize
(uri, ref.id getOrElse rootProject(uri))
}

View File

@ -99,11 +99,11 @@ trait Init[Scope]
def delegate(sMap: ScopedMap)(implicit delegates: Scope => Seq[Scope]): ScopedMap =
{
val md = memoDelegates(delegates)
def refMap(refKey: ScopedKey[_]) = new (ScopedKey ~> ScopedKey) { def apply[T](k: ScopedKey[T]) = mapReferenced(sMap, k, md(k.scope), refKey) }
def refMap(refKey: ScopedKey[_]) = new (ScopedKey ~> ScopedKey) { def apply[T](k: ScopedKey[T]) = delegateForKey(sMap, k, md(k.scope), refKey) }
val f = new (SettingSeq ~> SettingSeq) { def apply[T](ks: Seq[Setting[T]]) = ks.map{ s => s mapReferenced refMap(s.key) } }
sMap mapValues f
}
private[this] def mapReferenced[T](sMap: ScopedMap, k: ScopedKey[T], scopes: Seq[Scope], refKey: ScopedKey[_]): ScopedKey[T] =
private[this] def delegateForKey[T](sMap: ScopedMap, k: ScopedKey[T], scopes: Seq[Scope], refKey: ScopedKey[_]): ScopedKey[T] =
{
val scache = PMap.empty[ScopedKey, ScopedKey]
def resolve(search: Seq[Scope]): ScopedKey[T] =

View File

@ -588,4 +588,37 @@ object IO
Using.fileInputStream(file) { fin =>
Using.gzipInputStream(fin) { ing =>
Using.bufferedInputStream(ing)(f) }}
/** Converts an absolute File to a URI. The File is converted to a URI (toURI),
* normalized (normalize), encoded (toASCIIString), and a forward slash ('/') is appended to the path component if
* it does not already end with a slash.
*/
def directoryURI(dir: File): URI =
{
assertAbsolute(dir)
directoryURI(dir.toURI.normalize)
}
/** Converts an absolute File to a URI. The File is converted to a URI (toURI),
* normalized (normalize), encoded (toASCIIString), and a forward slash ('/') is appended to the path component if
* it does not already end with a slash.
*/
def directoryURI(uri: URI): URI =
{
assertAbsolute(uri)
val str = uri.toASCIIString
val dirStr = if(str.endsWith("/")) str else str + "/"
(new URI(dirStr)).normalize
}
/** Converts the given File to a URI. If the File is relative, the URI is relative, unlike File.toURI*/
def toURI(f: File): URI = if(f.isAbsolute) f.toURI else new URI(f.getPath)
def resolve(base: File, f: File): File =
{
assertAbsolute(base)
val fabs = if(f.isAbsolute) f else new File(directoryURI(new File(base, f.getPath)))
assertAbsolute(fabs)
fabs
}
def assertAbsolute(f: File) = assert(f.isAbsolute, "Not absolute: " + f)
def assertAbsolute(uri: URI) = assert(uri.isAbsolute, "Not absolute: " + uri)
}