Locked History Actions

sbt/dt_Launcher

ランチャ仕様

https://github.com/harrah/xsbt/wiki/Launcherの訳(2011/11/1時点)

訳注:この機能は、いわばJava Web Startに例えられるかもしれない。自身のアプリは何らかのリポジトリ中に格納しておき、ユーザはランチャと構成ファイル(jnlpファイルのようなもの)をあらかじめダウンロードしておいてもらえば、あとはそれを起動するだけで、勝手にアプリ及び依存をダウンロードして実行してくれるというようなもの。

sbtランチャコンポーネントは、Scalaやアプリケーションがシステム中に存在しない状態で、Scalaアプリケーションを起動するための自己完結したjarファイルだ。 唯一の必須条件としてはランチャそのもの、オプションでコンフィギュレーションファイル、それと1.6以上のJavaランタイムだけである。

概要

ユーザはランチャjarをダウンロードして、それを実行するためのスクリプトを作成する。 このドキュメントでは、このスクリプトのことをlaunchと呼ぶ。 unitなら以下のようになるだろう。

java -jar sbt-launcher.jar "$@"

ユーザはアプリケーションに必要なコンフィギュレーションファイルをダウンロードし(これをmy.app.configurationと呼ぶことにする)、 ラウンチに必要なスクリプトを作成する(myappと呼ぶことにする)。

launch @my.app.configuration "$@"

ユーザは以下のようにアプリケーションを起動することができる。

myapp arg1 arg2 ...

sbtディストリビューションのランチャのように、ダウンロードしたランチャは、提供されたコンフィギュレーションによってscalaとアプリケーションを取得してくる。 バージョンは固定していてもいいし、異なるコンフィギュレーションファイルから読み込んでもいい(その場所も設定可能だ)。 Scalaとアプリケーションjarのダウンロード場所も指定可能だ。 検索されるリポジトリも指定可能だ。 プロパティファイル中のオプショナルな初期化も構成可能だ。

ランチャが必要なjarをダウンロードすると、アプリケーションをロードして、そのエントリポイントを呼び出す。 アプリケーションは、それがどのようにして呼び出されたかという情報を受取る。 コマンドライン引数、カレントディレクトリ、scalaバージョン、アプリケーションID(organization, name, version)。 加えて、アプリケーションは、Scalaのjarや、あるいはコンフィギュレーションに記述されたいかなるバージョンのScalaもリポジトリから取得するためのクラスローダを取得することをランチャに指示することができる。 他のアプリをダウンロードして実行させることも可能だ。 アプリケーションが終了したら、それをランチャに終了コードと共に伝え、異なるScalaバージョン、異なるアプリバージョン、異なる引数でアプリをリロードすることを伝えることができる。

他にもセットアップオプションがいくつかある。 ランチャjarの中にコンフィギュレーションファイルを埋め込んだり、それを単一のダウンロードファイルとすることなどだ。

このドキュメントでは、アプリケーションの構成、記述、配布、実行について説明する。

コンフィギュレーション

ランチャは以下の方法で構成することができる。以下は優先順に並べる

  • jar中の/sbt/sbt.boot.properties を入れ替える。
  • クラスパス中に、sbt.boot.propertiesという構成ファイルを入れる。/sbtプリフィックス無しでルートクラスパスに加える。
  • 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.

コンフィギュレーションファイルは行ベースであり、UTF-8エンコーディングとして読み込まれ、以下の構文を持つ。 'nl'が改行あるいはファイル終了を意味し、'text'は改行なしでかつ丸括弧やカギカッコ無しのプレーンテキストである。

configuration ::= scala app repositories boot log app-properties
  scala ::= '[' 'scala' ']' nl version nl classifiers nl
  app ::= '[' 'app' ']' nl org nl name nl version nl components nl class nl cross-versioned 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
  app-properties ::= '[' 'app-properties' ']' nl property*
  ivy ::= '[' 'ivy' ']' nl homeDirectory

    directory ::= 'directory' ':' path
    bootProperties ::= 'properties' ':' path
    search ::= 'search' ':' ('none'|'nearest'|'root-first'|'only') (',' path)*
    logLevel ::= 'log-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

    org ::= 'org' ':' text
    name ::= 'name' ':' text
    class ::= 'class' ':' text
    components ::= 'components' ':' component (',' component)*
    cross-versioned ::= 'cross-versioned' ':'  boolean
    resources ::= 'resources' ':' path (',' path)*

    repository ::= ( predefinedRepository | customRepository ) nl
      predefinedRepository ::= 'local' | 'maven-local' | 'maven-central' | 'scala-tools-releases' | 'scala-tools-snapshots'
      customRepository ::= label ':' url [ [',' ivy-pattern] ',' artifact-pattern]

    property ::= label ':' propertyDefinition (',' propertyDefinition)* nl
      propertyDefinition ::= mode '=' (set | prompt)
        mode ::= 'quick' | 'new' | 'fill'
        set ::= 'set' '(' value ')'
        prompt ::= 'prompt'  '(' label ')' ('[' default ']')?

    boolean ::= 'true' | 'false'
    path, propertyName, label, default ::= text

