From dff30c036e0fa925e164494a3d06f92c4b1bbfb9 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Wed, 11 Aug 2010 18:50:44 -0400 Subject: [PATCH] sbt and Scala classifiers can be configured in build.properties The sbt.classifiers property sets the classifiers for sbt and scala.classifiers sets it for Scala. Note that when creating a project, Scala and sbt are retrieved right after project setup. Changes to *.classifiers will only apply if project/boot is removed or a new version of Scala is retrieved. --- launch/ConfigurationParser.scala | 39 +++++++---- launch/Launch.scala | 8 +-- launch/LaunchConfiguration.scala | 64 ++++++++++--------- launch/Provider.scala | 2 +- ...olveVersions.scala => ResolveValues.scala} | 27 +++++--- .../main/resources/sbt/sbt.boot.properties | 3 +- 6 files changed, 84 insertions(+), 59 deletions(-) rename launch/{ResolveVersions.scala => ResolveValues.scala} (53%) diff --git a/launch/ConfigurationParser.scala b/launch/ConfigurationParser.scala index c7e702249..bbc5e8075 100644 --- a/launch/ConfigurationParser.scala +++ b/launch/ConfigurationParser.scala @@ -5,12 +5,20 @@ package xsbt.boot import Pre._ +import ConfigurationParser._ import java.lang.Character.isWhitespace import java.io.{BufferedReader, File, FileInputStream, InputStreamReader, Reader, StringReader} import java.net.{MalformedURLException, URL} import java.util.regex.Pattern import scala.collection.immutable.List +object ConfigurationParser +{ + def trim(s: Array[String]) = s.map(_.trim).toList + def ids(value: String) = trim(value.split(",")).filter(isNonEmpty) + + implicit val readIDs = ids _ +} class ConfigurationParser extends NotNull { def apply(file: File): LaunchConfiguration = Using(new InputStreamReader(new FileInputStream(file), "UTF-8"))(apply) @@ -36,24 +44,30 @@ class ConfigurationParser extends NotNull val (properties, m6) = processSection(m5, "app-properties", getAppProperties) val (cacheDir, m7) = processSection(m6, "ivy", getIvy) check(m7, "section") - val classifiers = Classifiers("" :: scalaClassifiers, "" :: appClassifiers) // the added "" ensures that the main jars are retrieved + val classifiers = Classifiers(scalaClassifiers, appClassifiers) new LaunchConfiguration(scalaVersion, IvyOptions(cacheDir, classifiers, repositories), app, boot, logging, properties) } def getScala(m: LabelMap) = { val (scalaVersion, m1) = getVersion(m, "Scala version", "scala.version") - val (scalaClassifiers, m2) = ids(m1, "classifiers", Nil) + val (scalaClassifiers, m2) = getClassifiers(m1, "Scala classifiers") check(m2, "label") (scalaVersion, scalaClassifiers) } - def getVersion(m: LabelMap, label: String, defaultName: String): (Version, LabelMap) = process(m, "version", processVersion(label, defaultName)) - def processVersion(label: String, defaultName: String)(value: Option[String]): Version = - value.map(version(label)).getOrElse(new Version.Implicit(defaultName, None)) - def version(label: String)(value: String): Version = + def getClassifiers(m: LabelMap, label: String): (Value[List[String]], LabelMap) = + process(m, "classifiers", processClassifiers(label)) + def processClassifiers(label: String)(value: Option[String]): Value[List[String]] = + value.map(readValue[List[String]](label)) getOrElse new Explicit(Nil) + + def getVersion(m: LabelMap, label: String, defaultName: String): (Value[String], LabelMap) = process(m, "version", processVersion(label, defaultName)) + def processVersion(label: String, defaultName: String)(value: Option[String]): Value[String] = + value.map(readValue[String](label)).getOrElse(new Implicit(defaultName, None)) + + def readValue[T](label: String)(implicit read: String => T): String => Value[T] = value => { - if(isEmpty(value)) error(label + " cannot be empty (omit version declaration to use the default version)") - try { parsePropertyValue(label, value)(Version.Implicit.apply) } - catch { case e: BootException => new Version.Explicit(value) } + if(isEmpty(value)) error(label + " cannot be empty (omit declaration to use the default)") + try { parsePropertyValue(label, value)(Value.readImplied[T]) } + catch { case e: BootException => new Explicit(read(value)) } } def processSection[T](sections: SectionMap, name: String, f: LabelMap => T) = process[String,LabelMap,T](sections, name, m => f(m default(x => None))) @@ -65,7 +79,7 @@ class ConfigurationParser extends NotNull def getOrNone[K,V](map: ListMap[K,Option[V]], k: K) = orElse(map.get(k), None) def ids(map: LabelMap, name: String, default: List[String]) = { - val result = map(name).map(value => trim(value.split(",")).filter(isNonEmpty)) + val result = map(name) map ConfigurationParser.ids (orElse(result, default), map - name) } def bool(map: LabelMap, name: String, default: Boolean): (Boolean, LabelMap) = @@ -105,7 +119,7 @@ class ConfigurationParser extends NotNull case (tpe :: paths, newM) => (Search(tpe, toFiles(paths)), newM) } - def getApplication(m: LabelMap): (Application, List[String]) = + def getApplication(m: LabelMap): (Application, Value[List[String]]) = { val (org, m1) = id(m, "org", "org.scala-tools.sbt") val (name, m2) = id(m1, "name", "sbt") @@ -114,7 +128,7 @@ class ConfigurationParser extends NotNull val (components, m5) = ids(m4, "components", List("default")) val (crossVersioned, m6) = id(m5, "cross-versioned", "true") val (resources, m7) = ids(m6, "resources", Nil) - val (classifiers, m8) = ids(m7, "classifiers", Nil) + val (classifiers, m8) = getClassifiers(m7, "Application classifiers") check(m8, "label") val classpathExtra = toArray(toFiles(resources)) val app = new Application(org, name, rev, main, components, toBoolean(crossVersioned), classpathExtra) @@ -156,7 +170,6 @@ class ConfigurationParser extends NotNull 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).toList type LabelMap = ListMap[String, Option[String]] // section-name -> label -> value diff --git a/launch/Launch.scala b/launch/Launch.scala index 4a4ed9c15..74caf83dc 100644 --- a/launch/Launch.scala +++ b/launch/Launch.scala @@ -34,7 +34,7 @@ object Launch def initialized(currentDirectory: File, parsed: LaunchConfiguration, arguments: List[String]): Unit = { parsed.logging.debug("Parsed configuration: " + parsed) - val resolved = ResolveVersions(parsed) + val resolved = ResolveValues(parsed) resolved.logging.debug("Resolved configuration: " + resolved) explicit(currentDirectory, resolved, arguments) } @@ -91,7 +91,7 @@ class Launch private[xsbt](val bootDirectory: File, val ivyOptions: IvyOptions) override def classpath = array(compilerJar, libraryJar) def baseDirectories = List(scalaHome) def testLoadClasses = TestLoadScalaClasses - def target = new UpdateScala(classifiers.forScala) + def target = new UpdateScala(Value.get(classifiers.forScala)) def failLabel = "Scala " + version def lockFile = updateLockFile def extraClasspath = array() @@ -106,7 +106,7 @@ class Launch private[xsbt](val bootDirectory: File, val ivyOptions: IvyOptions) def parentLoader = ScalaProvider.this.loader def baseDirectories = appHome :: id.mainComponents.map(components.componentLocation).toList def testLoadClasses = List(id.mainClass) - def target = new UpdateApp(Application(id), classifiers.app) + def target = new UpdateApp(Application(id), Value.get(classifiers.app)) def failLabel = id.name + " " + id.version def lockFile = updateLockFile def mainClasspath = fullClasspath @@ -140,7 +140,7 @@ object Launcher { val parsed = ResolvePaths(Configuration.parse(configLocation, baseDirectory), baseDirectory) Initialize.process(parsed.boot.properties, parsed.appProperties, Initialize.selectQuick) - val config = ResolveVersions(parsed) + val config = ResolveValues(parsed) val launcher = apply(config) val scalaProvider = launcher.getScala(config.getScalaVersion) scalaProvider.app(config.app.toID) diff --git a/launch/LaunchConfiguration.scala b/launch/LaunchConfiguration.scala index 4b9c333bc..0f52e475b 100644 --- a/launch/LaunchConfiguration.scala +++ b/launch/LaunchConfiguration.scala @@ -8,40 +8,45 @@ import java.io.File import java.net.URL import scala.collection.immutable.List -final case class LaunchConfiguration(scalaVersion: Version, ivyConfiguration: IvyOptions, app: Application, boot: BootSetup, logging: Logging, appProperties: List[AppProperty]) extends NotNull +//TODO: use copy constructor, check size change +final case class LaunchConfiguration(scalaVersion: Value[String], ivyConfiguration: IvyOptions, app: Application, boot: BootSetup, logging: Logging, appProperties: List[AppProperty]) extends NotNull { - def getScalaVersion = Version.get(scalaVersion) - def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(new Version.Explicit(newScalaVersion), ivyConfiguration, app, boot, logging, appProperties) + def getScalaVersion = Value.get(scalaVersion) + def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration, app, boot, logging, appProperties) def withApp(app: Application) = LaunchConfiguration(scalaVersion, ivyConfiguration, app, boot, logging, appProperties) - def withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, ivyConfiguration, app.withVersion(new Version.Explicit(newAppVersion)), boot, logging, appProperties) - def withVersions(newScalaVersion: String, newAppVersion: String) = LaunchConfiguration(new Version.Explicit(newScalaVersion), ivyConfiguration, app.withVersion(new Version.Explicit(newAppVersion)), boot, logging, appProperties) + def withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, ivyConfiguration, app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties) + // TODO: withExplicit + def withVersions(newScalaVersion: String, newAppVersion: String, classifiers0: Classifiers) = + LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration.copy(classifiers = classifiers0), app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties) def map(f: File => File) = LaunchConfiguration(scalaVersion, ivyConfiguration, app.map(f), boot.map(f), logging, appProperties) } final case class IvyOptions(cacheDirectory: Option[File], classifiers: Classifiers, repositories: List[Repository]) extends NotNull -final case class Classifiers(forScala: List[String], app: List[String]) extends NotNull -sealed trait Version extends NotNull -object Version -{ - final class Explicit(val value: String) extends Version { override def toString = value } - final class Implicit(val name: String, val default: Option[String]) extends Version - { - require(isNonEmpty(name), "Name cannot be empty") - require(default.isEmpty || isNonEmpty(default.get), "Default cannot be the empty string") - override def toString = name + (default match { case Some(d) => "[" + d + "]"; case None => "" }) - } - object Implicit - { - def apply(s: String, name: String, default: Option[String]): Version = - if(s == "read") new Implicit(name, default) else error("Expected 'read', got '" + s +"'") - } - def get(v: Version) = v match { case e: Version.Explicit => e.value; case _ => throw new BootException("Unresolved version: " + v) } +sealed trait Value[T] +final class Explicit[T](val value: T) extends Value[T] { + override def toString = value.toString +} +final class Implicit[T](val name: String, val default: Option[T]) extends Value[T] +{ + require(isNonEmpty(name), "Name cannot be empty") + override def toString = name + (default match { case Some(d) => "[" + d + "]"; case None => "" }) +} +object Value +{ + def get[T](v: Value[T]): T = v match { case e: Explicit[T] => e.value; case _ => throw new BootException("Unresolved version: " + v) } + def readImplied[T](s: String, name: String, default: Option[String])(implicit read: String => T): Value[T] = + if(s == "read") new Implicit(name, default map read) else error("Expected 'read', got '" + s +"'") } -final case class Application(groupID: String, name: String, version: Version, main: String, components: List[String], crossVersioned: Boolean, classpathExtra: Array[File]) extends NotNull +final case class Classifiers(forScala: Value[List[String]], app: Value[List[String]]) +object Classifiers { + def apply(forScala: List[String], app: List[String]):Classifiers = Classifiers(new Explicit(forScala), new Explicit(app)) +} + +final case class Application(groupID: String, name: String, version: Value[String], main: String, components: List[String], crossVersioned: Boolean, classpathExtra: Array[File]) extends NotNull { - def getVersion = Version.get(version) - def withVersion(newVersion: Version) = Application(groupID, name, newVersion, main, components, crossVersioned, classpathExtra) + def getVersion = Value.get(version) + def withVersion(newVersion: Value[String]) = Application(groupID, name, newVersion, main, components, crossVersioned, classpathExtra) def toID = AppID(groupID, name, getVersion, main, toArray(components), crossVersioned, classpathExtra) def map(f: File => File) = Application(groupID, name, version, main, components, crossVersioned, classpathExtra.map(f)) } @@ -52,7 +57,7 @@ object Application def apply(id: xsbti.ApplicationID): Application = { import id._ - Application(groupID, name, new Version.Explicit(version), mainClass, mainComponents.toList, crossVersioned, classpathExtra) + Application(groupID, name, new Explicit(version), mainClass, mainComponents.toList, crossVersioned, classpathExtra) } } @@ -100,10 +105,9 @@ final class PromptProperty(val label: String, val default: Option[String]) exten final class Logging(level: LogLevel.Value) extends NotNull { - import LogLevel._ - def log(s: => String, at: Value) = if(level.id <= at.id) stream(at).println("[" + at + "] " + s) - def debug(s: => String) = log(s, Debug) - private def stream(at: Value) = if(at == Error) System.err else System.out + def log(s: => String, at: LogLevel.Value) = if(level.id <= at.id) stream(at).println("[" + at + "] " + s) + def debug(s: => String) = log(s, LogLevel.Debug) + private def stream(at: LogLevel.Value) = if(at == LogLevel.Error) System.err else System.out } object LogLevel extends Enumeration { diff --git a/launch/Provider.scala b/launch/Provider.scala index f0327f409..141eb683a 100644 --- a/launch/Provider.scala +++ b/launch/Provider.scala @@ -26,7 +26,7 @@ trait Provider extends NotNull def retrieveCorrupt(missing: Iterable[String]): Nothing = fail(": missing " + missing.mkString(", ")) private def fail(extra: String) = throw new xsbti.RetrieveException(versionString, "Could not retrieve " + failLabel + extra) - private def versionString: String = target match { case _: UpdateScala => configuration.scalaVersion; case a: UpdateApp => Version.get(a.id.version) } + private def versionString: String = target match { case _: UpdateScala => configuration.scalaVersion; case a: UpdateApp => Value.get(a.id.version) } val (jars, loader) = Locks(lockFile, new initialize) private final class initialize extends Callable[(Array[File], ClassLoader)] diff --git a/launch/ResolveVersions.scala b/launch/ResolveValues.scala similarity index 53% rename from launch/ResolveVersions.scala rename to launch/ResolveValues.scala index 397d87953..5e1d19dc0 100644 --- a/launch/ResolveVersions.scala +++ b/launch/ResolveValues.scala @@ -7,9 +7,9 @@ import Pre._ import java.io.{File, FileInputStream} import java.util.Properties -object ResolveVersions +object ResolveValues { - def apply(conf: LaunchConfiguration): LaunchConfiguration = (new ResolveVersions(conf))() + def apply(conf: LaunchConfiguration): LaunchConfiguration = (new ResolveValues(conf))() private def trim(s: String) = if(s eq null) None else notEmpty(s.trim) private def notEmpty(s: String) = if(isEmpty(s)) None else Some(s) private def readProperties(propertiesFile: File) = @@ -21,8 +21,8 @@ object ResolveVersions } } -import ResolveVersions.{readProperties, trim} -final class ResolveVersions(conf: LaunchConfiguration) extends NotNull +import ResolveValues.{readProperties, trim} +final class ResolveValues(conf: LaunchConfiguration) extends NotNull { private def propertiesFile = conf.boot.properties private lazy val properties = readProperties(propertiesFile) @@ -31,17 +31,24 @@ final class ResolveVersions(conf: LaunchConfiguration) extends NotNull import conf._ val scalaVersion = resolve(conf.scalaVersion) val appVersion = resolve(app.version) - withVersions(scalaVersion, appVersion) + val classifiers = resolveClassifiers(ivyConfiguration.classifiers) + withVersions(scalaVersion, appVersion, classifiers) } - def resolve(v: Version): String = + def resolveClassifiers(classifiers: Classifiers): Classifiers = { + import ConfigurationParser.readIDs + // the added "" ensures that the main jars are retrieved + val scalaClassifiers = "" :: resolve(classifiers.forScala) + val appClassifiers = "" :: resolve(classifiers.app) + Classifiers(new Explicit(scalaClassifiers), new Explicit(appClassifiers)) + } + def resolve[T](v: Value[T])(implicit read: String => T): T = v match { - case e: Version.Explicit => e.value - case i: Version.Implicit => - trim(properties.getProperty(i.name)) orElse + case e: Explicit[t] => e.value + case i: Implicit[t] => + trim(properties.getProperty(i.name)) map read orElse i.default getOrElse error("No " + i.name + " specified in " + propertiesFile) } - } } \ 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 79c7602df..b00ef57e1 100644 --- a/launch/src/main/resources/sbt/sbt.boot.properties +++ b/launch/src/main/resources/sbt/sbt.boot.properties @@ -1,6 +1,6 @@ [scala] version: 2.8.0 - #classifiers: sources, javadoc + classifiers: read(scala.classifiers)[] [app] org: org.scala-tools.sbt @@ -9,6 +9,7 @@ class: sbt.xMain components: xsbti cross-versioned: true + classifiers: read(sbt.classifiers)[] #classifiers: sources, javadoc [repositories]