From 2687836ca5df48ad3cfb8f7053c97d3be451aa42 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Sun, 23 Jan 2011 22:34:17 -0500 Subject: [PATCH] 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 --- main/Build.scala | 76 +++++++++++++++++++++++++--------- main/Main.scala | 55 +++++++++++++++--------- main/Project.scala | 8 ++-- main/Scope.scala | 3 +- util/collection/Settings.scala | 4 +- util/io/IO.scala | 33 +++++++++++++++ 6 files changed, 132 insertions(+), 47 deletions(-) diff --git a/main/Build.scala b/main/Build.scala index 33415a8f4..0c0a33aac 100644 --- a/main/Build.scala +++ b/main/Build.scala @@ -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)) diff --git a/main/Main.scala b/main/Main.scala index 22548cc28..2b05af488 100644 --- a/main/Main.scala +++ b/main/Main.scala @@ -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, "") { (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 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") } \ No newline at end of file diff --git a/main/Project.scala b/main/Project.scala index 61951717e..ee72432f1 100644 --- a/main/Project.scala +++ b/main/Project.scala @@ -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) diff --git a/main/Scope.scala b/main/Scope.scala index 9e3d69aa1..b7cf694aa 100644 --- a/main/Scope.scala +++ b/main/Scope.scala @@ -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)) } diff --git a/util/collection/Settings.scala b/util/collection/Settings.scala index cd94f012a..c779274e6 100644 --- a/util/collection/Settings.scala +++ b/util/collection/Settings.scala @@ -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] = diff --git a/util/io/IO.scala b/util/io/IO.scala index 07537d92a..4ac2898e9 100644 --- a/util/io/IO.scala +++ b/util/io/IO.scala @@ -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) }