mirror of https://github.com/sbt/sbt.git
Project creation and property filling
This commit is contained in:
parent
8e08708792
commit
f5b3aa47fc
|
|
@ -6,6 +6,7 @@ import scala.util.parsing.input.{Reader, StreamReader}
|
|||
import java.lang.Character.isWhitespace
|
||||
import java.io.File
|
||||
import java.net.{MalformedURLException, URL}
|
||||
import scala.collection.immutable.TreeMap
|
||||
|
||||
class ConfigurationParser extends Parsers with NotNull
|
||||
{
|
||||
|
|
@ -29,15 +30,16 @@ class ConfigurationParser extends Parsers with NotNull
|
|||
val (repositories, m3) = processSection(m2, "repositories", getRepositories)
|
||||
val (boot, m4) = processSection(m3, "boot", getBoot)
|
||||
val (logging, m5) = processSection(m4, "log", getLogging)
|
||||
check(m5, "section")
|
||||
new LaunchConfiguration(scalaVersion, app, repositories, boot, logging)
|
||||
val (properties, m6) = processSection(m5, "app-properties", getAppProperties)
|
||||
check(m6, "section")
|
||||
new LaunchConfiguration(scalaVersion, app, repositories, boot, logging, properties)
|
||||
}
|
||||
def getScalaVersion(m: LabelMap) = check("label", getVersion(m))
|
||||
def getVersion(m: LabelMap): (Version, LabelMap) = process(m, "version", processVersion)
|
||||
def processVersion(value: Option[String]): Version = value.map(version).getOrElse(Version.default)
|
||||
def version(value: String): Version =
|
||||
{
|
||||
if(value.isEmpty) error("Version cannot be empty (omit version declaration to use the default version)")
|
||||
if(value.isEmpty) throw new BootException("Version cannot be empty (omit version declaration to use the default version)")
|
||||
val tokens = trim(value.split(",", 2))
|
||||
import Version.{Explicit, Implicit}
|
||||
val defaultVersion = if(tokens.length == 2) Some(tokens(1)) else None
|
||||
|
|
@ -46,15 +48,20 @@ class ConfigurationParser extends Parsers with NotNull
|
|||
def processSection[T](sections: Map[String, LabelMap], name: String, f: LabelMap => T) =
|
||||
process[String,LabelMap,T](sections, name, m => f(m withDefaultValue(None)))
|
||||
def process[K,V,T](sections: Map[K,V], name: K, f: V => T): (T, Map[K,V]) = ( f(sections(name)), sections - name)
|
||||
def check(map: Map[String, _], label: String): Unit = if(map.isEmpty) () else { error(map.keys.mkString("Invalid " + label + "(s): ", ",","")) }
|
||||
def check(map: Map[String, _], label: String): Unit = if(map.isEmpty) () else { throw new BootException(map.keys.mkString("Invalid " + label + "(s): ", ",","")) }
|
||||
def check[T](label: String, pair: (T, Map[String, _])): T = { check(pair._2, label); pair._1 }
|
||||
def id(map: Map[String, Option[String]], name: String, default: String): (String, LabelMap) =
|
||||
def id(map: LabelMap, name: String, default: String): (String, LabelMap) =
|
||||
(map.getOrElse(name, None).getOrElse(default), map - name)
|
||||
def ids(map: Map[String, Option[String]], name: String, default: Seq[String]) =
|
||||
def ids(map: LabelMap, name: String, default: Seq[String]) =
|
||||
{
|
||||
val result = map(name).map(value => trim(value.split(",")).filter(!_.isEmpty)).getOrElse(default)
|
||||
(result, map - name)
|
||||
}
|
||||
def bool(map: LabelMap, name: String, default: Boolean): (Boolean, LabelMap) =
|
||||
{
|
||||
val (b, m) = id(map, name, default.toString)
|
||||
(b.toBoolean, m)
|
||||
}
|
||||
def toFile(path: String): File = new File(path)// if the path is relative, it will be resolve by Launch later
|
||||
def file(map: LabelMap, name: String, default: File): (File, LabelMap) =
|
||||
(map.getOrElse(name, None).map(toFile).getOrElse(default), map - name)
|
||||
|
|
@ -64,8 +71,11 @@ class ConfigurationParser extends Parsers with NotNull
|
|||
val (dir, m1) = file(m, "directory", toFile("project/boot"))
|
||||
val (props, m2) = file(m1, "properties", toFile("project/build.properties"))
|
||||
val (search, m3) = getSearch(m2, props)
|
||||
check(m3, "label")
|
||||
BootSetup(dir, props, search)
|
||||
val (enableQuick, m4) = bool(m3, "quick-option", false)
|
||||
val (promptFill, m5) = bool(m4, "prompt-fill", false)
|
||||
val (promptCreate, m6) = id(m5, "prompt-create", "")
|
||||
check(m6, "label")
|
||||
BootSetup(dir, props, search, promptCreate, enableQuick, promptFill)
|
||||
}
|
||||
def getLogging(m: LabelMap): Logging = check("label", process(m, "level", getLevel))
|
||||
def getLevel(m: Option[String]) = m.map(LogLevel.apply).getOrElse(Logging(LogLevel.Info))
|
||||
|
|
@ -95,10 +105,35 @@ class ConfigurationParser extends Parsers with NotNull
|
|||
case (key, None) => Predefined(key)
|
||||
case (key, Some(value)) =>
|
||||
val r = trim(value.split(",",2))
|
||||
val url = try { new URL(r(0)) } catch { case e: MalformedURLException => error("Invalid URL specified for '" + key + "': " + e.getMessage) }
|
||||
val url = try { new URL(r(0)) } catch { case e: MalformedURLException => throw new BootException("Invalid URL specified for '" + key + "': " + e.getMessage) }
|
||||
if(r.length == 2) Ivy(key, url, r(1)) else Maven(key, url)
|
||||
}
|
||||
}
|
||||
def getAppProperties(m: LabelMap): Seq[AppProperty] =
|
||||
for((name, Some(value)) <- m.toSeq) yield
|
||||
{
|
||||
val map = Map() ++ trim(value.split(",")).map(parsePropertyDefinition(name))
|
||||
AppProperty(name)(map.get("quick"), map.get("new"), map.get("fill"))
|
||||
}
|
||||
def parsePropertyDefinition(name: String)(value: String) = value.split("=",2) match {
|
||||
case Array(mode,value) => (mode, parsePropertyValue(name, value)(defineProperty(name)))
|
||||
case x => throw new BootException("Invalid property definition '" + x + "' for property '" + name + "'")
|
||||
}
|
||||
def defineProperty(name: String)(action: String, requiredArg: String, optionalArg: Option[String]) =
|
||||
action match
|
||||
{
|
||||
case "prompt" => PromptProperty(requiredArg, optionalArg)
|
||||
case "set" => SetProperty(requiredArg)
|
||||
case _ => throw new BootException("Unknown action '" + action + "' for property '" + name + "'")
|
||||
}
|
||||
private lazy val propertyPattern = """(.+)\((.*)\)(?:\[(.*)\])?""".r.pattern
|
||||
def parsePropertyValue[T](name: String, definition: String)(f: (String, String, Option[String]) => T): T =
|
||||
{
|
||||
val m = propertyPattern.matcher(definition)
|
||||
if(!m.matches()) throw new BootException("Invalid property definition '" + definition + "' for property '" + name + "'")
|
||||
val optionalArg = m.group(3)
|
||||
f(m.group(1), m.group(2), if(optionalArg eq null) None else Some(optionalArg))
|
||||
}
|
||||
def trim(s: Array[String]) = s.map(_.trim)
|
||||
|
||||
// line parsing
|
||||
|
|
@ -111,13 +146,13 @@ class ConfigurationParser extends Parsers with NotNull
|
|||
{
|
||||
type State = (SectionMap, Option[String])
|
||||
val s: State =
|
||||
( ( (Map.empty withDefaultValue(Map.empty), None): State) /: lines ) {
|
||||
( ( (Map.empty withDefaultValue(TreeMap.empty[String,Option[String]]), None): State) /: lines ) {
|
||||
case (x, Comment) => x
|
||||
case ( (map, _), Section(name) ) => (map, Some(name))
|
||||
case ( (_, None), l: Labeled ) => error("Label " + l.label + " is not in a section")
|
||||
case ( (_, None), l: Labeled ) => throw new BootException("Label " + l.label + " is not in a section")
|
||||
case ( (map, s @ Some(section)), l: Labeled ) =>
|
||||
val sMap = map(section)
|
||||
if( sMap.contains(l.label) ) error("Duplicate label '" + l.label + "' in section '" + section + "'")
|
||||
if( sMap.contains(l.label) ) throw new BootException("Duplicate label '" + l.label + "' in section '" + section + "'")
|
||||
else ( map(section) = (sMap(l.label) = l.value), s )
|
||||
}
|
||||
s._1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package xsbt.boot
|
||||
|
||||
import java.io.{File, FileInputStream, FileOutputStream}
|
||||
import java.util.Properties
|
||||
|
||||
object Initialize
|
||||
{
|
||||
def create(file: File, promptCreate: String, enableQuick: Boolean, spec: Seq[AppProperty])
|
||||
{
|
||||
SimpleReader.readLine(promptCreate + " (y/N" + (if(enableQuick) "/s" else "") + ") ") match
|
||||
{
|
||||
case None => throw new BootException("")
|
||||
case Some(line) =>
|
||||
line.toLowerCase match
|
||||
{
|
||||
case "y" | "yes" => file.createNewFile(); process(file, spec, _.create)
|
||||
case "n" | "no" | "" => throw new BootException("")
|
||||
case "s" => process(file, spec, _.quick)
|
||||
}
|
||||
}
|
||||
}
|
||||
def fill(file: File, spec: Seq[AppProperty]): Unit = process(file, spec, _.fill)
|
||||
def process(file: File, appProperties: Seq[AppProperty], select: AppProperty => Option[PropertyInit])
|
||||
{
|
||||
val properties = new Properties
|
||||
if(file.exists)
|
||||
Using(new FileInputStream(file))( properties.load )
|
||||
for(property <- appProperties; init <- select(property) if properties.getProperty(property.name) == null)
|
||||
initialize(properties, property.name, init)
|
||||
Using(new FileOutputStream(file))( out => properties.save(out, "") )
|
||||
}
|
||||
def initialize(properties: Properties, name: String, init: PropertyInit)
|
||||
{
|
||||
init match
|
||||
{
|
||||
case SetProperty(value) => properties.setProperty(name, value)
|
||||
case PromptProperty(label, default) =>
|
||||
def noValue = throw new BootException("No value provided for " + label)
|
||||
SimpleReader.readLine(label + default.toList.map(" [" + _ + "]").mkString + ": ") match
|
||||
{
|
||||
case None => noValue
|
||||
case Some(line) =>
|
||||
val value = if(line.isEmpty) default.getOrElse(noValue) else line
|
||||
properties.setProperty(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,8 +17,17 @@ object Launch
|
|||
val config = Configuration.parse(configLocation, currentDirectory)
|
||||
Find(config, currentDirectory) match { case (resolved, baseDirectory) => parsed(baseDirectory, resolved, arguments) }
|
||||
}
|
||||
|
||||
def parsed(currentDirectory: File, parsed: LaunchConfiguration, arguments: Seq[String]): Unit =
|
||||
{
|
||||
val propertiesFile = parsed.boot.properties
|
||||
import parsed.boot.{enableQuick, promptCreate, promptFill}
|
||||
if(!promptCreate.isEmpty && !propertiesFile.exists)
|
||||
Initialize.create(propertiesFile, promptCreate, enableQuick, parsed.appProperties)
|
||||
else if(promptFill)
|
||||
Initialize.fill(propertiesFile, parsed.appProperties)
|
||||
initialized(currentDirectory, parsed, arguments)
|
||||
}
|
||||
def initialized(currentDirectory: File, parsed: LaunchConfiguration, arguments: Seq[String]): Unit =
|
||||
ResolveVersions(parsed) match { case (resolved, finish) => explicit(currentDirectory, resolved, arguments, finish) }
|
||||
|
||||
def explicit(currentDirectory: File, explicit: LaunchConfiguration, arguments: Seq[String], setupComplete: () => Unit): Unit =
|
||||
|
|
|
|||
|
|
@ -3,34 +3,32 @@ package xsbt.boot
|
|||
import java.io.File
|
||||
import java.net.URL
|
||||
|
||||
final case class LaunchConfiguration(scalaVersion: Version, app: Application, repositories: Seq[Repository], boot: BootSetup, logging: Logging) extends NotNull
|
||||
final case class LaunchConfiguration(scalaVersion: Version, app: Application, repositories: Seq[Repository], boot: BootSetup, logging: Logging, appProperties: Seq[AppProperty]) extends NotNull
|
||||
{
|
||||
def getScalaVersion = Version.get(scalaVersion)
|
||||
def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(Version.Explicit(newScalaVersion), app, repositories, boot, logging)
|
||||
def withApp(app: Application) = LaunchConfiguration(scalaVersion, app, repositories, boot, logging)
|
||||
def withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, app.withVersion(Version.Explicit(newAppVersion)), repositories, boot, logging)
|
||||
def withVersions(newScalaVersion: String, newAppVersion: String) = LaunchConfiguration(Version.Explicit(newScalaVersion), app.withVersion(Version.Explicit(newAppVersion)), repositories, boot, logging)
|
||||
def map(f: File => File) = LaunchConfiguration(scalaVersion, app, repositories, boot.map(f), logging)
|
||||
def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(Version.Explicit(newScalaVersion), app, repositories, boot, logging, appProperties)
|
||||
def withApp(app: Application) = LaunchConfiguration(scalaVersion, app, repositories, boot, logging, appProperties)
|
||||
def withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, app.withVersion(Version.Explicit(newAppVersion)), repositories, boot, logging, appProperties)
|
||||
def withVersions(newScalaVersion: String, newAppVersion: String) = LaunchConfiguration(Version.Explicit(newScalaVersion), app.withVersion(Version.Explicit(newAppVersion)), repositories, boot, logging, appProperties)
|
||||
def map(f: File => File) = LaunchConfiguration(scalaVersion, app, repositories, boot.map(f), logging, appProperties)
|
||||
}
|
||||
|
||||
sealed trait Version extends NotNull
|
||||
object Version
|
||||
{
|
||||
final case class Explicit(value: String) extends Version
|
||||
final case class Implicit(tpe: Implicit.Value, default: Option[String]) extends Version
|
||||
final case class Implicit(default: Option[String]) extends Version
|
||||
{
|
||||
require(default.isEmpty || !default.get.isEmpty, "Default cannot be empty")
|
||||
}
|
||||
|
||||
object Implicit extends RichEnum
|
||||
object Implicit
|
||||
{
|
||||
val Read = Value("read")
|
||||
val Prompt = Value("prompt")
|
||||
val ReadOrPrompt = Value("read-or-prompt")
|
||||
def apply(s: String, default: Option[String]): Either[String, Implicit] = fromString(s).right.map(t =>Implicit(t, default))
|
||||
def apply(s: String, default: Option[String]): Either[String, Implicit] =
|
||||
if(s == "read") Right(Implicit(default)) else Left("Expected 'read', got '" + s +"'")
|
||||
}
|
||||
def get(v: Version) = v match { case Version.Explicit(v) => v; case _ => throw new BootException("Unresolved version: " + v) }
|
||||
def default = Implicit(Implicit.ReadOrPrompt, None)
|
||||
def default = Implicit(None)
|
||||
}
|
||||
|
||||
sealed abstract class RichEnum extends Enumeration
|
||||
|
|
@ -87,10 +85,16 @@ object Search extends RichEnum
|
|||
def apply(s: String, paths: Seq[File]): Search = Search(toValue(s), paths)
|
||||
}
|
||||
|
||||
final case class BootSetup(directory: File, properties: File, search: Search) extends NotNull
|
||||
final case class BootSetup(directory: File, properties: File, search: Search, promptCreate: String, enableQuick: Boolean, promptFill: Boolean) extends NotNull
|
||||
{
|
||||
def map(f: File => File) = BootSetup(f(directory), f(properties), search)
|
||||
def map(f: File => File) = BootSetup(f(directory), f(properties), search, promptCreate, enableQuick, promptFill)
|
||||
}
|
||||
final case class AppProperty(name: String)(val quick: Option[PropertyInit], val create: Option[PropertyInit], val fill: Option[PropertyInit]) extends NotNull
|
||||
|
||||
sealed trait PropertyInit extends NotNull
|
||||
final case class SetProperty(value: String) extends PropertyInit
|
||||
final case class PromptProperty(label: String, default: Option[String]) extends PropertyInit
|
||||
|
||||
final case class Logging(level: LogLevel.Value) extends NotNull
|
||||
object LogLevel extends RichEnum
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import java.util.Properties
|
|||
|
||||
object ResolvedVersion extends Enumeration
|
||||
{
|
||||
val Explicit, Read, Prompted = Value
|
||||
val Explicit, Read = Value
|
||||
}
|
||||
final case class ResolvedVersion(v: String, method: ResolvedVersion.Value) extends NotNull
|
||||
|
||||
|
|
@ -22,15 +22,9 @@ object ResolveVersions
|
|||
Using( new FileInputStream(propertiesFile) )( properties.load )
|
||||
properties
|
||||
}
|
||||
private def userDeclined(label: String) = throw new BootException("No " + label +" version specified.")
|
||||
private def promptVersion(label: String, default: Option[String]) =
|
||||
{
|
||||
val message = label + default.map(" [" + _ + "]").getOrElse("") + " : "
|
||||
SimpleReader.readLine(message).flatMap(x => notEmpty(x) orElse(default)) getOrElse(userDeclined(label))
|
||||
}
|
||||
}
|
||||
|
||||
import ResolveVersions.{doNothing, notEmpty, promptVersion, readProperties, trim, userDeclined}
|
||||
import ResolveVersions.{doNothing, notEmpty, readProperties, trim}
|
||||
final class ResolveVersions(conf: LaunchConfiguration) extends NotNull
|
||||
{
|
||||
private def propertiesFile = conf.boot.properties
|
||||
|
|
@ -41,11 +35,7 @@ final class ResolveVersions(conf: LaunchConfiguration) extends NotNull
|
|||
val appVersionProperty = app.name.toLowerCase.replaceAll("\\s+",".") + ".version"
|
||||
val scalaVersion = (new Resolve("scala.version", "Scala"))(conf.scalaVersion)
|
||||
val appVersion = (new Resolve(appVersionProperty, app.name))(app.version)
|
||||
val prompted = Seq((scalaVersion, conf.scalaVersion), (appVersion, app.version)).exists {
|
||||
case (resolved, original) => resolved.method == ResolvedVersion.Prompted &&
|
||||
( original match { case Version.Implicit(Version.Implicit.ReadOrPrompt, _) => true; case _ => false } )
|
||||
}
|
||||
val finish = if(!prompted) doNothing else () => Using( new FileOutputStream(propertiesFile) ) { out => properties.store(out, "") }
|
||||
val finish = () => Using( new FileOutputStream(propertiesFile) ) { out => properties.store(out, "") }
|
||||
( withVersions(scalaVersion.v, appVersion.v), finish )
|
||||
}
|
||||
private final class Resolve(versionProperty: String, label: String) extends NotNull
|
||||
|
|
@ -54,22 +44,12 @@ final class ResolveVersions(conf: LaunchConfiguration) extends NotNull
|
|||
def apply(v: Version): ResolvedVersion =
|
||||
{
|
||||
import Version.{Explicit, Implicit}
|
||||
import Implicit.{Prompt, Read, ReadOrPrompt}
|
||||
v match
|
||||
{
|
||||
case e: Explicit => ResolvedVersion(e.value, ResolvedVersion.Explicit)
|
||||
case Implicit(Read , default) => ResolvedVersion(readVersion() orElse default getOrElse noVersionInFile, ResolvedVersion.Read )
|
||||
case Implicit(Prompt, default) => ResolvedVersion(promptVersion(label, default), ResolvedVersion.Prompted)
|
||||
case Implicit(ReadOrPrompt, default) => readOrPromptVersion(default)
|
||||
case Implicit(default) => ResolvedVersion(readVersion() orElse default getOrElse noVersionInFile, ResolvedVersion.Read )
|
||||
}
|
||||
}
|
||||
def readVersion() = trim(properties.getProperty(versionProperty))
|
||||
def readOrPromptVersion(default: Option[String]) =
|
||||
readVersion().map(v => ResolvedVersion(v, ResolvedVersion.Read)) getOrElse
|
||||
{
|
||||
val prompted = promptVersion(label, default)
|
||||
properties.setProperty(versionProperty, prompted)
|
||||
ResolvedVersion( prompted, ResolvedVersion.Prompted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
[scala]
|
||||
version: read-or-prompt, 2.7.5
|
||||
version: read
|
||||
|
||||
[app]
|
||||
org: org.scala-tools.sbt
|
||||
name: xsbt
|
||||
version: read-or-prompt, 0.7.0_13
|
||||
class: xsbt.Main
|
||||
name: sbt
|
||||
version: read
|
||||
class: sbt.xMain
|
||||
components: xsbti
|
||||
cross-versioned: true
|
||||
|
||||
|
|
@ -20,6 +20,17 @@
|
|||
[boot]
|
||||
directory: project/boot
|
||||
properties: project/build.properties
|
||||
prompt-create: true
|
||||
prompt-fill: true
|
||||
quick-option: true
|
||||
|
||||
[log]
|
||||
level: info
|
||||
level: info
|
||||
|
||||
[app-properties]
|
||||
project.name: quick=set[test], new=prompt, fill=prompt
|
||||
project.version: quick=set[1.0], new=prompt[1.0], fill=prompt[1.0]
|
||||
scala.version: quick=set[2.7.5], new=prompt[2.7.5], fill=prompt[2.7.5]
|
||||
sbt.version: quick=set[0.5.6-SNAPSHOT], new=prompt[0.5.6-SNAPSHOT], fill=prompt[0.5.6-SNAPSHOT]
|
||||
project.scratch: quick=set[true]
|
||||
project.initialize: quick=set[true], new=set[true]
|
||||
|
|
@ -78,8 +78,8 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
|
|||
}
|
||||
trait TestDependencies extends Project
|
||||
{
|
||||
val sc = "org.scala-tools.testing" % "scalacheck" % "1.5" % "test->default"
|
||||
val sp = "org.scala-tools.testing" % "specs" % "1.6.0" % "test->default"
|
||||
val sc = "org.scala-tools.testing" %% "scalacheck" % "1.5" % "test"
|
||||
val sp = "org.scala-tools.testing" % "specs" % "1.6.0" % "test"
|
||||
}
|
||||
class StandardTaskProject(info: ProjectInfo) extends Base(info)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue