From f5b3aa47fc59957505e3d31f6245c2f33309c8d8 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Wed, 14 Oct 2009 20:53:15 -0400 Subject: [PATCH] Project creation and property filling --- launch/ConfigurationParser.scala | 59 +++++++++++++++---- launch/Create.scala | 48 +++++++++++++++ launch/Launch.scala | 11 +++- launch/LaunchConfiguration.scala | 34 ++++++----- launch/ResolveVersions.scala | 28 ++------- .../main/resources/sbt/sbt.boot.properties | 21 +++++-- project/build/XSbt.scala | 4 +- 7 files changed, 146 insertions(+), 59 deletions(-) create mode 100644 launch/Create.scala diff --git a/launch/ConfigurationParser.scala b/launch/ConfigurationParser.scala index 98632c00d..737eb0531 100644 --- a/launch/ConfigurationParser.scala +++ b/launch/ConfigurationParser.scala @@ -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 diff --git a/launch/Create.scala b/launch/Create.scala new file mode 100644 index 000000000..d656fbe32 --- /dev/null +++ b/launch/Create.scala @@ -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) + } + } + } +} diff --git a/launch/Launch.scala b/launch/Launch.scala index d68feae8f..42d6479b5 100644 --- a/launch/Launch.scala +++ b/launch/Launch.scala @@ -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 = diff --git a/launch/LaunchConfiguration.scala b/launch/LaunchConfiguration.scala index a242d4db3..151495c75 100644 --- a/launch/LaunchConfiguration.scala +++ b/launch/LaunchConfiguration.scala @@ -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 { diff --git a/launch/ResolveVersions.scala b/launch/ResolveVersions.scala index e772b7af9..8a33e4ac4 100644 --- a/launch/ResolveVersions.scala +++ b/launch/ResolveVersions.scala @@ -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) - } } } \ No newline at end of file diff --git a/launch/src/main/resources/sbt/sbt.boot.properties b/launch/src/main/resources/sbt/sbt.boot.properties index a33ae640a..13b8e76cc 100644 --- a/launch/src/main/resources/sbt/sbt.boot.properties +++ b/launch/src/main/resources/sbt/sbt.boot.properties @@ -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 \ No newline at end of file + 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] \ No newline at end of file diff --git a/project/build/XSbt.scala b/project/build/XSbt.scala index 06f636fdc..0c792e615 100644 --- a/project/build/XSbt.scala +++ b/project/build/XSbt.scala @@ -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) {