Project creation and property filling

This commit is contained in:
Mark Harrah 2009-10-14 20:53:15 -04:00
parent 8e08708792
commit f5b3aa47fc
7 changed files with 146 additions and 59 deletions

View File

@ -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

48
launch/Create.scala Normal file
View File

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

View File

@ -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 =

View File

@ -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
{

View File

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

View File

@ -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]

View File

@ -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)
{