From 418b85490776faff4f3c0b6505735984ab3a407a Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Thu, 12 Dec 2013 12:31:23 -0500 Subject: [PATCH] Launcher can now load servers in addition to regular applications. * Add new ServerMain interface * AppProvider can now choose to load ServerMain or AppMain classes * Explicitly document what happens if something doesn't match an expected interface * Improve error message on inability to load something. * Parse new [server] section that denotes a service and is lock file * Ability to serialize launch configurations. * Attempt to look for active listening server via the lock file * Forks the launcher itself to run servers from serialized launch configuration. * Testing echo server. * Tests to detect basic server functionality will work. * Revamp all the documentation for the launcher, giving it its own section. * Full documentation on launcher configuration files. Revamp launcher documentation to be a bit more in-depth, and split bits into sections. --- .../src/main/java/xsbti/AppMain.java | 19 + .../src/main/java/xsbti/AppProvider.java | 7 +- .../interface/src/main/java/xsbti/Server.java | 36 ++ .../src/main/java/xsbti/ServerMain.java | 17 + launch/src/main/scala/xsbt/boot/Boot.scala | 40 +- .../main/scala/xsbt/boot/Configuration.scala | 21 +- .../scala/xsbt/boot/ConfigurationParser.scala | 17 +- launch/src/main/scala/xsbt/boot/Create.scala | 10 +- .../main/scala/xsbt/boot/Enumeration.scala | 4 +- launch/src/main/scala/xsbt/boot/Launch.scala | 81 +++- .../scala/xsbt/boot/LaunchConfiguration.scala | 35 +- launch/src/main/scala/xsbt/boot/Pre.scala | 25 ++ .../main/scala/xsbt/boot/ResolveValues.scala | 9 +- .../scala/xsbt/boot/ServerApplication.scala | 200 +++++++++ launch/src/main/scala/xsbt/boot/Update.scala | 2 +- launch/src/test/scala/ServerLocatorTest.scala | 53 +++ .../main/scala/xsbt/boot/test/Servers.scala | 74 ++++ src/sphinx/Detailed-Topics/Advanced-Index.rst | 1 - src/sphinx/Detailed-Topics/Launcher.rst | 390 +----------------- src/sphinx/Detailed-Topics/index.rst | 1 + src/sphinx/Launcher/Architecture.rst | 108 +++++ src/sphinx/Launcher/Configuration.rst | 260 ++++++++++++ src/sphinx/Launcher/GettingStarted.rst | 232 +++++++++++ src/sphinx/Launcher/classloaders.png | Bin 0 -> 22551 bytes src/sphinx/Launcher/index.rst | 14 + src/sphinx/index.rst | 1 - 26 files changed, 1206 insertions(+), 451 deletions(-) create mode 100644 launch/interface/src/main/java/xsbti/Server.java create mode 100644 launch/interface/src/main/java/xsbti/ServerMain.java create mode 100644 launch/src/main/scala/xsbt/boot/ServerApplication.scala create mode 100644 launch/src/test/scala/ServerLocatorTest.scala create mode 100644 launch/test-sample/src/main/scala/xsbt/boot/test/Servers.scala create mode 100644 src/sphinx/Launcher/Architecture.rst create mode 100644 src/sphinx/Launcher/Configuration.rst create mode 100644 src/sphinx/Launcher/GettingStarted.rst create mode 100644 src/sphinx/Launcher/classloaders.png create mode 100644 src/sphinx/Launcher/index.rst diff --git a/launch/interface/src/main/java/xsbti/AppMain.java b/launch/interface/src/main/java/xsbti/AppMain.java index ffd1e4c36..b24e02212 100644 --- a/launch/interface/src/main/java/xsbti/AppMain.java +++ b/launch/interface/src/main/java/xsbti/AppMain.java @@ -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); } \ No newline at end of file diff --git a/launch/interface/src/main/java/xsbti/AppProvider.java b/launch/interface/src/main/java/xsbti/AppProvider.java index 24744c83c..ab3914210 100644 --- a/launch/interface/src/main/java/xsbti/AppProvider.java +++ b/launch/interface/src/main/java/xsbti/AppProvider.java @@ -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(); diff --git a/launch/interface/src/main/java/xsbti/Server.java b/launch/interface/src/main/java/xsbti/Server.java new file mode 100644 index 000000000..d4eca176d --- /dev/null +++ b/launch/interface/src/main/java/xsbti/Server.java @@ -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(); +} \ No newline at end of file diff --git a/launch/interface/src/main/java/xsbti/ServerMain.java b/launch/interface/src/main/java/xsbti/ServerMain.java new file mode 100644 index 000000000..da3c8ce2b --- /dev/null +++ b/launch/interface/src/main/java/xsbti/ServerMain.java @@ -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); +} \ No newline at end of file diff --git a/launch/src/main/scala/xsbt/boot/Boot.scala b/launch/src/main/scala/xsbt/boot/Boot.scala index 06ee1ba82..665407cff 100644 --- a/launch/src/main/scala/xsbt/boot/Boot.scala +++ b/launch/src/main/scala/xsbt/boot/Boot.scala @@ -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)) diff --git a/launch/src/main/scala/xsbt/boot/Configuration.scala b/launch/src/main/scala/xsbt/boot/Configuration.scala index e9464406a..4028e89cf 100644 --- a/launch/src/main/scala/xsbt/boot/Configuration.scala +++ b/launch/src/main/scala/xsbt/boot/Configuration.scala @@ -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)) } diff --git a/launch/src/main/scala/xsbt/boot/ConfigurationParser.scala b/launch/src/main/scala/xsbt/boot/ConfigurationParser.scala index 8b1252e4a..659573550 100644 --- a/launch/src/main/scala/xsbt/boot/ConfigurationParser.scala +++ b/launch/src/main/scala/xsbt/boot/ConfigurationParser.scala @@ -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} diff --git a/launch/src/main/scala/xsbt/boot/Create.scala b/launch/src/main/scala/xsbt/boot/Create.scala index b22cd2324..17e549781 100644 --- a/launch/src/main/scala/xsbt/boot/Create.scala +++ b/launch/src/main/scala/xsbt/boot/Create.scala @@ -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) { diff --git a/launch/src/main/scala/xsbt/boot/Enumeration.scala b/launch/src/main/scala/xsbt/boot/Enumeration.scala index 3e5a6f89d..e65309f2a 100644 --- a/launch/src/main/scala/xsbt/boot/Enumeration.scala +++ b/launch/src/main/scala/xsbt/boot/Enumeration.scala @@ -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 + ")")) } \ No newline at end of file diff --git a/launch/src/main/scala/xsbt/boot/Launch.scala b/launch/src/main/scala/xsbt/boot/Launch.scala index 4604ce2ef..f27441918 100644 --- a/launch/src/main/scala/xsbt/boot/Launch.scala +++ b/launch/src/main/scala/xsbt/boot/Launch.scala @@ -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) diff --git a/launch/src/main/scala/xsbt/boot/LaunchConfiguration.scala b/launch/src/main/scala/xsbt/boot/LaunchConfiguration.scala index f8ccd1782..be1f0fc4a 100644 --- a/launch/src/main/scala/xsbt/boot/LaunchConfiguration.scala +++ b/launch/src/main/scala/xsbt/boot/LaunchConfiguration.scala @@ -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) diff --git a/launch/src/main/scala/xsbt/boot/Pre.scala b/launch/src/main/scala/xsbt/boot/Pre.scala index 05a9585d1..26b83aee9 100644 --- a/launch/src/main/scala/xsbt/boot/Pre.scala +++ b/launch/src/main/scala/xsbt/boot/Pre.scala @@ -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)) + } + } } diff --git a/launch/src/main/scala/xsbt/boot/ResolveValues.scala b/launch/src/main/scala/xsbt/boot/ResolveValues.scala index b04cdb949..952d9d970 100644 --- a/launch/src/main/scala/xsbt/boot/ResolveValues.scala +++ b/launch/src/main/scala/xsbt/boot/ResolveValues.scala @@ -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 diff --git a/launch/src/main/scala/xsbt/boot/ServerApplication.scala b/launch/src/main/scala/xsbt/boot/ServerApplication.scala new file mode 100644 index 000000000..3f592b151 --- /dev/null +++ b/launch/src/main/scala/xsbt/boot/ServerApplication.scala @@ -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) + } +} \ No newline at end of file diff --git a/launch/src/main/scala/xsbt/boot/Update.scala b/launch/src/main/scala/xsbt/boot/Update.scala index 92e1bec92..8d1c2e206 100644 --- a/launch/src/main/scala/xsbt/boot/Update.scala +++ b/launch/src/main/scala/xsbt/boot/Update.scala @@ -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) diff --git a/launch/src/test/scala/ServerLocatorTest.scala b/launch/src/test/scala/ServerLocatorTest.scala new file mode 100644 index 000000000..7e0b30c36 --- /dev/null +++ b/launch/src/test/scala/ServerLocatorTest.scala @@ -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)) + } + } +} \ No newline at end of file diff --git a/launch/test-sample/src/main/scala/xsbt/boot/test/Servers.scala b/launch/test-sample/src/main/scala/xsbt/boot/test/Servers.scala new file mode 100644 index 000000000..930e565a9 --- /dev/null +++ b/launch/test-sample/src/main/scala/xsbt/boot/test/Servers.scala @@ -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 + } + + +} \ No newline at end of file diff --git a/src/sphinx/Detailed-Topics/Advanced-Index.rst b/src/sphinx/Detailed-Topics/Advanced-Index.rst index 28928ed7e..884a96292 100644 --- a/src/sphinx/Detailed-Topics/Advanced-Index.rst +++ b/src/sphinx/Detailed-Topics/Advanced-Index.rst @@ -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 diff --git a/src/sphinx/Detailed-Topics/Launcher.rst b/src/sphinx/Detailed-Topics/Launcher.rst index 6573f2348..eced0102b 100644 --- a/src/sphinx/Detailed-Topics/Launcher.rst +++ b/src/sphinx/Detailed-Topics/Launcher.rst @@ -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 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 `. diff --git a/src/sphinx/Detailed-Topics/index.rst b/src/sphinx/Detailed-Topics/index.rst index f7bebe4fc..7b551dd7d 100644 --- a/src/sphinx/Detailed-Topics/index.rst +++ b/src/sphinx/Detailed-Topics/index.rst @@ -19,3 +19,4 @@ Other resources include the :doc:`Examples ` and Tasks-and-Commands Plugins-and-Best-Practices Advanced-Index + /Launcher/index diff --git a/src/sphinx/Launcher/Architecture.rst b/src/sphinx/Launcher/Architecture.rst new file mode 100644 index 000000000..2e62f84b7 --- /dev/null +++ b/src/sphinx/Launcher/Architecture.rst @@ -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_2.10.3/ + lib/ + + /// + / + +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`. diff --git a/src/sphinx/Launcher/Configuration.rst b/src/sphinx/Launcher/Configuration.rst new file mode 100644 index 000000000..b110d5411 --- /dev/null +++ b/src/sphinx/Launcher/Configuration.rst @@ -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 diff --git a/src/sphinx/Launcher/GettingStarted.rst b/src/sphinx/Launcher/GettingStarted.rst new file mode 100644 index 000000000..66b8f6494 --- /dev/null +++ b/src/sphinx/Launcher/GettingStarted.rst @@ -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 ` + + +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 ` + +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. diff --git a/src/sphinx/Launcher/classloaders.png b/src/sphinx/Launcher/classloaders.png new file mode 100644 index 0000000000000000000000000000000000000000..6f0c1b003b5a502ba9eab4416dfe003e9c4ad5d7 GIT binary patch literal 22551 zcmeFYXH-+$7caU25mAaq{)&JUdqYrqkBYs3h=52J0qIB&Aqho642E-+0{e z^|4(epAHqD*=PFNtaSHw*{(+lTN+QTlaju*t5f>cZrrYIzR9v1bgNSRt{hjAUU#6Q zA#(EvsqrUj4jK^;-DkbGZXy@HJy!D6p(MAoVz|Mv&dE^gO#hIz1Wu0Pz`QC%cOK0y zi$(tEM!`(2U}ZR%!xfW-m33)SAY1?E`acr*|DyzwsL`S`)HegUF?iACA^LN;BJdMC z7FRg1kCh1XSDU_;x5-QUH9GvOD2^}sssy^jz0AswZU0y@Cq_cKV&W8pwxEY+wp7C4?rIF0y0TJ`9E(Q`NqA{z~-bE zTh9HnJUBgY&+r;A@@)V(%xDXHZS{5DfHy&I6w)9O4|&1oGXnIjV?+4J#MN=j!7I>- zz1cmUBAyqL$ykMSSLbtAe_}<$Gf4A0zXpi7*4Y?}4Kar4g`7*R)rWUooaB5yp}eQA zm&>sWr4M%^MMcO2F_$31(=UT=HVCmanyQt_Z&le`O6FeArab(P;ht=xtS_J{t~u^ z^SA25rxVpY=O?^ugpH;IsJH=80A>;Zgs9BZSlFW48bQ0gbPw0!*QtI!aJvU|x}XTr zC#hAS7!#1OjQrZ&`tcvELI!=YqL<-AgV0J!#O2-A-QVR3Q@5Y1!8+B+?XIt_@u+R# zAcPcq&4@2Vs00^`_7eW-f~rOq)x*#k%zOynmkVX+bcbS*q8WZ(Sgr(;wfEJI0nrgo zoM$#XbUzLz!Cz$&JTRPfbp54OBBTz9W3@Wc4~9;RUYHae>C7UMMH<+aNHG_O>^B7u zscm-s=*B&PRq%OL#MsPXFMf^3rZ)MMP%~;}nuBE)Qt&-1jAcb;=j_l3k+T6@-mQj) zCyqb5hb;k6=;9U*R1y~?sT{@Ub5W%andi(BhO*&9t6Um_&;4nR;hIxgFE!T@{GOjz z(%T#FXC22(x~QFGNN*8Pj6W9A2`mw{52{y6Ume2a>t9rQe<}1Cd-ln;B&p+o=i$eH zeSfiELjcV`(CL5AnzTPWaWw5R3cg;0vvn%aYC%mGlH21fDU5pH9< zgbw_!neou7(m)2h_Zu-5;%Y>k$s)ixcrH|2Ca0a)$2okQ)WT}ac!{mh^%}Hpj}X!) zfKvz@)%H!CFH#t8Gnc$<#cjl1;XBeLqGuex`cN8f6++o%%(JOz_6pkvFnBqA0|-i= zY4=FuyZcG%ULH-HPks$_-Ke^O_%_lt&adOhwh*et*J-ED{9amwT{>9(yL_mxjIY+s;f^TTOoakFpqMdXY)Y6k4lo)SRFlp> z<2dQif4<=C{_Y#Z^V^HZP@#JhukEfZ^l;+lt4muO6Lo`APEmAv$IXdH@RRH)F4w|3 zNFHxK^?3bEtn!nQi^;#6vCXps(5ErS-y*}bh#aBdi0xt4=x1ksx1a6n+Hn7&;px93 z|2)pdEt@7E!ara~E#wYwGiKKZwTeJlhxRW}tEY6lQ{0+ExmpY^m6HvXaPf^ngdNTb$$gBSj4$>5U8`JCzs-KS zuSooyGsrw2TF)-IVYJTTLtGGARHDCpses%v=o{4PMwMHD9|2^5<}FFW<4-Td=cylh zm3Bp)qNeAey5)lJA;qs&goKJiw|_V0gcUyfDoMXH#cGl=`*5KDeE3N*2K}dzQmuk~ zOW;7^_nhSb^97XAVy&k!K|L+oucE^}!DfMb&DMCQDL16ZManYEMLGN)%IalC+{Aor zE8D~)_RXt&rCqR{kFUvCSbHru;y2&WFscbvF{%mob1Qg)?%ZNBIHb?LPVmy()W)Oj zjW3eZ6*^lxvvQ7KVm-Y+K=eOr~ZV56ZtgF16xk~ux03i-bf)ZS5J?{qb{3oC& zoqq)9G3vO~o-e8OjHBJx<=T{fh@F#hnm$){A0L)+4ZTvHx_xLHR1zf)f_sJd5!|;W z1yhP@=#$MyFc~eF9A`GJPCG!Y23xQoW|+%&)af&TD{wl=Y6U>SkG|8tYK)&TjcE;2 z0^ZYmbIyvjZo&MZOr3szyJuYq6Wmuk4(`dCy^IHDWzJjfsnOPRRcvZ4(#gy-nREJ9 z+~MjbFB9qua{}ySjK=D;=x(RAFh#{a z0=9wnbI}pJM8q%UlrC1<%Z?&`U;OB%`DsefEe6KkiXy)=nw9<<5K?Aw%T@y?9`!VH z?%DYvd&EsMJ4S5)^8JkZ^u{wgZ+TrlS}C%C1egzdveoOJ2A0G0V&HmLbHsLu1kf1k z0O5xo?}@mU#k>ZfExgLZ-VB(0D%1IIlhuxy^6&@U}`F!{cdR71)w z%BK2R*}~!PvY~+M)}&yn@c0k@BXV*<0^yK1??Zs0Tx(55DLK3-tm}Qh(9Nb@{ysdZ zJD?WXlJaW+`b{nWF`bnB>a@`B%!5np>dE;cF<>pmbcXDYXCF z!#!VGElK@uth(8IGG%0o?kh!qh6too!DwRLXhHIxowc)@Za6V^*Zb%yV%ANT1QtB; z!v8_qoM%BSQ@=$5Zfs3THx5^OcI7db6lBIfiY8?H4POF0V}el$VF~H1Mn{Sz_diJ( zjdN-9BO|e1Wl2QqCk@O_J29@XgF#lm_Pxk z{3nRQaOTVnv^Kv0br{zt6>5Lh?3~bzH(9j8sc)jXS>~^J<~A)|muOWC>-Vd)*a3aX zIWeuf7Z4LVZuiIt@oQkhfxSZR@8K42AtxUCKR_}06s^5+jODrz@tP(Q#B2!JX0hQ9+^mXAO6 zEGrQm@p%ooZJ;}qUylB6fwRP7;aIE@pY^)86mS<#mTp@7opQZB{!Oi}?HTo32cqs} zR;(K>$w(sIzS|Ve9NN(4+82`5qH1|;28!e37C$PD-zzEPuTQrlNROnKmei1xHrbp@AmCPb1src`qkw1Q3nzHk)>O0TU zz;4ws0hO2Lntd9-M?PX}@w(Aoxu>VwGlG|H!}LOAUx^##!$NHdG(zhM{B45!f0Bed z%SH8HYX*V~ZA@ckSN)|UDlQ_$k(SvrA7i)N-I~jV(t;Dibmm6~$M#=KG-g1yUrF_oY&_v~-Ye zg4c>?W0OI6pGz5eQPr-^@lV-NDEpOAF40RfNxhZfvS?Z-v z%W#;bl^GWM<~0wYKdi5!Gkm=25NIrtcl^GcU0XSrw>%iU67`9@%KSVl6QTW*0`(u3 zcNwx_TdAaMaZdA}Vsqu&TyfpSprGEFx zZEVm#7V*1JV>nIW;Sf}l7E$U&!evNq;mhDd0FFKl2Pz$uL{^5SyD&ulO)uhpfK0zD z_P%;#{B5xA1~5vjmwW0bD6Qx~C~hrJvsXu|ZA7IxtLJPrC>#4pTYr#ye)j>NJ?+z7Ia)I?d?lYR02Xj0Tu+V9vY&y+I=%-01!Pgj2I7cbtAg6d(k?Qox=^@FV)3#d!j zu)1e~Bv2zvRnIvq`e7$ZI+l;3G3YB#Iu~e8UqUpNdE$vi)=YMHSnBh+@3Xj)rX!bO zv;GLX(Z2Ug^SY4GcMyCuG)#S!WcZU<+lTKFKOKE6YD^Fezo}o_2?9fOK-z^kA&q~D znh2x=&%~v3j&klnFy|{hHqSSs*e^mcn5emTn!F#g*{(00gcCKyIL+T*#JS`_QGp?T z$NMh|{a{6Z+B=lWIki8%^BCv@{XklD;!oduIK6+>Zns(Ee?> zHN-{SG7qBGB)VzZtOXZtK{|!=PttlVT#%Q24--SNk$Gv6ioO@ISE;78;g17WR8o><}Q2al-Ki4Y;1#KYdp)O&$F}ZsD}El3~^Sz19e`7^Q|F-p4i?+7LO<9xnTOldEQTtKfa*v1Iw#w(Gy1UrJ0GFcES?u}!{qJ@Xo za?(WSU}^>?WTIY|=_t2?>Zus0n%vqZM4R{_DIq-Tf&gE_HDQ9644C;!UEl2hI7?Rn zx`4eI^4+7*0rOtpvz?uNp<0%4i8IBM8`pAWoj;P;;nG#ZSyD@%%n0Kmag&3oNfy;? zDrbF?NF^z1?~hB(gyDsOMii80=h1Usvj;#;+fgqjd`C=M*AYVNA|HhmHurtb0k=$g zv$0L^aOcufy_w1Cvb@epE$wE9C|Q7i0&rG;z&HJWPQ?FC(1+;%B<@C=UmSls6V5jU zGjTxu{skQezG(rNVVjLeEv?}KroccBZ!#~eMl<&-BGN?qfr#DHv}}yc5sk9E!rgw- z2gKAhpy1c!d{*n#ypp`XQ}mf&pbeq;A+FevS(%yOAF^50oY_P%F?TYh(CghHtqvS& zv3>YJ?d5kueG}$x{t+R?gv@y)q#(Bw#zUUfHe;oLnPAnB0bk$LV-j@uv#QgonaSf_ zV?DIObQ5M;rbXwzn{xaQ#79DEorxR(IQFTg4848V-lbJ=w&|O2=?p%`44Nbx`|*iC zq=h~S0Nzd;5YZu4Hpfsi=aWyL7-wcC&?A~~M_!CA3PK0An*7l``e;(fD(;m6+72e0@=8K-Q;BBCiIED8XsNShO@FGm)u{p#_|meG&d*hyVm-A(?3vQklc zo%)Z2hV8$45Rse)BqHJHMkyeP@kff*<`m_vwy2@-M_Iz#X%MfQAfX5o+c{&Cu4Lc) zhC(R4BGov&&Kv;D+-BA61U2pfQ8TbKv#8B2aUIP*r=7{a<3(&m4y;Mow4k#(ZJ&+KI*b#z$WIRg zlvZHm3s9g*n-iJm6K8E&aa8Ej%f~*c^+gMo)Z%*t z@&Euc{v`i_W%x<0P*dAZ32x`{x9q)6&MljCsVUq^n}{5pc`zS!C;e|q+34=hwElM0 zd)~Gv9}ht$CevQ2az6mH+1zQ}9CF_<{ z(aymAS6fDc)(%&DBC$2S^9_ z+n55tj^~@*t*z9QTD;Pw!Qlbq>SIfjQPHVuOr3Z#x722%_#w@#oo{vw@lvbmAb7k} zJC|~;TgulSHieqOgFZpmL@GzJ_H7poWj2Ln$wq@)5PFOxc2rh_%x+GJ0~N3;D)Mzn z*b~0q+TphU>f5)DvxtW%-btg1Mi}Bz#_$f1mwh;sv8ZO-DsXtSp0=IVTte(B1boii zoG3K8MBg0n6F$v`C$7#=L)X;r)!zFymL`ul=9~IKG4o%-I*HzW66?QJ^QVWv>Tg1( z9)^f*+{Ttq;OQpy?`-(U6168-p9a~d#k^k*q4|T9+H!t43VVKfBfL*+Faas0%XJVZ zAAI;0007r5*Q6-y=Jf_Fz5<0(6_r_KInY`r%-y`Yct%8T&%xu7tqufZR13D4T_<2A zU`V!35Xz3?75$Jb+xcbCpq*RFKUly&<-w^f9Y|hXl?Gs^9=Q`Ac|oUV*pQX=b;_)C z+T!q#pmof+Rx%Sq4}#6~e`SP+9H)=fCNwZZaY!6H!pw#MD%{4l<=r+yq)ox|;d|f^ zDlh}mY+a)%U3K*W;QSAaSzz=U?)>5(?DC-0^)-NU?fu_F)&oYR;Hwu`L6mbto4UaT z`L+vdVCFjSg6BZ_b8ymekPP^6`Ts_g4+jt{T`Jrk8?FY0%Z=_A&!pDZ{X-)!^jgtQ z5!JLcD0Xp^4M99blt_kIgg}bMkW0(LQ)qr~ptw&40uiQRg{4E)`q23nPf=-^zcTW- zb+9(D^u`n)7CYbF8X_LX3Wr^Kig2?nhL|4gWd6j@9#%H#S(b>Fm~GmJVvrvN;oPe| zD=DM@987azhFsR~Mo-xJT1X^Mv=c=oyo)l1_+$@&^WfYU+n)4F@?M*GCp;Ld5WAl-oneNs9&`niSr` zkU(`aCyiEQy`|z0V@SieehMYMeHF!i8dzcZ!v`l5kpe3e=xetK9(Hnv@>Ue=oNT(y z4?Jeq+{Uc-3XwtLX5^}GxYc-VF8dXcRr5NZ7;J-sh6(Ww^bD5nJ1}680bshoe$?LW zdMly{eaQk;rdIjUvU$7UM_UgeBgOBeZ-S?zqg15B_`TD`|*lJ_c(wV0fU{~iok`QVic2?#pin}Fmt$E1587L<|ff!NEw z@O%J8-+QT?i}-EsN;^$4@;Tg{E10RahkLUD)X^u?`*d_EeXd2raWy^NX+r@P?8R-e zqn8ZF^!t1Gc`NWqM~Oc*()$)+-RQ;(5biTNVNu}Ne!UhAg*ZaPRWlLS`B1lQNnDz) z@S(j^!WBb{n@srEL<4odA(pnJU(PSy zQ>^+IQdbeB9_#v|P1+w7;9 zwZEsZT_`0Lmk4lTVZCF*&jqbIZ1;y}t=~~TzsyH}5O=qG2|a}^t#CU&X$AuKHCOyy z9xfbNS8mdRlQL*u3D#g|11R9=!*|D4j=FF>2%L$i$77#G0S7VI;XxLHRtRSKWDCF7 z6*B>4m70&ifcz-B(;yRdmcUFpKItg;f;mDHL#HQt5$|u?xC$yzxaKME_5t386}Ta0 z-r~y9tx2_XK8BRw(QF@}E)txk;rKf4{N$BK;fKfYBJ>Q1$BA~y{bwa2kBER54u5SW z%dMy*JYPU3o9iT(0;Zs_%YT+|o(=DhN~(StMT&d>caq&A<(!#rRM{VYV=&`cT09z) z7S7u>NjF;f@%a3>RVKyA^M~2R4HtdQGBM11p|Gn$z+ExR$}}^dIaHoTtMR<=_u+jX z#f{%$^lM91VS;H*pPO1?Hv1V^RXDcQeb}#Ol{-=%&imqrhK8!=J4%^#=fzy)Tt#3m zj34XH>+rYM=Y*yN(8`PwYc=~fXyu}(nI(OTh6G((etPRM3TPdY4!`!7d@jId!je)B#mOFFTBhSU+)GY;sKq zCQJ4ne?-@|->0037(AkgbF!}5dUS+Lc6kz| z6&IL9{@l8aH$x@{?%`*O*1cUQl4{7FN>@Kb)4u&B;2?c3&-y7ZuZe#G-JkY3 z){@j@r@5mz)q@9rNhA-*epoQ|zTJnfyh6dce2mmiDm&W%`QqSXNx@S0ReDTIpfe-BzL$TkUt&dND z74}KkskYKTszyf!nC4!C-7h`gG5spn#~a%eo!Pagfh2x5>9&Obz(fk zz2P>FBZFe+veBYa&3$P0Rokq?J3{#Nr!#EAS?=o&%N>Yp+ed-dzT0a+Wgek^Z5*O# zCe>p7yn1b1DgI7#7v@mv4mCp=-X!x8QJkOBKEFIa^939Ge1wDb!jOHC(dSa|9ciYh zvfzqsr)p^bI(Epr-0%vQOsiK3;br@qm4JaoAu&qxKv@))$iFv)4ghTa7fF8MMqnlx z7%{!|<5dNheCT00<@P1tW9?JSjbGZ$u2m2`x(3YK0*m)fy=U|}VaZLI`9?2GlN)l@ zUrlJCaMihz#ll-TMS8=v2R00tC$iR8hNL*Jt2(&u-B)|^Btkuo4eu?bU#tvP_ZZKRMTV?_sVUW1j*nDpcN>OR#oH^bX= z83qF3alUZz+oYif$F@EYva*xj#cuq_qYghZ=`txsPrWhEHlwsC_BhJD-o~~R(UaCq zgk5J} zNz}01ZZf{&^2az@^Tye{Mghk{ zszEQsIL7-RhVAqD1(nlPDRN zxc`TGep|wpIn>gCN8cj}r?GvvlnVo-z335}$Flb_HwT3eCd40-rtgcFyestXANA zdG}E)OXGgvl0S?z_nD8c*xBva-W>cT>M~eu5I7e&xC_o!hLW`vx)uh6uB?Ieqr)bS z?JdC#xAN0(s4g;7?JQ@iJ+LKX%|o3*$gb#>KV(C}5;k>9AlT1w8dHMU4(Gq&N4YUB z^4o8!cr$^I7HjT{eEMnO$4(2|*Oa+DnWK5uj5sQQ;`J;n_G7u3*=*&ZSoyb(a^IOE zkJ!1RatVd?wwWDY3>HsRrzzPhLi(g0t(5O;v!~`Bz%e~=zc-z3-!Va)f)zsR_zYeSi__jdhrm_L9Fyql* z$DHsNNLNb7{)BV5Y{+<3=C#o&{Vk3z$o!@a&b)!e6UR6w zcDN%zKp`iRfp7!f;&|{aeOu zwV|~;M4ulY(tR(q=jrk?SA%xA*up^pHyyoBD=XRTfT zBBjYo9^ab?5ndj1dYFFjq^8ovzzx~27R63-`bD(nL^)FZ7vU?SI|0QLlUD56mP3W? zIM4cvilQ*(G~R9(re)NW-mLK@1$oS{7xOD$)aoR>#0%Th2DH7Dj!4)Y^eXtHuk>m4 zlkA8lljjAd)hF+$2dj15kSa_G_BgdyF=v&KMsp&Dz^aQ(fTFQ$4EEsZ;w`}8zb?yu zD%tFbqvw<&&V|4b-qP;(-W$~zV4L#K(w{k7A0UCHg{#)P9P4IMPRW1EaT&Dke$IAg zjQe}WWR15vs=*c6O*ksxvl=L+|?U6{@2zRvH!=Impi+Eczr2VZi9LZ zS_Qkd47KyPXOY>u z@pw+aNz%X(pdWTh2%zQtY?X6O!G0=#IEa5FYa=2Oogy!l50xR$l>)0-II3-V=1XEvy3DC(>Sk_ zmn3RLsk(J9t>V5P+`#kHEsK?u5{GLx-SWds3A$nl-D1*gKij&Q~BD^pkeysbYsFVP>HWLRz-`Bubh|&D~8T%NY`jyJt zg8AY7ID=njfb~hXS|F4q6dd~YFNl>H1Oopck_FZ${{h7Q?jbn5_An74y9VBX$XC@> z5KIGm@j$fezbKjFZH&`e$zToq`WyfH`~5$F81O%drdtBqgoPi=+IQ+8EEs(*cPI|t zXlMn%YKhF`(q>cUhs=cN zcj%m@O9=l}-C^f+26q$CX3}u`T_vTsxnn*5S^F=o46q_0qY*ydD6Ql8k2i%VN6E;DgPEG*;JG z#eY%G`Y>9U*%V?nL{WfwoH2GcogH_SLqxVQ#AClpr24YufF#>}3=>ZX%F4@(VTtfR zaiDMR+v3ZK)p3QLm$QbTCfd!qa{iHFiF(@wUyj0$?%%y5@*X`SL=OO7$I-S&s6^9E z*V9ki7Gu;?qofyOQZr<$MFzXZ_?Zc}<7W?~ttQ@F6-}gaN-@jbrl=i3R z+8ooJJ%UJ7IM!(9GV40yS$e6fu~F?MlAafJVj=dT{ITgb zZm}OK6#RN+aglYjk;0BDQv2R_GCyE2K?C*@m26=<-@hrbXHpsf_)WWg8}L2~7Rrj< z`VB91q9xnkV$~NHHQ&Q`w*QJ^RiLnzFPRj{;hK}e!kvKaSCUN z3K{whye~^Fd7b3O(ZSj7jS( zzfe)AsEbibk$tE8tTQ|!f`eQ`{1Cqdru)KR<~7 z&^_zAI=6i#Ox?~if~Dh8ZDvg;XHv%IW#wce9m*86+z;MA%(y>%}o8a^msLiqUb5@CbR(7mi=%uxd7i`MrwNYj|<^7-Jil zxIFDoMbRkDozs`8LbLX|H3fA8_m9X%qb#3a71kgU&4zXh-86V-HFyjSp7yr8za-&i z(6Boxv)ao0VNn=OG|Zv0vvY1$kK$4y<3tW#0CZOMw46(xU%gP0>U8obkbdLQ2BPOY zNgo5JD-#?J#w4*|VWRX}mY0AzL|<;g=6G;R!(VxDL+NI)cCaQK`rKmO3j)^{XtnK^ zOR@!VpU!!@SZIH^;Q3W1;4JO~<2bO|4F30b#pJ@lg8vptz`K9_%)vF+vF2JX{M!r; zvh#0IWx%Ew$@^X8Pnds$w1II_2?oO7t+!fa!cD`t@0yv zy#%Zg5cn)fpVeG_md6T?5EahO51A?&E5-uA? zFMZ!{bEjh*xm2W`525kh_w8xrY13^as9Q^s^s=G#qnAwa7`2tWu{Ji>fGYX@E5n}w zAA@owE=xVZ2%)mp&qWJxC=4AcuE21cwF$ydu3s|)LV(#UHhBmROJ=un{rWg7(16BZ zIC3(ZHjm=Q2e;--HX}v(MF}t=FBn!xVGPE)|EHB3kH9dFs2Ao>7sy!=yXwUibV+Yl z-~>L1-dwA>wC3+YA0I??IqAR`E#9qvb4W_h`qfrXm0Zt=-?Ftt(FieLFfvIO8#*d> zEBN*i%;oCQf;3ZVG!yDO&b>x5>Kim~TgFZ1#N1}^3XmdfuXu@`I5V(1Ot0f2*q75=l35LZ7G~L?Q|@A@SUjz*`zTmWwqN8k(<)ia<@1g11yrV!j%2<4Y$c*{!48 z0LhGqka#8uJ3T>PwUI~$U69-vg3wE{IKbWVkQ-lry3nb=7sfGs{g19$T;n-;MJ>0S zctz{xw}29K&I*!t?4wj@4Oqo?6?on0m>UbazVEiv_Fiu5tcGNEb$&5XPeU4BFB9?7 z98Zu>b)8-JYh!tL+HuLZEK%EPHK$?TxM%m*ieo;gSl+iMz(&5PPEp9~HdrMQ z_hjms zA8x2r*=e;fnFwAzw&~AUUa6q_)$4rv-rxI9k*~0JB)ziO@Aj_|Q1U1sM}23Ewj&om zK<9#qcP<|Yq=q;x3a&@`mwSj@n40W>kTVj4urmW52_L`wdYvuxed_ z{=D%jsS0I*eJ}mfQ@O42iW8#mbk)MoYdi)(P6R=+7CnkFMYsJ)RBJ>US|BH9unxAJ zDDePG8P=*td`t1aVieo^NR?a~Nw#(+Bm7_MHjrF-mwLC>ec({LZ@+C$#vam(yWHy_ zFlu{8+FfI?)MrTUqXpKapUwi|qa_j=JCY{Jqb;pCTDRuOx5f0$UK{PH4}*1bn@Yy| zbL?qFJ-EuNRNBpwLpx2|2Wnf?*5?Z-y-Si6`=9cxEYD7=|7K&8$QTGdUpfY}~YxiP=9->sDd zxMSJar4c@g%n!okv7cY@#J6ts=F;j^yT8I-Jx1;9gI@laAk=aqjkC8_|7$Cl$6cuH z_14+AlXpHk^NtSU;oSI?h?6%@Mx6}d={dI>Sm(-nLp?u6@V4N5aC!sJ$z)PN+-?A8 zOJMzSR{Mo=(^4G2_pU~Zbdgfo2`0Vu4k>~4u4y^}Zhphi@_L7Rb33exJA=eAh*%gc zjM{EaIZ83)I22+6>jj4^DegRu2W>a*#D4|x=hw|ccdJJPIK?8;vsYEuFb`I3tQ#6F zB0qYZbov;j@jCJ%x@DjB)9E`s+%1Px{v(`b#6ay3n6W0PE`%&Gb(DwYg!8<~HMpHs zrI@oX4>NvGmVTWctLUs%f_lExXFcKBg@-n4i4rMagN2@CD}9P(|3%Wb-;0y}7BR|W zH^9T!bwTGvP{gfXX}SKBL&{E^q8Bk=Et+MY{%u|dk6!`ZRHabu|^C% zVqtYb1JfC;r$!cS(bfO}Z%A&1!`2;`?PaJbgpI}e3Jt+e3oVU4+L1zLtWzhyO9&%1 zv|u#pWjLFxK+?V8u*GVSIesvJh*XHqV4~(5;mQ%(Nm-olNzo=UbMrn(9zeIQ6mA7q z1_b~_IHL#-xS5j;uVQw_8Y=k$wD4K&KF_Q=Fd_j^TeoW2&x`@9t<_*v^W^t;C!t_-TQ_+_ zn;>i;J-r`KtD1 zhvRq?q^1Mxkfd4Lu^!wR#+7eqV?1Q>zCxN+naU^E5j^1c;hg){siw= zv-sA8M0+N5NM9fQilM2(ZD}Jb0$`6ynXaYW((tk4eH}oQWP9>vN|Ac1Ds zo}+3Z1gFLsb1otfksp$kK;1}pH?ty)eE}PBA%0wZZlCOS^Ho1#vP60mMB z`ET8}Ue!{|o_`yrZrp;ays$d>U|Box$b2tIdqb`m)sVH#*dVi|k_jMit( z>ocU{^A8^nHwGX^X>UrjaOs^#f9Q4ZZ)Ug~*@!D2kiD!>Lz`BoWhtDcDw=tl^26?g z_io~P(|YB$vO&)Dyu{@pwz&jGIK179dhcs+wk~6T=d@jKZNgYqm;nH>w!-ais+ZJ_ zFRlQIvkjgM`TEHHb3TIY_y!>?@FWea)>u061+V>5}ri!Zg* zb-COtB|6_0&NX9mJIOBh2?16IszUCEQJROFqs+LaMAo`d<`F#dGv77(4=`$ahW{aW z$oU+wk6#AMOFd-&h}%@}Ml9=$DWjO|?VlT(@JTjMtg z6Y9IDhn1d#mez8#rTPw%CAFz7reJ)W`u+!HWof$s_|YyJICFkId2l=@;+l1v{8ge7 z5fA@%ByU4o%(0q{0c%zz&y{c^TQ1!&{h-Sn{Fb|z>#REt>tuMDR9B#x(#uT@0Ai5V z^Nu=B+JqpT0uZ-9ycce2^02lOracI~<^s0~aGx#lz5)J(pve4w1o;zpR+}uTxdX*$ z*$H+CgsZl#kpM6{XPzguw6O1+yYc#(P3isZcd?qbMA^$5T6|9fkC=gp5ejZ+JEkGV zcFod1KetbB()KX1;WQoFP@$3ZuJ4F==G1n!Ohhw+e5styrm`SLsMZDklYJX(@DIFW zzb~!qPwRIHZM|(~y<@rX$huKh?y{Geo};FBeDrFga$B4+rEYNI&#}eEhPv<_nqwaQL?X3MXQb3oraXF0ERYISwGaGm>)(wLM>Gia}#L2vhd zL|l2EeX|1DghL;QBB}{8oG4Sj=J6MgG>2YA$BC9&GO|NBF*Fb#{qI%3$JRXR(X>Mz z2%;pV>Z(4!AxJq5zSLQZ8j{xhEwJX1fe?Z{*cZR$pJ6kfwkKs#{sTVV&<6ggB2rCI zEy{Dp*53Xvu2EeJ8&u22#l366M|v;M*Z2Ct7&GSgvET3{?e|BXTU?95?^6aoa=Ow$ zSo>gs5<>n1up`KPmsRu33zxoDh8*oJ7!8%kTwk_?tRJ;Bf&*rYQf9~<+9C|v0h}-( zRlb@J{yJ$5-~Rt7G+P7M!Gr*WM>e$mdoQyFs{dcm2YC1YI?nr>!2b*xrNCn4abVlu zh|0gsss9UN13u+)S}UJsatRW4?K|Ye>8(xvs^ZM-UdiCD8uk#X&paXq#_k3d%ILvsY(t-lTjcr9up zs|6?ijc*6|<-Olu_g2*;o)LnM{}5j-?Bc;k54663N)KAtD>o&swbm zbN`QTzC%L`tAlJX@vDZlUP3nHm5Xrc0Ys$D=zq0Q@Lx8RTnki9tlJ0+ZUo4(5QPzb zhb(g#5=m}Zs}Fw}IlXD0%8#&P(pl{AEC`#~Dx!T{2YRlz5Q1K^-c&0=z?(R78 zObMB^{KcHJMCAy-S$%PC>-7!Qk1Tv~CI7WZ1tRF?0{M^KlFg;evAr#auDCq@uj0m> zW94(;5n9)|g1sk}Sgq4e&up_)jJT+JoY@-#A0vD_?>ZXHjqi-BVe#WdbaUko^7YH+ z1R{5MQ^?-p$qNJUEAUQBWQ=%3H$X9~k0^?Ya;UHpJ9SEEYCer0W3@;Atr1<&{CL58 zwaY7>sMemPa+RW!qcv=We(jotU#T$(wHfoH~2?E8JFtT?sR1#EUrd|_R&u<=s=*$~E^Ol0$G;WaJfl5B`egg)d5Z&lm(0qeuBxAJ7`->c zH$m+RpGCpSvdwIZxj&T&A`JNB(L(cwj6d{CbM5<%&8CL+soH((onwnH478ijzMlVE ziAyvaV=mfk62h&6ZZkZWE-_ZWikZzNi|c4yH2pnYv#NB{jU!DfQPLCF0K3<{zVd(B znktz0UkDFJQr_HUTj3J^T<}aNNbHlQDK@oBxxgBhc#o`>ddAMA=NG{B4H?zNJ??-)*#VF5+sDxMi3Ddq)OOd z57xS_ea+s#_cho4`F%gW_kG^?{l4#c?)$m#s2e%g)fc1Q9#khWioGm7l-EY2f&Vb$ zO)vF7{n^&`bTC5}JJ>O}j#|Y5iNniD#d34AnFv4&)*$`3X9f7HSlF}|+G@DAggsRJ z@i6Pp+naX4Y+>1P2lvFZ;(Sx{8s@4c&;KDTqB)qoLg2Aq(`NI*9GLlTt2;AY-$#Kx ziaI1(8%6pFxLUjG$#lP_u*^7IE|t#u2wf|l;=?gnHHrHy) z=%QE9(r#q1EJc0mik3w$9LO**(=V^ zZFyU!eaOn+0Ym+?6tqXf(`_K_+p&|DNQ3dYc6Zu!-N`s*%Y zgyzi6HXZ!dB3pR&Q13w0*GwBFE^sn}dvS$jqq8Jg>^M?)RJb+Y^=M2){kjNAWp5-@ z-2>efKYsccKb1J8Xl(d-$6fj9fuUJK;lTOcVT{=I^5DD5jKi9qjix@_1#_tdC-3{I zG@c`UgUK8`N1!dNPhL;d27EMX8MFiP#|rA=U`yB0q)V|&vlv&9f(3pmG%m8^8H#h- zW?;00y;6~8A;tGX=f%$GE#2Gsmd|Bxco)iiMChq%{dn(W>y=8X)oVmp#6siU=nzbj8&?)d3p-8uX^=)Vn z?GqS%&mc8#Tl^)};PpUB2cW%{%HCDByCrm9)JOY#1GoQj0;;Ja-4B-X-MEcpWbU%z zNR2`W=PYr9^;+kI?7Bp*z^{$0qQ;(uUfuNL=-=2sV(Y`a*uQkked zNPT3}NU?i&f{FM9wy3hPB*Awj14VW+6CZ{K&9oso-VcO*>j+Z7Ee(-ThBU@)N%&-h zrlx#kac{i!YOSmpsQf??6F8vP48awydM>T9sY#vl50}oGy>W&J=EM$)#QHDBg}l&{ z<}q3ReDxr1;S)81*g+D66r~~+)sR;!O=;-)bbwu0Z>g8ehCPsgP`%8$L^QCPC21}E zhS#TojtT7gT-aO^r!%R62E#9e){r#qlWABc+vRfFY1p zmuS(vG-KOMK8ODmXy5E494Y<^3< zYy)e}GnfgxioDd=^BHG!?4;Hp7}WrFWEw2UEuY`8xY&?A#9{Te~Tz z;6G_cqufAD1dWBncyc`yt{Mpt?))Ts%Q@dvLiVE^WeJh6yMiZBL7j`oOP*;~!w^!q zkB8B=5qVz=f3>KgefCNBCT={XKi}}rwHMnPz>zCiohV-n2HVi@?cN6p}&Apo_&2=@4UQEO= zc+B#7(&&QP8FGtZ0O=J$jlOKWr3Cq*#k=kY(;N}<;q+--y%vgCR?*u>Xw>+~Y9k`E z*iB6+u7C{9UX6`8i8-(r^gf>)d{Sfh_iK>P!^$E{b{9vs?x}O=u6)*r{~YU`%sY|n z7v<|$UOL8G^RB381WI{?gAW{16 z%XDQ1E6)KLt4qvWifc&_`A80NTZhBR#C2IvJKf7?f+?bB%{4C3Wby8Jt`Av;$!pSu zr=?8U4)%E8O)k6Xkv9KOT~QyICSQNi*4#Iol^^G>3k;ug8MPyQe5=nKXf``&kNH#m z_^8o{ZEFwW(du20b$=mRJBpgK-QRQNP}|f{UU^7k3{Onho+OZ2UbngA3 z0h2pHW?A?ju{Osxq*967ad=maRX}|nRDmO>pZvJ5P z*zj^guzOoVPWrvZ!MG<7<15!E+b)r-TQ+KOn@pyMagqJImB>N~j?Q)nH(lYk`(d*i z@pU{Mg0L4rbiO{yvA7fE@oIl zz*n1Btww9W)hfI^JdRa_lo^!Y)VDrazS6A0FwYT+-ui#lrAyLT?KA}N*AL}K+UK>H zA7H`)cAfct$p8i0ZDHb4W;nb?86Ub3#?j9>92|b!Wn=PTJh&h&MP-AgtnB&Iz|*tYBu)Ot-=&uRFlkL_Y31vESsmK z64$F$%*Xii5=ZL3&~SAUARNz!cl`u>!plPt3~&$m!i}_CX^n(bnUV2gp^EA1*9vgy z4JgHa==NI6{WK%5Z2Sx7NvC)Tjk1xVEsWo+nvC^R^GxbjOAL9Oz1rIt{zkHziwaW= ziCA4}kkqo(k~2YJm zU?TcL^f-q}MRxN!is+u}-624`Kg)CYquv~-{SrCaE9;Owmk$%r`Y{K(#+Kw`R zzwn+W(&8lWYkEL=P$~-ua1qpKuo?>S?bTd4C8QKRysQqp5l>`H3|PLdo->kaAz~a0 zJPUVoQ|wltCfE(gQ86UTly3QkNaYWz0av5e5!bcRvA~HGfSb!NOciJO^RgTni zSC~U*;)zo|TL&9EEG_0#LACs9`d;rmtY?f))n}l^xrjwHZkC2h@ zi2P;I>CFl-z17Nn;!u{^mB8dYTZP&;YT8|ofjGrPXj{0k=kRy|p~UW$$!82rwlc-P zGa6rFQEu_0pdgap|DDhU6@5t1tJZL4VtI{nxV1-_vve!Za0<})B$)@(#I33{9;=;v z!S|{@alOBB5?IVFQ-+g<3XV6o72IjPwLI+-p+kaxcIdlpKyHIogVMOjGtr0-?;X zGr`ICDtCTfrpZvQ$yzd8v-pWSUpkfsd%<$x8vkv)HH$^8T}rV_uLKgr!}Wlfbuvi^j>|N_qlafs^+cHx%Z@* zQ9DMosCR6eH}0EnKU_0&F8mI>^oPB*zOg(`G6cdqHn1U9Z43Rw2KxVj{hwI@5*K8U X{h8Bg`Qpg?Ih{Tc_zlDN;*I|TL1_QA literal 0 HcmV?d00001 diff --git a/src/sphinx/Launcher/index.rst b/src/sphinx/Launcher/index.rst new file mode 100644 index 000000000..fcc5802e9 --- /dev/null +++ b/src/sphinx/Launcher/index.rst @@ -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 \ No newline at end of file diff --git a/src/sphinx/index.rst b/src/sphinx/index.rst index 3169538b7..8a07fbe72 100644 --- a/src/sphinx/index.rst +++ b/src/sphinx/index.rst @@ -22,7 +22,6 @@ the :doc:`index of names and types `. 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.