sbtのデフォルトコンフィギュレーションファイルは以下のようなものだ。

[scala]
  version: 2.8.1

[app]
  org: org.scala-tools.sbt
  name: sbt
  version: read(sbt.version)[0.11.0]
  class: ${sbt.main.class-sbt.xMain}
  components: xsbti,extra
  cross-versioned: true

[repositories]
  local
  maven-local
  typesafe-ivy-releases: http://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext]
  maven-central
  scala-tools-releases
  scala-tools-snapshots

[boot]
 directory: ${sbt.boot.directory-project/boot/}

[ivy]
  ivy-home: ${sbt.ivy.home-${user.home}/.ivy2/}

The scala.version property specifies the version of Scala used to run the application.

If specified, the scala.classifiersproperty defines classifers, 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 true, the resolved module ID is {app.name+'_'+scala.version}. 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 classifers, like 'sources', of extra artifacts to retrieve for the application.

Jars are retrieved to the directory given by boot.directory. You can make this an absolute path to be shared by all sbt instances on the machine. You might see messages like:

Waiting for lock on <lock-file> to be available...

ifmultiple versions access it simultaneously.

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 set the Ivy cache for the application.

実行

スタートアップ時にランチャは、そのコンフィギュレーションファイルのコンフィギュレーションセクションを記述順に解析する。 もしScalaバージョンとアプリバージョンが'read'と指定されていると、ランチャは以下のようにしてそれらを決定する。 boot.propertiesプリパティとして指定されたファイルはバージョン取得のためにJavaプロパティファイルとして読み込まれる。

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 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 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' 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.

ラウンチ可能なアプリの作成

このセクションでは、このランチャでラウンチ可能なアプリの作り方を見ていく。 まずはじめに、ランチャインターフェースの依存を宣言する。 ランチャそれ自身の依存を宣言してはいけない。 ランチャインターフェースは、ランチャがコンパイルされたScalaバージョンと君自身のアプリがコンパイルされたScalaバージョンとの間で非互換が発生しないよう、厳格なJavaのinterfaceとしている。 ランチャインターフェースはランチャによって提供されるので、これはcompile-timeの依存しかない。 もし君がsbtによってビルドを行なっているのであれば、依存定義は次のようになる。

libraryDependencies += "org.scala-tools.sbt" %% "launcher-interface" % "0.11.0" % "provided"

resolvers <+= sbtResolver

'xsbti.AppMain'を実装したクラスへのエントリポイントを作る。 以下の例を見ればわかるだろう。

package xsbt.test
class Main extends xsbti.AppMain
{
        def run(configuration: xsbti.AppConfiguration) =
        {
                // アプリをラウンチするのに使用されるScalaのバージョンを取得する
                val scalaVersion = configuration.provider.scalaProvider.version

                // アプリのメッセージと引数を表示する。
                println("Hello world!  Running Scala " + scalaVersion)
                configuration.arguments.foreach(println)

                // 異なるバージョンのScalaで再起動が可能なことのデモ
                // それと、終了コードをどのようにして返すか
                scalaVersion match
                {
                        case "2.8.1" =>
                                new xsbti.Reboot {
                                        def arguments = configuration.arguments
                                        def baseDirectory = configuration.baseDirectory
                                        def scalaVersion = "2.9.1"
                                        def app = configuration.provider.id
                                }
                        case "2.9.1" => new Exit(1)
                        case _ => new Exit(0)
                }
        }
        class Exit(val code: Int) extends xsbti.Exit
}

次に、ランチャのためのコンフィギュレーションファイルを作成する。 上記のクラス用としては、こんな感じだ。

[scala]
  version: 2.9.1
[app]
  org: org.scala-tools.sbt
  name: xsbt-test
  version: 0.11.0
  class: xsbt.test.Main
  cross-versioned: true
[repositories]
  local
  maven-local
  maven-central
  scala-tools-releases
#  scala-tools-snapshots
[boot]
 directory: boot

そして、publish-localあるいは+publish-localしてアプリケーションを作成すればよい。

アプリを実行する

上述したように、実際にアプリを実行させるためには、いくつか選択肢がある。 一つは、ダウンロード用に変更したjarを提供することだ。 もう一つは、ダウンロード用のコンフィギュレーションファイルを提供することだ。

  • launcher.jar中の/sbt/sbt.boot.propertiesを入れ替え、変更したjarを配布する。この場合、ユーザは「java -jar your-launcher.jar arg1 arg2 ...」を実行するスクリプトが必要になる。.
  • ユーザはlauncher jarをダウンロードし、君はコンフィギュレーションファイルを提供する
  • * ユーザは「java -Dsbt.boot.properties=your.boot.properties -jar launcher.jar」を実行する必要がある。
  • * ユーザが既にlauncherを実行するスクリプトを持っているなら(これをlaunchと呼ぶ)、ユーザは「launch @your.boot.properties your-arg-1 your-arg-2」を実行する必要がある。