diff --git a/launch.specification b/launch.specification index 989304d65..2863b7b65 100644 --- a/launch.specification +++ b/launch.specification @@ -2,14 +2,31 @@ 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.5 or greater. += Overview = + +A user downloads the sbt launcher jar and creates a script to run it (here, the script will be assumed to be called 'launch'): + 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. The difference is that this behavior is now configurable. The versions may be fixed or read from a 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. + +Once the launcher has downloaded the 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 launcher 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, and different arguments. + +There are some other options for setup, such as putting the configuration file inside the sbt 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 the following ways in increasing order of precedence: +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. 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. -The configuration file is read as UTF-8 encoded and is defined by the following grammer (nl is a newline or end of file): +The configuration file is line-based, read as UTF-8 encoded, and defined by the following grammer. '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): configuration ::= scala app repositories boot log app-properties scala ::= '[' 'scala' ']' nl versionSpecification nl @@ -80,15 +97,15 @@ The default configuration file for sbt looks like: project.scratch: quick=set(true) project.initialize: quick=set(true), new=set(true) -The scala.version property specifies the version of Scala used to run the application. 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 cross-versioned is true, the resolved module is {app.name+'_'+scala.version} +The scala.version property specifies the version of Scala used to run the application. 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 true, the resolved module ID is {app.name+'_'+scala.version} 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. == 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 implicit (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 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 file does not exist, the default value provided is used. If no default was provided, an error is generated. +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. No locking is done on the directory, so it should not be shared system-wide. The launcher retrieves the requested version of Scala to [boot.directory]/[scala.version]/lib/. If this directory already exists, the launcher takes a shortcut for performance and assumes that the jars have already been downloaded. If the directory does not exists, the launcher uses Apache Ivy to resolve and retrieve the jars. A similar process occurs for the application itself. It and its dependencies are retreived to [boot.directory]/[scala.version]/[app.org]/[app.name]/. +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. No locking is done on the directory, so it should not be shared system-wide. The launcher retrieves the requested version of Scala to ${boot.directory}/${scala.version}/lib/. If this directory already exists, the launcher takes a shortcut for 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 retreived to ${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'. An application that does not use components will have all of its jars in this class loader. @@ -96,11 +113,11 @@ The main class for the application is then instantiated. It must be a public cl == 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 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 compiling your application. Additionally, 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: +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: val launchInterface = "org.scala-tools.sbt" % "launcher-interface" % "0.7" % "provided" -Make the entry point to your class implement 'xsbti.AppMain'. For example: +Make the entry point to your class implement 'xsbti.AppMain'. An example that uses some of the information av: package xsbt.test class Main extends xsbti.AppMain @@ -123,13 +140,14 @@ class Main extends xsbti.AppMain def scalaVersion = "2.7.4" def app = configuration.provider.id } - case "2.7.4" => new xsbti.Exit{ def code = 1 } - case _ => new xsbti.Exit{ def code = 0 } + case "2.7.4" => new Exit(1) + case _ => new Exit(0) } } + class Exit(val code) extends xsbti.Exit } -Define a configuration file for the launcher. For the above class, it might look like: +Next, define a configuration file for the launcher. For the above class, it might look like: [scala] version: 2.7.5 @@ -150,7 +168,9 @@ Define a configuration file for the launcher. For the above class, it might loo Then, +publish-local 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. -1) Replace the /sbt/sbt.boot.properties file in the launcher jar and distribute the modified jar. The user would need to run 'java -jar your-launcher.jar'. -2) The user downloads the vanilla sbt launcher jar and you provide the sbt.boot.properties file. The user would need to run 'java -Dsbt.boot.properties=your.boot.properties -jar sbt-launcher.jar' -3) The user sets up the sbt launcher, including the bash script. You provide the sbt.boot.properties file and the user runs sbt @your.boot.properties . \ No newline at end of file +1) Replace the /sbt/sbt.boot.properties file in the launcher jar and distribute the modified jar. The user would need to run 'java -jar your-launcher.jar arg1 arg2 ...'. +2) The user downloads the vanilla sbt launcher jar and you provide the sbt.boot.properties file. The user would need to run 'java -Dsbt.boot.properties=your.boot.properties -jar sbt-launcher.jar'. +3) The user sets up the sbt launcher, including the bash script. You provide the sbt.boot.properties file and the user runs sbt @your.boot.properties arg1 arg2. \ 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 fed5445c7..33818b855 100644 --- a/launch/interface/src/main/java/xsbti/AppProvider.java +++ b/launch/interface/src/main/java/xsbti/AppProvider.java @@ -2,10 +2,16 @@ package xsbti; public interface AppProvider { + /** Returns the ScalaProvider that this AppProvider will use. */ public ScalaProvider scalaProvider(); + /** The ID of the application that will be created by 'newMain' or 'mainClass'.*/ public ApplicationID id(); + /** Loads the class for the entry point for the application given by 'id'. This method will return the same class + * every invocation. That is, the ClassLoader is not recreated each call.*/ public Class mainClass(); + /** Creates a new instance of the entry point of the application given by 'id'. + * It is guaranteed that newMain().getClass() == mainClass()*/ public AppMain newMain(); public ComponentProvider components(); diff --git a/launch/interface/src/main/java/xsbti/Exit.java b/launch/interface/src/main/java/xsbti/Exit.java index 64e08f174..f88c8c591 100644 --- a/launch/interface/src/main/java/xsbti/Exit.java +++ b/launch/interface/src/main/java/xsbti/Exit.java @@ -1,4 +1,7 @@ package xsbti; + +/** A launched application returns an instance of this class in order to communicate to the launcher +* that the application is completely finished and the launcher should exit with the given exit code.*/ public interface Exit extends MainResult { public int code(); diff --git a/launch/interface/src/main/java/xsbti/Launcher.java b/launch/interface/src/main/java/xsbti/Launcher.java index 22ccc04c2..cd5752a9c 100644 --- a/launch/interface/src/main/java/xsbti/Launcher.java +++ b/launch/interface/src/main/java/xsbti/Launcher.java @@ -1,6 +1,5 @@ package xsbti; - public interface Launcher { public static final int InterfaceVersion = 1; diff --git a/launch/interface/src/main/java/xsbti/MainResult.java b/launch/interface/src/main/java/xsbti/MainResult.java index 48ea398bf..e81aede2d 100644 --- a/launch/interface/src/main/java/xsbti/MainResult.java +++ b/launch/interface/src/main/java/xsbti/MainResult.java @@ -1,4 +1,8 @@ package xsbti; -public interface MainResult {} - +/** A launched application should return an instance of this from its 'run' method +* to communicate to the launcher what should be done now that the application +* has competed. This interface should be treated as 'sealed', with Exit and Reboot the only +* direct subtypes. +*/ +public interface MainResult {} \ No newline at end of file diff --git a/launch/interface/src/main/java/xsbti/Reboot.java b/launch/interface/src/main/java/xsbti/Reboot.java index 6122d1f8f..cb978c32a 100644 --- a/launch/interface/src/main/java/xsbti/Reboot.java +++ b/launch/interface/src/main/java/xsbti/Reboot.java @@ -2,6 +2,9 @@ package xsbti; import java.io.File; +/** A launched application returns an instance of this class in order to communicate to the launcher +* that the application should be restarted. Different versions of the application and Scala can be used. +* The application can be given different arguments and a new working directory as well.*/ public interface Reboot extends MainResult { public String[] arguments(); diff --git a/launch/interface/src/main/java/xsbti/ScalaProvider.java b/launch/interface/src/main/java/xsbti/ScalaProvider.java index 09f967d6d..381985c61 100644 --- a/launch/interface/src/main/java/xsbti/ScalaProvider.java +++ b/launch/interface/src/main/java/xsbti/ScalaProvider.java @@ -16,6 +16,7 @@ public interface ScalaProvider public File libraryJar(); public File compilerJar(); /** Creates an application provider that will use 'loader()' as the parent ClassLoader for - * the application given by 'id'.*/ + * the application given by 'id'. This method will retrieve the application if it has not already + * been retrieved.*/ public AppProvider app(ApplicationID id); } \ No newline at end of file diff --git a/notes b/notes index cb0454be3..20bd79b57 100644 --- a/notes +++ b/notes @@ -5,23 +5,30 @@ As usual: - Typesafe - Robust, flexible API +ISSUES + - Scala issue #2265: cannot use structural types or libraries that use structural types, or OOME: PermGen will occur. Running/testing user code with structural types might also cause OOME, but is unlikely unless the Scala standard library or compiler uses structural types. + +TODO + - immutable LinkedSet that preserves order of section declarations for repositories and app.properties + - launcher interface versioning + Task engine - method tasks will be normal tasks that pull the command line from a CommandLine task - possibly have per task logging, including configuration (e.g. 'debug compile') - unnamed tasks log to parent task - in parallel, optionally one task always logging -- Have Interfaces subproject that depends on no other project and defines interfaces in package xsbti. They are written in Java and cannot refer to Scala classes (compileOrder = JavaThenScala). These interfaces are loaded by the root loader and can be used to pass objects across ClassLoader boundaries. -- launcher/main interface is not static (no system properties!) +- Have Interfaces subproject that depends on no other project and defines interfaces in package xsbti. They are written in Java and cannot refer to Scala classes (compileOrder = JavaThenScala). These interfaces can be used to pass objects across ClassLoader boundaries. +- launcher/main interface is not static (no system properties should be used past the static entry point) - simple, well-defined ClassLoaders - use Exceptions instead of Option/Either - every component gets its own subproject -- can use any version of compiler/Scala that is source compatible +- can use any version of compiler/Scala that is source compatible with the compiler-interface component - Logger passed by implicit parameter - build xsbt using normal cross-build conventions -- compiler: raw interface (no dependency analysis) or with dependency analysis +- compiler: raw interface (no dependency analysis, but still in same jvm) or with dependency analysis - compiler: can specify scala-library.jar and scala-compiler.jar + version instead of retrieving the ClassLoader -- minimal dependence on main xsbt logger from subcomponents: use thin interface for subcomponents and implement interface in separate files in main xsbt +- minimal dependence on main xsbt logger from subcomponents: use thin interface for subcomponents Dependency Management - resolvers are completely defined in project definition (use Resolver.withDefaultResolvers)