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.
This commit is contained in:
Mark Harrah 2010-08-11 18:50:44 -04:00
parent e750de2c3b
commit dff30c036e
6 changed files with 84 additions and 59 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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