sbt/main/Project.scala

385 lines
18 KiB
Scala

/* sbt -- Simple Build Tool
* Copyright 2011 Mark Harrah
*/
package sbt
import java.io.File
import java.net.URI
import Project._
import Types.Endo
import Keys.{appConfiguration, buildStructure, commands, configuration, historyPath, projectCommand, sessionSettings, shellPrompt, thisProject, thisProjectRef, watch}
import Scope.ThisScope
import CommandSupport.logger
import compiler.Eval
sealed trait ProjectDefinition[PR <: ProjectReference]
{
def id: String
def base: File
def configurations: Seq[Configuration]
def settings: Seq[Project.Setting[_]]
def aggregate: Seq[PR]
def delegates: Seq[PR]
def dependencies: Seq[Project.ClasspathDep[PR]]
def uses: Seq[PR] = aggregate ++ dependencies.map(_.project)
def referenced: Seq[PR] = delegates ++ uses
}
final case class ResolvedProject(id: String, base: File, aggregate: Seq[ProjectRef], dependencies: Seq[Project.ResolvedClasspathDependency], delegates: Seq[ProjectRef],
settings: Seq[Project.Setting[_]], configurations: Seq[Configuration]) extends ProjectDefinition[ProjectRef]
final case class Project(id: String, base: File, aggregate: Seq[ProjectReference] = Nil, dependencies: Seq[Project.ClasspathDependency] = Nil, delegates: Seq[ProjectReference] = Nil,
settings: Seq[Project.Setting[_]] = Project.defaultSettings, configurations: Seq[Configuration] = Configurations.default) extends ProjectDefinition[ProjectReference]
{
def dependsOn(deps: Project.ClasspathDependency*): Project = copy(dependencies = dependencies ++ deps)
def delegates(from: ProjectReference*): Project = copy(delegates = delegates ++ from)
def aggregate(refs: ProjectReference*): Project = copy(aggregate = aggregate ++ refs)
def configs(cs: Configuration*): Project = copy(configurations = configurations ++ cs)
def settings(ss: Project.Setting[_]*): Project = copy(settings = settings ++ ss)
def resolve(resolveRef: ProjectReference => ProjectRef): ResolvedProject =
{
def resolveRefs(prs: Seq[ProjectReference]) = prs map resolveRef
def resolveDeps(ds: Seq[Project.ClasspathDependency]) = ds map resolveDep
def resolveDep(d: Project.ClasspathDependency) = Project.ResolvedClasspathDependency(resolveRef(d.project), d.configuration)
ResolvedProject(id, base, aggregate = resolveRefs(aggregate), dependencies = resolveDeps(dependencies), delegates = resolveRefs(delegates), settings, configurations)
}
def resolveBuild(resolveRef: ProjectReference => ProjectReference): Project =
{
def resolveRefs(prs: Seq[ProjectReference]) = prs map resolveRef
def resolveDeps(ds: Seq[Project.ClasspathDependency]) = ds map resolveDep
def resolveDep(d: Project.ClasspathDependency) = Project.ClasspathDependency(resolveRef(d.project), d.configuration)
copy(aggregate = resolveRefs(aggregate), dependencies = resolveDeps(dependencies), delegates = resolveRefs(delegates))
}
}
final case class Extracted(structure: Load.BuildStructure, session: SessionSettings, currentRef: ProjectRef, rootProject: URI => String)
{
lazy val currentUnit = structure units curi
lazy val currentProject = currentUnit defined cid
def curi = currentRef.build
def cid = currentRef.project
}
object Project extends Init[Scope]
{
def defaultSettings: Seq[Setting[_]] = Defaults.defaultSettings
sealed trait ClasspathDep[PR <: ProjectReference] { def project: PR; def configuration: Option[String] }
final case class ResolvedClasspathDependency(project: ProjectRef, configuration: Option[String]) extends ClasspathDep[ProjectRef]
final case class ClasspathDependency(project: ProjectReference, configuration: Option[String]) extends ClasspathDep[ProjectReference]
final class Constructor(p: ProjectReference) {
def %(conf: String): ClasspathDependency = new ClasspathDependency(p, Some(conf))
}
def getOrError[T](state: State, key: AttributeKey[T], msg: String): T = state get key getOrElse error(msg)
def structure(state: State): Load.BuildStructure = getOrError(state, buildStructure, "No build loaded.")
def session(state: State): SessionSettings = getOrError(state, sessionSettings, "Session not initialized.")
def extract(state: State): Extracted =
{
val se = session(state)
val st = structure(state)
Extracted(st, se, se.current, Load.getRootProject(st.units))
}
def getProjectForReference(ref: Reference, structure: Load.BuildStructure): Option[ResolvedProject] =
ref match { case pr: ProjectRef => getProject(pr, structure); case _ => None }
def getProject(ref: ProjectRef, structure: Load.BuildStructure): Option[ResolvedProject] =
(structure.units get ref.build).flatMap(_.defined get ref.project)
def setProject(session: SessionSettings, structure: Load.BuildStructure, s: State): State =
{
val newAttrs = s.attributes.put(buildStructure, structure).put(sessionSettings, session)
val newState = s.copy(attributes = newAttrs)
updateCurrent(newState.runExitHooks())
}
def current(state: State): ProjectRef = session(state).current
def updateCurrent(s: State): State =
{
val structure = Project.structure(s)
val ref = Project.current(s)
val project = Load.getProject(structure.units, ref.build, ref.project)
logger(s).info("Set current project to " + ref.project + " (in build " + ref.build +")")
def get[T](k: SettingKey[T]): Option[T] = k in ref get structure.data
def commandsIn(axis: ResolvedReference) = commands in axis get structure.data toList ;
val allCommands = commandsIn(ref) ++ commandsIn(BuildRef(ref.build)) ++ (commands in Global get structure.data toList )
val history = get(historyPath) flatMap identity
val prompt = get(shellPrompt)
val watched = get(watch)
val commandDefs = allCommands.distinct.flatten[Command].map(_ tag (projectCommand, true))
val newProcessors = commandDefs ++ BuiltinCommands.removeTagged(s.processors, projectCommand)
val newAttrs = setCond(Watched.Configuration, watched, s.attributes).put(historyPath.key, history)
s.copy(attributes = setCond(shellPrompt.key, prompt, newAttrs), processors = newProcessors)
}
def setCond[T](key: AttributeKey[T], vopt: Option[T], attributes: AttributeMap): AttributeMap =
vopt match { case Some(v) => attributes.put(key, v); case None => attributes.remove(key) }
def makeSettings(settings: Seq[Setting[_]], delegates: Scope => Seq[Scope], scopeLocal: ScopedKey[_] => Seq[Setting[_]]) =
translateUninitialized( make(settings)(delegates, scopeLocal) )
def display(scoped: ScopedKey[_]): String = Scope.display(scoped.scope, scoped.key.label)
def display(ref: Reference): String =
ref match
{
case pr: ProjectReference => display(pr)
case br: BuildReference => display(br)
}
def display(ref: BuildReference) =
ref match
{
case ThisBuild => "<this>"
case BuildRef(uri) => "[" + uri + "]"
}
def display(ref: ProjectReference) =
ref match
{
case ThisProject => "(<this>)<this>"
case LocalProject(id) => "(<this>)" + id
case RootProject(uri) => "(" + uri + ")<root>"
case ProjectRef(uri, id) => "(" + uri + ")" + id
}
def fillTaskAxis(scoped: ScopedKey[_]): ScopedKey[_] =
ScopedKey(Scope.fillTaskAxis(scoped.scope, scoped.key), scoped.key)
def mapScope(f: Scope => Scope) = new (ScopedKey ~> ScopedKey) { def apply[T](key: ScopedKey[T]) =
ScopedKey( f(key.scope), key.key)
}
def transform(g: Scope => Scope, ss: Seq[Setting[_]]): Seq[Setting[_]] = {
val f = mapScope(g)
ss.map(_ mapKey f mapReferenced f)
}
def transformRef(g: Scope => Scope, ss: Seq[Setting[_]]): Seq[Setting[_]] = {
val f = mapScope(g)
ss.map(_ mapReferenced f)
}
def translateUninitialized[T](f: => T): T =
try { f } catch { case u: Project.Uninitialized =>
val msg = "Uninitialized reference to " + display(u.key) + " from " + display(u.refKey)
throw new Uninitialized(u.key, u.refKey, msg)
}
def delegates(structure: Load.BuildStructure, scope: Scope, key: AttributeKey[_]): Seq[ScopedKey[_]] =
structure.delegates(scope).map(d => ScopedKey(d, key))
def details(structure: Load.BuildStructure, scope: Scope, key: AttributeKey[_]): String =
{
val scoped = ScopedKey(scope,key)
val value =
(structure.data.get(scope, key)) match {
case None => "No entry for key."
case Some(v: Task[_]) => "Task"
case Some(v: InputTask[_]) => "Input task"
case Some(v) => "Value:\n\t" + v.toString
}
val definedIn = structure.data.definingScope(scope, key) match { case Some(sc) => "Provided by:\n\t" + display(scoped); case None => "" }
val cMap = compiled(structure.settings)(structure.delegates, structure.scopeLocal)
val related = cMap.keys.filter(k => k.key == key && k.scope != scope)
val depends = cMap.get(scoped) match { case Some(c) => c.dependencies.toSet; case None => Set.empty }
val reverse = reverseDependencies(cMap, scoped)
def printScopes(label: String, scopes: Iterable[ScopedKey[_]]) =
if(scopes.isEmpty) "" else scopes.map(display).mkString(label + ":\n\t", "\n\t", "\n")
value + "\n" +
definedIn + "\n" +
printScopes("Dependencies", depends) +
printScopes("Reverse dependencies", reverse) +
printScopes("Delegates", delegates(structure, scope, key)) +
printScopes("Related", related)
}
def reverseDependencies(cMap: CompiledMap, scoped: ScopedKey[_]): Iterable[ScopedKey[_]] =
for( (key,compiled) <- cMap; dep <- compiled.dependencies if dep == scoped) yield key
def inConfig(conf: Configuration)(ss: Seq[Setting[_]]): Seq[Setting[_]] =
inScope(ThisScope.copy(config = Select(conf)) )( (configuration :== conf) +: ss)
def inTask(t: Scoped)(ss: Seq[Setting[_]]): Seq[Setting[_]] =
inScope(ThisScope.copy(task = Select(t.key)) )( ss )
def inScope(scope: Scope)(ss: Seq[Setting[_]]): Seq[Setting[_]] =
Project.transform(Scope.replaceThis(scope), ss)
object LoadAction extends Enumeration {
val Return, Current, Plugins = Value
}
import LoadAction._
import complete.DefaultParsers._
val loadActionParser = token(Space ~> ("plugins" ^^^ Plugins | "return" ^^^ Return)) ?? Current
val ProjectReturn = AttributeKey[List[File]]("project-return")
def projectReturn(s: State): List[File] = s.attributes get ProjectReturn getOrElse Nil
def setProjectReturn(s: State, pr: List[File]): State = s.copy(attributes = s.attributes.put( ProjectReturn, pr) )
def loadAction(s: State, action: LoadAction.Value) = action match {
case Return =>
projectReturn(s) match
{
case current :: returnTo :: rest => (setProjectReturn(s, returnTo :: rest), returnTo)
case _ => error("Not currently in a plugin definition")
}
case Current =>
val base = s.configuration.baseDirectory
projectReturn(s) match { case Nil => (setProjectReturn(s, base :: Nil), base); case x :: xs => (s, x) }
case Plugins =>
val extracted = Project.extract(s)
val newBase = extracted.currentUnit.unit.plugins.base
val newS = setProjectReturn(s, newBase :: projectReturn(s))
(newS, newBase)
}
}
import SessionSettings._
final case class SessionSettings(currentBuild: URI, currentProject: Map[URI, String], original: Seq[Setting[_]], prepend: SessionMap, append: SessionMap, currentEval: () => Eval)
{
assert(currentProject contains currentBuild, "Current build (" + currentBuild + ") not associated with a current project.")
def setCurrent(build: URI, project: String, eval: () => Eval): SessionSettings = copy(currentBuild = build, currentProject = currentProject.updated(build, project), currentEval = eval)
def current: ProjectRef = ProjectRef(currentBuild, currentProject(currentBuild))
def appendSettings(s: Seq[SessionSetting]): SessionSettings = copy(append = modify(append, _ ++ s))
def prependSettings(s: Seq[SessionSetting]): SessionSettings = copy(prepend = modify(prepend, s ++ _))
def mergeSettings: Seq[Setting[_]] = merge(prepend) ++ original ++ merge(append)
def clearExtraSettings: SessionSettings = copy(prepend = Map.empty, append = Map.empty)
private[this] def merge(map: SessionMap): Seq[Setting[_]] = map.values.toSeq.flatten[SessionSetting].map(_._1)
private[this] def modify(map: SessionMap, onSeq: Endo[Seq[SessionSetting]]): SessionMap =
{
val cur = current
map.updated(cur, onSeq(map.getOrElse( cur, Nil)))
}
}
object SessionSettings
{
type SessionSetting = (Setting[_], String)
type SessionMap = Map[ProjectRef, Seq[SessionSetting]]
def reapply(session: SessionSettings, s: State): State =
BuiltinCommands.reapply(session, Project.structure(s), s)
def clearSettings(s: State): State =
withSettings(s)(session => reapply(session.copy(append = session.append - session.current), s))
def clearAllSettings(s: State): State =
withSettings(s)(session => reapply(session.clearExtraSettings, s))
def withSettings(s: State)(f: SessionSettings => State): State =
{
val extracted = Project extract s
import extracted._
if(session.prepend.isEmpty && session.append.isEmpty)
{
logger(s).info("No session settings defined.")
s
}
else
f(session)
}
def removeRanges[T](in: Seq[T], ranges: Seq[(Int,Int)]): Seq[T] =
{
val asSet = (Set.empty[Int] /: ranges) { case (s, (hi,lo)) => s ++ (hi to lo) }
in.zipWithIndex.flatMap { case (t, index) => if(asSet(index+1)) Nil else t :: Nil }
}
def removeSettings(s: State, ranges: Seq[(Int,Int)]): State =
withSettings(s) { session =>
val current = session.current
val newAppend = session.append.updated(current, removeRanges(session.append.getOrElse(current, Nil), ranges))
reapply(session.copy( append = newAppend ), s)
}
def saveAllSettings(s: State): State = saveSomeSettings(s)(_ => true)
def saveSettings(s: State): State =
{
val current = Project.session(s).current
saveSomeSettings(s)( _ == current)
}
def saveSomeSettings(s: State)(include: ProjectRef => Boolean): State =
withSettings(s){session =>
for( (ref, settings) <- session.append if !settings.isEmpty && include(ref) )
writeSettings(ref, settings, Project.structure(s))
reapply(session.copy(original = session.mergeSettings, append = Map.empty, prepend = Map.empty), s)
}
def writeSettings(pref: ProjectRef, settings: Seq[SessionSetting], structure: Load.BuildStructure)
{
val project = Project.getProject(pref, structure).getOrElse(error("Invalid project reference " + pref))
val appendTo: File = BuildPaths.configurationSources(project.base).headOption.getOrElse(new File(project.base, "build.sbt"))
val baseAppend = settingStrings(settings).flatMap("" :: _ :: Nil)
val adjustedLines = if(appendTo.isFile && hasTrailingBlank(IO readLines appendTo) ) baseAppend else "" +: baseAppend
IO.writeLines(appendTo, adjustedLines, append = true)
}
def hasTrailingBlank(lines: Seq[String]) = lines.takeRight(1).exists(_.trim.isEmpty)
def printAllSettings(s: State): State =
withSettings(s){ session =>
for( (ref, settings) <- session.append if !settings.isEmpty) {
println("In " + Project.display(ref))
printSettings(settings)
}
s
}
def printSettings(s: State): State =
withSettings(s){ session =>
printSettings(session.append.getOrElse(session.current, Nil))
s
}
def printSettings(settings: Seq[SessionSetting]): Unit =
for((stringRep, index) <- settingStrings(settings).zipWithIndex)
println(" " + (index+1) + ". " + stringRep)
def settingStrings(s: Seq[SessionSetting]): Seq[String] = s.map(_._2)
def Help = """session <command>
Manipulates session settings, which are temporary settings that do not persist past the current sbt execution (that is, the current session).
Valid commands are:
clear, clear-all
Removes temporary settings added using 'set' and re-evaluates all settings.
For 'clear', only the settings defined for the current project are cleared.
For 'clear-all', all settings in all projects are cleared.
list, list-all
Prints a numbered list of session settings defined.
The numbers may be used to remove individual settings or ranges of settings using 'remove'.
For 'list', only the settings for the current project are printed.
For 'list-all', all settings in all projets are printed.
remove <range-spec>
<range-spec> is a comma-separated list of individual numbers or ranges of numbers.
For example, 'remove 1,3,5-7'.
The temporary settings at the given indices for the current project are removed and all settings are re-evaluated.
Use the 'list' command to see a numbered list of settings for the current project.
save, save-all
Makes the session settings permanent by writing them to a '.sbt' configuration file.
For 'save', only the current project's settings are saved (the settings for other projects are left alone).
For 'save-all', the session settings are saved for all projects.
The session settings defined for a project are appended to the first '.sbt' configuration file in that project.
If no '.sbt' configuration file exists, the settings are written to 'build.sbt' in the project's base directory."""
sealed trait SessionCommand
final class Print(val all: Boolean) extends SessionCommand
final class Clear(val all: Boolean) extends SessionCommand
final class Save(val all: Boolean) extends SessionCommand
final class Remove(val ranges: Seq[(Int,Int)]) extends SessionCommand
import complete._
import DefaultParsers._
lazy val parser =
token(Space) ~>
(token("list-all" ^^^ new Print(true)) | token("list" ^^^ new Print(false)) | token("clear" ^^^ new Clear(false)) |
token("save-all" ^^^ new Save(true)) | token("save" ^^^ new Save(false)) | token("clear-all" ^^^ new Clear(true)) |
remove)
lazy val remove = token("remove") ~> token(Space) ~> natSelect.map(ranges => new Remove(ranges))
def natSelect = rep1sep(token(range, "<range>"), ',')
def range: Parser[(Int,Int)] = (NatBasic ~ ('-' ~> NatBasic).?).map { case lo ~ hi => (lo, hi getOrElse lo)}
def command(s: State) = Command.applyEffect(parser){
case p: Print => if(p.all) printAllSettings(s) else printSettings(s)
case v: Save => if(v.all) saveAllSettings(s) else saveSettings(s)
case c: Clear => if(c.all) clearAllSettings(s) else clearSettings(s)
case r: Remove => removeSettings(s,r.ranges)
}
}
trait ProjectConstructors
{
implicit def configDependencyConstructor[T <% ProjectReference](p: T): Project.Constructor = new Project.Constructor(p)
implicit def classpathDependency[T <% ProjectReference](p: T): Project.ClasspathDependency = new Project.ClasspathDependency(p, None)
}