diff --git a/main/Act.scala b/main/Act.scala index 5de1c5b2c..39c40c135 100644 --- a/main/Act.scala +++ b/main/Act.scala @@ -7,32 +7,103 @@ package sbt import Keys.{sessionSettings, thisProject} import Load.BuildStructure import complete.{DefaultParsers, Parser} + import Aggregation.{KeyValue,Values} import DefaultParsers._ import Types.idFun import java.net.URI import CommandSupport.ShowCommand +final class ParsedKey(val key: ScopedKey[_], val mask: ScopeMask) object Act { val GlobalString = "*" // this does not take aggregation into account - def scopedKey(index: KeyIndex, current: ProjectRef, defaultConfigs: Option[ResolvedReference] => Seq[String], keyMap: Map[String, AttributeKey[_]]): Parser[ScopedKey[_]] = - { - for { - proj <- optProjectRef(index, current) - confAmb <- config( index configs proj ) - keyConfs <- key(index, proj, configs(confAmb, defaultConfigs, proj), keyMap) - keyConfTaskExtra <- { - val andTaskExtra = (key: AttributeKey[_], conf: Option[String]) => - taskExtrasParser(index.tasks(proj, conf, key.label), keyMap, IMap.empty) map { case (task, extra) => (key, conf, task, extra) } - oneOf(keyConfs map { _ flatMap andTaskExtra.tupled }) + def scopedKey(index: KeyIndex, current: ProjectRef, defaultConfigs: Option[ResolvedReference] => Seq[String], + keyMap: Map[String, AttributeKey[_]], data: Settings[Scope]): Parser[ScopedKey[_]] = + scopedKeySelected(index, current, defaultConfigs, keyMap, data).map(_.key) + + // the index should be an aggregated index for proper tab completion + def scopedKeyAggregated(current: ProjectRef, defaultConfigs: Option[ResolvedReference] => Seq[String], structure: BuildStructure): KeysParser = + for(selected <- scopedKeySelected(structure.index.aggregateKeyIndex, current, defaultConfigs, structure.index.keyMap, structure.data) ) yield + Aggregation.aggregate(selected.key, selected.mask, structure.extra) + + def scopedKeySelected(index: KeyIndex, current: ProjectRef, defaultConfigs: Option[ResolvedReference] => Seq[String], + keyMap: Map[String, AttributeKey[_]], data: Settings[Scope]): Parser[ParsedKey] = + scopedKeyFull(index, current, defaultConfigs, keyMap) flatMap { choices => + select(choices, data)( Project.showRelativeKey(current, index.buildURIs.size > 1) ) } - } yield { - val (key, conf, task, extra) = keyConfTaskExtra - ScopedKey( Scope( toAxis(proj, Global), toAxis(conf map ConfigKey.apply, Global), task, extra), key ) - } + + def scopedKeyFull(index: KeyIndex, current: ProjectRef, defaultConfigs: Option[ResolvedReference] => Seq[String], keyMap: Map[String, AttributeKey[_]]): Parser[Seq[Parser[ParsedKey]]] = + { + def taskKeyExtra(proj: Option[ResolvedReference], confAmb: ParsedAxis[String], baseMask: ScopeMask): Seq[Parser[ParsedKey]] = + for { + conf <- configs(confAmb, defaultConfigs, proj, index) + } yield for { + taskAmb <- taskAxis(conf, index.tasks(proj, conf), keyMap) + task = resolveTask(taskAmb) + key <- key(index, proj, conf, task, keyMap) + extra <- extraAxis(keyMap, IMap.empty) + } yield { + val mask = baseMask.copy(task = taskAmb.isExplicit, extra = true) + new ParsedKey( makeScopedKey( proj, conf, task, extra, key ), mask) + } + + for { + rawProject <- optProjectRef(index, current) + proj = resolveProject(rawProject, current) + confAmb <- config( index configs proj ) + partialMask = ScopeMask(rawProject.isExplicit, confAmb.isExplicit, false, false) + } yield + taskKeyExtra(proj, confAmb, partialMask) } + def makeScopedKey(proj: Option[ResolvedReference], conf: Option[String], task: Option[AttributeKey[_]], extra: ScopeAxis[AttributeMap], key: AttributeKey[_]): ScopedKey[_] = + ScopedKey( Scope( toAxis(proj, Global), toAxis(conf map ConfigKey.apply, Global), toAxis(task, Global), extra), key ) + + def select(allKeys: Seq[Parser[ParsedKey]], data: Settings[Scope])(implicit show: Show[ScopedKey[_]]): Parser[ParsedKey] = + seq(allKeys) flatMap { ss => + val default = ss.headOption match { + case None => noValidKeys + case Some(x) => success(x) + } + selectFromValid(ss filter isValid(data), default) + } + def selectFromValid(ss: Seq[ParsedKey], default: Parser[ParsedKey])(implicit show: Show[ScopedKey[_]]): Parser[ParsedKey] = + selectByTask(selectByConfig(ss)) match + { + case Seq() => default + case Seq(single) => success(single) + case multi => failure("Ambiguous keys: " + showAmbiguous(keys(multi))) + } + private[this] def keys(ss: Seq[ParsedKey]): Seq[ScopedKey[_]] = ss.map(_.key) + def selectByConfig(ss: Seq[ParsedKey]): Seq[ParsedKey] = + ss match + { + case Seq() => Nil + case Seq(x, tail @ _*) => // select the first configuration containing a valid key + tail.takeWhile(_.key.scope.config == x.key.scope.config) match + { + case Seq() => x :: Nil + case xs => x +: xs + } + } + def selectByTask(ss: Seq[ParsedKey]): Seq[ParsedKey] = + { + val (selects, globals) = ss.partition(_.key.scope.task.isSelect) + if(globals.nonEmpty) globals else selects + } + + def noValidKeys = failure("No such key.") + + def showAmbiguous(keys: Seq[ScopedKey[_]])(implicit show: Show[ScopedKey[_]]): String = + keys.take(3).map(x => show(x)).mkString("", ", ", if(keys.size > 3) ", ..." else "") + + def isValid(data: Settings[Scope])(parsed: ParsedKey): Boolean = + { + val key = parsed.key + data.definingScope(key.scope, key.key) == Some(key.scope) + } + def examples(p: Parser[String], exs: Set[String], label: String): Parser[String] = p !!! ("Expected " + label) examples exs def examplesStrict(p: Parser[String], exs: Set[String], label: String): Parser[String] = @@ -42,31 +113,29 @@ object Act p.? map { opt => toAxis(opt, ifNone) } def toAxis[T](opt: Option[T], ifNone: ScopeAxis[T]): ScopeAxis[T] = opt match { case Some(t) => Select(t); case None => ifNone } - def defaultConfigs(data: Settings[Scope])(project: ProjectRef): Seq[String] = - thisProject in project get data map( _.configurations.map(_.name)) getOrElse Nil - def config(confs: Set[String]): Parser[Option[String]] = - token( (examplesStrict(ID, confs, "configuration") | GlobalString) <~ ':' ).? + def config(confs: Set[String]): Parser[ParsedAxis[String]] = + token( (GlobalString ^^^ ParsedGlobal | value(examples(ID, confs, "configuration")) ) <~ ':' ) ?? Omitted - def configs(explicit: Option[String], defaultConfigs: Option[ResolvedReference] => Seq[String], proj: Option[ResolvedReference]): List[Option[String]] = + def configs(explicit: ParsedAxis[String], defaultConfigs: Option[ResolvedReference] => Seq[String], proj: Option[ResolvedReference], index: KeyIndex): Seq[Option[String]] = explicit match { - case None => None :: defaultConfigs(proj).map(c => Some(c)).toList - case Some(GlobalString) => None :: Nil - case Some(_) => explicit :: Nil + case Omitted => None +: defaultConfigs(proj).flatMap(nonEmptyConfig(index, proj)) + case ParsedGlobal => None :: Nil + case pv: ParsedValue[String] => Some(pv.value) :: Nil } + def nonEmptyConfig(index: KeyIndex, proj: Option[ResolvedReference]): String => Seq[Option[String]] = config => + if(index.isEmpty(proj, Some(config))) Nil else Some(config) :: Nil - def key(index: KeyIndex, proj: Option[ResolvedReference], confs: Seq[Option[String]], keyMap: Map[String,AttributeKey[_]]): Parser[Seq[Parser[(AttributeKey[_], Option[String])]]] = + def key(index: KeyIndex, proj: Option[ResolvedReference], conf: Option[String], task: Option[AttributeKey[_]], keyMap: Map[String,AttributeKey[_]]): Parser[AttributeKey[_]] = { - val confMap = confs map { conf => (conf, index.keys(proj, conf)) } toMap; - val allKeys = (Set.empty[String] /: confMap.values)(_ ++ _) - token(ID !!! "Expected key" examples allKeys).map { keyString => - val valid = confs.filter{ conf => confMap(conf) contains keyString } - def get(conf: Option[String]) = getKey(keyMap, keyString, k => (k, conf)) - val parsers = valid map { conf => getKey(keyMap, keyString, k => (k, conf)) } - if(parsers.isEmpty) get(None) :: Nil else parsers - } + def keyParser(keys: Set[String]): Parser[AttributeKey[_]] = + token(ID !!! "Expected key" examples keys) flatMap { keyString=> + getKey(keyMap, keyString, idFun) + } + keyParser(index.keys(proj, conf, task)) } + def getKey[T](keyMap: Map[String,AttributeKey[_]], keyString: String, f: AttributeKey[_] => T): Parser[T] = keyMap.get(keyString) match { case Some(k) => success(f(k)) @@ -75,30 +144,28 @@ object Act val spacedComma = token(OptSpace ~ ',' ~ OptSpace) - def taskExtrasParser(tasks: Set[AttributeKey[_]], knownKeys: Map[String, AttributeKey[_]], knownValues: IMap[AttributeKey, Set]): Parser[(ScopeAxis[AttributeKey[_]], ScopeAxis[AttributeMap])] = + def extraAxis(knownKeys: Map[String, AttributeKey[_]], knownValues: IMap[AttributeKey, Set]): Parser[ScopeAxis[AttributeMap]] = { - val extras = extrasParser(knownKeys, knownValues) - val taskParser = optionalAxis(taskAxisParser(tasks, knownKeys), Global) - val taskAndExtra = - taskParser flatMap { taskAxis => - if(taskAxis.isSelect) - optionalAxis(spacedComma ~> extras, Global) map { x => (taskAxis, x) } - else - extras map { x => (taskAxis, Select(x)) } - } - val base = token('(', hide = _ == 1 && tasks.isEmpty) ~> taskAndExtra <~ token(')') - base ?? ( (Global, Global) ) + val extrasP = extrasParser(knownKeys, knownValues) + val extras = token('(', hide = _ == 1 && knownValues.isEmpty) ~> extrasP <~ token(')') + optionalAxis(extras, Global) } - def taskAxisParser(tasks: Set[AttributeKey[_]], allKnown: Map[String, AttributeKey[_]]): Parser[AttributeKey[_]] = + def taskAxis(d: Option[String], tasks: Set[AttributeKey[_]], allKnown: Map[String, AttributeKey[_]]): Parser[ParsedAxis[AttributeKey[_]]] = { val knownKeys: Map[String, AttributeKey[_]] = tasks.toSeq.map(key => (key.label, key)).toMap - val known = allKnown ++ knownKeys - val valid = known.keys.toSet - val suggested = knownKeys.keys.toSet - val keyP = filterStrings(examples(ID, suggested, "key"), valid, "key") map known - token("for" ~ Space) ~> token(keyP) + val valid = allKnown ++ knownKeys + val suggested = knownKeys.keySet + val keyP = filterStrings(examples(ID, suggested, "key"), valid.keySet, "key") map valid + (token(value(keyP) | GlobalString ^^^ ParsedGlobal ) <~ token("::".id) ) ?? Omitted } + def resolveTask(task: ParsedAxis[AttributeKey[_]]): Option[AttributeKey[_]] = + task match + { + case ParsedGlobal | Omitted => None + case t: ParsedValue[AttributeKey[_]] => Some(t.value) + } + def filterStrings(base: Parser[String], valid: Set[String], label: String): Parser[String] = base.filter(valid, Command.invalidValue(label, valid)) @@ -122,14 +189,14 @@ object Act def knownIDParser[T](knownKeys: Map[String, T], label: String): Parser[T] = token(examplesStrict(ID, knownKeys.keys.toSet, label)) map knownKeys - def projectRef(index: KeyIndex, currentBuild: URI): Parser[Option[ResolvedReference]] = + def projectRef(index: KeyIndex, currentBuild: URI): Parser[ParsedAxis[ResolvedReference]] = { - val global = token(GlobalString <~ '/') ^^^ None - global | some(resolvedReference(index, currentBuild, '/')) + val global = token(GlobalString ~ '/') ^^^ ParsedGlobal + global | value(resolvedReference(index, currentBuild, '/')) } def resolvedReference(index: KeyIndex, currentBuild: URI, trailing: Parser[_]): Parser[ResolvedReference] = { - def projectID(uri: URI) = token( examplesStrict(ID, index projects uri, "project ID") <~ trailing ) + def projectID(uri: URI) = token( examples(ID, index projects uri, "project ID") <~ trailing ) def projectRef(uri: URI) = projectID(uri) map { id => ProjectRef(uri, id) } val uris = index.buildURIs @@ -138,36 +205,60 @@ object Act buildRef flatMap { case None => projectRef(currentBuild) - case Some(uri) => projectRef(uri) | token(trailing ~> success(BuildRef(uri))) + case Some(uri) => projectRef(uri) | token(trailing ^^^ BuildRef(uri)) } } - def optProjectRef(index: KeyIndex, current: ProjectRef): Parser[Option[ResolvedReference]] = - projectRef(index, current.build) ?? Some(current) + def optProjectRef(index: KeyIndex, current: ProjectRef): Parser[ParsedAxis[ResolvedReference]] = + projectRef(index, current.build) ?? Omitted + def resolveProject(parsed: ParsedAxis[ResolvedReference], current: ProjectRef): Option[ResolvedReference] = + parsed match + { + case Omitted => Some(current) + case ParsedGlobal => None + case pv: ParsedValue[ResolvedReference] => Some(pv.value) + } def actParser(s: State): Parser[() => State] = requireSession(s, actParser0(s)) - private[this] def actParser0(state: State) = + private[this] def actParser0(state: State): Parser[() => State] = { val extracted = Project extract state import extracted.{showKey, structure} showParser.flatMap { show => - scopedKeyParser(extracted) flatMap Aggregation.valueParser(state, structure, show) + aggregatedKeyParser(extracted) flatMap { kvs => + Aggregation.evaluatingParser(state, structure, show)( keyValues(structure)(kvs) ) + } } } def showParser = token( (ShowCommand ~ Space) ^^^ true) ?? false def scopedKeyParser(state: State): Parser[ScopedKey[_]] = scopedKeyParser(Project extract state) - def scopedKeyParser(extracted: Extracted): Parser[ScopedKey[_]] = - { - import extracted._ - def confs(uri: URI) = if(structure.units.contains(uri)) defaultConfigs(structure.data)(ProjectRef(uri, rootProject(uri))) else Nil - val defaultConfs: Option[ResolvedReference] => Seq[String] = { - case None => confs(structure.root) - case Some(BuildRef(uri)) => confs(uri) - case Some(ref: ProjectRef) => if(Project.getProject(ref, structure).isDefined) defaultConfigs(structure.data)(ref) else Nil + def scopedKeyParser(extracted: Extracted): Parser[ScopedKey[_]] = scopedKeyParser(extracted.structure, extracted.currentRef) + def scopedKeyParser(structure: BuildStructure, currentRef: ProjectRef): Parser[ScopedKey[_]] = + scopedKey(structure.index.keyIndex, currentRef, structure.extra.configurationsForAxis, structure.index.keyMap, structure.data) + + type KeysParser = Parser[Seq[ScopedKey[T]] forSome { type T}] + def aggregatedKeyParser(state: State): KeysParser = aggregatedKeyParser(Project extract state) + def aggregatedKeyParser(extracted: Extracted): KeysParser = aggregatedKeyParser(extracted.structure, extracted.currentRef) + def aggregatedKeyParser(structure: BuildStructure, currentRef: ProjectRef): KeysParser = + scopedKeyAggregated(currentRef, structure.extra.configurationsForAxis, structure) + + def keyValues[T](state: State)(keys: Seq[ScopedKey[T]]): Values[T] = keyValues(Project extract state)(keys) + def keyValues[T](extracted: Extracted)(keys: Seq[ScopedKey[T]]): Values[T] = keyValues(extracted.structure)(keys) + def keyValues[T](structure: BuildStructure)(keys: Seq[ScopedKey[T]]): Values[T] = + keys.flatMap { key => + structure.data.get(key.scope, key.key) map { value => + KeyValue(key, value) + } } - scopedKey(structure.index.keyIndex, currentRef, defaultConfs, structure.index.keyMap) - } def requireSession[T](s: State, p: => Parser[T]): Parser[T] = if(s get sessionSettings isEmpty) failure("No project loaded") else p + + sealed trait ParsedAxis[+T] { + final def isExplicit = this != Omitted + } + final object ParsedGlobal extends ParsedAxis[Nothing] + final object Omitted extends ParsedAxis[Nothing] + final class ParsedValue[T](val value: T) extends ParsedAxis[T] + def value[T](t: Parser[T]): Parser[ParsedAxis[T]] = t map { v => new ParsedValue(v) } } \ No newline at end of file diff --git a/main/Aggregation.scala b/main/Aggregation.scala index 2423c5d7a..831e16eff 100644 --- a/main/Aggregation.scala +++ b/main/Aggregation.scala @@ -4,7 +4,7 @@ package sbt import Project.ScopedKey - import Load.BuildStructure + import Load.{BuildStructure,LoadedBuildUnit} import Keys.{aggregate, showSuccess, showTiming, timingFormat} import sbt.complete.Parser import java.net.URI @@ -14,50 +14,7 @@ package sbt sealed trait Aggregation final object Aggregation { - def apply(dependencies: Seq[ProjectReference], transitive: Boolean = true): Aggregation = new Explicit(dependencies, transitive) - implicit def fromBoolean(b: Boolean): Aggregation = if(b) Enabled else Disabled - val Enabled = new Implicit(true) - val Disabled = new Implicit(false) - final case class Implicit(enabled: Boolean) extends Aggregation - final class Explicit(val dependencies: Seq[ProjectReference], val transitive: Boolean) extends Aggregation - final case class KeyValue[+T](key: ScopedKey[_], value: T) - def getTasks[T](key: ScopedKey[T], structure: BuildStructure, transitive: Boolean): Seq[KeyValue[T]] = - getTasks0(key, structure, transitive, new mutable.HashMap[(ScopedKey[_], Boolean), Seq[KeyValue[T]]]) - private type Memo[T] = mutable.Map[(ScopedKey[_], Boolean), Seq[KeyValue[T]]] - private[this] def getTasks0[T](key: ScopedKey[T], structure: BuildStructure, transitive: Boolean, memo: Memo[T]): Seq[KeyValue[T]] = - memo.getOrElseUpdate( (key, transitive), { - val task = structure.data.get(key.scope, key.key).toList.map(t => KeyValue(key,t)) - if(transitive) aggregateDeps(key, structure, memo) ++ task else task - }) - def projectAggregate(key: ScopedKey[_], structure: BuildStructure): Seq[ProjectRef] = - { - val project = key.scope.project.toOption.flatMap { ref => Project.getProjectForReference(ref, structure) } - project match { case Some(p) => p.aggregate; case None => Nil } - } - private[this] def aggregateDeps[T](key: ScopedKey[T], structure: BuildStructure, memo: Memo[T]): Seq[KeyValue[T]] = - { - val aggregated = aggregate in Scope.fillTaskAxis(key.scope, key.key) get structure.data getOrElse Enabled - val (agg, transitive) = - aggregated match - { - case Implicit(false) => (Nil, false) - case Implicit(true) => (projectAggregate(key, structure), true) - case e: Explicit => (e.dependencies, e.transitive) - } - val currentBuild = key.scope.project.toOption.flatMap { case ProjectRef(uri, _) => Some(uri); case BuildRef(ref) => Some(ref); case _ => None } - agg flatMap { a => - val resolved = subCurrentBuild(a, currentBuild) - val newKey = ScopedKey(key.scope.copy(project = Select(resolved)), key.key) - getTasks(newKey, structure, transitive) - } - } - private def subCurrentBuild(ref: Reference, currentBuild: Option[URI]): Reference = - currentBuild match - { - case None => ref - case Some(current) => Scope.resolveBuildOnly(current, ref) - } def printSettings[T](xs: Seq[KeyValue[T]], log: Logger)(implicit display: Show[ScopedKey[_]]) = xs match @@ -66,6 +23,7 @@ final object Aggregation case _ => xs foreach { case KeyValue(key, value) => log.info(display(key) + "\n\t" + value.toString) } } type Values[T] = Seq[KeyValue[T]] + type AnyKeys = Values[_] def seqParser[T](ps: Values[Parser[T]]): Parser[Seq[KeyValue[T]]] = seq(ps.map { case KeyValue(k,p) => p.map(v => KeyValue(k,v) ) }) def applyTasks[T](s: State, structure: BuildStructure, ps: Values[Parser[Task[T]]], show: Boolean)(implicit display: Show[ScopedKey[_]]): Parser[() => State] = @@ -141,10 +99,11 @@ final object Aggregation runTasks(s, structure, roots, dummies, show) } } - def valueParser(s: State, structure: BuildStructure, show: Boolean)(key: ScopedKey[_])(implicit display: Show[ScopedKey[_]]): Parser[() => State] = - getTasks(key, structure, true).toList match + + def evaluatingParser[T](s: State, structure: BuildStructure, show: Boolean)(keys: Seq[KeyValue[T]])(implicit display: Show[ScopedKey[_]]): Parser[() => State] = + keys.toList match { - case Nil => failure("No such setting/task: " + display(key)) + case Nil => failure("No such setting/task") case xs @ KeyValue(_, _: InputStatic[t]) :: _ => applyTasks(s, structure, maps(xs.asInstanceOf[Values[InputStatic[t]]])(_.parser(s)), show) case xs @ KeyValue(_, _: InputDynamic[t]) :: _ => applyDynamicTasks(s, structure, xs.asInstanceOf[Values[InputDynamic[t]]], show) case xs @ KeyValue(_, _: Task[t]) :: _ => applyTasks(s, structure, maps(xs.asInstanceOf[Values[Task[t]]])(x => success(x)), show) @@ -152,4 +111,56 @@ final object Aggregation } private[this] def maps[T, S](vs: Values[T])(f: T => S): Values[S] = vs map { case KeyValue(k,v) => KeyValue(k, f(v)) } + + + def projectAggregates[Proj](proj: Option[Reference], extra: BuildUtil[Proj], reverse: Boolean): Seq[ProjectRef] = + { + val resRef = proj.map(p => extra.projectRefFor(extra.resolveRef(p))) + resRef.toList.flatMap(ref => + if(reverse) extra.aggregates.reverse(ref) else extra.aggregates.forward(ref) + ) + } + + def aggregate[T, Proj](key: ScopedKey[T], rawMask: ScopeMask, extra: BuildUtil[Proj], reverse: Boolean = false): Seq[ScopedKey[T]] = + { + val mask = rawMask.copy(project = true) + Dag.topologicalSort(key) { k => + if(reverse) + reverseAggregatedKeys(k, extra, mask) + else if(aggregationEnabled(key, extra.data)) + aggregatedKeys(k, extra, mask) + else + Nil + } + } + def reverseAggregatedKeys[T](key: ScopedKey[T], extra: BuildUtil[_], mask: ScopeMask): Seq[ScopedKey[T]] = + projectAggregates(key.scope.project.toOption, extra, reverse = true) flatMap { ref => + val toResolve = key.scope.copy(project = Select(ref)) + val resolved = Resolve(extra, Global, key.key, mask)(toResolve) + val skey = ScopedKey(resolved, key.key) + if( aggregationEnabled(skey, extra.data) ) skey :: Nil else Nil + } + + def aggregatedKeys[T](key: ScopedKey[T], extra: BuildUtil[_], mask: ScopeMask): Seq[ScopedKey[T]] = + projectAggregates(key.scope.project.toOption, extra, reverse = false) map { ref => + val toResolve = key.scope.copy(project = Select(ref)) + val resolved = Resolve(extra, Global, key.key, mask)(toResolve) + ScopedKey(resolved, key.key) + } + + def aggregationEnabled(key: ScopedKey[_], data: Settings[Scope]): Boolean = + Keys.aggregate in Scope.fillTaskAxis(key.scope, key.key) get data getOrElse true + + def relation(units: Map[URI, LoadedBuildUnit]): Relation[ProjectRef, ProjectRef] = + { + val depPairs = + for { + (uri, unit) <- units.toIterable + project <- unit.defined.values + ref = ProjectRef(uri, project.id) + agg <- project.aggregate + } yield + (ref, agg) + Relation.empty ++ depPairs + } } \ No newline at end of file diff --git a/main/BuildUtil.scala b/main/BuildUtil.scala new file mode 100644 index 000000000..fec4d346a --- /dev/null +++ b/main/BuildUtil.scala @@ -0,0 +1,40 @@ +package sbt + +import java.net.URI + +final class BuildUtil[Proj]( + val keyIndex: KeyIndex, + val data: Settings[Scope], + val root: URI, + val rootProjectID: URI => String, + val project: (URI, String) => Proj, + val configurations: Proj => Seq[ConfigKey], + val aggregates: Relation[ProjectRef, ProjectRef] +) +{ + def rootProject(uri: URI): Proj = + project(uri, rootProjectID(uri)) + + def resolveRef(ref: Reference): ResolvedReference = + Scope.resolveReference(root, rootProjectID, ref) + + def projectFor(ref: ResolvedReference): Proj = ref match { + case ProjectRef(uri, id) => project(uri, id) + case BuildRef(uri) => rootProject(uri) + } + def projectRefFor(ref: ResolvedReference): ProjectRef = ref match { + case p: ProjectRef => p + case BuildRef(uri) => ProjectRef(uri, rootProjectID(uri)) + } + def projectForAxis(ref: Option[ResolvedReference]): Proj = ref match { + case Some(ref) => projectFor(ref) + case None => rootProject(root) + } + def exactProject(refOpt: Option[Reference]): Option[Proj] = refOpt map resolveRef flatMap { + case ProjectRef(uri, id) => Some(project(uri, id)) + case _ => None + } + + val configurationsForAxis: Option[ResolvedReference] => Seq[String] = + refOpt => configurations(projectForAxis(refOpt)).map(_.name) +} \ No newline at end of file diff --git a/main/Defaults.scala b/main/Defaults.scala index 51de2bd3c..99e51b8ba 100644 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -98,7 +98,7 @@ object Defaults extends BuildCommon sbtPlugin :== false, crossPaths :== true, classpathTypes :== Set("jar", "bundle"), - aggregate :== Aggregation.Enabled, + aggregate :== true, maxErrors :== 100, showTiming :== true, timingFormat :== Aggregation.defaultFormat, diff --git a/main/KeyIndex.scala b/main/KeyIndex.scala index a4a5794ee..c3456b808 100644 --- a/main/KeyIndex.scala +++ b/main/KeyIndex.scala @@ -5,6 +5,7 @@ package sbt import java.net.URI import Project.ScopedKey + import Load.BuildStructure import complete.DefaultParsers.validID import Types.{idFun, some} @@ -13,6 +14,9 @@ object KeyIndex def empty: ExtendableKeyIndex = new KeyIndex0(emptyBuildIndex) def apply(known: Iterable[ScopedKey[_]]): ExtendableKeyIndex = (empty /: known) { _ add _ } + def aggregate(known: Iterable[ScopedKey[_]], extra: BuildUtil[_]): ExtendableKeyIndex = + (empty /: known) { (index, key) => index.addAggregated(key, extra) } + def combine(indices: Seq[KeyIndex]): KeyIndex = new KeyIndex { def buildURIs = concat(_.buildURIs) def projects(uri: URI) = concat(_.projects(uri)) @@ -36,6 +40,10 @@ import KeyIndex._ trait KeyIndex { + // TODO, optimize + def isEmpty(proj: Option[ResolvedReference], conf: Option[String]): Boolean = keys(proj, conf).isEmpty + def isEmpty(proj: Option[ResolvedReference], conf: Option[String], task: Option[AttributeKey[_]]): Boolean = keys(proj, conf, task).isEmpty + def buildURIs: Set[URI] def projects(uri: URI): Set[String] def configs(proj: Option[ResolvedReference]): Set[String] @@ -48,6 +56,7 @@ trait KeyIndex trait ExtendableKeyIndex extends KeyIndex { def add(scoped: ScopedKey[_]): ExtendableKeyIndex + def addAggregated(scoped: ScopedKey[_], extra: BuildUtil[_]): ExtendableKeyIndex } // task axis <-> key private final class AKeyIndex(val data: Relation[ Option[AttributeKey[_]], String]) @@ -106,13 +115,22 @@ private final class KeyIndex0(val data: BuildIndex) extends ExtendableKeyIndex } private[this] def optConfigs(project: Option[ResolvedReference]): Seq[Option[String]] = None +: (configs(project).toSeq map some.fn) + def addAggregated(scoped: ScopedKey[_], extra: BuildUtil[_]): ExtendableKeyIndex = + if(validID(scoped.key.label)) + { + val aggregateProjects = Aggregation.aggregate(scoped, ScopeMask(), extra, reverse = true) + ((this: ExtendableKeyIndex) /: aggregateProjects)(_ add _) + } + else + this + def add(scoped: ScopedKey[_]): ExtendableKeyIndex = if(validID(scoped.key.label)) add0(scoped) else this private[this] def add0(scoped: ScopedKey[_]): ExtendableKeyIndex = { val (build, project) = parts(scoped.scope.project.toOption) - add(build, project, scoped.scope.config, scoped.scope.task, scoped.key) + add1(build, project, scoped.scope.config, scoped.scope.task, scoped.key) } - def add(uri: Option[URI], id: Option[String], config: ScopeAxis[ConfigKey], task: ScopeAxis[AttributeKey[_]], key: AttributeKey[_]): ExtendableKeyIndex = + private[this] def add1(uri: Option[URI], id: Option[String], config: ScopeAxis[ConfigKey], task: ScopeAxis[AttributeKey[_]], key: AttributeKey[_]): ExtendableKeyIndex = new KeyIndex0( data.add(uri, id, config.toOption.map(_.name), task.toOption, key) ) } diff --git a/main/Keys.scala b/main/Keys.scala index e551e1dbb..cd2ba855b 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -149,7 +149,7 @@ object Keys val definesClass = TaskKey[DefinesClass]("defines-class", "Internal use: provides a function that determines whether the provided file contains a given class.") val doc = TaskKey[File]("doc", "Generates API documentation.") val copyResources = TaskKey[Seq[(File,File)]]("copy-resources", "Copies resources to the output directory.") - val aggregate = SettingKey[Aggregation]("aggregate", "Configures task aggregation.") + val aggregate = SettingKey[Boolean]("aggregate", "Configures task aggregation.") // package keys val packageBin = TaskKey[File]("package-bin", "Produces a main artifact, such as a binary jar.") @@ -298,7 +298,7 @@ object Keys val sbtDependency = SettingKey[ModuleID]("sbt-dependency", "Provides a definition for declaring the current version of sbt.") val sbtVersion = SettingKey[String]("sbt-version", "Provides the version of sbt. This setting should be not be modified.") val sbtBinaryVersion = SettingKey[String]("sbt-binary-version", "Defines the binary compatibility version substring.") - val skip = TaskKey[Boolean]("skip", "For tasks that support it (currently only 'compile'), setting skip to true will force the task to not to do its work. This exact semantics may vary by task.") + val skip = TaskKey[Boolean]("skip", "For tasks that support it (currently only 'compile' and 'update'), setting skip to true will force the task to not to do its work. This exact semantics may vary by task.") // special val sessionVars = AttributeKey[SessionVar.Map]("session-vars", "Bindings that exist for the duration of the session.") diff --git a/main/Load.scala b/main/Load.scala index 83466fe65..ef3c3b73d 100644 --- a/main/Load.scala +++ b/main/Load.scala @@ -121,7 +121,7 @@ object Load val settings = finalTransforms(buildConfigurations(loaded, getRootProject(projects), rootEval, config.injectSettings)) val delegates = config.delegates(loaded) val data = Project.makeSettings(settings, delegates, config.scopeLocal)( Project.showLoadingKey( loaded ) ) - val index = structureIndex(data, settings) + val index = structureIndex(data, settings, loaded.extra(data)) val streams = mkStreams(projects, loaded.root, data) (rootEval, new BuildStructure(projects, loaded.root, settings, data, index, streams, delegates, config.scopeLocal)) } @@ -160,12 +160,14 @@ object Load def setDefinitionKey[T](tk: Task[T], key: ScopedKey[_]): Task[T] = if(isDummy(tk)) tk else Task(tk.info.set(Keys.taskDefinitionKey, key), tk.work) - def structureIndex(data: Settings[Scope], settings: Seq[Setting[_]]): StructureIndex = + def structureIndex(data: Settings[Scope], settings: Seq[Setting[_]], extra: KeyIndex => BuildUtil[_]): StructureIndex = { val keys = Index.allKeys(settings) val attributeKeys = Index.attributeKeys(data) ++ keys.map(_.key) val scopedKeys = keys ++ data.allKeys( (s,k) => ScopedKey(s,k)) - new StructureIndex(Index.stringToKeyMap(attributeKeys), Index.taskToKeyMap(data), Index.triggers(data), KeyIndex(scopedKeys)) + val keyIndex = KeyIndex(scopedKeys) + val aggIndex = KeyIndex.aggregate(scopedKeys, extra(keyIndex)) + new StructureIndex(Index.stringToKeyMap(attributeKeys), Index.taskToKeyMap(data), Index.triggers(data), keyIndex, aggIndex) } // Reevaluates settings after modifying them. Does not recompile or reload any build components. @@ -173,7 +175,7 @@ object Load { val transformed = finalTransforms(newSettings) val newData = Project.makeSettings(transformed, structure.delegates, structure.scopeLocal) - val newIndex = structureIndex(newData, transformed) + val newIndex = structureIndex(newData, transformed, index => buildUtil(structure.root, structure.units, index, newData)) val newStreams = mkStreams(structure.units, structure.root, newData) new BuildStructure(units = structure.units, root = structure.root, settings = transformed, data = newData, index = newIndex, streams = newStreams, delegates = structure.delegates, scopeLocal = structure.scopeLocal) } @@ -556,6 +558,7 @@ object Load { checkCycles(units) def allProjectRefs: Seq[(ProjectRef, ResolvedProject)] = for( (uri, unit) <- units.toSeq; (id, proj) <- unit.defined ) yield ProjectRef(uri, id) -> proj + def extra(data: Settings[Scope])(keyIndex: KeyIndex): BuildUtil[ResolvedProject] = buildUtil(root, units, keyIndex, data) } def checkCycles(units: Map[URI, LoadedBuildUnit]) { @@ -578,6 +581,7 @@ object Load final class LoadedBuildUnit(val unit: BuildUnit, val defined: Map[String, ResolvedProject], val rootProjects: Seq[String], val buildSettings: Seq[Setting[_]]) extends BuildUnitBase { assert(!rootProjects.isEmpty, "No root projects defined for build unit " + unit) + val root = rootProjects.head def localBase = unit.localBase def classpath: Seq[File] = unit.definitions.target ++ unit.plugins.classpath def loader = unit.definitions.loader @@ -595,8 +599,16 @@ object Load def allProjects(build: URI): Seq[ResolvedProject] = units(build).defined.values.toSeq def allProjectRefs: Seq[ProjectRef] = units.toSeq flatMap { case (build, unit) => refs(build, unit.defined.values.toSeq) } def allProjectRefs(build: URI): Seq[ProjectRef] = refs(build, allProjects(build)) + val extra: BuildUtil[ResolvedProject] = buildUtil(root, units, index.keyIndex, data) private[this] def refs(build: URI, projects: Seq[ResolvedProject]): Seq[ProjectRef] = projects.map { p => ProjectRef(build, p.id) } } + def buildUtil(root: URI, units: Map[URI, LoadedBuildUnit], keyIndex: KeyIndex, data: Settings[Scope]): BuildUtil[ResolvedProject] = + { + val getp = (build: URI, project: String) => Load.getProject(units, build, project) + val configs = (_: ResolvedProject).configurations.map(c => ConfigKey(c.name)) + val aggregates = Aggregation.relation(units) + new BuildUtil(keyIndex, data, root, Load getRootProject units, getp, configs, aggregates) + } final case class LoadBuildConfiguration(stagingDirectory: File, classpath: Seq[Attributed[File]], loader: ClassLoader, compilers: Compilers, evalPluginDef: (BuildStructure, State) => Seq[Attributed[File]], definesClass: DefinesClass, delegates: LoadedBuild => Scope => Seq[Scope], scopeLocal: ScopeLocal, injectSettings: InjectSettings, globalPlugin: Option[GlobalPlugin], log: Logger) { lazy val (globalPluginClasspath, globalPluginLoader) = pluginDefinitionLoader(this, Load.globalPluginClasspath(globalPlugin)) @@ -605,5 +617,11 @@ object Load final case class InjectSettings(global: Seq[Setting[_]], project: Seq[Setting[_]], projectLoaded: ClassLoader => Seq[Setting[_]]) // 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) + final class StructureIndex( + val keyMap: Map[String, AttributeKey[_]], + val taskToKey: Map[Task[_], ScopedKey[Task[_]]], + val triggers: Triggers[Task], + val keyIndex: KeyIndex, + val aggregateKeyIndex: KeyIndex + ) } diff --git a/main/Main.scala b/main/Main.scala index a81032304..acb565613 100644 --- a/main/Main.scala +++ b/main/Main.scala @@ -8,6 +8,7 @@ package sbt import HistoryCommands.{Start => HistoryPrefix} import compiler.EvalImports import Types.{const,idFun} + import Aggregation.AnyKeys import Command.applyEffect import Keys.{analysis,historyPath,globalLogging,shellPrompt} @@ -427,9 +428,9 @@ object BuiltinCommands } } def lastGrep = Command(LastGrepCommand, lastGrepBrief, lastGrepDetailed)(lastGrepParser) { - case (s, (pattern,Some(sk))) => + case (s, (pattern,Some(sks))) => val (str, ref, display) = extractLast(s) - Output.lastGrep(sk, str, str.streams(s), pattern, printLast(s))(display) + Output.lastGrep(sks, str.streams(s), pattern, printLast(s))(display) keepLastLog(s) case (s, (pattern, None)) => for(logFile <- lastLogFile(s)) yield @@ -458,15 +459,19 @@ object BuiltinCommands def allKeyParser(s: State): Parser[AttributeKey[_]] = { val keyMap = Project.structure(s).index.keyMap - token((Space ~> ID) !!! "Expected key" examples keyMap.keySet) flatMap { key => Act.getKey(keyMap, key, idFun) } + token(Space ~> (ID !!! "Expected key" examples keyMap.keySet)) flatMap { key => Act.getKey(keyMap, key, idFun) } } + val spacedKeyParser = (s: State) => Act.requireSession(s, token(Space) ~> Act.scopedKeyParser(s)) - val optSpacedKeyParser = (s: State) => spacedKeyParser(s).? - def lastGrepParser(s: State) = Act.requireSession(s, (token(Space) ~> token(NotSpace, "")) ~ optSpacedKeyParser(s)) - def last = Command(LastCommand, lastBrief, lastDetailed)(optSpacedKeyParser) { - case (s,Some(sk)) => + val spacedAggregatedParser = (s: State) => Act.requireSession(s, token(Space) ~> Act.aggregatedKeyParser(s)) + val aggregatedKeyValueParser: State => Parser[Option[AnyKeys]] = + (s: State) => spacedAggregatedParser(s).map(x => Act.keyValues(s)(x) ).? + + def lastGrepParser(s: State) = Act.requireSession(s, (token(Space) ~> token(NotSpace, "")) ~ aggregatedKeyValueParser(s)) + def last = Command(LastCommand, lastBrief, lastDetailed)(aggregatedKeyValueParser) { + case (s,Some(sks)) => val (str, ref, display) = extractLast(s) - Output.last(sk, str, str.streams(s), printLast(s))(display) + Output.last(sks, str.streams(s), printLast(s))(display) keepLastLog(s) case (s, None) => for(logFile <- lastLogFile(s)) yield diff --git a/main/Output.scala b/main/Output.scala index 7fb1ca977..665e24a81 100644 --- a/main/Output.scala +++ b/main/Output.scala @@ -7,8 +7,7 @@ package sbt import java.io.File import Keys.{Streams, TaskStreams} import Project.ScopedKey - import Load.BuildStructure - import Aggregation.{getTasks, KeyValue} + import Aggregation.{KeyValue, Values} import Types.idFun import annotation.tailrec import scala.Console.{BOLD, RESET} @@ -30,16 +29,16 @@ object Output } final val DefaultTail = "> " - def last(key: ScopedKey[_], structure: BuildStructure, streams: Streams, printLines: Seq[String] => Unit)(implicit display: Show[ScopedKey[_]]): Unit = - printLines( flatLines(lastLines(key, structure, streams))(idFun) ) + def last(keys: Values[_], streams: Streams, printLines: Seq[String] => Unit)(implicit display: Show[ScopedKey[_]]): Unit = + printLines( flatLines(lastLines(keys, streams))(idFun) ) def last(file: File, printLines: Seq[String] => Unit, tailDelim: String = DefaultTail): Unit = printLines(tailLines(file, tailDelim)) - def lastGrep(key: ScopedKey[_], structure: BuildStructure, streams: Streams, patternString: String, printLines: Seq[String] => Unit)(implicit display: Show[ScopedKey[_]]): Unit = + def lastGrep(keys: Values[_], streams: Streams, patternString: String, printLines: Seq[String] => Unit)(implicit display: Show[ScopedKey[_]]): Unit = { val pattern = Pattern compile patternString - val lines = flatLines( lastLines(key, structure, streams) )(_ flatMap showMatches(pattern)) + val lines = flatLines( lastLines(keys, streams) )(_ flatMap showMatches(pattern)) printLines( lines ) } def lastGrep(file: File, patternString: String, printLines: Seq[String] => Unit, tailDelim: String = DefaultTail): Unit = @@ -47,7 +46,7 @@ object Output def grep(lines: Seq[String], patternString: String): Seq[String] = lines flatMap showMatches(Pattern compile patternString) - def flatLines(outputs: Seq[KeyValue[Seq[String]]])(f: Seq[String] => Seq[String])(implicit display: Show[ScopedKey[_]]): Seq[String] = + def flatLines(outputs: Values[Seq[String]])(f: Seq[String] => Seq[String])(implicit display: Show[ScopedKey[_]]): Seq[String] = { val single = outputs.size == 1 outputs flatMap { case KeyValue(key, lines) => @@ -57,10 +56,9 @@ object Output } def bold(s: String) = if(ConsoleLogger.formatEnabled) BOLD + s + RESET else s - def lastLines(key: ScopedKey[_], structure: BuildStructure, streams: Streams): Seq[KeyValue[Seq[String]]] = + def lastLines(keys: Values[_], streams: Streams): Values[Seq[String]] = { - val aggregated = Aggregation.getTasks(key, structure, true) - val outputs = aggregated map { case KeyValue(key, value) => KeyValue(key, lastLines(key, streams)) } + val outputs = keys map { case KeyValue(key, value) => KeyValue(key, lastLines(key, streams)) } outputs.filterNot(_.value.isEmpty) } def lastLines(key: ScopedKey[_], mgr: Streams): Seq[String] = mgr.use(key) { s => IO.readLines(s.readText( Project.fillTaskAxis(key) )) } diff --git a/main/Project.scala b/main/Project.scala index 68746e6b9..b87de657f 100755 --- a/main/Project.scala +++ b/main/Project.scala @@ -85,7 +85,8 @@ final case class Extracted(structure: BuildStructure, session: SessionSettings, def runAggregated[T](key: TaskKey[T], state: State): State = { val rkey = resolve(key.scopedKey) - val tasks = Aggregation.getTasks(rkey, structure, true) + val keys = Aggregation.aggregate(rkey, ScopeMask(), structure.extra) + val tasks = Act.keyValues(structure)(keys) Aggregation.runTasks(state, structure, tasks, Aggregation.Dummies(KNil, HNil), show = false )(showKey) } private[this] def resolve[T](key: ScopedKey[T]): ScopedKey[T] = @@ -211,7 +212,11 @@ object Project extends Init[Scope] with ProjectExtra def makeSettings(settings: Seq[Setting[_]], delegates: Scope => Seq[Scope], scopeLocal: ScopedKey[_] => Seq[Setting[_]])(implicit display: Show[ScopedKey[_]]) = make(settings)(delegates, scopeLocal, display) + def equal(a: ScopedKey[_], b: ScopedKey[_], mask: ScopeMask): Boolean = + a.key == b.key && Scope.equal(a.scope, b.scope, mask) + def displayFull(scoped: ScopedKey[_]): String = Scope.display(scoped.scope, scoped.key.label) + def displayMasked(scoped: ScopedKey[_], mask: ScopeMask): String = Scope.displayMasked(scoped.scope, scoped.key.label, mask) def display(ref: Reference): String = ref match { diff --git a/main/Reference.scala b/main/Reference.scala index 8704e6cf9..814a04ce7 100644 --- a/main/Reference.scala +++ b/main/Reference.scala @@ -45,6 +45,10 @@ object RootProject } object Reference { + def buildURI(ref: ResolvedReference): URI = ref match { + case BuildRef(b) => b + case ProjectRef(b, _) => b + } /** Extracts the build URI from a Reference if one has been explicitly defined.*/ def uri(ref: Reference): Option[URI] = ref match { case RootProject(b) => Some(b) diff --git a/main/Resolve.scala b/main/Resolve.scala new file mode 100644 index 000000000..712f9a49f --- /dev/null +++ b/main/Resolve.scala @@ -0,0 +1,45 @@ +package sbt + + import java.net.URI + +object Resolve +{ + def apply(index: BuildUtil[_], current: ScopeAxis[Reference], key: AttributeKey[_], mask: ScopeMask): Scope => Scope = + { + val rs = + resolveProject(current, mask) _ :: + resolveExtra(mask) _ :: + resolveTask(mask) _ :: + resolveConfig(index, key, mask) _ :: + Nil + scope => (scope /: rs) { (s, f) => f(s) } + } + def resolveTask(mask: ScopeMask)(scope: Scope): Scope = + if(mask.task) scope else scope.copy(task = Global) + + def resolveProject(current: ScopeAxis[Reference], mask: ScopeMask)(scope: Scope): Scope = + if(mask.project) scope else scope.copy(project = current) + + def resolveExtra(mask: ScopeMask)(scope: Scope): Scope = + if(mask.extra) scope else scope.copy(extra = Global) + + def resolveConfig[P](index: BuildUtil[P], key: AttributeKey[_], mask: ScopeMask)(scope: Scope): Scope = + if(mask.config) + scope + else + { + val (resolvedRef, proj) = scope.project match { + case Select(ref) => + val r = index resolveRef ref + (Some(r), index.projectFor(r)) + case Global | This => + (None, index.rootProject(index.root)) + } + val task = scope.task.toOption + val keyIndex = index.keyIndex + val definesKey = (c: ScopeAxis[ConfigKey]) => keyIndex.keys(resolvedRef, c.toOption.map(_.name), task) contains key.label + val projectConfigs = index.configurations(proj).map(ck => Select(ck)) + val config: ScopeAxis[ConfigKey] = (Global +: projectConfigs) find definesKey getOrElse Global + scope.copy(config = config) + } +} diff --git a/main/Scope.scala b/main/Scope.scala index eb5173d14..8baaf759c 100644 --- a/main/Scope.scala +++ b/main/Scope.scala @@ -72,8 +72,11 @@ object Scope case RootProject(uri) => RootProject(resolveBuild(current, uri)) case ProjectRef(uri, id) => ProjectRef(resolveBuild(current, uri), id) } - def resolveBuild(current: URI, uri: URI): URI = - IO.directoryURI(current resolve uri) + def resolveBuild(current: URI, uri: URI): URI = + if(!uri.isAbsolute && current.isOpaque && uri.getSchemeSpecificPart == ".") + current // this handles the shortcut of referring to the current build using "." + else + IO.directoryURI(current resolve uri) def resolveReference(current: URI, rootProject: URI => String, ref: Reference): ResolvedReference = ref match @@ -98,19 +101,28 @@ object Scope } def display(config: ConfigKey): String = config.name + ":" - def display(scope: Scope, sep: String): String = display(scope, sep, ref => Project.display(ref) + "/") - def display(scope: Scope, sep: String, showProject: Reference => String): String = + def display(scope: Scope, sep: String): String = displayMasked(scope, sep, showProject, ScopeMask()) + def displayMasked(scope: Scope, sep: String, mask: ScopeMask): String = displayMasked(scope, sep, showProject, mask) + def display(scope: Scope, sep: String, showProject: Reference => String): String = displayMasked(scope, sep, showProject, ScopeMask()) + def displayMasked(scope: Scope, sep: String, showProject: Reference => String, mask: ScopeMask): String = { import scope.{project, config, task, extra} - val projectPrefix = project.foldStrict(showProject, "*/", "./") val configPrefix = config.foldStrict(display, "*:", ".:") - val taskPostfix = task.foldStrict(x => ("for " + x.label) :: Nil, Nil, Nil) - val extraPostfix = extra.foldStrict(_.entries.map( _.toString ).toList, Nil, Nil) - val extras = taskPostfix ::: extraPostfix + val taskPrefix = task.foldStrict(_.label + "::", "", ".::") + val extras = extra.foldStrict(_.entries.map( _.toString ).toList, Nil, Nil) val postfix = if(extras.isEmpty) "" else extras.mkString("(", ", ", ")") - projectPrefix + configPrefix + sep + postfix + mask.concatShow(projectPrefix(project, showProject), configPrefix, taskPrefix, sep, postfix) } + def equal(a: Scope, b: Scope, mask: ScopeMask): Boolean = + (!mask.project || a.project == b.project) && + (!mask.config || a.config == b.config) && + (!mask.task || a.task == b.task) && + (!mask.extra || a.extra == b.extra) + + def projectPrefix(project: ScopeAxis[Reference], show: Reference => String = showProject): String = project.foldStrict(show, "*/", "./") + def showProject = (ref: Reference) => Project.display(ref) + "/" + def parseScopedKey(command: String): (Scope, String) = { val ScopedKeyRegex(_, projectID, _, config, key) = command @@ -241,6 +253,24 @@ object ScopeAxis { implicit def scopeAxisToScope(axis: ScopeAxis[Nothing]): Scope = Scope(axis, axis, axis, axis) + def fromOption[T](o: Option[T]): ScopeAxis[T] = o match { + case Some(v) => Select(v) + case None => Global + } +} +/** Specifies the Scope axes that should be used for an operation. `true` indicates an axis should be used. */ +final case class ScopeMask(project: Boolean = true, config: Boolean = true, task: Boolean = true, extra: Boolean = true) +{ + def concatShow(p: String, c: String, t: String, sep: String, x: String): String = + { + val sb = new StringBuilder + if(project) sb.append(p) + if(config) sb.append(c) + if(task) sb.append(t) + sb.append(sep) + if(extra) sb.append(x) + sb.toString + } } final case class ConfigKey(name: String) diff --git a/main/src/test/scala/ParseKey.scala b/main/src/test/scala/ParseKey.scala new file mode 100644 index 000000000..aa15b09fc --- /dev/null +++ b/main/src/test/scala/ParseKey.scala @@ -0,0 +1,129 @@ +package sbt + + import Project._ + import java.net.URI + import TestBuild._ + import complete._ + + import org.scalacheck._ + import Gen._ + import Prop._ + import Arbitrary.arbBool + +/** Tests that the scoped key parser in Act can correctly parse a ScopedKey converted by Project.show*Key. +* This includes properly resolving omitted components.*/ +object ParseKey extends Properties("Key parser test") +{ + final val MaxKeys = 5 + final val MaxScopedKeys = 100 + + implicit val gstructure = genStructure + + property("An explicitly specified axis is always parsed to that explicit value") = + forAllNoShrink(structureDefinedKey) { (skm: StructureKeyMask) => + import skm.{structure, key, mask} + + val expected = resolve(structure, key, mask) + val string = Project.displayMasked(key, mask) + + ("Key: " + Project.displayFull(key)) |: + parseExpected(structure, string, expected, mask) + } + + property("An unspecified project axis resolves to the current project") = + forAllNoShrink(structureDefinedKey) { (skm: StructureKeyMask) => + import skm.{structure, key} + + val mask = skm.mask.copy(project = false) + val string = Project.displayMasked(key, mask) + + ("Key: " + Project.displayFull(key)) |: + ("Mask: " + mask) |: + ("Current: " + structure.current) |: + parse(structure, string) { + case Left(err) => false + case Right(sk) => sk.scope.project == Select(structure.current) + } + } + + property("An unspecified task axis resolves to Global") = + forAllNoShrink(structureDefinedKey) { (skm: StructureKeyMask) => + import skm.{structure, key} + val mask = skm.mask.copy(task = false) + val string = Project.displayMasked(key, mask) + + ("Key: " + Project.displayFull(key)) |: + ("Mask: " + mask) |: + parse(structure, string) { + case Left(err) => false + case Right(sk) => sk.scope.task == Global + } + } + + property("An unspecified configuration axis resolves to the first configuration directly defining the key or else Global") = + forAllNoShrink(structureDefinedKey) { (skm: StructureKeyMask) => + import skm.{structure, key} + val mask = ScopeMask(config = false) + val string = Project.displayMasked(key, mask) + val resolvedConfig = Resolve.resolveConfig(structure.extra, key.key, mask)(key.scope).config + + ("Key: " + Project.displayFull(key)) |: + ("Mask: " + mask) |: + ("Expected configuration: " + resolvedConfig.map(_.name)) |: + parse(structure, string) { + case Right(sk) => sk.scope.config == resolvedConfig + case Left(err) => false + } + } + + lazy val structureDefinedKey: Gen[StructureKeyMask] = structureKeyMask { s => + for( scope <- TestBuild.scope(s.env); key <- oneOf(s.allAttributeKeys.toSeq)) yield ScopedKey(scope, key) + } + def structureKeyMask(genKey: Structure => Gen[ScopedKey[_]])(implicit maskGen: Gen[ScopeMask], structureGen: Gen[Structure]): Gen[StructureKeyMask] = + for(mask <- maskGen; structure <- structureGen; key <- genKey(structure)) yield + new StructureKeyMask(structure, key, mask) + final class StructureKeyMask(val structure: Structure, val key: ScopedKey[_], val mask: ScopeMask) + + def resolve(structure: Structure, key: ScopedKey[_], mask: ScopeMask): ScopedKey[_] = + ScopedKey(Resolve(structure.extra, Select(structure.current), key.key, mask)(key.scope), key.key) + + def parseExpected(structure: Structure, s: String, expected: ScopedKey[_], mask: ScopeMask): Prop = + ("Expected: " + Project.displayFull(expected)) |: + ("Mask: " + mask) |: + parse(structure, s) { + case Left(err) => false + case Right(sk) => Project.equal(sk, expected, mask) + } + + def parse(structure: Structure, s: String)(f: Either[String,ScopedKey[_]] => Prop): Prop = + { + val parser = makeParser(structure) + val parsed = DefaultParsers.result(parser, s).left.map(_().toString) + val showParsed = parsed.right.map(Project.displayFull) + ("Key string: '" + s + "'") |: + ("Parsed: " + showParsed) |: + ("Structure: " + structure) |: + f(parsed) + } + + def genStructure(implicit genEnv: Gen[Env]): Gen[Structure] = + structureGenF { (scopes: Seq[Scope], env: Env, current: ProjectRef) => + val settings = for(scope <- scopes; t <- env.tasks) yield Project.setting(ScopedKey(scope, t.key), Project.value("")) + TestBuild.structure(env, settings, current) + } + + def structureGenF(f: (Seq[Scope], Env, ProjectRef) => Structure)(implicit genEnv: Gen[Env]): Gen[Structure] = + structureGen( (s,e,p) => Gen.value(f(s,e,p))) + def structureGen(f: (Seq[Scope], Env, ProjectRef) => Gen[Structure])(implicit genEnv: Gen[Env]): Gen[Structure] = + for { + env <- genEnv + loadFactor <- choose(0.0, 1.0) + scopes <- pickN(loadFactor, env.allFullScopes) + current <- oneOf(env.allProjects.unzip._1) + structure <- f(scopes, env, current) + } yield + structure + + def pickN[T](load: Double, from: Seq[T]): Gen[Seq[T]] = + pick( (load*from.size).toInt, from ) +} diff --git a/main/src/test/scala/TestBuild.scala b/main/src/test/scala/TestBuild.scala index 5f6934276..56caee148 100644 --- a/main/src/test/scala/TestBuild.scala +++ b/main/src/test/scala/TestBuild.scala @@ -9,18 +9,19 @@ package sbt import org.scalacheck._ import Prop._ import Gen._ + import Arbitrary.arbBool // Notes: // Generator doesn't produce cross-build project dependencies or do anything with the 'extra' axis object TestBuild { - val MaxTasks = 10 - val MaxProjects = 10 - val MaxConfigs = 10 - val MaxBuilds = 10 - val MaxIDSize = 10 - val MaxDeps = 10 - val KeysPerEnv = 25 + val MaxTasks = 6 + val MaxProjects = 7 + val MaxConfigs = 5 + val MaxBuilds = 4 + val MaxIDSize = 8 + val MaxDeps = 8 + val KeysPerEnv = 10 val MaxTasksGen = chooseShrinkable(1, MaxTasks) val MaxProjectsGen = chooseShrinkable(1, MaxProjects) @@ -38,7 +39,7 @@ object TestBuild final class Keys(val env: Env, val scopes: Seq[Scope]) { override def toString = env + "\n" + scopes.mkString("Scopes:\n\t", "\n\t", "") - val delegated = scopes map env.delegates + lazy val delegated = scopes map env.delegates } final case class Structure(env: Env, current: ProjectRef, data: Settings[Scope], keyIndex: KeyIndex, keyMap: Map[String, AttributeKey[_]]) @@ -49,8 +50,46 @@ object TestBuild { val scopeStrings = for( (scope, map) <- data.data ) yield - Scope.display(scope, "") + showKeys(map) - scopeStrings.mkString("\n\t") + (Scope.display(scope, ""), showKeys(map)) + scopeStrings.toSeq.sorted.map(t => t._1 + t._2).mkString("\n\t") + } + val extra: BuildUtil[Proj] = + { + val getp = (build: URI, project: String) => env.buildMap(build).projectMap(project) + new BuildUtil(keyIndex, data, env.root.uri, env.rootProject, getp, _.configurations.map(c => ConfigKey(c.name)), Relation.empty) + } + + lazy val allAttributeKeys: Set[AttributeKey[_]] = data.data.values.flatMap(_.keys).toSet + lazy val (taskAxes, globalTaskAxis, onlyTaskAxis, multiTaskAxis) = + { + import collection.{breakOut, mutable} + import mutable.HashSet + + // task axis of Scope is set to Global and the value of the second map is the original task axis + val taskAxesMappings = + for( (scope, keys) <- data.data.toIterable; key <- keys.keys ) yield + (ScopedKey(scope.copy(task = Global), key), scope.task) : (ScopedKey[_], ScopeAxis[AttributeKey[_]]) + + val taskAxes = Relation.empty ++ taskAxesMappings + val global = new HashSet[ScopedKey[_]] + val single = new HashSet[ScopedKey[_]] + val multi = new HashSet[ScopedKey[_]] + for( (skey, tasks) <- taskAxes.forwardMap) + { + def makeKey(task: ScopeAxis[AttributeKey[_]]) = ScopedKey(skey.scope.copy(task = task), skey.key) + val hasGlobal = tasks(Global) + if(hasGlobal) + global += skey + else + { + val keys = tasks map makeKey + if( keys.size == 1) + single ++= keys + else if(keys.size > 1) + multi ++= keys + } + } + (taskAxes, global.toSet, single.toSet, multi.toSet) } } final class Env(val builds: Seq[Build], val tasks: Seq[Taskk]) @@ -61,7 +100,8 @@ object TestBuild val taskMap = mapBy(tasks)(getKey) def project(ref: ProjectRef) = buildMap(ref.build).projectMap(ref.project) def projectFor(ref: ResolvedReference) = ref match { case pr: ProjectRef => project(pr); case BuildRef(uri) => buildMap(uri).root } - def allProjects = builds.flatMap(_.allProjects) + + lazy val allProjects = builds.flatMap(_.allProjects) def rootProject(uri: URI): String = buildMap(uri).root.id def inheritConfig(ref: ResolvedReference, config: ConfigKey) = projectFor(ref).confMap(config.name).extended map toConfigKey def inheritTask(task: AttributeKey[_]) = taskMap.get(task) match { case None => Nil; case Some(t) => t.delegates map getKey } @@ -78,9 +118,13 @@ object TestBuild inheritTask, (ref, mp) => Nil ) - def allFullScopes: Seq[Scope] = - for((ref, p) <- allProjects; t <- tasks; c <- p.configurations) yield - Scope(project = Select(ref), config = Select(ConfigKey(c.name)), task = Select(t.key), extra = Global) + lazy val allFullScopes: Seq[Scope] = + for { + (ref, p) <- (Global, root.root) +: allProjects.map { case (ref, p) => (Select(ref), p) } + t <- Global +: tasks.map(t => Select(t.key)) + c <- Global +: p.configurations.map(c => Select(ConfigKey(c.name))) + } yield + Scope(project = ref, config = c, task = t, extra = Global) } def getKey: Taskk => AttributeKey[_] = _.key def toConfigKey: Config => ConfigKey = c => ConfigKey(c.name) @@ -93,7 +137,7 @@ object TestBuild } final class Proj(val id: String, val delegates: Seq[ProjectRef], val configurations: Seq[Config]) { - override def toString = "Project " + id + "\n Delegates:\n " + delegates.mkString("\n ") + + override def toString = "Project " + id + "\n Delegates:\n " + delegates.mkString("\n ") + "\n Configurations:\n " + configurations.mkString("\n ") val confMap = mapBy(configurations)(_.name) } @@ -111,7 +155,7 @@ object TestBuild implicit lazy val arbKeys: Arbitrary[Keys] = Arbitrary(keysGen) lazy val keysGen: Gen[Keys] = for(env <- mkEnv; keyCount <- chooseShrinkable(1, KeysPerEnv); keys <- listOfN(keyCount, scope(env)) ) yield new Keys(env, keys) - + def scope(env: Env): Gen[Scope] = for { build <- oneOf(env.builds) @@ -135,7 +179,7 @@ object TestBuild case Some(BuildRef(uri)) => confs(uri) case Some(ref: ProjectRef) => env.project(ref).configurations.map(_.name) } - Act.scopedKey(keyIndex, current, defaultConfs, keyMap)//, data) + Act.scopedKey(keyIndex, current, defaultConfs, keyMap, data) } def structure(env: Env, settings: Seq[Setting[_]], current: ProjectRef): Structure = @@ -155,6 +199,13 @@ object TestBuild envGen(buildGen(uriGen, pGen), tGen) } + implicit def maskGen(implicit arbBoolean: Arbitrary[Boolean]): Gen[ScopeMask] = + { + val b = arbBoolean.arbitrary + for(p <- b; c <- b; t <- b; x <- b) yield + ScopeMask(project = p, config = c, task = t, extra = x) + } + implicit lazy val idGen: Gen[String] = for(size <- chooseShrinkable(1, MaxIDSize); cs <- listOfN(size, alphaChar)) yield cs.mkString implicit lazy val optIDGen: Gen[Option[String]] = frequency( (1, idGen map some.fn), (1, None) ) implicit lazy val uriGen: Gen[URI] = for(sch <- idGen; ssp <- idGen; frag <- optIDGen) yield new URI(sch, ssp, frag.orNull) diff --git a/util/collection/Relation.scala b/util/collection/Relation.scala index c5195ffb7..dce3d9048 100644 --- a/util/collection/Relation.scala +++ b/util/collection/Relation.scala @@ -17,7 +17,6 @@ object Relation make(forward, reverse) } - private[sbt] def remove[X,Y](map: M[X,Y], from: X, to: Y): M[X,Y] = map.get(from) match { case Some(tos) => diff --git a/util/collection/Show.scala b/util/collection/Show.scala index b19a6ca2d..fe4e85950 100644 --- a/util/collection/Show.scala +++ b/util/collection/Show.scala @@ -2,4 +2,8 @@ package sbt trait Show[T] { def apply(t: T): String +} +object Show +{ + def apply[T](f: T => String): Show[T] = new Show[T] { def apply(t: T): String = f(t) } } \ No newline at end of file diff --git a/util/complete/Parser.scala b/util/complete/Parser.scala index 5c7bcddc3..a23ae3f48 100644 --- a/util/complete/Parser.scala +++ b/util/complete/Parser.scala @@ -431,7 +431,8 @@ private final class ParserSeq[T](a: Seq[Parser[T]], errors: => Seq[String]) exte { val res = a.map(_.resultEmpty) val (failures, values) = separate(res)(_.toEither) - if(failures.isEmpty) Value(values) else mkFailures(failures.flatMap(_()) ++ errors) +// if(failures.isEmpty) Value(values) else mkFailures(failures.flatMap(_()) ++ errors) + if(values.nonEmpty) Value(values) else mkFailures(failures.flatMap(_()) ++ errors) } def result = { val success = a.flatMap(_.result) diff --git a/util/io/IO.scala b/util/io/IO.scala index 3aef64075..deccf50bd 100644 --- a/util/io/IO.scala +++ b/util/io/IO.scala @@ -613,7 +613,7 @@ object IO */ def directoryURI(uri: URI): URI = { - assertAbsolute(uri) + if(!uri.isAbsolute) return uri;//assertAbsolute(uri) val str = uri.toASCIIString val dirStr = if(str.endsWith("/") || uri.getScheme != "file") str else str + "/" (new URI(dirStr)).normalize