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 Pre._
import ConfigurationParser._
import java.lang.Character.isWhitespace import java.lang.Character.isWhitespace
import java.io.{BufferedReader, File, FileInputStream, InputStreamReader, Reader, StringReader} import java.io.{BufferedReader, File, FileInputStream, InputStreamReader, Reader, StringReader}
import java.net.{MalformedURLException, URL} import java.net.{MalformedURLException, URL}
import java.util.regex.Pattern import java.util.regex.Pattern
import scala.collection.immutable.List 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 class ConfigurationParser extends NotNull
{ {
def apply(file: File): LaunchConfiguration = Using(new InputStreamReader(new FileInputStream(file), "UTF-8"))(apply) 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 (properties, m6) = processSection(m5, "app-properties", getAppProperties)
val (cacheDir, m7) = processSection(m6, "ivy", getIvy) val (cacheDir, m7) = processSection(m6, "ivy", getIvy)
check(m7, "section") 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) new LaunchConfiguration(scalaVersion, IvyOptions(cacheDir, classifiers, repositories), app, boot, logging, properties)
} }
def getScala(m: LabelMap) = def getScala(m: LabelMap) =
{ {
val (scalaVersion, m1) = getVersion(m, "Scala version", "scala.version") 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") check(m2, "label")
(scalaVersion, scalaClassifiers) (scalaVersion, scalaClassifiers)
} }
def getVersion(m: LabelMap, label: String, defaultName: String): (Version, LabelMap) = process(m, "version", processVersion(label, defaultName)) def getClassifiers(m: LabelMap, label: String): (Value[List[String]], LabelMap) =
def processVersion(label: String, defaultName: String)(value: Option[String]): Version = process(m, "classifiers", processClassifiers(label))
value.map(version(label)).getOrElse(new Version.Implicit(defaultName, None)) def processClassifiers(label: String)(value: Option[String]): Value[List[String]] =
def version(label: String)(value: String): Version = 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)") if(isEmpty(value)) error(label + " cannot be empty (omit declaration to use the default)")
try { parsePropertyValue(label, value)(Version.Implicit.apply) } try { parsePropertyValue(label, value)(Value.readImplied[T]) }
catch { case e: BootException => new Version.Explicit(value) } catch { case e: BootException => new Explicit(read(value)) }
} }
def processSection[T](sections: SectionMap, name: String, f: LabelMap => T) = def processSection[T](sections: SectionMap, name: String, f: LabelMap => T) =
process[String,LabelMap,T](sections, name, m => f(m default(x => None))) 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 getOrNone[K,V](map: ListMap[K,Option[V]], k: K) = orElse(map.get(k), None)
def ids(map: LabelMap, name: String, default: List[String]) = 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) (orElse(result, default), map - name)
} }
def bool(map: LabelMap, name: String, default: Boolean): (Boolean, LabelMap) = 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) 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 (org, m1) = id(m, "org", "org.scala-tools.sbt")
val (name, m2) = id(m1, "name", "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 (components, m5) = ids(m4, "components", List("default"))
val (crossVersioned, m6) = id(m5, "cross-versioned", "true") val (crossVersioned, m6) = id(m5, "cross-versioned", "true")
val (resources, m7) = ids(m6, "resources", Nil) val (resources, m7) = ids(m6, "resources", Nil)
val (classifiers, m8) = ids(m7, "classifiers", Nil) val (classifiers, m8) = getClassifiers(m7, "Application classifiers")
check(m8, "label") check(m8, "label")
val classpathExtra = toArray(toFiles(resources)) val classpathExtra = toArray(toFiles(resources))
val app = new Application(org, name, rev, main, components, toBoolean(crossVersioned), classpathExtra) 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) val optionalArg = m.group(3)
f(m.group(1), m.group(2), if(optionalArg eq null) None else Some(optionalArg)) 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]] type LabelMap = ListMap[String, Option[String]]
// section-name -> label -> value // section-name -> label -> value

View File

