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) }