From 1c9cabc69ff27f37a1f185915f504ebb4c2fa84a Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Thu, 15 Oct 2009 18:10:11 -0400 Subject: [PATCH] Update launch specification, fix launch initialization, general cleanup --- launch.specification | 45 ++++++++---- launch/ConfigurationParser.scala | 2 +- launch/Create.scala | 3 +- launch/Launch.scala | 68 +++---------------- launch/Locks.scala | 54 +++++++++++++++ launch/ResolveVersions.scala | 25 +++---- .../main/resources/sbt/sbt.boot.properties | 15 ++-- 7 files changed, 112 insertions(+), 100 deletions(-) create mode 100644 launch/Locks.scala diff --git a/launch.specification b/launch.specification index bff5f4c98..989304d65 100644 --- a/launch.specification +++ b/launch.specification @@ -5,25 +5,26 @@ The sbt launcher component is a self-contained jar that boots a Scala applicatio == Configuration == The launcher may be configured in the following ways in increasing order of precedence: - * Replace the /sbt/sbt.launch.config file in the jar - * Put a configuration file named sbt.launch.config file on the classpath. Put it in the classpath root without the /sbt prefix. + * 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): -configuration ::= scala app repositories boot log +configuration ::= scala app repositories boot log app-properties scala ::= '[' 'scala' ']' nl versionSpecification nl app ::= '[' 'app' ']' nl org nl name nl versionSpecification nl components nl class nl cross-versioned nl repositories ::= '[' 'repositories' ']' nl (repository nl)* boot ::= '[' 'boot' ']' nl directory nl properties nl search nl - log ::= '[' log ']' nl logLevel nl + log ::= '[' 'log' ']' nl logLevel nl + app-properties ::= '[' 'app-properties' ']' nl property* directory ::= 'directory' ':' path properties ::= 'properties' ':' path search ::= 'search' ':' ('none'|'nearest'|'root-first'|'only') (',' path)* logLevel ::= 'log-level' ':' ('debug' | 'info' | 'warn' | 'error') - versionSpecification ::= 'version' ':' ( ( ('read'|'prompt'|'read-or-prompt') [defaultVersion] ) | fixedVersion ) + versionSpecification ::= 'version' ':' ( ( 'read' [defaultVersion] ) | fixedVersion ) defaultVersion ::= text fixedVersion ::= text @@ -36,42 +37,56 @@ configuration ::= scala app repositories boot log repository ::= ( predefinedRepository | ( label ':' url [',' pattern] ) ) nl predefinedRepository ::= 'local' | 'maven-local' | 'maven-central' | 'scala-tools-releases' | 'scala-tools-snapshots' + property ::= label ':' propertyDefinition (',' propertyDefinition)* nl + propertyDefinition ::= ('quick' | 'new' | 'fill') '=' (set | prompt) + set ::= 'set' '(' text ')' + prompt ::= 'prompt' '(' text ')' ('[' text ']')? + The default configuration file for sbt looks like: [scala] - version: read-or-prompt, 2.7.5 + version: read [app] org: org.scala-tools.sbt - name: xsbt - version: read-or-prompt, 0.7.0_13 - class: xsbt.Main - components: xsbti, default + name: sbt + version: read + class: sbt.xMain + components: xsbti cross-versioned: true [repositories] local maven-local - Sbt Repository, http://simple-build-tool.googlecode.com/svn/artifacts/, [revision]/[type]s/[artifact].[ext] maven-central scala-tools-releases - scala-tools-snapshots [boot] directory: project/boot properties: project/build.properties - search: none + prompt-create: Project does not exist, create new project? + prompt-fill: true + quick-option: true [log] level: info +[app-properties] + project.name: quick=set(test), new=prompt(Name), fill=prompt(Name) + project.version: quick=set(1.0), new=prompt(Version)[1.0], fill=prompt(Version)[1.0] + build.init.scala.version: quick=set(2.7.5), new=prompt(Scala version)[2.7.5] + scala.version: quick=set(2.7.5), new=set(2.7.5), fill=set(2.7.5) + sbt.version: quick=set(0.5.6-SNAPSHOT), new=prompt(sbt version)[0.5.6-SNAPSHOT], fill=prompt(sbt version)[0.5.6-SNAPSHOT] + 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 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 (read, prompt, or read-or-prompt), the launcher determines them in the following manner. If the implicit is specified to be 'read', 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. If the implicit is 'prompt', the user is prompted for the version to use and is provided a default option if one was specified. If the implicit is 'read-or-prompt', the file given by 'boot.properties' is read. If the version is not specified there, the user is prompted and is provided a default option if one was specified. The file specified by 'boot.properties' is updated with the value specified by the user. +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. 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]/. @@ -136,6 +151,6 @@ Define a configuration file for the launcher. For the above class, it might loo Then, +publish-local the application to make it available. 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.boot.properties file in the launcher jar and distribute the modified jar. The user would need to run 'java -jar your-launcher.jar'. +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 diff --git a/launch/ConfigurationParser.scala b/launch/ConfigurationParser.scala index 737eb0531..c9eebd14c 100644 --- a/launch/ConfigurationParser.scala +++ b/launch/ConfigurationParser.scala @@ -126,7 +126,7 @@ class ConfigurationParser extends Parsers with NotNull case "set" => SetProperty(requiredArg) case _ => throw new BootException("Unknown action '" + action + "' for property '" + name + "'") } - private lazy val propertyPattern = """(.+)\((.*)\)(?:\[(.*)\])?""".r.pattern + private lazy val propertyPattern = """(.+)\((.*)\)(?:\[(.*)\])?""".r.pattern // examples: prompt(Version)[1.0] or set(1.0) def parsePropertyValue[T](name: String, definition: String)(f: (String, String, Option[String]) => T): T = { val m = propertyPattern.matcher(definition) diff --git a/launch/Create.scala b/launch/Create.scala index d656fbe32..4d734defc 100644 --- a/launch/Create.scala +++ b/launch/Create.scala @@ -13,7 +13,7 @@ object Initialize case Some(line) => line.toLowerCase match { - case "y" | "yes" => file.createNewFile(); process(file, spec, _.create) + case "y" | "yes" => process(file, spec, _.create) case "n" | "no" | "" => throw new BootException("") case "s" => process(file, spec, _.quick) } @@ -27,6 +27,7 @@ object Initialize Using(new FileInputStream(file))( properties.load ) for(property <- appProperties; init <- select(property) if properties.getProperty(property.name) == null) initialize(properties, property.name, init) + file.getParentFile.mkdirs() Using(new FileOutputStream(file))( out => properties.save(out, "") ) } def initialize(properties: Properties, name: String, init: PropertyInit) diff --git a/launch/Launch.scala b/launch/Launch.scala index 42d6479b5..f4526b2ef 100644 --- a/launch/Launch.scala +++ b/launch/Launch.scala @@ -1,9 +1,7 @@ package xsbt.boot -import java.io.{File, FileOutputStream} +import java.io.File import java.net.URL -import java.nio.channels.FileChannel -import java.util.concurrent.Callable object Launch { @@ -28,11 +26,11 @@ object Launch initialized(currentDirectory, parsed, arguments) } def initialized(currentDirectory: File, parsed: LaunchConfiguration, arguments: Seq[String]): Unit = - ResolveVersions(parsed) match { case (resolved, finish) => explicit(currentDirectory, resolved, arguments, finish) } + explicit(currentDirectory, ResolveVersions(parsed), arguments) - def explicit(currentDirectory: File, explicit: LaunchConfiguration, arguments: Seq[String], setupComplete: () => Unit): Unit = + def explicit(currentDirectory: File, explicit: LaunchConfiguration, arguments: Seq[String]): Unit = launch( run(new Launch(explicit.boot.directory, explicit.repositories)) ) ( - RunConfiguration(explicit.getScalaVersion, explicit.app.toID, currentDirectory, arguments, setupComplete) ) + RunConfiguration(explicit.getScalaVersion, explicit.app.toID, currentDirectory, arguments) ) def run(launcher: xsbti.Launcher)(config: RunConfiguration): xsbti.MainResult = { @@ -41,21 +39,19 @@ object Launch val appProvider: xsbti.AppProvider = scalaProvider.app(app) val appConfig: xsbti.AppConfiguration = new AppConfiguration(arguments.toArray, workingDirectory, appProvider) - val main = appProvider.newMain() - setupComplete() - main.run(appConfig) + appProvider.newMain().run(appConfig) } final def launch(run: RunConfiguration => xsbti.MainResult)(config: RunConfiguration) { run(config) match { case e: xsbti.Exit => System.exit(e.code) - case r: xsbti.Reboot => launch(run)(RunConfiguration(r.scalaVersion, r.app, r.baseDirectory, r.arguments, () => ())) + case r: xsbti.Reboot => launch(run)(RunConfiguration(r.scalaVersion, r.app, r.baseDirectory, r.arguments)) case x => throw new BootException("Invalid main result: " + x + (if(x eq null) "" else " (class: " + x.getClass + ")")) } } } -case class RunConfiguration(scalaVersion: String, app: xsbti.ApplicationID, workingDirectory: File, arguments: Seq[String], setupComplete: () => Unit) extends NotNull +case class RunConfiguration(scalaVersion: String, app: xsbti.ApplicationID, workingDirectory: File, arguments: Seq[String]) extends NotNull import BootConfiguration.{appDirectoryName, baseDirectoryName, ScalaDirectoryName, TestLoadScalaClasses} class Launch(val bootDirectory: File, repositories: Seq[Repository]) extends xsbti.Launcher @@ -128,52 +124,4 @@ object ComponentProvider baseDirectory.mkdirs() new File(baseDirectory, "sbt.components.lock") } -} -// gets a file lock by first getting a JVM-wide lock. -object Locks extends xsbti.GlobalLock -{ - import scala.collection.mutable.HashMap - private[this] val locks = new HashMap[File, GlobalLock] - def apply[T](file: File, action: Callable[T]) = - { - val canonFile = file.getCanonicalFile - synchronized { locks.getOrElseUpdate(canonFile, new GlobalLock(canonFile)).withLock(action) } - } - - private[this] class GlobalLock(file: File) - { - private[this] var fileLocked = false - def withLock[T](run: Callable[T]): T = - synchronized - { - if(fileLocked) - run.call - else - { - fileLocked = true - try { withFileLock(run) } - finally { fileLocked = false } - } - } - private[this] def withFileLock[T](run: Callable[T]): T = - { - def withChannel(channel: FileChannel) = - { - val freeLock = channel.tryLock - if(freeLock eq null) - { - println("Waiting for lock on " + file + " to be available..."); - val lock = channel.lock - try { run.call } - finally { lock.release() } - } - else - { - try { run.call } - finally { freeLock.release() } - } - } - Using(new FileOutputStream(file).getChannel)(withChannel) - } - } -} +} \ No newline at end of file diff --git a/launch/Locks.scala b/launch/Locks.scala new file mode 100644 index 000000000..99c996c35 --- /dev/null +++ b/launch/Locks.scala @@ -0,0 +1,54 @@ +package xsbt.boot + +import java.io.{File, FileOutputStream} +import java.nio.channels.FileChannel +import java.util.concurrent.Callable + +// gets a file lock by first getting a JVM-wide lock. +object Locks extends xsbti.GlobalLock +{ + import scala.collection.mutable.HashMap + private[this] val locks = new HashMap[File, GlobalLock] + def apply[T](file: File, action: Callable[T]) = + { + val canonFile = file.getCanonicalFile + synchronized { locks.getOrElseUpdate(canonFile, new GlobalLock(canonFile)).withLock(action) } + } + + private[this] class GlobalLock(file: File) + { + private[this] var fileLocked = false + def withLock[T](run: Callable[T]): T = + synchronized + { + if(fileLocked) + run.call + else + { + fileLocked = true + try { withFileLock(run) } + finally { fileLocked = false } + } + } + private[this] def withFileLock[T](run: Callable[T]): T = + { + def withChannel(channel: FileChannel) = + { + val freeLock = channel.tryLock + if(freeLock eq null) + { + println("Waiting for lock on " + file + " to be available..."); + val lock = channel.lock + try { run.call } + finally { lock.release() } + } + else + { + try { run.call } + finally { freeLock.release() } + } + } + Using(new FileOutputStream(file).getChannel)(withChannel) + } + } +} diff --git a/launch/ResolveVersions.scala b/launch/ResolveVersions.scala index 8a33e4ac4..e314cba41 100644 --- a/launch/ResolveVersions.scala +++ b/launch/ResolveVersions.scala @@ -1,18 +1,13 @@ package xsbt.boot -import java.io.{File, FileInputStream, FileOutputStream} +import java.io.{File, FileInputStream} import java.util.Properties -object ResolvedVersion extends Enumeration -{ - val Explicit, Read = Value -} -final case class ResolvedVersion(v: String, method: ResolvedVersion.Value) extends NotNull +final case class ResolvedVersion(v: String, wasExplicit: Boolean) extends NotNull object ResolveVersions { - def apply(conf: LaunchConfiguration): (LaunchConfiguration, () => Unit) = (new ResolveVersions(conf))() - private def doNothing = () => () + def apply(conf: LaunchConfiguration): LaunchConfiguration = (new ResolveVersions(conf))() private def trim(s: String) = if(s eq null) None else notEmpty(s.trim) private def notEmpty(s: String) = if(s.isEmpty) None else Some(s) private def readProperties(propertiesFile: File) = @@ -24,30 +19,28 @@ object ResolveVersions } } -import ResolveVersions.{doNothing, notEmpty, readProperties, trim} +import ResolveVersions.{readProperties, trim} final class ResolveVersions(conf: LaunchConfiguration) extends NotNull { private def propertiesFile = conf.boot.properties private lazy val properties = readProperties(propertiesFile) - def apply(): (LaunchConfiguration, () => Unit) = + def apply(): LaunchConfiguration = { import conf._ val appVersionProperty = app.name.toLowerCase.replaceAll("\\s+",".") + ".version" val scalaVersion = (new Resolve("scala.version", "Scala"))(conf.scalaVersion) val appVersion = (new Resolve(appVersionProperty, app.name))(app.version) - val finish = () => Using( new FileOutputStream(propertiesFile) ) { out => properties.store(out, "") } - ( withVersions(scalaVersion.v, appVersion.v), finish ) + withVersions(scalaVersion, appVersion) } private final class Resolve(versionProperty: String, label: String) extends NotNull { def noVersionInFile = throw new BootException("No " + versionProperty + " specified in " + propertiesFile) - def apply(v: Version): ResolvedVersion = + def apply(v: Version): String = { - import Version.{Explicit, Implicit} v match { - case e: Explicit => ResolvedVersion(e.value, ResolvedVersion.Explicit) - case Implicit(default) => ResolvedVersion(readVersion() orElse default getOrElse noVersionInFile, ResolvedVersion.Read ) + case e: Version.Explicit => e.value + case Version.Implicit(default) => readVersion() orElse default getOrElse noVersionInFile } } def readVersion() = trim(properties.getProperty(versionProperty)) diff --git a/launch/src/main/resources/sbt/sbt.boot.properties b/launch/src/main/resources/sbt/sbt.boot.properties index 13b8e76cc..93b9bf1bb 100644 --- a/launch/src/main/resources/sbt/sbt.boot.properties +++ b/launch/src/main/resources/sbt/sbt.boot.properties @@ -20,7 +20,7 @@ [boot] directory: project/boot properties: project/build.properties - prompt-create: true + prompt-create: Project does not exist, create new project? prompt-fill: true quick-option: true @@ -28,9 +28,10 @@ level: info [app-properties] - project.name: quick=set[test], new=prompt, fill=prompt - project.version: quick=set[1.0], new=prompt[1.0], fill=prompt[1.0] - scala.version: quick=set[2.7.5], new=prompt[2.7.5], fill=prompt[2.7.5] - sbt.version: quick=set[0.5.6-SNAPSHOT], new=prompt[0.5.6-SNAPSHOT], fill=prompt[0.5.6-SNAPSHOT] - project.scratch: quick=set[true] - project.initialize: quick=set[true], new=set[true] \ No newline at end of file + project.name: quick=set(test), new=prompt(Name), fill=prompt(Name) + project.version: quick=set(1.0), new=prompt(Version)[1.0], fill=prompt(Version)[1.0] + build.init.scala.version: quick=set(2.7.5), new=prompt(Scala version)[2.7.5] + scala.version: quick=set(2.7.5), new=set(2.7.5), fill=set(2.7.5) + sbt.version: quick=set(0.5.6-SNAPSHOT), new=prompt(sbt version)[0.5.6-SNAPSHOT], fill=prompt(sbt version)[0.5.6-SNAPSHOT] + project.scratch: quick=set(true) + project.initialize: quick=set(true), new=set(true) \ No newline at end of file