@ -34,7 +34,7 @@ object Launch
def initialized(currentDirectory: File, parsed: LaunchConfiguration, arguments: List[String]): Unit = def initialized(currentDirectory: File, parsed: LaunchConfiguration, arguments: List[String]): Unit =
{ {
parsed.logging.debug("Parsed configuration: " + parsed) parsed.logging.debug("Parsed configuration: " + parsed)
val resolved = ResolveVersions(parsed) val resolved = ResolveValues(parsed)
resolved.logging.debug("Resolved configuration: " + resolved) resolved.logging.debug("Resolved configuration: " + resolved)
explicit(currentDirectory, resolved, arguments) explicit(currentDirectory, resolved, arguments)
} }
@ -91,7 +91,7 @@ class Launch private[xsbt](val bootDirectory: File, val ivyOptions: IvyOptions)
override def classpath = array(compilerJar, libraryJar) override def classpath = array(compilerJar, libraryJar)
def baseDirectories = List(scalaHome) def baseDirectories = List(scalaHome)
def testLoadClasses = TestLoadScalaClasses def testLoadClasses = TestLoadScalaClasses
def target = new UpdateScala(classifiers.forScala) def target = new UpdateScala(Value.get(classifiers.forScala))
def failLabel = "Scala " + version def failLabel = "Scala " + version
def lockFile = updateLockFile def lockFile = updateLockFile
def extraClasspath = array() def extraClasspath = array()
@ -106,7 +106,7 @@ class Launch private[xsbt](val bootDirectory: File, val ivyOptions: IvyOptions)
def parentLoader = ScalaProvider.this.loader def parentLoader = ScalaProvider.this.loader
def baseDirectories = appHome :: id.mainComponents.map(components.componentLocation).toList def baseDirectories = appHome :: id.mainComponents.map(components.componentLocation).toList
def testLoadClasses = List(id.mainClass) 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 failLabel = id.name + " " + id.version
def lockFile = updateLockFile def lockFile = updateLockFile
def mainClasspath = fullClasspath def mainClasspath = fullClasspath
@ -140,7 +140,7 @@ object Launcher
{ {
val parsed = ResolvePaths(Configuration.parse(configLocation, baseDirectory), baseDirectory) val parsed = ResolvePaths(Configuration.parse(configLocation, baseDirectory), baseDirectory)
Initialize.process(parsed.boot.properties, parsed.appProperties, Initialize.selectQuick) Initialize.process(parsed.boot.properties, parsed.appProperties, Initialize.selectQuick)
val config = ResolveVersions(parsed) val config = ResolveValues(parsed)
val launcher = apply(config) val launcher = apply(config)
val scalaProvider = launcher.getScala(config.getScalaVersion) val scalaProvider = launcher.getScala(config.getScalaVersion)
scalaProvider.app(config.app.toID) scalaProvider.app(config.app.toID)

View File

@ -8,40 +8,45 @@ import java.io.File
import java.net.URL import java.net.URL
import scala.collection.immutable.List 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 getScalaVersion = Value.get(scalaVersion)
def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(new Version.Explicit(newScalaVersion), ivyConfiguration, app, boot, logging, appProperties) 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 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 withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, ivyConfiguration, app.withVersion(new 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) // 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) 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 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 sealed trait Value[T]
{ final class Explicit[T](val value: T) extends Value[T] {
def apply(s: String, name: String, default: Option[String]): Version = override def toString = value.toString
if(s == "read") new Implicit(name, default) else error("Expected 'read', got '" + s +"'") }
} final class Implicit[T](val name: String, val default: Option[T]) extends Value[T]
def get(v: Version) = v match { case e: Version.Explicit => e.value; case _ => throw new BootException("Unresolved version: " + v) } {
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 getVersion = Value.get(version)
def withVersion(newVersion: Version) = Application(groupID, name, newVersion, main, components, crossVersioned, classpathExtra) 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 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)) 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 = def apply(id: xsbti.ApplicationID): Application =
{ {
import id._ 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 final class Logging(level: LogLevel.Value) extends NotNull
{ {
import LogLevel._ def log(s: => String, at: LogLevel.Value) = if(level.id <= at.id) stream(at).println("[" + at + "] " + s)
def log(s: => String, at: Value) = if(level.id <= at.id) stream(at).println("[" + at + "] " + s) def debug(s: => String) = log(s, LogLevel.Debug)
def debug(s: => String) = log(s, Debug) private def stream(at: LogLevel.Value) = if(at == LogLevel.Error) System.err else System.out
private def stream(at: Value) = if(at == Error) System.err else System.out
} }
object LogLevel extends Enumeration object LogLevel extends Enumeration
{ {

View File

@ -26,7 +26,7 @@ trait Provider extends NotNull
def retrieveCorrupt(missing: Iterable[String]): Nothing = fail(": missing " + missing.mkString(", ")) def retrieveCorrupt(missing: Iterable[String]): Nothing = fail(": missing " + missing.mkString(", "))
private def fail(extra: String) = private def fail(extra: String) =
throw new xsbti.RetrieveException(versionString, "Could not retrieve " + failLabel + extra) 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) val (jars, loader) = Locks(lockFile, new initialize)
private final class initialize extends Callable[(Array[File], ClassLoader)] private final class initialize extends Callable[(Array[File], ClassLoader)]

View File

@ -7,9 +7,9 @@ import Pre._
import java.io.{File, FileInputStream} import java.io.{File, FileInputStream}
import java.util.Properties 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 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 notEmpty(s: String) = if(isEmpty(s)) None else Some(s)
private def readProperties(propertiesFile: File) = private def readProperties(propertiesFile: File) =
@ -21,8 +21,8 @@ object ResolveVersions
} }
} }
import ResolveVersions.{readProperties, trim} import ResolveValues.{readProperties, trim}
final class ResolveVersions(conf: LaunchConfiguration) extends NotNull final class ResolveValues(conf: LaunchConfiguration) extends NotNull
{ {
private def propertiesFile = conf.boot.properties private def propertiesFile = conf.boot.properties
private lazy val properties = readProperties(propertiesFile) private lazy val properties = readProperties(propertiesFile)
@ -31,17 +31,24 @@ final class ResolveVersions(conf: LaunchConfiguration) extends NotNull
import conf._ import conf._
val scalaVersion = resolve(conf.scalaVersion) val scalaVersion = resolve(conf.scalaVersion)
val appVersion = resolve(app.version) 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 v match
{ {
case e: Version.Explicit => e.value case e: Explicit[t] => e.value
case i: Version.Implicit => case i: Implicit[t] =>
trim(properties.getProperty(i.name)) orElse trim(properties.getProperty(i.name)) map read orElse
i.default getOrElse i.default getOrElse
error("No " + i.name + " specified in " + propertiesFile) error("No " + i.name + " specified in " + propertiesFile)
} }
}
} }

View File

@ -1,6 +1,6 @@
[scala] [scala]
version: 2.8.0 version: 2.8.0
#classifiers: sources, javadoc classifiers: read(scala.classifiers)[]
[app] [app]
org: org.scala-tools.sbt org: org.scala-tools.sbt
@ -9,6 +9,7 @@
class: sbt.xMain class: sbt.xMain
components: xsbti components: xsbti
cross-versioned: true cross-versioned: true
classifiers: read(sbt.classifiers)[]
#classifiers: sources, javadoc #classifiers: sources, javadoc
[repositories] [repositories]