mirror of https://github.com/sbt/sbt.git
Merge pull request #1066 from jsuereth/wip/launcher-improvements
Launcher can now load servers in addition to regular applications.
This commit is contained in:
commit
4207978f89
|
|
@ -1,6 +1,25 @@
|
|||
package xsbti;
|
||||
|
||||
/**
|
||||
* The main entry interface for launching applications. Classes which implement this interface
|
||||
* can be launched via the sbt launcher.
|
||||
*
|
||||
* In addition, classes can be adapted into this interface by the launcher if they have a static method
|
||||
* matching one of these signatures:
|
||||
*
|
||||
* - public static void main(String[] args)
|
||||
* - public static int main(String[] args)
|
||||
* - public static xsbti.Exit main(String[] args)
|
||||
*
|
||||
*/
|
||||
public interface AppMain
|
||||
{
|
||||
/** Run the application and return the result.
|
||||
*
|
||||
* @param configuration The configuration used to run the application. Includes arguments and access to launcher features.
|
||||
* @return
|
||||
* The result of running this app.
|
||||
* Note: the result can be things like "Please reboot this application".
|
||||
*/
|
||||
public MainResult run(AppConfiguration configuration);
|
||||
}
|
||||
|
|
@ -3,9 +3,10 @@ package xsbti;
|
|||
import java.io.File;
|
||||
|
||||
/**
|
||||
* This represents an interface that can generate applications.
|
||||
* This represents an interface that can generate applications or servers.
|
||||
*
|
||||
* An application is somethign which will run and return an exit value.
|
||||
* This provider grants access to launcher related features associated with
|
||||
* the id.
|
||||
*/
|
||||
public interface AppProvider
|
||||
{
|
||||
|
|
@ -33,6 +34,8 @@ public interface AppProvider
|
|||
* It is NOT guaranteed that newMain().getClass() == mainClass().
|
||||
* The sbt launcher can wrap generic static main methods. In this case, there will be a wrapper class,
|
||||
* and you must use the `entryPoint` method.
|
||||
* @throws IncompatibleClassChangeError if the configuration used for this Application does not
|
||||
* represent a launched application.
|
||||
*/
|
||||
public AppMain newMain();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
package xsbti;
|
||||
|
||||
/** A running server.
|
||||
*
|
||||
* A class implementing this must:
|
||||
*
|
||||
* 1. Expose an HTTP port that clients can connect to, returned via the uri method.
|
||||
* 2. Accept HTTP HEAD requests against the returned URI. These are used as "ping" messages to ensure
|
||||
* a server is still alive, when new clients connect.
|
||||
* 3. Create a new thread to execute its service
|
||||
* 4. Block the calling thread until the server is shutdown via awaitTermination()
|
||||
*/
|
||||
public interface Server {
|
||||
/**
|
||||
* @return
|
||||
* A URI denoting the Port which clients can connect to.
|
||||
*
|
||||
* Note: we use a URI so that the server can bind to different IP addresses (even a public one) if desired.
|
||||
* Note: To verify that a server is "up", the sbt launcher will attempt to connect to
|
||||
* this URI's address and port with a socket. If the connection is accepted, the server is assumed to
|
||||
* be working.
|
||||
*/
|
||||
public java.net.URI uri();
|
||||
/**
|
||||
* This should block the calling thread until the server is shutdown.
|
||||
*
|
||||
* @return
|
||||
* The result that should occur from the server.
|
||||
* Can be:
|
||||
* - xsbti.Exit: Shutdown this launch
|
||||
* - xsbti.Reboot: Restart the server
|
||||
*
|
||||
*
|
||||
*/
|
||||
public xsbti.MainResult awaitTermination();
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package xsbti;
|
||||
|
||||
/** The main entry point for a launched service. This allows applciations
|
||||
* to instantiate server instances.
|
||||
*/
|
||||
public interface ServerMain {
|
||||
/**
|
||||
* This method should launch one or more thread(s) which run the service. After the service has
|
||||
* been started, it should return the port/URI it is listening for connections on.
|
||||
*
|
||||
* @param configuration
|
||||
* The configuration used to launch this service.
|
||||
* @return
|
||||
* A running server.
|
||||
*/
|
||||
public Server start(AppConfiguration configuration);
|
||||
}
|
||||
|
|
@ -5,36 +5,48 @@
|
|||
|
||||
import java.io.File
|
||||
|
||||
|
||||
// The entry point to the launcher
|
||||
object Boot
|
||||
{
|
||||
def main(args: Array[String])
|
||||
{
|
||||
args match {
|
||||
case Array("--version") =>
|
||||
println("sbt launcher version " + Package.getPackage("xsbt.boot").getImplementationVersion)
|
||||
case _ =>
|
||||
System.clearProperty("scala.home") // avoid errors from mixing Scala versions in the same JVM
|
||||
System.setProperty("jline.shutdownhook", "false") // shutdown hooks cause class loader leaks
|
||||
System.setProperty("jline.esc.timeout", "0") // starts up a thread otherwise
|
||||
CheckProxy()
|
||||
run(args)
|
||||
}
|
||||
val config = parseArgs(args)
|
||||
// If we havne't exited, we set up some hooks and launch
|
||||
System.clearProperty("scala.home") // avoid errors from mixing Scala versions in the same JVM
|
||||
System.setProperty("jline.shutdownhook", "false") // shutdown hooks cause class loader leaks
|
||||
System.setProperty("jline.esc.timeout", "0") // starts up a thread otherwise
|
||||
CheckProxy()
|
||||
run(config)
|
||||
}
|
||||
def parseArgs(args: Array[String]): LauncherArguments = {
|
||||
@annotation.tailrec
|
||||
def parse(args: List[String], isLocate: Boolean, remaining: List[String]): LauncherArguments =
|
||||
args match {
|
||||
case "--version" :: rest =>
|
||||
println("sbt launcher version " + Package.getPackage("xsbt.boot").getImplementationVersion)
|
||||
exit(1)
|
||||
case "--locate" :: rest => parse(rest, true, remaining)
|
||||
case next :: rest => parse(rest, isLocate, next :: remaining)
|
||||
case Nil => new LauncherArguments(remaining.reverse, isLocate)
|
||||
}
|
||||
parse(args.toList, false, Nil)
|
||||
}
|
||||
|
||||
// this arrangement is because Scala does not always properly optimize away
|
||||
// the tail recursion in a catch statement
|
||||
final def run(args: Array[String]): Unit = runImpl(args) match {
|
||||
final def run(args: LauncherArguments): Unit = runImpl(args) match {
|
||||
case Some(newArgs) => run(newArgs)
|
||||
case None => ()
|
||||
}
|
||||
private def runImpl(args: Array[String]): Option[Array[String]] =
|
||||
private def runImpl(args: LauncherArguments): Option[LauncherArguments] =
|
||||
try
|
||||
Launch(args.toList) map exit
|
||||
Launch(args) map exit
|
||||
catch
|
||||
{
|
||||
case b: BootException => errorAndExit(b.toString)
|
||||
case r: xsbti.RetrieveException => errorAndExit("Error: " + r.getMessage)
|
||||
case r: xsbti.FullReload => Some(r.arguments)
|
||||
case r: xsbti.FullReload => Some(new LauncherArguments(r.arguments.toList, false))
|
||||
case e: Throwable =>
|
||||
e.printStackTrace
|
||||
errorAndExit(Pre.prefixError(e.toString))
|
||||
|
|
|
|||
|
|
@ -10,21 +10,34 @@ import java.util.regex.Pattern
|
|||
import scala.collection.immutable.List
|
||||
import annotation.tailrec
|
||||
|
||||
object ConfigurationStorageState extends Enumeration {
|
||||
val PropertiesFile = value("properties-file")
|
||||
val SerializedFile = value("serialized-file")
|
||||
}
|
||||
|
||||
object Configuration
|
||||
{
|
||||
import ConfigurationStorageState._
|
||||
final val SysPropPrefix = "-D"
|
||||
def parse(file: URL, baseDirectory: File) = Using( new InputStreamReader(file.openStream, "utf8") )( (new ConfigurationParser).apply )
|
||||
@tailrec def find(args: List[String], baseDirectory: File): (URL, List[String]) =
|
||||
|
||||
/**
|
||||
* Finds the configuration location.
|
||||
*
|
||||
* Note: Configuration may be previously serialized by a launcher.
|
||||
*/
|
||||
@tailrec def find(args: List[String], baseDirectory: File): (URL, List[String], ConfigurationStorageState.Value) =
|
||||
args match
|
||||
{
|
||||
case head :: tail if head.startsWith("@") => (directConfiguration(head.substring(1), baseDirectory), tail)
|
||||
case head :: tail if head.startsWith("@load:") => (directConfiguration(head.substring(6), baseDirectory), tail, SerializedFile)
|
||||
case head :: tail if head.startsWith("@") => (directConfiguration(head.substring(1), baseDirectory), tail, PropertiesFile)
|
||||
case head :: tail if head.startsWith(SysPropPrefix) =>
|
||||
setProperty(head stripPrefix SysPropPrefix)
|
||||
find(tail, baseDirectory)
|
||||
case _ =>
|
||||
val propertyConfigured = System.getProperty("sbt.boot.properties")
|
||||
val url = if(propertyConfigured == null) configurationOnClasspath else configurationFromFile(propertyConfigured, baseDirectory)
|
||||
(url , args)
|
||||
(url, args, PropertiesFile)
|
||||
}
|
||||
def setProperty(head: String)
|
||||
{
|
||||
|
|
@ -108,7 +121,7 @@ object Configuration
|
|||
// We have to hard code them here in order to use them to determine the location of sbt.boot.properties itself
|
||||
def guessSbtVersion: Option[String] =
|
||||
{
|
||||
val props = ResolveValues.readProperties(new File(DefaultBuildProperties))
|
||||
val props = Pre.readProperties(new File(DefaultBuildProperties))
|
||||
Option(props.getProperty(SbtVersionProperty))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,11 +78,14 @@ class ConfigurationParser
|
|||
val (logging, m5) = processSection(m4, "log", getLogging)
|
||||
val (properties, m6) = processSection(m5, "app-properties", getAppProperties)
|
||||
val ((ivyHome, checksums, isOverrideRepos, rConfigFile), m7) = processSection(m6, "ivy", getIvy)
|
||||
check(m7, "section")
|
||||
val (serverOptions, m8) = processSection(m7, "server", getServer)
|
||||
check(m8, "section")
|
||||
val classifiers = Classifiers(scalaClassifiers, appClassifiers)
|
||||
val repositories = rConfigFile map readRepositoriesConfig getOrElse defaultRepositories
|
||||
val ivyOptions = IvyOptions(ivyHome, classifiers, repositories, checksums, isOverrideRepos)
|
||||
new LaunchConfiguration(scalaVersion, ivyOptions, app, boot, logging, properties)
|
||||
|
||||
// TODO - Read server properties...
|
||||
new LaunchConfiguration(scalaVersion, ivyOptions, app, boot, logging, properties, serverOptions)
|
||||
}
|
||||
def getScala(m: LabelMap) =
|
||||
{
|
||||
|
|
@ -178,6 +181,16 @@ class ConfigurationParser
|
|||
val app = new Application(org, name, rev, main, components, LaunchCrossVersion(crossVersioned), classpathExtra)
|
||||
(app, classifiers)
|
||||
}
|
||||
def getServer(m: LabelMap): (Option[ServerConfiguration]) =
|
||||
{
|
||||
val (lock, m1) = optfile(m, "lock")
|
||||
// TODO - JVM args
|
||||
val (args, m2) = optfile(m1, "jvmargs")
|
||||
val (props, m3) = optfile(m2, "jvmprops")
|
||||
lock map { file =>
|
||||
ServerConfiguration(file, args, props)
|
||||
}
|
||||
}
|
||||
def getRepositories(m: LabelMap): List[Repository.Repository] =
|
||||
{
|
||||
import Repository.{Ivy, Maven, Predefined}
|
||||
|
|
|
|||
|
|
@ -33,17 +33,11 @@ object Initialize
|
|||
def fill(file: File, spec: List[AppProperty]): Unit = process(file, spec, selectFill)
|
||||
def process(file: File, appProperties: List[AppProperty], select: AppProperty => Option[PropertyInit])
|
||||
{
|
||||
val properties = new Properties
|
||||
if(file.exists)
|
||||
Using(new FileInputStream(file))( properties.load )
|
||||
val properties = readProperties(file)
|
||||
val uninitialized =
|
||||
for(property <- appProperties; init <- select(property) if properties.getProperty(property.name) == null) yield
|
||||
initialize(properties, property.name, init)
|
||||
if(!uninitialized.isEmpty)
|
||||
{
|
||||
file.getParentFile.mkdirs()
|
||||
Using(new FileOutputStream(file))( out => properties.store(out, "") )
|
||||
}
|
||||
if(!uninitialized.isEmpty) writeProperties(properties, file, "")
|
||||
}
|
||||
def initialize(properties: Properties, name: String, init: PropertyInit)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ package xsbt.boot
|
|||
import Pre._
|
||||
import scala.collection.immutable.List
|
||||
|
||||
class Enumeration
|
||||
class Enumeration extends Serializable
|
||||
{
|
||||
def elements: List[Value] = members
|
||||
private lazy val members: List[Value] =
|
||||
|
|
@ -25,6 +25,6 @@ class Enumeration
|
|||
}
|
||||
def value(s: String) = new Value(s, 0)
|
||||
def value(s: String, i: Int) = new Value(s, i)
|
||||
final class Value(override val toString: String, val id: Int)
|
||||
final class Value(override val toString: String, val id: Int) extends Serializable
|
||||
def toValue(s: String): Value = elements.find(_.toString == s).getOrElse(error("Expected one of " + elements.mkString(",") + " (got: " + s + ")"))
|
||||
}
|
||||
|
|
@ -6,20 +6,64 @@ package xsbt.boot
|
|||
import Pre._
|
||||
import BootConfiguration.{CompilerModuleName, JAnsiVersion, LibraryModuleName}
|
||||
import java.io.File
|
||||
import java.net.{URL, URLClassLoader}
|
||||
import java.net.{URL, URLClassLoader, URI}
|
||||
import java.util.concurrent.Callable
|
||||
import scala.collection.immutable.List
|
||||
import scala.annotation.tailrec
|
||||
import ConfigurationStorageState._
|
||||
|
||||
class LauncherArguments(val args: List[String], val isLocate: Boolean)
|
||||
|
||||
object Launch
|
||||
{
|
||||
def apply(arguments: List[String]): Option[Int] = apply( (new File("")).getAbsoluteFile , arguments )
|
||||
def apply(arguments: LauncherArguments): Option[Int] = apply( (new File("")).getAbsoluteFile , arguments )
|
||||
|
||||
def apply(currentDirectory: File, arguments: List[String]): Option[Int] = {
|
||||
val (configLocation, newArguments) = Configuration.find(arguments, currentDirectory)
|
||||
val config = parseAndInitializeConfig(configLocation, currentDirectory)
|
||||
launch(run(Launcher(config)))(makeRunConfig(currentDirectory, config, newArguments))
|
||||
def apply(currentDirectory: File, arguments: LauncherArguments): Option[Int] = {
|
||||
val (configLocation, newArgs2, state) = Configuration.find(arguments.args, currentDirectory)
|
||||
val config = state match {
|
||||
case SerializedFile => LaunchConfiguration.restore(configLocation)
|
||||
case PropertiesFile => parseAndInitializeConfig(configLocation, currentDirectory)
|
||||
}
|
||||
if(arguments.isLocate) {
|
||||
if(!newArgs2.isEmpty) {
|
||||
// TODO - Print the arguments without exploding proguard size.
|
||||
System.err.println("Warning: --locate option ignores arguments.")
|
||||
}
|
||||
locate(currentDirectory, config)
|
||||
} else {
|
||||
// First check to see if there are java system properties we need to set. Then launch the application.
|
||||
updateProperties(config)
|
||||
launch(run(Launcher(config)))(makeRunConfig(currentDirectory, config, newArgs2))
|
||||
}
|
||||
}
|
||||
/** Locate a server, print where it is, and exit. */
|
||||
def locate(currentDirectory: File, config: LaunchConfiguration): Option[Int] = {
|
||||
config.serverConfig match {
|
||||
case Some(_) =>
|
||||
val uri = ServerLocator.locate(currentDirectory, config)
|
||||
System.out.println(uri.toASCIIString)
|
||||
Some(0)
|
||||
case None => sys.error(s"${config.app.groupID}-${config.app.main} is not configured as a server.")
|
||||
}
|
||||
}
|
||||
/** Some hackery to allow sys.props to be configured via a file. If this launch config has
|
||||
* a valid file configured, we load the properties and and apply them to this jvm.
|
||||
*/
|
||||
def updateProperties(config: LaunchConfiguration): Unit = {
|
||||
config.serverConfig match {
|
||||
case Some(config) =>
|
||||
config.jvmPropsFile match {
|
||||
case Some(file) if file.exists =>
|
||||
try setSystemProperties(readProperties(file))
|
||||
catch {
|
||||
case e: Exception => throw new RuntimeException(s"Unable to load server properties file: ${file}", e)
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
case None =>
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses the configuration *and* runs the initialization code that will remove variable references. */
|
||||
def parseAndInitializeConfig(configLocation: URL, currentDirectory: File): LaunchConfiguration =
|
||||
{
|
||||
|
|
@ -84,6 +128,10 @@ object Launch
|
|||
Thread.currentThread.setContextClassLoader(loader)
|
||||
try { eval } finally { Thread.currentThread.setContextClassLoader(oldLoader) }
|
||||
}
|
||||
|
||||
// Cache of classes for lookup later.
|
||||
val ServerMainClass = classOf[xsbti.ServerMain]
|
||||
val AppMainClass = classOf[xsbti.AppMain]
|
||||
}
|
||||
final class RunConfiguration(val scalaVersion: Option[String], val app: xsbti.ApplicationID, val workingDirectory: File, val arguments: List[String])
|
||||
|
||||
|
|
@ -240,27 +288,32 @@ class Launch private[xsbt](val bootDirectory: File, val lockBoot: Boolean, val i
|
|||
(scalaHome, libDirectory)
|
||||
}
|
||||
|
||||
def appProvider(appID: xsbti.ApplicationID, app: RetrievedModule, scalaProvider0: xsbti.ScalaProvider, appHome: File): xsbti.AppProvider = new xsbti.AppProvider
|
||||
{
|
||||
def appProvider(appID: xsbti.ApplicationID, app: RetrievedModule, scalaProvider0: xsbti.ScalaProvider, appHome: File): xsbti.AppProvider =
|
||||
new xsbti.AppProvider {
|
||||
import Launch.{ServerMainClass,AppMainClass}
|
||||
val scalaProvider = scalaProvider0
|
||||
val id = appID
|
||||
def mainClasspath = app.fullClasspath
|
||||
lazy val loader = app.createLoader(scalaProvider.loader)
|
||||
// TODO - For some reason we can't call this from vanilla scala. We get a
|
||||
// no such method exception UNLESS we're in the same project.
|
||||
lazy val entryPoint: Class[T] forSome { type T } =
|
||||
{
|
||||
val c = Class.forName(id.mainClass, true, loader)
|
||||
if(classOf[xsbti.AppMain].isAssignableFrom(c)) c
|
||||
else if(PlainApplication.isPlainApplication(c)) c
|
||||
else sys.error(s"Class: ${c} is not an instance of xsbti.AppMain nor does it have one of these static methods:\n"+
|
||||
" * void main(String[] args)\n * int main(String[] args)\n * xsbti.Exit main(String[] args)")
|
||||
else if(ServerApplication.isServerApplication(c)) c
|
||||
else sys.error(s"${c} is not an instance of xsbti.AppMain, xsbti.ServerMain nor does it have one of these static methods:\n"+
|
||||
" * void main(String[] args)\n * int main(String[] args)\n * xsbti.Exit main(String[] args)\n")
|
||||
}
|
||||
// Deprecated API. Remove when we can.
|
||||
def mainClass: Class[T] forSome { type T <: xsbti.AppMain } = entryPoint.asSubclass(classOf[xsbti.AppMain])
|
||||
def mainClass: Class[T] forSome { type T <: xsbti.AppMain } = entryPoint.asSubclass(AppMainClass)
|
||||
def newMain(): xsbti.AppMain = {
|
||||
if(PlainApplication.isPlainApplication(entryPoint)) PlainApplication(entryPoint)
|
||||
else mainClass.newInstance
|
||||
if(ServerApplication.isServerApplication(entryPoint)) ServerApplication(this)
|
||||
else if(PlainApplication.isPlainApplication(entryPoint)) PlainApplication(entryPoint)
|
||||
else if(AppMainClass.isAssignableFrom(entryPoint)) mainClass.newInstance
|
||||
else throw new IncompatibleClassChangeError(s"Main class ${entryPoint.getName} is not an instance of xsbti.AppMain, xsbti.ServerMain nor does it have a valid `main` method.")
|
||||
}
|
||||
|
||||
lazy val components = componentProvider(appHome)
|
||||
}
|
||||
def componentProvider(appHome: File) = new ComponentProvider(appHome, lockBoot)
|
||||
|
|
|
|||
|
|
@ -9,27 +9,46 @@ import java.net.URL
|
|||
import scala.collection.immutable.List
|
||||
|
||||
//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])
|
||||
final case class LaunchConfiguration(scalaVersion: Value[String], ivyConfiguration: IvyOptions, app: Application, boot: BootSetup, logging: Logging, appProperties: List[AppProperty], serverConfig: Option[ServerConfiguration])
|
||||
{
|
||||
def isServer: Boolean = serverConfig.isDefined
|
||||
def getScalaVersion = {
|
||||
val sv = Value.get(scalaVersion)
|
||||
if(sv == "auto") None else Some(sv)
|
||||
}
|
||||
|
||||
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 Explicit(newAppVersion)), boot, logging, appProperties)
|
||||
def withScalaVersion(newScalaVersion: String) = LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration, app, boot, logging, appProperties, serverConfig)
|
||||
def withApp(app: Application) = LaunchConfiguration(scalaVersion, ivyConfiguration, app, boot, logging, appProperties, serverConfig)
|
||||
def withAppVersion(newAppVersion: String) = LaunchConfiguration(scalaVersion, ivyConfiguration, app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties, serverConfig)
|
||||
// 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)
|
||||
LaunchConfiguration(new Explicit(newScalaVersion), ivyConfiguration.copy(classifiers = classifiers0), app.withVersion(new Explicit(newAppVersion)), boot, logging, appProperties, serverConfig)
|
||||
|
||||
def map(f: File => File) = LaunchConfiguration(scalaVersion, ivyConfiguration.map(f), app.map(f), boot.map(f), logging, appProperties)
|
||||
def map(f: File => File) = LaunchConfiguration(scalaVersion, ivyConfiguration.map(f), app.map(f), boot.map(f), logging, appProperties, serverConfig.map(_ map f))
|
||||
}
|
||||
object LaunchConfiguration {
|
||||
// Saves a launch configuration into a file. This is only safe if it is loaded by the *same* launcher version.
|
||||
def save(config: LaunchConfiguration, f: File): Unit = {
|
||||
val out = new java.io.ObjectOutputStream(new java.io.FileOutputStream(f))
|
||||
try out.writeObject(config)
|
||||
finally out.close()
|
||||
}
|
||||
// Restores a launch configuration from a file. This is only safe if it is loaded by the *same* launcher version.
|
||||
def restore(url: URL): LaunchConfiguration = {
|
||||
val in = new java.io.ObjectInputStream(url.openConnection.getInputStream)
|
||||
try in.readObject.asInstanceOf[LaunchConfiguration]
|
||||
finally in.close()
|
||||
}
|
||||
}
|
||||
final case class ServerConfiguration(lockFile: File, jvmArgs: Option[File], jvmPropsFile: Option[File]) {
|
||||
def map(f: File => File) =
|
||||
ServerConfiguration(f(lockFile), jvmArgs map f, jvmPropsFile map f)
|
||||
}
|
||||
final case class IvyOptions(ivyHome: Option[File], classifiers: Classifiers, repositories: List[Repository.Repository], checksums: List[String], isOverrideRepositories: Boolean)
|
||||
{
|
||||
def map(f: File => File) = IvyOptions(ivyHome.map(f), classifiers, repositories, checksums, isOverrideRepositories)
|
||||
}
|
||||
sealed trait Value[T]
|
||||
sealed trait Value[T] extends Serializable
|
||||
final class Explicit[T](val value: T) extends Value[T] {
|
||||
override def toString = value.toString
|
||||
}
|
||||
|
|
@ -130,7 +149,7 @@ sealed trait PropertyInit
|
|||
final class SetProperty(val value: String) extends PropertyInit
|
||||
final class PromptProperty(val label: String, val default: Option[String]) extends PropertyInit
|
||||
|
||||
final class Logging(level: LogLevel.Value)
|
||||
final class Logging(level: LogLevel.Value) extends Serializable
|
||||
{
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ object Pre
|
|||
classes.toList.filter(classMissing)
|
||||
}
|
||||
def toURLs(files: Array[File]): Array[URL] = files.map(_.toURI.toURL)
|
||||
def toFile(url: URL): File =
|
||||
try { new File(url.toURI) }
|
||||
catch { case _: java.net.URISyntaxException => new File(url.getPath) }
|
||||
|
||||
|
||||
def delete(f: File)
|
||||
{
|
||||
|
|
@ -82,4 +86,25 @@ object Pre
|
|||
}
|
||||
final val isWindows: Boolean = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows")
|
||||
final val isCygwin: Boolean = isWindows && java.lang.Boolean.getBoolean("sbt.cygwin")
|
||||
|
||||
import java.util.Properties
|
||||
import java.io.{FileInputStream,FileOutputStream}
|
||||
private[boot] def readProperties(propertiesFile: File) =
|
||||
{
|
||||
val properties = new Properties
|
||||
if(propertiesFile.exists)
|
||||
Using( new FileInputStream(propertiesFile) )( properties.load )
|
||||
properties
|
||||
}
|
||||
private[boot] def writeProperties(properties: Properties, file: File, msg: String): Unit = {
|
||||
file.getParentFile.mkdirs()
|
||||
Using(new FileOutputStream(file))( out => properties.store(out, msg) )
|
||||
}
|
||||
private[boot] def setSystemProperties(properties: Properties): Unit = {
|
||||
val nameItr = properties.stringPropertyNames.iterator
|
||||
while(nameItr.hasNext) {
|
||||
val propName = nameItr.next
|
||||
System.setProperty(propName, properties.getProperty(propName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,16 +12,9 @@ object ResolveValues
|
|||
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[boot] def readProperties(propertiesFile: File) =
|
||||
{
|
||||
val properties = new Properties
|
||||
if(propertiesFile.exists)
|
||||
Using( new FileInputStream(propertiesFile) )( properties.load )
|
||||
properties
|
||||
}
|
||||
}
|
||||
|
||||
import ResolveValues.{readProperties, trim}
|
||||
import ResolveValues.{trim}
|
||||
final class ResolveValues(conf: LaunchConfiguration)
|
||||
{
|
||||
private def propertiesFile = conf.boot.properties
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
package xsbt
|
||||
package boot
|
||||
|
||||
import java.io.File
|
||||
import scala.util.control.NonFatal
|
||||
import java.net.URI
|
||||
import java.io.IOException
|
||||
import Pre._
|
||||
import scala.annotation.tailrec
|
||||
|
||||
/** A wrapper around 'raw' static methods to meet the sbt application interface. */
|
||||
class ServerApplication private (provider: xsbti.AppProvider) extends xsbti.AppMain {
|
||||
import ServerApplication._
|
||||
|
||||
override def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = {
|
||||
val serverMain = provider.entryPoint.asSubclass(ServerMainClass).newInstance
|
||||
val server = serverMain.start(configuration)
|
||||
System.out.println(s"${SERVER_SYNCH_TEXT}${server.uri}")
|
||||
server.awaitTermination()
|
||||
}
|
||||
}
|
||||
/** An object that lets us detect compatible "plain" applications and launch them reflectively. */
|
||||
object ServerApplication {
|
||||
val SERVER_SYNCH_TEXT = "[SERVER-URI]"
|
||||
val ServerMainClass = classOf[xsbti.ServerMain]
|
||||
// TODO - We should also adapt friendly static methods into servers, perhaps...
|
||||
// We could even structurally type things that have a uri + awaitTermination method...
|
||||
def isServerApplication(clazz: Class[_]): Boolean =
|
||||
ServerMainClass.isAssignableFrom(clazz)
|
||||
def apply(provider: xsbti.AppProvider): xsbti.AppMain =
|
||||
new ServerApplication(provider)
|
||||
|
||||
}
|
||||
object ServerLocator {
|
||||
// TODO - Probably want to drop this to reduce classfile size
|
||||
private def locked[U](file: File)(f: => U): U = {
|
||||
Locks(file, new java.util.concurrent.Callable[U] {
|
||||
def call(): U = f
|
||||
})
|
||||
}
|
||||
// We use the lock file they give us to write the server info. However,
|
||||
// it seems we cannot both use the server info file for locking *and*
|
||||
// read from it successfully. Locking seems to blank the file. SO, we create
|
||||
// another file near the info file to lock.a
|
||||
def makeLockFile(f: File): File =
|
||||
new File(f.getParentFile, s"${f.getName}.lock")
|
||||
// Launch the process and read the port...
|
||||
def locate(currentDirectory: File, config: LaunchConfiguration): URI =
|
||||
config.serverConfig match {
|
||||
case None => sys.error("No server lock file configured. Cannot locate server.")
|
||||
case Some(sc) => locked(makeLockFile(sc.lockFile)) {
|
||||
readProperties(sc.lockFile) match {
|
||||
case Some(uri) if isReachable(uri) => uri
|
||||
case _ =>
|
||||
val uri = ServerLauncher.startServer(currentDirectory, config)
|
||||
writeProperties(sc.lockFile, uri)
|
||||
uri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val SERVER_URI_PROPERTY = "server.uri"
|
||||
def readProperties(f: File): Option[java.net.URI] = {
|
||||
try {
|
||||
val props = Pre.readProperties(f)
|
||||
props.getProperty(SERVER_URI_PROPERTY) match {
|
||||
case null => None
|
||||
case uri => Some(new java.net.URI(uri))
|
||||
}
|
||||
} catch {
|
||||
case e: IOException => None
|
||||
}
|
||||
}
|
||||
def writeProperties(f: File, uri: URI): Unit = {
|
||||
val props = new java.util.Properties
|
||||
props.setProperty(SERVER_URI_PROPERTY, uri.toASCIIString)
|
||||
val output = new java.io.FileOutputStream(f)
|
||||
val df = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ")
|
||||
df.setTimeZone(java.util.TimeZone.getTimeZone("UTC"))
|
||||
Pre.writeProperties(props, f, s"Server Startup at ${df.format(new java.util.Date)}")
|
||||
}
|
||||
|
||||
def isReachable(uri: java.net.URI): Boolean =
|
||||
try {
|
||||
// TODO - For now we assume if we can connect, it means
|
||||
// that the server is working...
|
||||
val socket = new java.net.Socket(uri.getHost, uri.getPort)
|
||||
try socket.isConnected
|
||||
finally socket.close()
|
||||
} catch {
|
||||
case e: IOException => false
|
||||
}
|
||||
}
|
||||
/** A helper class that dumps incoming values into a print stream. */
|
||||
class StreamDumper(in: java.io.BufferedReader, out: java.io.PrintStream) extends Thread {
|
||||
// Don't block the application for this thread.
|
||||
setDaemon(true)
|
||||
private val running = new java.util.concurrent.atomic.AtomicBoolean(true)
|
||||
override def run(): Unit = {
|
||||
def read(): Unit = if(running.get) in.readLine match {
|
||||
case null => ()
|
||||
case line =>
|
||||
out.println(line)
|
||||
read()
|
||||
}
|
||||
read()
|
||||
out.close()
|
||||
}
|
||||
|
||||
def close(): Unit = running.set(false)
|
||||
}
|
||||
object ServerLauncher {
|
||||
import ServerApplication.SERVER_SYNCH_TEXT
|
||||
def startServer(currentDirectory: File, config: LaunchConfiguration): URI = {
|
||||
val serverConfig = config.serverConfig match {
|
||||
case Some(c) => c
|
||||
case None => throw new RuntimeException("Logic Failure: Attempting to start a server that isn't configured to be a server. Please report a bug.")
|
||||
}
|
||||
val launchConfig = java.io.File.createTempFile("sbtlaunch", "config")
|
||||
launchConfig.deleteOnExit()
|
||||
LaunchConfiguration.save(config, launchConfig)
|
||||
val jvmArgs: List[String] = serverConfig.jvmArgs map readLines match {
|
||||
case Some(args) => args
|
||||
case None => Nil
|
||||
}
|
||||
val cmd: List[String] =
|
||||
("java" :: jvmArgs) ++
|
||||
("-jar" :: defaultLauncherLookup.getCanonicalPath :: s"@load:${launchConfig.toURI.toURL.toString}" :: Nil)
|
||||
launchProcessAndGetUri(cmd, currentDirectory)
|
||||
}
|
||||
|
||||
// Here we try to isolate all the stupidity of dealing with Java processes.
|
||||
def launchProcessAndGetUri(cmd: List[String], cwd: File): URI = {
|
||||
// TODO - Handle windows path stupidity in arguments.
|
||||
val pb = new java.lang.ProcessBuilder()
|
||||
pb.command(cmd:_*)
|
||||
pb.directory(cwd)
|
||||
val process = pb.start()
|
||||
// First we need to grab all the input streams, and close the ones we don't care about.
|
||||
process.getOutputStream.close()
|
||||
val stderr = process.getErrorStream
|
||||
val stdout = process.getInputStream
|
||||
// Now we start dumping out errors.
|
||||
val errorDumper = new StreamDumper(new java.io.BufferedReader(new java.io.InputStreamReader(stderr)), System.err)
|
||||
errorDumper.start()
|
||||
// Now we look for the URI synch value, and then make sure we close the output files.
|
||||
try readUntilSynch(new java.io.BufferedReader(new java.io.InputStreamReader(stdout))) match {
|
||||
case Some(uri) => uri
|
||||
case _ => sys.error("Failed to start server!")
|
||||
} finally {
|
||||
errorDumper.close()
|
||||
stdout.close()
|
||||
stderr.close()
|
||||
}
|
||||
}
|
||||
|
||||
object ServerUriLine {
|
||||
def unapply(in: String): Option[URI] =
|
||||
if(in startsWith SERVER_SYNCH_TEXT) {
|
||||
Some(new URI(in.substring(SERVER_SYNCH_TEXT.size)))
|
||||
} else None
|
||||
}
|
||||
/** Reads an input steam until it hits the server synch text and server URI. */
|
||||
def readUntilSynch(in: java.io.BufferedReader): Option[URI] = {
|
||||
@tailrec
|
||||
def read(): Option[URI] = in.readLine match {
|
||||
case null => None
|
||||
case ServerUriLine(uri) => Some(uri)
|
||||
case line => read()
|
||||
}
|
||||
try read()
|
||||
finally in.close()
|
||||
}
|
||||
/** Reads all the lines in a file. If it doesn't exist, returns an empty list. Forces UTF-8 strings. */
|
||||
def readLines(f: File): List[String] =
|
||||
if(!f.exists) Nil else {
|
||||
val reader = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(f), "UTF-8"))
|
||||
@tailrec
|
||||
def read(current: List[String]): List[String] =
|
||||
reader.readLine match {
|
||||
case null => current.reverse
|
||||
case line => read(line :: current)
|
||||
}
|
||||
try read(Nil)
|
||||
finally reader.close()
|
||||
}
|
||||
|
||||
def defaultLauncherLookup: File =
|
||||
try {
|
||||
val classInLauncher = classOf[AppConfiguration]
|
||||
val fileOpt = for {
|
||||
domain <- Option(classInLauncher.getProtectionDomain)
|
||||
source <- Option(domain.getCodeSource)
|
||||
location = source.getLocation
|
||||
} yield toFile(location)
|
||||
fileOpt.getOrElse(throw new RuntimeException("Could not inspect protection domain or code source"))
|
||||
} catch {
|
||||
case e: Throwable => throw new RuntimeException("Unable to find sbt-launch.jar.", e)
|
||||
}
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@ final class Update(config: UpdateConfiguration)
|
|||
val optionProps =
|
||||
Option(System.getProperty("sbt.boot.credentials")) orElse
|
||||
Option(System.getenv("SBT_CREDENTIALS")) map ( path =>
|
||||
ResolveValues.readProperties(new File(path))
|
||||
Pre.readProperties(new File(path))
|
||||
)
|
||||
optionProps match {
|
||||
case Some(props) => extractCredentials("realm","host","user","password")(props)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package xsbt.boot
|
||||
|
||||
import java.io.{File,InputStream}
|
||||
import java.net.URL
|
||||
import java.util.Properties
|
||||
import xsbti._
|
||||
import org.specs2._
|
||||
import mutable.Specification
|
||||
import LaunchTest._
|
||||
import sbt.IO.{createDirectory, touch,withTemporaryDirectory}
|
||||
import java.net.URI
|
||||
|
||||
object ServerLocatorTest extends Specification
|
||||
{
|
||||
"ServerLocator" should {
|
||||
// TODO - Maybe use scalacheck to randomnly generate URIs
|
||||
"read and write server URI properties" in {
|
||||
withTemporaryDirectory { dir =>
|
||||
val propFile = new File(dir, "server.properties")
|
||||
val expected = new java.net.URI("http://localhost:8080")
|
||||
ServerLocator.writeProperties(propFile, expected)
|
||||
ServerLocator.readProperties(propFile) must equalTo(Some(expected))
|
||||
}
|
||||
}
|
||||
"detect listening ports" in {
|
||||
val serverSocket = new java.net.ServerSocket(0)
|
||||
object serverThread extends Thread {
|
||||
override def run(): Unit = {
|
||||
// Accept one connection.
|
||||
val result = serverSocket.accept()
|
||||
result.close()
|
||||
serverSocket.close()
|
||||
}
|
||||
}
|
||||
serverThread.start()
|
||||
val uri = new java.net.URI(s"http://${serverSocket.getInetAddress.getHostAddress}:${serverSocket.getLocalPort}")
|
||||
ServerLocator.isReachable(uri) must beTrue
|
||||
}
|
||||
}
|
||||
"ServerLauncher" should {
|
||||
"detect start URI from reader" in {
|
||||
val expected = new java.net.URI("http://localhost:8080")
|
||||
val input = s"""|Some random text
|
||||
|to start the server
|
||||
|${ServerApplication.SERVER_SYNCH_TEXT}${expected.toASCIIString}
|
||||
|Some more output.""".stripMargin
|
||||
val inputStream = new java.io.BufferedReader(new java.io.StringReader(input))
|
||||
val result = try ServerLauncher.readUntilSynch(inputStream)
|
||||
finally inputStream.close()
|
||||
result must equalTo(Some(expected))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/** These are packaged and published locally and the resulting artifact is used to test the launcher.*/
|
||||
package xsbt.boot.test
|
||||
|
||||
import java.net.Socket
|
||||
import java.net.SocketTimeoutException
|
||||
|
||||
class EchoServer extends xsbti.ServerMain
|
||||
{
|
||||
def start(configuration: xsbti.AppConfiguration): xsbti.Server =
|
||||
{
|
||||
object server extends xsbti.Server {
|
||||
// TODO - Start a server.
|
||||
val serverSocket = new java.net.ServerSocket(0)
|
||||
val port = serverSocket.getLocalPort
|
||||
val addr = serverSocket.getInetAddress.getHostAddress
|
||||
override val uri =new java.net.URI(s"http://${addr}:${port}")
|
||||
// Check for stop every second.
|
||||
serverSocket.setSoTimeout(1000)
|
||||
object serverThread extends Thread {
|
||||
private val running = new java.util.concurrent.atomic.AtomicBoolean(true)
|
||||
override def run(): Unit = {
|
||||
while(running.get) try {
|
||||
val clientSocket = serverSocket.accept()
|
||||
// Handle client connections
|
||||
object clientSocketThread extends Thread {
|
||||
override def run(): Unit = {
|
||||
echoTo(clientSocket)
|
||||
}
|
||||
}
|
||||
clientSocketThread.start()
|
||||
} catch {
|
||||
case e: SocketTimeoutException => // Ignore
|
||||
}
|
||||
}
|
||||
// Simple mechanism to dump input to output.
|
||||
private def echoTo(socket: Socket): Unit = {
|
||||
val input = new java.io.BufferedReader(new java.io.InputStreamReader(socket.getInputStream))
|
||||
val output = new java.io.BufferedWriter(new java.io.OutputStreamWriter(socket.getOutputStream))
|
||||
import scala.util.control.Breaks._
|
||||
try {
|
||||
// Lame way to break out.
|
||||
breakable {
|
||||
def read(): Unit = input.readLine match {
|
||||
case null => ()
|
||||
case "kill" =>
|
||||
running.set(false)
|
||||
serverSocket.close()
|
||||
break()
|
||||
case line =>
|
||||
output.write(line)
|
||||
output.flush()
|
||||
read()
|
||||
}
|
||||
read()
|
||||
}
|
||||
} finally {
|
||||
output.close()
|
||||
input.close()
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start the thread immediately
|
||||
serverThread.start()
|
||||
override def awaitTermination(): xsbti.MainResult = {
|
||||
serverThread.join()
|
||||
new Exit(0)
|
||||
}
|
||||
}
|
||||
server
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ Before reading anything in here, you will need the information in the
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
Launcher
|
||||
Scripts
|
||||
TaskInputs
|
||||
Understanding-incremental-recompilation
|
||||
|
|
|
|||
|
|
@ -1,387 +1,5 @@
|
|||
======================
|
||||
Launcher Specification
|
||||
======================
|
||||
============
|
||||
Sbt Launcher
|
||||
============
|
||||
|
||||
The sbt launcher component is a self-contained jar that boots a Scala
|
||||
application without Scala or the application already existing on the
|
||||
system. The only prerequisites are the launcher jar itself, an optional
|
||||
configuration file, and a java runtime version 1.6 or greater.
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
A user downloads the launcher jar and creates a script to run it. In
|
||||
this documentation, the script will be assumed to be called `launch`.
|
||||
For unix, the script would look like:
|
||||
`java -jar sbt-launcher.jar "$@"`
|
||||
|
||||
The user then downloads the configuration file for the application (call
|
||||
it `my.app.configuration`) and creates a script to launch it (call it
|
||||
`myapp`): `launch @my.app.configuration "$@"`
|
||||
|
||||
The user can then launch the application using `myapp arg1 arg2 ...`
|
||||
|
||||
Like the launcher used to distribute `sbt`, the downloaded launcher
|
||||
jar will retrieve Scala and the application according to the provided
|
||||
configuration file. The versions may be fixed or read from a different
|
||||
configuration file (the location of which is also configurable). The
|
||||
location to which the Scala and application jars are downloaded is
|
||||
configurable as well. The repositories searched are configurable.
|
||||
Optional initialization of a properties file on launch is configurable.
|
||||
|
||||
Once the launcher has downloaded the necessary jars, it loads the
|
||||
application and calls its entry point. The application is passed
|
||||
information about how it was called: command line arguments, current
|
||||
working directory, Scala version, and application ID (organization,
|
||||
name, version). In addition, the application can ask the launcher to
|
||||
perform operations such as obtaining the Scala jars and a
|
||||
`ClassLoader` for any version of Scala retrievable from the
|
||||
repositories specified in the configuration file. It can request that
|
||||
other applications be downloaded and run. When the application
|
||||
completes, it can tell the launcher to exit with a specific exit code or
|
||||
to reload the application with a different version of Scala, a different
|
||||
version of the application, or different arguments.
|
||||
|
||||
There are some other options for setup, such as putting the
|
||||
configuration file inside the launcher jar and distributing that as a
|
||||
single download. The rest of this documentation describes the details of
|
||||
configuring, writing, distributing, and running the application.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
The launcher may be configured in one of the following ways in
|
||||
increasing order of precedence:
|
||||
|
||||
- Replace the `/sbt/sbt.boot.properties` file in the jar
|
||||
- Put a configuration file named `sbt.boot.properties` on the
|
||||
classpath. Put it in the classpath root without the `/sbt` prefix.
|
||||
- Specify the location of an alternate configuration on the command
|
||||
line, either as a path or an absolute URI. This can be done by
|
||||
either specifying the location as the system property
|
||||
`sbt.boot.properties` or as the first argument to the launcher
|
||||
prefixed by `'@'`. The system property has lower precedence.
|
||||
Resolution of a relative path is first attempted against the current
|
||||
working directory, then against the user's home directory, and then
|
||||
against the directory containing the launcher jar. An error is
|
||||
generated if none of these attempts succeed.
|
||||
|
||||
Syntax
|
||||
~~~~~~
|
||||
|
||||
The configuration file is line-based, read as UTF-8 encoded, and defined
|
||||
by the following grammar. `'nl'` is a newline or end of file and
|
||||
`'text'` is plain text without newlines or the surrounding delimiters
|
||||
(such as parentheses or square brackets):
|
||||
|
||||
.. productionlist::
|
||||
configuration: `scala` `app` `repositories` `boot` `log` `appProperties`
|
||||
scala: "[" "scala" "]" `nl` `version` `nl` `classifiers` `nl`
|
||||
app: "[" "app" "]" `nl` `org` `nl` `name` `nl` `version` `nl` `components` `nl` `class` `nl` `crossVersioned` `nl` `resources` `nl` `classifiers` `nl`
|
||||
repositories: "[" "repositories" "]" `nl` (`repository` `nl`)*
|
||||
boot: "[" "boot" "]" `nl` `directory` `nl` `bootProperties` `nl` `search` `nl` `promptCreate` `nl` `promptFill` `nl` `quickOption` `nl`
|
||||
log: "["' "log" "]" `nl` `logLevel` `nl`
|
||||
appProperties: "[" "app-properties" "]" nl (property nl)*
|
||||
ivy: "[" "ivy" "]" `nl` `homeDirectory` `nl` `checksums` `nl` `overrideRepos` `nl` `repoConfig` `nl`
|
||||
directory: "directory" ":" `path`
|
||||
bootProperties: "properties" ":" `path`
|
||||
search: "search" ":" ("none" | "nearest" | "root-first" | "only" ) ("," `path`)*
|
||||
logLevel: "level" ":" ("debug" | "info" | "warn" | "error")
|
||||
promptCreate: "prompt-create" ":" `label`
|
||||
promptFill: "prompt-fill" ":" `boolean`
|
||||
quickOption: "quick-option" ":" `boolean`
|
||||
version: "version" ":" `versionSpecification`
|
||||
versionSpecification: `readProperty` | `fixedVersion`
|
||||
readProperty: "read" "(" `propertyName` ")" "[" `default` "]"
|
||||
fixedVersion: text
|
||||
classifiers: "classifiers" ":" text ("," text)*
|
||||
homeDirectory: "ivy-home" ":" `path`
|
||||
checksums: "checksums" ":" `checksum` ("," `checksum`)*
|
||||
overrideRepos: "override-build-repos" ":" `boolean`
|
||||
repoConfig: "repository-config" ":" `path`
|
||||
org: "org" ":" text
|
||||
name: "name" ":" text
|
||||
class: "class" ":" text
|
||||
components: "components" ":" `component` ("," `component`)*
|
||||
crossVersioned: "cross-versioned" ":" ("true" | "false" | "none" | "binary" | "full")
|
||||
resources: "resources" ":" `path` ("," `path`)*
|
||||
repository: ( `predefinedRepository` | `customRepository` ) `nl`
|
||||
predefinedRepository: "local" | "maven-local" | "maven-central"
|
||||
customRepository: `label` ":" `url` [ ["," `ivyPattern`] ["," `artifactPattern`] [", mavenCompatible"] [", bootOnly"]]
|
||||
property: `label` ":" `propertyDefinition` ("," `propertyDefinition`)*
|
||||
propertyDefinition: `mode` "=" (`set` | `prompt`)
|
||||
mode: "quick" | "new" | "fill"
|
||||
set: "set" "(" value ")"
|
||||
prompt: "prompt" "(" `label` ")" ("[" `default` "]")?
|
||||
boolean: "true" | "false"
|
||||
nl: "\r\n" | "\n" | "\r"
|
||||
path: text
|
||||
propertyName: text
|
||||
label: text
|
||||
default: text
|
||||
checksum: text
|
||||
ivyPattern: text
|
||||
artifactPattern: text
|
||||
url: text
|
||||
component: text
|
||||
|
||||
In addition to the grammar specified here, property values may include
|
||||
variable substitutions. A variable substitution has one of these forms:
|
||||
|
||||
- `${variable.name}`
|
||||
- `${variable.name-default}`
|
||||
|
||||
where `variable.name` is the name of a system property. If a system
|
||||
property by that name exists, the value is substituted. If it does not
|
||||
exists and a default is specified, the default is substituted after
|
||||
recursively substituting variables in it. If the system property does
|
||||
not exist and no default is specified, the original string is not
|
||||
substituted.
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
The default configuration file for sbt looks like:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
[scala]
|
||||
version: ${sbt.scala.version-auto}
|
||||
|
||||
[app]
|
||||
org: ${sbt.organization-org.scala-sbt}
|
||||
name: sbt
|
||||
version: ${sbt.version-read(sbt.version)[\ |release|\ ]}
|
||||
class: ${sbt.main.class-sbt.xMain}
|
||||
components: xsbti,extra
|
||||
cross-versioned: ${sbt.cross.versioned-false}
|
||||
|
||||
[repositories]
|
||||
local
|
||||
typesafe-ivy-releases: http://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly
|
||||
maven-central
|
||||
sonatype-snapshots: https://oss.sonatype.org/content/repositories/snapshots
|
||||
|
||||
[boot]
|
||||
directory: ${sbt.boot.directory-${sbt.global.base-${user.home}/.sbt}/boot/}
|
||||
|
||||
[ivy]
|
||||
ivy-home: ${sbt.ivy.home-${user.home}/.ivy2/}
|
||||
checksums: ${sbt.checksums-sha1,md5}
|
||||
override-build-repos: ${sbt.override.build.repos-false}
|
||||
repository-config: ${sbt.repository.config-${sbt.global.base-${user.home}/.sbt}/repositories}
|
||||
|
||||
Semantics
|
||||
~~~~~~~~~
|
||||
|
||||
The `scala.version` property specifies the version of Scala used to
|
||||
run the application. If the application is not cross-built, this may be
|
||||
set to `auto` and it will be auto-detected from the application's
|
||||
dependencies. If specified, the `scala.classifiers` property defines
|
||||
classifiers, such as 'sources', of extra Scala artifacts to retrieve.
|
||||
|
||||
The `app.org`, `app.name`, and `app.version` properties specify
|
||||
the organization, module ID, and version of the application,
|
||||
respectively. These are used to resolve and retrieve the application
|
||||
from the repositories listed in `[repositories]`. If
|
||||
`app.cross-versioned` is `binary`, the resolved module ID is
|
||||
`{app.name+'_'+CrossVersion.binaryScalaVersion(scala.version)}`.
|
||||
If `app.cross-versioned` is `true` or `full`, the resolved module ID is
|
||||
`{app.name+'_'+scala.version}`. The `scala.version` property must be
|
||||
specified and cannot be `auto` when cross-versioned. The paths given
|
||||
in `app.resources` are added to the application's classpath. If the
|
||||
path is relative, it is resolved against the application's working
|
||||
directory. If specified, the `app.classifiers` property defines
|
||||
classifiers, like 'sources', of extra artifacts to retrieve for the
|
||||
application.
|
||||
|
||||
Jars are retrieved to the directory given by `boot.directory`. By
|
||||
default, this is an absolute path that is shared by all launched
|
||||
instances on the machine. If multiple versions access it simultaneously.
|
||||
, you might see messages like:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
Waiting for lock on <lock-file> to be available...
|
||||
|
||||
This boot directory may be relative to the current directory instead. In
|
||||
this case, the launched application will have a separate boot directory
|
||||
for each directory it is launched in.
|
||||
|
||||
The `boot.properties` property specifies the location of the
|
||||
properties file to use if `app.version` or `scala.version` is
|
||||
specified as `read`. The `prompt-create`, `prompt-fill`, and
|
||||
`quick-option` properties together with the property definitions in
|
||||
`[app.properties]` can be used to initialize the `boot.properties`
|
||||
file.
|
||||
|
||||
The app.class property specifies the name of the entry point to the
|
||||
application. An application entry point must be a public class with a
|
||||
no-argument constructor that implements `xsbti.AppMain`. The
|
||||
`AppMain` interface specifies the entry method signature 'run'. The
|
||||
run method is passed an instance of AppConfiguration, which provides
|
||||
access to the startup environment. `AppConfiguration` also provides an
|
||||
interface to retrieve other versions of Scala or other applications.
|
||||
Finally, the return type of the run method is `xsbti.MainResult`,
|
||||
which has two subtypes: `xsbti.Reboot` and `xsbti.Exit`. To exit
|
||||
with a specific code, return an instance of `xsbti.Exit` with the
|
||||
requested code. To restart the application, return an instance of
|
||||
Reboot. You can change some aspects of the configuration with a reboot,
|
||||
such as the version of Scala, the application ID, and the arguments.
|
||||
|
||||
The `ivy.cache-directory` property provides an alternative location
|
||||
for the Ivy cache used by the launcher. This does not automatically set
|
||||
the Ivy cache for the application, but the application is provided this
|
||||
location through the AppConfiguration instance. The `checksums`
|
||||
property selects the checksum algorithms (sha1 or md5) that are used to
|
||||
verify artifacts downloaded by the launcher. `override-build-repos` is
|
||||
a flag that can inform the application that the repositories configured
|
||||
for the launcher should be used in the application. If
|
||||
`repository-config` is defined, the file it specifies should contain a
|
||||
`[repositories]` section that is used in place of the section in the
|
||||
original configuration file.
|
||||
|
||||
Execution
|
||||
---------
|
||||
|
||||
On startup, the launcher searches for its configuration in the order
|
||||
described in the Configuration section and then parses it. If either the
|
||||
Scala version or the application version are specified as 'read', the
|
||||
launcher determines them in the following manner. The file given by the
|
||||
'boot.properties' property is read as a Java properties file to obtain
|
||||
the version. The expected property names are `${app.name}.version` for
|
||||
the application version (where `${app.name}` is replaced with the
|
||||
value of the `app.name` property from the boot configuration file) and
|
||||
`scala.version` for the Scala version. If the properties file does not
|
||||
exist, the default value provided is used. If no default was provided,
|
||||
an error is generated.
|
||||
|
||||
Once the final configuration is resolved, the launcher proceeds to
|
||||
obtain the necessary jars to launch the application. The
|
||||
`boot.directory` property is used as a base directory to retrieve jars
|
||||
to. Locking is done on the directory, so it can be shared system-wide.
|
||||
The launcher retrieves the requested version of Scala to
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
${boot.directory}/${scala.version}/lib/
|
||||
|
||||
If this directory already exists, the launcher takes a shortcut for
|
||||
startup performance and assumes that the jars have already been
|
||||
downloaded. If the directory does not exist, the launcher uses Apache
|
||||
Ivy to resolve and retrieve the jars. A similar process occurs for the
|
||||
application itself. It and its dependencies are retrieved to
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
${boot.directory}/${scala.version}/${app.org}/${app.name}/.
|
||||
|
||||
Once all required code is downloaded, the class loaders are set up. The
|
||||
launcher creates a class loader for the requested version of Scala. It
|
||||
then creates a child class loader containing the jars for the requested
|
||||
'app.components' and with the paths specified in `app.resources`. An
|
||||
application that does not use components will have all of its jars in
|
||||
this class loader.
|
||||
|
||||
The main class for the application is then instantiated. It must be a
|
||||
public class with a public no-argument constructor and must conform to
|
||||
xsbti.AppMain. The `run` method is invoked and execution passes to the
|
||||
application. The argument to the 'run' method provides configuration
|
||||
information and a callback to obtain a class loader for any version of
|
||||
Scala that can be obtained from a repository in [repositories]. The
|
||||
return value of the run method determines what is done after the
|
||||
application executes. It can specify that the launcher should restart
|
||||
the application or that it should exit with the provided exit code.
|
||||
|
||||
Creating a Launched Application
|
||||
-------------------------------
|
||||
|
||||
This section shows how to make an application that is launched by this
|
||||
launcher. First, declare a dependency on the launcher-interface. Do not
|
||||
declare a dependency on the launcher itself. The launcher interface
|
||||
consists strictly of Java interfaces in order to avoid binary
|
||||
incompatibility between the version of Scala used to compile the
|
||||
launcher and the version used to compile your application. The launcher
|
||||
interface class will be provided by the launcher, so it is only a
|
||||
compile-time dependency. If you are building with sbt, your dependency
|
||||
definition would be:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
libraryDependencies += "org.scala-sbt" % "launcher-interface" % "|release|" % "provided"
|
||||
|
||||
resolvers += sbtResolver.value
|
||||
|
||||
Make the entry point to your class implement 'xsbti.AppMain'. An example
|
||||
that uses some of the information:
|
||||
|
||||
.. code-block:: scala
|
||||
|
||||
package xsbt.test
|
||||
class Main extends xsbti.AppMain
|
||||
{
|
||||
def run(configuration: xsbti.AppConfiguration) =
|
||||
{
|
||||
// get the version of Scala used to launch the application
|
||||
val scalaVersion = configuration.provider.scalaProvider.version
|
||||
|
||||
// Print a message and the arguments to the application
|
||||
println("Hello world! Running Scala " + scalaVersion)
|
||||
configuration.arguments.foreach(println)
|
||||
|
||||
// demonstrate the ability to reboot the application into different versions of Scala
|
||||
// and how to return the code to exit with
|
||||
scalaVersion match
|
||||
{
|
||||
case "2.9.3" =>
|
||||
new xsbti.Reboot {
|
||||
def arguments = configuration.arguments
|
||||
def baseDirectory = configuration.baseDirectory
|
||||
def scalaVersion = "2.10.2
|
||||
def app = configuration.provider.id
|
||||
}
|
||||
case "2.10.2" => new Exit(1)
|
||||
case _ => new Exit(0)
|
||||
}
|
||||
}
|
||||
class Exit(val code: Int) extends xsbti.Exit
|
||||
}
|
||||
|
||||
Next, define a configuration file for the launcher. For the above class,
|
||||
it might look like:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
[scala]
|
||||
version: |scalaRelease|
|
||||
[app]
|
||||
org: org.scala-sbt
|
||||
name: xsbt-test
|
||||
version: |release|
|
||||
class: xsbt.test.Main
|
||||
cross-versioned: binary
|
||||
[repositories]
|
||||
local
|
||||
maven-central
|
||||
[boot]
|
||||
directory: ${user.home}/.myapp/boot
|
||||
|
||||
Then, `publishLocal` or `+publishLocal` the application to make it
|
||||
available.
|
||||
|
||||
Running an Application
|
||||
----------------------
|
||||
|
||||
As mentioned above, there are a few options to actually run the
|
||||
application. The first involves providing a modified jar for download.
|
||||
The second two require providing a configuration file for download.
|
||||
|
||||
- Replace the /sbt/sbt.boot.properties file in the launcher jar and
|
||||
distribute the modified jar. The user would need a script to run
|
||||
`java -jar your-launcher.jar arg1 arg2 ...`.
|
||||
- The user downloads the launcher jar and you provide the configuration
|
||||
file.
|
||||
|
||||
- The user needs to run `java -Dsbt.boot.properties=your.boot.properties -jar launcher.jar`.
|
||||
- The user already has a script to run the launcher (call it
|
||||
'launch'). The user needs to run `launch @your.boot.properties your-arg-1 your-arg-2`
|
||||
This docuemntation has been moved to :doc:`The Launcher section </Launcher/index>`.
|
||||
|
|
|
|||
|
|
@ -19,3 +19,4 @@ Other resources include the :doc:`Examples </Examples/index>` and
|
|||
Tasks-and-Commands
|
||||
Plugins-and-Best-Practices
|
||||
Advanced-Index
|
||||
/Launcher/index
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
=========================
|
||||
Sbt Launcher Architecture
|
||||
=========================
|
||||
|
||||
The sbt launcher is a mechanism whereby modules can be loaded from ivy and
|
||||
executed within a jvm. It abstracts the mechanism of grabbing and caching jars,
|
||||
allowing users to focus on what application they want and control its versions.
|
||||
|
||||
The launcher's primary goal is to take configuration for applications, mostly
|
||||
just ivy coordinates and a main class, and start the application. The
|
||||
launcher resolves the ivy module, caches the required runtime jars and
|
||||
starts the application.
|
||||
|
||||
The sbt launcher provides the application with the means to load a different
|
||||
application when it completes, exit normally, or load additional applications
|
||||
from inside another.
|
||||
|
||||
The sbt launcher provides these core functions:
|
||||
|
||||
* Module Resolution
|
||||
* Classloader Caching and Isolation
|
||||
* File Locking
|
||||
* Service Discovery and Isolation
|
||||
|
||||
Module Resolution
|
||||
~~~~~~~~~~~~~~~~~
|
||||
The primary purpose of the sbt launcher is to resolve applications and run them.
|
||||
This is done through the `[app]` configuration section. See :doc:Configuration
|
||||
for more information on how to configure module resolution.
|
||||
|
||||
Module resolution is performed using the Ivy dependency managemnet library. This
|
||||
library supports loading artifacts from Maven repositories as well.
|
||||
|
||||
Classloader Caching and Isolation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The sbt launcher's classloading structure is different than just starting an
|
||||
application in the standard java mechanism. Every application loaded by
|
||||
by the launcher is given its own classloader. This classloader is a child
|
||||
of the Scala classloader used by the application. The Scala classloader can see
|
||||
all of the `xsbti.*` classes from the launcher itself.
|
||||
|
||||
Here's an example classloader layout from an sbt launched application.
|
||||
|
||||
.. image:: classloaders.png
|
||||
|
||||
In this diagram, three different applications were loaded. Two of these use the
|
||||
same version of Scala (2.9.2). In this case, sbt can share the same classloader
|
||||
for these applications. This has the benefit that any JIT optimisations performed
|
||||
on scala classes can be re-used between applications thanks to the shared
|
||||
classloader.
|
||||
|
||||
|
||||
Caching
|
||||
~~~~~~~
|
||||
The sbt launcher creates a secondary cache on top of Ivy's own cache. This helps
|
||||
isolate applications from errors resulting from unstable revisions, like
|
||||
`-SNAPSHOT`. For any launched application, the launcher creates a directory
|
||||
to store all its jars. Here's an example layout.
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
${boot.directory}/
|
||||
scala_2.9.2/
|
||||
lib/
|
||||
<scala library jars>
|
||||
<org>/<name>/<version>/
|
||||
<application-1 jars>
|
||||
<org>/<name>/<version>/
|
||||
<application-2 jars>
|
||||
scala_2.10.3/
|
||||
lib/
|
||||
<scala library jars>
|
||||
<org>/<name>/<version>/
|
||||
<application-3 jars>/
|
||||
|
||||
Locking
|
||||
~~~~~~~
|
||||
In addition to providing a secondary cache, the launcher also provides a mechanism
|
||||
of safely doing file-based locks. This is used in two places directly by the
|
||||
launcher:
|
||||
|
||||
1. Locking the boot directory.
|
||||
2. Ensuring located servers have at most one active process.
|
||||
|
||||
This feature requires a filesystem which supports locking. It is exposed via the
|
||||
`xsbti.GlobalLock` interface.
|
||||
|
||||
*Note: This is both a thread and file lock. Not only are we limiting access to a single process, but also a single thread within that process.*
|
||||
|
||||
Service Discovery and Isolation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The launcher also provides a mechanism to ensure that only one instance of a
|
||||
server is running, while dynamically starting it when a client requests. This
|
||||
is done through the `--locate` flag on the launcher. When the launcher is
|
||||
started with the `--locate` flag it will do the following:
|
||||
|
||||
1. Lock on the configured server lock file.
|
||||
2. Read the server properties to find the URI of the previous server.
|
||||
3. If the port is still listening to connection requests, print this URI
|
||||
on the command line.
|
||||
4. If the port is not listening, start a new server and write the URI
|
||||
on the command line.
|
||||
5. Release all locks and shutdown.
|
||||
|
||||
The configured `server.lock` file is thus used to prevent multiple servers from
|
||||
running. Sbt itself uses this to prevent more than one server running on any
|
||||
given project directory by configuring `server.lock` to be
|
||||
`${user.dir}/.sbtserver`.
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
==========================
|
||||
Sbt Launcher Configuration
|
||||
==========================
|
||||
|
||||
The launcher may be configured in one of the following ways in
|
||||
increasing order of precedence:
|
||||
|
||||
- Replace the `/sbt/sbt.boot.properties` file in the launcher jar
|
||||
- Put a configuration file named `sbt.boot.properties` on the
|
||||
classpath. Put it in the classpath root without the `/sbt` prefix.
|
||||
- Specify the location of an alternate configuration on the command
|
||||
line, either as a path or an absolute URI. This can be done by
|
||||
either specifying the location as the system property
|
||||
`sbt.boot.properties` or as the first argument to the launcher
|
||||
prefixed by `'@'`. The system property has lower precedence.
|
||||
Resolution of a relative path is first attempted against the current
|
||||
working directory, then against the user's home directory, and then
|
||||
against the directory containing the launcher jar.
|
||||
|
||||
An error is generated if none of these attempts succeed.
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
The default configuration file for sbt as an application looks like:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
[scala]
|
||||
version: ${sbt.scala.version-auto}
|
||||
|
||||
[app]
|
||||
org: ${sbt.organization-org.scala-sbt}
|
||||
name: sbt
|
||||
version: ${sbt.version-read(sbt.version)[\ |release|\ ]}
|
||||
class: ${sbt.main.class-sbt.xMain}
|
||||
components: xsbti,extra
|
||||
cross-versioned: ${sbt.cross.versioned-false}
|
||||
|
||||
[repositories]
|
||||
local
|
||||
typesafe-ivy-releases: http://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly
|
||||
maven-central
|
||||
sonatype-snapshots: https://oss.sonatype.org/content/repositories/snapshots
|
||||
|
||||
[boot]
|
||||
directory: ${sbt.boot.directory-${sbt.global.base-${user.home}/.sbt}/boot/}
|
||||
|
||||
[ivy]
|
||||
ivy-home: ${sbt.ivy.home-${user.home}/.ivy2/}
|
||||
checksums: ${sbt.checksums-sha1,md5}
|
||||
override-build-repos: ${sbt.override.build.repos-false}
|
||||
repository-config: ${sbt.repository.config-${sbt.global.base-${user.home}/.sbt}/repositories}
|
||||
|
||||
Let's look at all the launcher configuration sections in detail:
|
||||
|
||||
1. Scala Configuration
|
||||
----------------------
|
||||
The `[scala]` section is used to configure the version of Scala.
|
||||
It has one property:
|
||||
|
||||
* `version` - The version of scala an application uses, or `auto` if the
|
||||
application is not cross-versioned.
|
||||
* `classifiers` - The (optional) list of additional scala artifacts to resolve,
|
||||
e.g. `sources`.
|
||||
|
||||
|
||||
2. Applicaiton Identification
|
||||
-----------------------------
|
||||
The `[app]` section configures how the launcher will look for your application
|
||||
using the Ivy dependency manager. It consists of the following properties:
|
||||
|
||||
* `org` - The organization associated with the Ivy module.
|
||||
(`groupId` in maven vernacular)
|
||||
* `name` - The name of the Ivy module. (`artifactId` in maven vernacular)
|
||||
* `version` - The revision of the Ivy module.
|
||||
* `class` - The name of the "entry point" into the application. An entry
|
||||
point must be a class which meets one of the following critera
|
||||
- Extends the `xsbti.AppMain` interface.
|
||||
- Extends the `xsbti.ServerMain` interfaces.
|
||||
- Contains a method with the signature `static void main(String[])`
|
||||
- Contains a method with the signature `static int main(String[])`
|
||||
- Contains a method with the signature `static xsbti.Exit main(String[])`
|
||||
* `components` - An optional list of additional components that Ivy should
|
||||
resolve.
|
||||
* `cross-versioned` - An optional string denoting how this application is
|
||||
published.
|
||||
If `app.cross-versioned` is `binary`, the resolved module ID is
|
||||
`{app.name+'_'+CrossVersion.binaryScalaVersion(scala.version)}`.
|
||||
If `app.cross-versioned` is `true` or `full`, the resolved module ID is
|
||||
`{app.name+'_'+scala.version}`. The `scala.version` property must be
|
||||
specified and cannot be `auto` when cross-versioned.
|
||||
* `resources` - An optional list of jar files that should be added to
|
||||
the application's classpath.
|
||||
* `classifiers` - An optional list of additional classifiers that should be
|
||||
resolved with this application, e.g. `sources`.
|
||||
|
||||
3. Repositories Section
|
||||
-----------------------
|
||||
The `[repositories]` section configures where and how Ivy will look for
|
||||
your application. Each line denotes a repository where Ivy will look.
|
||||
|
||||
*Note: This section configured the default location where Ivy will look, but
|
||||
this can be overriden via user configuration.*
|
||||
|
||||
There are several built-in strings that can be used for common repositories:
|
||||
|
||||
* `local` - the local ivy repository `~/.ivy2/local`.
|
||||
* `maven-local` - The local maven repository `~/.ivy2/local`.
|
||||
* `maven-central` - The maven central repository `repo.maven.org`.
|
||||
|
||||
Besides built in repositories, other repositories can be configured using
|
||||
the following syntax:
|
||||
|
||||
.. parsed-literal::
|
||||
name: url(, pattern)(,descriptorOptional)(,skipConsistencyCheck)
|
||||
|
||||
The `name` property is an identifier which Ivy uses to cache modules
|
||||
resolved from this location. The `name` should be unique across all
|
||||
repositories.
|
||||
|
||||
The `url` property is the base `url` where Ivy should look for modules.
|
||||
|
||||
The `pattern` property is an optional specification of *how* Ivy should
|
||||
look for modules. By default, the launcher assumes repositories are in
|
||||
the maven style format.
|
||||
|
||||
The `skipConsistencyCheck` string is used to tell ivy not to validate checksums
|
||||
and signatures of files it resolves.
|
||||
|
||||
4. The Boot section
|
||||
-------------------
|
||||
The `[boot]` section is used to configure where the sbt launcher will store
|
||||
its cache and configuration information. It consists of the following properties:
|
||||
|
||||
* `directory` - The directory defined here is used to store all cached JARs
|
||||
resolved launcher.
|
||||
* `properties` - (optional) A properties file to use for any `read` variables.
|
||||
|
||||
5. The Ivy section
|
||||
------------------
|
||||
The `[ivy]` section is used to configure the Ivy dependency manager for
|
||||
resolving applications. It consists of the following properties:
|
||||
|
||||
* `ivy-home` - The home directory for Ivy. This determines where the
|
||||
`ivy-local` repository is located, and also where the ivy cache is
|
||||
stored. Defaults to `~/.ivy2`
|
||||
* `ivy.cache-directory` - provides an alternative location for the Ivy
|
||||
cache used by the launcher. This does not automatically set the Ivy
|
||||
cache for the application, but the application is provided this location
|
||||
through the AppConfiguration instance.
|
||||
* `checksums` - The comma-separated list of checksums that Ivy should use
|
||||
to verify artifacts have correctly resolved, e.g. `md5` or `sha1`.
|
||||
* `override-build-repos` - If this is set, then the `isOverrideRepositories`
|
||||
method on `xsbti.Launcher` interface will return its value. The use of this
|
||||
method is application specific, but in the case of sbt denotes that the
|
||||
configuration of repositories in the launcher should override those used
|
||||
by any build. Applications should respect this convention if they can.
|
||||
* `repository-config` - This specifies a configuration location where
|
||||
ivy repositories can also be configured. If this file exists, then its contents
|
||||
override the `[repositories]` section.
|
||||
|
||||
|
||||
6. The Server Section
|
||||
---------------------
|
||||
When using the `--locate` feature of the launcher, this section configures
|
||||
how a server is started. It consists of the following properties:
|
||||
|
||||
* `lock` - The file that controls access to the running server. This file
|
||||
will contain the active port used by a server and must be located on a
|
||||
a filesystem that supports locking.
|
||||
* `jvmargs` - A file that contains line-separated JVM arguments that where
|
||||
use when starting the server.
|
||||
* `jvmprops` - The location of a properties file that will define override
|
||||
properties in the server. All properties defined in this file will
|
||||
be set as `-D` java properties.
|
||||
|
||||
Variable Substitution
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
Property values may include variable substitutions. A variable substitution has
|
||||
one of these forms:
|
||||
|
||||
- `${variable.name}`
|
||||
- `${variable.name-default}`
|
||||
|
||||
where `variable.name` is the name of a system property. If a system
|
||||
property by that name exists, the value is substituted. If it does not
|
||||
exists and a default is specified, the default is substituted after
|
||||
recursively substituting variables in it. If the system property does
|
||||
not exist and no default is specified, the original string is not
|
||||
substituted.
|
||||
|
||||
There is also a special variable substitution:
|
||||
|
||||
- `read(property.name)[default]`
|
||||
|
||||
This will look in the file configured by `boot.properties` for a value. If
|
||||
there is no `boot.properties` file configured, or the property does not existt,
|
||||
then the default value is chosen.
|
||||
|
||||
|
||||
|
||||
Syntax
|
||||
~~~~~~
|
||||
|
||||
The configuration file is line-based, read as UTF-8 encoded, and defined
|
||||
by the following grammar. `'nl'` is a newline or end of file and
|
||||
`'text'` is plain text without newlines or the surrounding delimiters
|
||||
(such as parentheses or square brackets):
|
||||
|
||||
.. productionlist::
|
||||
configuration: `scala` `app` `repositories` `boot` `log` `appProperties`
|
||||
scala: "[" "scala" "]" `nl` `version` `nl` `classifiers` `nl`
|
||||
app: "[" "app" "]" `nl` `org` `nl` `name` `nl` `version` `nl` `components` `nl` `class` `nl` `crossVersioned` `nl` `resources` `nl` `classifiers` `nl`
|
||||
repositories: "[" "repositories" "]" `nl` (`repository` `nl`)*
|
||||
boot: "[" "boot" "]" `nl` `directory` `nl` `bootProperties` `nl` `search` `nl` `promptCreate` `nl` `promptFill` `nl` `quickOption` `nl`
|
||||
log: "["' "log" "]" `nl` `logLevel` `nl`
|
||||
appProperties: "[" "app-properties" "]" nl (property nl)*
|
||||
ivy: "[" "ivy" "]" `nl` `homeDirectory` `nl` `checksums` `nl` `overrideRepos` `nl` `repoConfig` `nl`
|
||||
directory: "directory" ":" `path`
|
||||
bootProperties: "properties" ":" `path`
|
||||
search: "search" ":" ("none" | "nearest" | "root-first" | "only" ) ("," `path`)*
|
||||
logLevel: "level" ":" ("debug" | "info" | "warn" | "error")
|
||||
promptCreate: "prompt-create" ":" `label`
|
||||
promptFill: "prompt-fill" ":" `boolean`
|
||||
quickOption: "quick-option" ":" `boolean`
|
||||
version: "version" ":" `versionSpecification`
|
||||
versionSpecification: `readProperty` | `fixedVersion`
|
||||
readProperty: "read" "(" `propertyName` ")" "[" `default` "]"
|
||||
fixedVersion: text
|
||||
classifiers: "classifiers" ":" text ("," text)*
|
||||
homeDirectory: "ivy-home" ":" `path`
|
||||
checksums: "checksums" ":" `checksum` ("," `checksum`)*
|
||||
overrideRepos: "override-build-repos" ":" `boolean`
|
||||
repoConfig: "repository-config" ":" `path`
|
||||
org: "org" ":" text
|
||||
name: "name" ":" text
|
||||
class: "class" ":" text
|
||||
components: "components" ":" `component` ("," `component`)*
|
||||
crossVersioned: "cross-versioned" ":" ("true" | "false" | "none" | "binary" | "full")
|
||||
resources: "resources" ":" `path` ("," `path`)*
|
||||
repository: ( `predefinedRepository` | `customRepository` ) `nl`
|
||||
predefinedRepository: "local" | "maven-local" | "maven-central"
|
||||
customRepository: `label` ":" `url` [ ["," `ivyPattern`] ["," `artifactPattern`] [", mavenCompatible"] [", bootOnly"]]
|
||||
property: `label` ":" `propertyDefinition` ("," `propertyDefinition`)*
|
||||
propertyDefinition: `mode` "=" (`set` | `prompt`)
|
||||
mode: "quick" | "new" | "fill"
|
||||
set: "set" "(" value ")"
|
||||
prompt: "prompt" "(" `label` ")" ("[" `default` "]")?
|
||||
boolean: "true" | "false"
|
||||
nl: "\r\n" | "\n" | "\r"
|
||||
path: text
|
||||
propertyName: text
|
||||
label: text
|
||||
default: text
|
||||
checksum: text
|
||||
ivyPattern: text
|
||||
artifactPattern: text
|
||||
url: text
|
||||
component: text
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
=====================================
|
||||
Getting Started with the Sbt Launcher
|
||||
=====================================
|
||||
|
||||
The sbt launcher component is a self-contained jar that boots a Scala
|
||||
application or server without Scala or the application already existing
|
||||
on the system. The only prerequisites are the launcher jar itself, an
|
||||
optional configuration file, and a java runtime version 1.6 or greater.
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
A user downloads the launcher jar and creates a script to run it. In
|
||||
this documentation, the script will be assumed to be called `launch`.
|
||||
For unix, the script would look like:
|
||||
`java -jar sbt-launcher.jar "$@"`
|
||||
|
||||
The user can now launch servers and applications which provide sbt
|
||||
launcher configuration.
|
||||
|
||||
Applications
|
||||
------------
|
||||
|
||||
To launch an application, the user then downloads the configuration
|
||||
file for the application (call it `my.app.configuration`) and creates
|
||||
a script to launch it (call it `myapp`): `launch @my.app.configuration "$@"`
|
||||
|
||||
The user can then launch the application using `myapp arg1 arg2 ...`
|
||||
|
||||
More on launcher configuration can be found at :doc:`Launcher Configuration </Launcher/Configuration>`
|
||||
|
||||
|
||||
Servers
|
||||
-------
|
||||
|
||||
The sbt launcher can be used to launch and discover running servers
|
||||
on the system. The launcher can be used to launch servers similarly to
|
||||
applications. However, if desired, the launcher can also be used to
|
||||
ensure that only one instance of a server is running at time. This is done
|
||||
by having clients always use the launcher as a *service locator*.
|
||||
|
||||
To discover where a server is running (or launch it if it is not running),
|
||||
the user downloads the configuration file for the server
|
||||
(call it `my.server.configuration`) and creates a script to discover
|
||||
the server (call it `find-myserver`): `launch --locate @my.server.properties`.
|
||||
|
||||
This command will print out one string, the URI at which to reach the server,
|
||||
e.g. `sbt://127.0.0.1:65501`. Clients should use the IP/port to connect to
|
||||
to the server and initiate their connection.
|
||||
|
||||
When using the `locate` feature, the sbt launcher makes these following
|
||||
restrictions to servers:
|
||||
|
||||
- The Server must have a starting class that extends
|
||||
the `xsbti.ServerMain` class
|
||||
- The Server must have an entry point (URI) that clients
|
||||
can use to detect the server
|
||||
- The server must have defined a lock file which the launcher can
|
||||
use to ensure that only one instance is running at a time
|
||||
- The filesystem on which the lock file resides must support
|
||||
locking.
|
||||
- The server must allow the launcher to open a socket against the port
|
||||
without sending any data. This is used to check if a previous
|
||||
server is still alive.
|
||||
|
||||
|
||||
Resolving Applications/Servers
|
||||
------------------------------
|
||||
|
||||
Like the launcher used to distribute `sbt`, the downloaded launcher
|
||||
jar will retrieve Scala and the application according to the provided
|
||||
configuration file. The versions may be fixed or read from a different
|
||||
configuration file (the location of which is also configurable). The
|
||||
location to which the Scala and application jars are downloaded is
|
||||
configurable as well. The repositories searched are configurable.
|
||||
Optional initialization of a properties file on launch is configurable.
|
||||
|
||||
Once the launcher has downloaded the necessary jars, it loads the
|
||||
application/server and calls its entry point. The application is passed
|
||||
information about how it was called: command line arguments, current
|
||||
working directory, Scala version, and application ID (organization,
|
||||
name, version). In addition, the application can ask the launcher to
|
||||
perform operations such as obtaining the Scala jars and a
|
||||
`ClassLoader` for any version of Scala retrievable from the
|
||||
repositories specified in the configuration file. It can request that
|
||||
other applications be downloaded and run. When the application
|
||||
completes, it can tell the launcher to exit with a specific exit code or
|
||||
to reload the application with a different version of Scala, a different
|
||||
version of the application, or different arguments.
|
||||
|
||||
There are some other options for setup, such as putting the
|
||||
configuration file inside the launcher jar and distributing that as a
|
||||
single download. The rest of this documentation describes the details of
|
||||
configuring, writing, distributing, and running the application.
|
||||
|
||||
|
||||
Creating a Launched Application
|
||||
-------------------------------
|
||||
|
||||
This section shows how to make an application that is launched by this
|
||||
launcher. First, declare a dependency on the launcher-interface. Do not
|
||||
declare a dependency on the launcher itself. The launcher interface
|
||||
consists strictly of Java interfaces in order to avoid binary
|
||||
incompatibility between the version of Scala used to compile the
|
||||
launcher and the version used to compile your application. The launcher
|
||||
interface class will be provided by the launcher, so it is only a
|
||||
compile-time dependency. If you are building with sbt, your dependency
|
||||
definition would be:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
libraryDependencies += "org.scala-sbt" % "launcher-interface" % "|release|" % "provided"
|
||||
|
||||
resolvers += sbtResolver.value
|
||||
|
||||
Make the entry point to your class implement 'xsbti.AppMain'. An example
|
||||
that uses some of the information:
|
||||
|
||||
.. code-block:: scala
|
||||
|
||||
package xsbt.test
|
||||
class Main extends xsbti.AppMain
|
||||
{
|
||||
def run(configuration: xsbti.AppConfiguration) =
|
||||
{
|
||||
// get the version of Scala used to launch the application
|
||||
val scalaVersion = configuration.provider.scalaProvider.version
|
||||
|
||||
// Print a message and the arguments to the application
|
||||
println("Hello world! Running Scala " + scalaVersion)
|
||||
configuration.arguments.foreach(println)
|
||||
|
||||
// demonstrate the ability to reboot the application into different versions of Scala
|
||||
// and how to return the code to exit with
|
||||
scalaVersion match
|
||||
{
|
||||
case "2.9.3" =>
|
||||
new xsbti.Reboot {
|
||||
def arguments = configuration.arguments
|
||||
def baseDirectory = configuration.baseDirectory
|
||||
def scalaVersion = "2.10.2
|
||||
def app = configuration.provider.id
|
||||
}
|
||||
case "2.10.2" => new Exit(1)
|
||||
case _ => new Exit(0)
|
||||
}
|
||||
}
|
||||
class Exit(val code: Int) extends xsbti.Exit
|
||||
}
|
||||
|
||||
Next, define a configuration file for the launcher. For the above class,
|
||||
it might look like:
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
[scala]
|
||||
version: |scalaRelease|
|
||||
[app]
|
||||
org: org.scala-sbt
|
||||
name: xsbt-test
|
||||
version: |release|
|
||||
class: xsbt.test.Main
|
||||
cross-versioned: binary
|
||||
[repositories]
|
||||
local
|
||||
maven-central
|
||||
[boot]
|
||||
directory: ${user.home}/.myapp/boot
|
||||
|
||||
Then, `publishLocal` or `+publishLocal` the application to make it
|
||||
available. For more information, please see :doc:`Launcher Configuration </Detailed-Topics/Launcher/Configuration>`
|
||||
|
||||
Running an Application
|
||||
----------------------
|
||||
|
||||
As mentioned above, there are a few options to actually run the
|
||||
application. The first involves providing a modified jar for download.
|
||||
The second two require providing a configuration file for download.
|
||||
|
||||
- Replace the /sbt/sbt.boot.properties file in the launcher jar and
|
||||
distribute the modified jar. The user would need a script to run
|
||||
`java -jar your-launcher.jar arg1 arg2 ...`.
|
||||
- The user downloads the launcher jar and you provide the configuration
|
||||
file.
|
||||
|
||||
- The user needs to run `java -Dsbt.boot.properties=your.boot.properties -jar launcher.jar`.
|
||||
- The user already has a script to run the launcher (call it
|
||||
'launch'). The user needs to run `launch @your.boot.properties your-arg-1 your-arg-2`
|
||||
|
||||
|
||||
Execution
|
||||
---------
|
||||
|
||||
Let's review what's happening when the launcher starts your application.
|
||||
|
||||
On startup, the launcher searches for its configuration and then
|
||||
parses it. Once the final configuration is resolved, the launcher
|
||||
proceeds to obtain the necessary jars to launch the application. The
|
||||
`boot.directory` property is used as a base directory to retrieve jars
|
||||
to. Locking is done on the directory, so it can be shared system-wide.
|
||||
The launcher retrieves the requested version of Scala to
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
${boot.directory}/${scala.version}/lib/
|
||||
|
||||
If this directory already exists, the launcher takes a shortcut for
|
||||
startup performance and assumes that the jars have already been
|
||||
downloaded. If the directory does not exist, the launcher uses Apache
|
||||
Ivy to resolve and retrieve the jars. A similar process occurs for the
|
||||
application itself. It and its dependencies are retrieved to
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
${boot.directory}/${scala.version}/${app.org}/${app.name}/.
|
||||
|
||||
Once all required code is downloaded, the class loaders are set up. The
|
||||
launcher creates a class loader for the requested version of Scala. It
|
||||
then creates a child class loader containing the jars for the requested
|
||||
'app.components' and with the paths specified in `app.resources`. An
|
||||
application that does not use components will have all of its jars in
|
||||
this class loader.
|
||||
|
||||
The main class for the application is then instantiated. It must be a
|
||||
public class with a public no-argument constructor and must conform to
|
||||
xsbti.AppMain. The `run` method is invoked and execution passes to the
|
||||
application. The argument to the 'run' method provides configuration
|
||||
information and a callback to obtain a class loader for any version of
|
||||
Scala that can be obtained from a repository in [repositories]. The
|
||||
return value of the run method determines what is done after the
|
||||
application executes. It can specify that the launcher should restart
|
||||
the application or that it should exit with the provided exit code.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
==============
|
||||
Sbt Launcher
|
||||
==============
|
||||
|
||||
The sbt launcher provides a generic container that can load and run programs
|
||||
resolved using the Ivy dependency manager. Sbt uses this as its own deployment
|
||||
mechanism.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
GettingStarted
|
||||
Configuration
|
||||
Architecture
|
||||
|
|
@ -22,7 +22,6 @@ the :doc:`index of names and types <Name-Index>`.
|
|||
Examples/index
|
||||
Name-Index
|
||||
|
||||
|
||||
.. The following includes documents that are not important enough to be in a visible toctree
|
||||
They are linked from other documents, which is enough.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue