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.lang.Character.isWhitespace
import java.io.File import java.io.File
import java.net.{MalformedURLException, URL} import java.net.{MalformedURLException, URL}
import scala.collection.immutable.TreeMap
class ConfigurationParser extends Parsers with NotNull class ConfigurationParser extends Parsers with NotNull
{ {
@ -29,15 +30,16 @@ class ConfigurationParser extends Parsers with NotNull
val (repositories, m3) = processSection(m2, "repositories", getRepositories) val (repositories, m3) = processSection(m2, "repositories", getRepositories)
val (boot, m4) = processSection(m3, "boot", getBoot) val (boot, m4) = processSection(m3, "boot", getBoot)
val (logging, m5) = processSection(m4, "log", getLogging) val (logging, m5) = processSection(m4, "log", getLogging)
check(m5, "section") val (properties, m6) = processSection(m5, "app-properties", getAppProperties)
new LaunchConfiguration(scalaVersion, app, repositories, boot, logging) check(m6, "section")
new LaunchConfiguration(scalaVersion, app, repositories, boot, logging, properties)
} }
def getScalaVersion(m: LabelMap) = check("label", getVersion(m)) def getScalaVersion(m: LabelMap) = check("label", getVersion(m))
def getVersion(m: LabelMap): (Version, LabelMap) = process(m, "version", processVersion) def getVersion(m: LabelMap): (Version, LabelMap) = process(m, "version", processVersion)
def processVersion(value: Option[String]): Version = value.map(version).getOrElse(Version.default) def processVersion(value: Option[String]): Version = value.map(version).getOrElse(Version.default)
def version(value: String): Version = 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)) val tokens = trim(value.split(",", 2))
import Version.{Explicit, Implicit} import Version.{Explicit, Implicit}
val defaultVersion = if(tokens.length == 2) Some(tokens(1)) else None 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) = def processSection[T](sections: Map[String, LabelMap], name: String, f: LabelMap => T) =
process[String,LabelMap,T](sections, name, m => f(m withDefaultValue(None))) 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 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 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) (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) val result = map(name).map(value => trim(value.split(",")).filter(!_.isEmpty)).getOrElse(default)
(result, map - name) (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 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) = def file(map: LabelMap, name: String, default: File): (File, LabelMap) =
(map.getOrElse(name, None).map(toFile).getOrElse(default), map - name) (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 (dir, m1) = file(m, "directory", toFile("project/boot"))
val (props, m2) = file(m1, "properties", toFile("project/build.properties")) val (props, m2) = file(m1, "properties", toFile("project/build.properties"))
val (search, m3) = getSearch(m2, props) val (search, m3) = getSearch(m2, props)
check(m3, "label") val (enableQuick, m4) = bool(m3, "quick-option", false)
BootSetup(dir, props, search) 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 getLogging(m: LabelMap): Logging = check("label", process(m, "level", getLevel))
def getLevel(m: Option[String]) = m.map(LogLevel.apply).getOrElse(Logging(LogLevel.Info)) 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, None) => Predefined(key)
case (key, Some(value)) => case (key, Some(value)) =>
val r = trim(value.split(",",2)) 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) 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) def trim(s: Array[String]) = s.map(_.trim)
// line parsing // line parsing
@ -111,13 +146,13 @@ class ConfigurationParser extends Parsers with NotNull
{ {
type State = (SectionMap, Option[String]) type State = (SectionMap, Option[String])
val s: State = 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 (x, Comment) => x
case ( (map, _), Section(name) ) => (map, Some(name)) 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 ) => case ( (map, s @ Some(section)), l: Labeled ) =>
val sMap = map(section) 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 ) else ( map(section) = (sMap(l.label) = l.value), s )
} }
s._1 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) val config = Configuration.parse(configLocation, currentDirectory)
Find(config, currentDirectory) match { case (resolved, baseDirectory) => parsed(baseDirectory, resolved, arguments) } Find(config, currentDirectory) match { case (resolved, baseDirectory) => parsed(baseDirectory, resolved, arguments) }
} }
def parsed(currentDirectory: File, parsed: LaunchConfiguration, arguments: Seq[String]): Unit = 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) } ResolveVersions(parsed) match { case (resolved, finish) => explicit(currentDirectory, resolved, arguments, finish) }
def explicit(currentDirectory: File, explicit: LaunchConfiguration, arguments: Seq[String], setupComplete: () => Unit): Unit = 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.io.File
import java.net.URL 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 getScalaVersion = Version.get(scalaVersion)
def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(Version.Explicit(newScalaVersion), app, repositories, boot, logging) def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(Version.Explicit(newScalaVersion), app, repositories, boot, logging, appProperties)
def withApp(app: Application) = LaunchConfiguration(scalaVersion, app, repositories, boot, logging) 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) 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) 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) def map(f: File => File) = LaunchConfiguration(scalaVersion, app, repositories, boot.map(f), logging, appProperties)
} }
sealed trait Version extends NotNull sealed trait Version extends NotNull
object Version object Version
{ {
final case class Explicit(value: String) extends 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") require(default.isEmpty || !default.get.isEmpty, "Default cannot be empty")
} }
object Implicit extends RichEnum object Implicit
{ {
val Read = Value("read") def apply(s: String, default: Option[String]): Either[String, Implicit] =
val Prompt = Value("prompt") if(s == "read") Right(Implicit(default)) else Left("Expected 'read', got '" + s +"'")
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 get(v: Version) = v match { case Version.Explicit(v) => v; case _ => throw new BootException("Unresolved version: " + v) } 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 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) 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 final case class Logging(level: LogLevel.Value) extends NotNull
object LogLevel extends RichEnum object LogLevel extends RichEnum
{ {

View File

@ -5,7 +5,7 @@ import java.util.Properties
object ResolvedVersion extends Enumeration object ResolvedVersion extends Enumeration
{ {
val Explicit, Read, Prompted = Value val Explicit, Read = Value
} }
final case class ResolvedVersion(v: String, method: ResolvedVersion.Value) extends NotNull final case class ResolvedVersion(v: String, method: ResolvedVersion.Value) extends NotNull
@ -22,15 +22,9 @@ object ResolveVersions
Using( new FileInputStream(propertiesFile) )( properties.load ) Using( new FileInputStream(propertiesFile) )( properties.load )
properties 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 final class ResolveVersions(conf: LaunchConfiguration) extends NotNull
{ {
private def propertiesFile = conf.boot.properties 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 appVersionProperty = app.name.toLowerCase.replaceAll("\\s+",".") + ".version"
val scalaVersion = (new Resolve("scala.version", "Scala"))(conf.scalaVersion) val scalaVersion = (new Resolve("scala.version", "Scala"))(conf.scalaVersion)
val appVersion = (new Resolve(appVersionProperty, app.name))(app.version) val appVersion = (new Resolve(appVersionProperty, app.name))(app.version)
val prompted = Seq((scalaVersion, conf.scalaVersion), (appVersion, app.version)).exists { val finish = () => Using( new FileOutputStream(propertiesFile) ) { out => properties.store(out, "") }
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, "") }
( withVersions(scalaVersion.v, appVersion.v), finish ) ( withVersions(scalaVersion.v, appVersion.v), finish )
} }
private final class Resolve(versionProperty: String, label: String) extends NotNull 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 = def apply(v: Version): ResolvedVersion =
{ {
import Version.{Explicit, Implicit} import Version.{Explicit, Implicit}
import Implicit.{Prompt, Read, ReadOrPrompt}
v match v match
{ {
case e: Explicit => ResolvedVersion(e.value, ResolvedVersion.Explicit) case e: Explicit => ResolvedVersion(e.value, ResolvedVersion.Explicit)
case Implicit(Read , default) => ResolvedVersion(readVersion() orElse default getOrElse noVersionInFile, ResolvedVersion.Read ) case Implicit(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)
} }
} }
def readVersion() = trim(properties.getProperty(versionProperty)) 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] [scala]
version: read-or-prompt, 2.7.5 version: read
[app] [app]
org: org.scala-tools.sbt org: org.scala-tools.sbt
name: xsbt name: sbt
version: read-or-prompt, 0.7.0_13 version: read
class: xsbt.Main class: sbt.xMain
components: xsbti components: xsbti
cross-versioned: true cross-versioned: true
@ -20,6 +20,17 @@
[boot] [boot]
directory: project/boot directory: project/boot
properties: project/build.properties properties: project/build.properties
prompt-create: true
prompt-fill: true
quick-option: true
[log] [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 trait TestDependencies extends Project
{ {
val sc = "org.scala-tools.testing" % "scalacheck" % "1.5" % "test->default" val sc = "org.scala-tools.testing" %% "scalacheck" % "1.5" % "test"
val sp = "org.scala-tools.testing" % "specs" % "1.6.0" % "test->default" val sp = "org.scala-tools.testing" % "specs" % "1.6.0" % "test"
} }
class StandardTaskProject(info: ProjectInfo) extends Base(info) class StandardTaskProject(info: ProjectInfo) extends Base(info)
{ {